Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into instance_copy_progress

This commit is contained in:
Trial97 2023-11-23 14:08:49 +02:00
commit e43a557a0b
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
373 changed files with 15507 additions and 3457 deletions

5
.clang-tidy Normal file
View File

@ -0,0 +1,5 @@
Checks:
- modernize-use-using
- readability-avoid-const-params-in-decls
SystemHeaders: false

1
.envrc
View File

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

View File

@ -20,11 +20,11 @@ jobs:
if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name)) if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name))
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs - name: Create backport PRs
uses: korthout/backport-action@v1.4.0 uses: korthout/backport-action@v2.1.1
with: with:
# Config README: https://github.com/korthout/backport-action#backport-action # Config README: https://github.com/korthout/backport-action#backport-action
pull_description: |- pull_description: |-

View File

@ -37,56 +37,43 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: ubuntu-20.04 - os: ubuntu-20.04
qt_ver: 5 qt_ver: 5
- os: ubuntu-20.04 - os: ubuntu-20.04
qt_ver: 6 qt_ver: 6
qt_host: linux qt_host: linux
qt_arch: '' qt_arch: ""
qt_version: '6.2.4' qt_version: "6.2.4"
qt_modules: 'qt5compat qtimageformats' qt_modules: "qt5compat qtimageformats"
qt_tools: '' qt_tools: ""
- os: windows-2022 - os: windows-2022
name: "Windows-MinGW-w64" name: "Windows-MinGW-w64"
msystem: clang64 msystem: clang64
vcvars_arch: 'amd64_x86' 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 - os: windows-2022
name: "Windows-MSVC" name: "Windows-MSVC"
msystem: '' msystem: ""
architecture: 'x64' architecture: "x64"
vcvars_arch: 'amd64' vcvars_arch: "amd64"
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: windows
qt_arch: '' qt_arch: ''
qt_version: '6.5.2' qt_version: '6.6.0'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats'
qt_tools: '' qt_tools: ''
- os: windows-2022 - os: windows-2022
name: "Windows-MSVC-arm64" name: "Windows-MSVC-arm64"
msystem: '' msystem: ""
architecture: 'arm64' architecture: "arm64"
vcvars_arch: 'amd64_arm64' vcvars_arch: "amd64_arm64"
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: windows
qt_arch: 'win64_msvc2019_arm64' qt_arch: 'win64_msvc2019_arm64'
qt_version: '6.5.2' qt_version: '6.6.0'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats'
qt_tools: '' qt_tools: ''
@ -96,7 +83,7 @@ jobs:
qt_ver: 6 qt_ver: 6
qt_host: mac qt_host: mac
qt_arch: '' qt_arch: ''
qt_version: '6.5.2' qt_version: '6.6.0'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats'
qt_tools: '' qt_tools: ''
@ -105,9 +92,9 @@ jobs:
macosx_deployment_target: 10.13 macosx_deployment_target: 10.13
qt_ver: 5 qt_ver: 5
qt_host: mac qt_host: mac
qt_version: '5.15.2' qt_version: "5.15.2"
qt_modules: '' qt_modules: ""
qt_tools: '' qt_tools: ""
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -125,11 +112,11 @@ jobs:
# PREPARE # PREPARE
## ##
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: 'true' submodules: "true"
- name: 'Setup MSYS2' - name: "Setup MSYS2"
if: runner.os == 'Windows' && matrix.msystem != '' if: runner.os == 'Windows' && matrix.msystem != ''
uses: msys2/setup-msys2@v2 uses: msys2/setup-msys2@v2
with: with:
@ -164,7 +151,7 @@ jobs:
- name: Retrieve ccache cache (Windows MinGW-w64) - name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v3.3.1 uses: actions/cache@v3.3.2
with: with:
path: '${{ github.workspace }}\.ccache' path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
@ -214,12 +201,12 @@ jobs:
if: runner.os == 'Windows' && matrix.architecture == 'arm64' if: runner.os == 'Windows' && matrix.architecture == 'arm64'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v3
with: with:
aqtversion: '==3.1.*' aqtversion: "==3.1.*"
py7zrversion: '>=0.20.2' py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
host: 'windows' host: "windows"
target: 'desktop' target: "desktop"
arch: '' arch: ""
modules: ${{ matrix.qt_modules }} modules: ${{ matrix.qt_modules }}
tools: ${{ matrix.qt_tools }} tools: ${{ matrix.qt_tools }}
cache: ${{ inputs.is_qt_cached }} cache: ${{ inputs.is_qt_cached }}
@ -231,11 +218,11 @@ jobs:
if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '') if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '')
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v3
with: with:
aqtversion: '==3.1.*' aqtversion: "==3.1.*"
py7zrversion: '>=0.20.2' py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }} host: ${{ matrix.qt_host }}
target: 'desktop' target: "desktop"
arch: ${{ matrix.qt_arch }} arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }} modules: ${{ matrix.qt_modules }}
tools: ${{ matrix.qt_tools }} tools: ${{ matrix.qt_tools }}
@ -283,12 +270,12 @@ jobs:
if: runner.os == 'Windows' && matrix.msystem != '' if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0} shell: msys2 {0}
run: | 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=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -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=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja
- name: Configure CMake (Windows MSVC) - name: Configure CMake (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == '' if: runner.os == 'Windows' && matrix.msystem == ''
run: | 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=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }}
# https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix) # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix)
if ("${{ env.CCACHE_VAR }}") if ("${{ env.CCACHE_VAR }}")
{ {
@ -303,7 +290,7 @@ jobs:
- name: Configure CMake (Linux) - name: Configure CMake (Linux)
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: | run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -G Ninja cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -G Ninja
## ##
# BUILD # BUILD
@ -402,7 +389,6 @@ jobs:
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 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) - name: Fetch codesign certificate (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: bash # yes, we are not using MSYS2 or PowerShell here shell: bash # yes, we are not using MSYS2 or PowerShell here
@ -415,7 +401,7 @@ jobs:
if (Get-Content ./codesign.pfx){ if (Get-Content ./codesign.pfx){
cd ${{ env.INSTALL_DIR }} cd ${{ env.INSTALL_DIR }}
# We ship the exact same executable for portable and non-portable editions, so signing just once is fine # 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 SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe
} else { } else {
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
} }
@ -507,15 +493,7 @@ jobs:
export LD_LIBRARY_PATH export LD_LIBRARY_PATH
chmod +x AppImageUpdate-x86_64.AppImage chmod +x AppImageUpdate-x86_64.AppImage
./AppImageUpdate-x86_64.AppImage --appimage-extract cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin
mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/optional
mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins
cp -r squashfs-root/usr/bin/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin
cp -r squashfs-root/usr/lib/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib
cp -r squashfs-root/usr/optional/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/optional
cp -r squashfs-root/usr/optional/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins
export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync" export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync"
@ -523,7 +501,7 @@ jobs:
export SIGN=1 export SIGN=1
export SIGN_KEY=${{ secrets.GPG_PRIVATE_KEY_ID }} export SIGN_KEY=${{ secrets.GPG_PRIVATE_KEY_ID }}
mkdir -p ~/.gnupg/ mkdir -p ~/.gnupg/
printf "$GPG_PRIVATE_KEY" | base64 --decode > ~/.gnupg/private.key echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key
gpg --import ~/.gnupg/private.key gpg --import ~/.gnupg/private.key
else else
echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY
@ -569,14 +547,14 @@ jobs:
if: runner.os == 'Linux' && matrix.qt_ver != 6 if: runner.os == 'Linux' && matrix.qt_ver != 6
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ runner.os }}-Qt5-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher.tar.gz path: PrismLauncher.tar.gz
- name: Upload binary tarball (Linux, portable, Qt 5) - name: Upload binary tarball (Linux, portable, Qt 5)
if: runner.os == 'Linux' && matrix.qt_ver != 6 if: runner.os == 'Linux' && matrix.qt_ver != 6
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: PrismLauncher-${{ runner.os }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-portable.tar.gz path: PrismLauncher-portable.tar.gz
- name: Upload binary tarball (Linux, Qt 6) - name: Upload binary tarball (Linux, Qt 6)
@ -616,14 +594,14 @@ jobs:
flatpak: flatpak:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: bilelmoussaoui/flatpak-github-actions:kde-5.15-22.08 image: bilelmoussaoui/flatpak-github-actions:kde-5.15-23.08
options: --privileged options: --privileged
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
if: inputs.build_type == 'Debug' if: inputs.build_type == 'Debug'
with: with:
submodules: 'true' submodules: "true"
- name: Build Flatpak (Linux) - name: Build Flatpak (Linux)
if: inputs.build_type == 'Debug' if: inputs.build_type == 'Debug'
uses: flatpak/flatpak-github-actions/flatpak-builder@v6 uses: flatpak/flatpak-github-actions/flatpak-builder@v6

View File

@ -8,7 +8,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: 'true' submodules: 'true'

View File

@ -3,26 +3,25 @@ name: Build Application
on: on:
push: push:
branches-ignore: branches-ignore:
- 'renovate/**' - "renovate/**"
paths-ignore: paths-ignore:
- '**.md' - "**.md"
- '**/LICENSE' - "**/LICENSE"
- 'flake.lock' - "flake.lock"
- 'packages/**' - "packages/**"
- '.github/ISSUE_TEMPLATE/**' - ".github/ISSUE_TEMPLATE/**"
- '.markdownlint**' - ".markdownlint**"
pull_request: pull_request:
paths-ignore: paths-ignore:
- '**.md' - "**.md"
- '**/LICENSE' - "**/LICENSE"
- 'flake.lock' - "flake.lock"
- 'packages/**' - "packages/**"
- '.github/ISSUE_TEMPLATE/**' - ".github/ISSUE_TEMPLATE/**"
- '.markdownlint**' - ".markdownlint**"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build_debug: build_debug:
name: Build Debug name: Build Debug
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
@ -34,3 +33,5 @@ jobs:
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}

View File

@ -3,10 +3,9 @@ name: Build Application and Make Release
on: on:
push: push:
tags: tags:
- '*' - "*"
jobs: jobs:
build_release: build_release:
name: Build Release name: Build Release
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
@ -18,6 +17,8 @@ jobs:
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}
create_release: create_release:
needs: build_release needs: build_release
@ -26,10 +27,10 @@ jobs:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: 'true' submodules: "true"
path: 'PrismLauncher-source' path: "PrismLauncher-source"
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
- name: Grab and store version - name: Grab and store version
@ -41,8 +42,8 @@ jobs:
mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }} mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }}
mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
mv PrismLauncher-Linux-Qt6*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux-Qt6*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz
mv PrismLauncher-Linux-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Portable-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
mv PrismLauncher-Linux*/PrismLauncher.tar.gz PrismLauncher-Linux-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux-Qt5*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz
mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage
mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync
mv PrismLauncher-macOS-Legacy*/PrismLauncher.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz mv PrismLauncher-macOS-Legacy*/PrismLauncher.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz
@ -86,8 +87,8 @@ jobs:
draft: true draft: true
prerelease: false prerelease: false
files: | files: |
PrismLauncher-Linux-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage
PrismLauncher-Linux-x86_64.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync
PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz
@ -95,9 +96,6 @@ jobs:
PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe
PrismLauncher-Windows-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-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe

View File

@ -16,10 +16,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: cachix/install-nix-action@v22 - uses: cachix/install-nix-action@6a9a9e84a173d90b3ffb42c5ddaf9ea033fad011 # v23
- uses: DeterminateSystems/update-flake-lock@v19 - uses: DeterminateSystems/update-flake-lock@v20
with: with:
commit-msg: "chore(nix): update lockfile" commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile"

View File

@ -188,8 +188,11 @@ set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_M
# Build platform. # Build platform.
set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.")
# Channel list URL # Github repo URL with releases for updater
set(Launcher_UPDATER_BASE "" CACHE STRING "Base URL for the updater.") set(Launcher_UPDATER_GITHUB_REPO "https://github.com/PrismLauncher/PrismLauncher" CACHE STRING "Base github URL for the updater.")
# Name to help updater identify valid artifacts
set(Launcher_BUILD_ARTIFACT "" CACHE STRING "Artifact name to help the updater identify valid artifacts.")
# The metadata server # The metadata server
set(Launcher_META_URL "https://meta.prismlauncher.org/v1/" CACHE STRING "URL to fetch Launcher's meta files from.") set(Launcher_META_URL "https://meta.prismlauncher.org/v1/" CACHE STRING "URL to fetch Launcher's meta files from.")
@ -245,6 +248,11 @@ set(Launcher_MSA_CLIENT_ID "c36a9fb6-4f2a-41ff-90bd-ae7cc92031eb" CACHE STRING "
# This key was issued specifically for Prism Launcher # This key was issued specifically for Prism Launcher
set(Launcher_CURSEFORGE_API_KEY "$2a$10$wuAJuNZuted3NORVmpgUC.m8sI.pv1tOPKZyBgLFGjxFp/br0lZCC" CACHE STRING "API key for the CurseForge platform") set(Launcher_CURSEFORGE_API_KEY "$2a$10$wuAJuNZuted3NORVmpgUC.m8sI.pv1tOPKZyBgLFGjxFp/br0lZCC" CACHE STRING "API key for the CurseForge platform")
set(Launcher_COMPILER_NAME ${CMAKE_CXX_COMPILER_ID})
set(Launcher_COMPILER_VERSION ${CMAKE_CXX_COMPILER_VERSION})
set(Launcher_COMPILER_TARGET_SYSTEM ${CMAKE_SYSTEM_NAME})
set(Launcher_COMPILER_TARGET_SYSTEM_VERSION ${CMAKE_SYSTEM_VERSION})
set(Launcher_COMPILER_TARGET_PROCESSOR ${CMAKE_SYSTEM_PROCESSOR})
#### Check the current Git commit and branch #### Check the current Git commit and branch
include(GetGitRevisionDescription) include(GetGitRevisionDescription)
@ -339,6 +347,11 @@ add_subdirectory(program_info)
####################################### Install layout ####################################### ####################################### Install layout #######################################
set(Launcher_ENABLE_UPDATER NO) set(Launcher_ENABLE_UPDATER NO)
set(Launcher_BUILD_UPDATER NO)
if (NOT APPLE AND (NOT Launcher_UPDATER_GITHUB_REPO STREQUAL "" AND NOT Launcher_BUILD_ARTIFACT STREQUAL ""))
set(Launcher_BUILD_UPDATER YES)
endif()
if(NOT (UNIX AND APPLE)) if(NOT (UNIX AND APPLE))
# Install "portable.txt" if selected component is "portable" # Install "portable.txt" if selected component is "portable"

View File

@ -18,11 +18,18 @@
</a> </a>
- All downloads and instructions for Prism Launcher can be found on our [Website](https://prismlauncher.org/download). - 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). - Last build status can be found in the [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions) tab (this also includes the pull requests status).
### Development Builds ### 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. Please understand that these builds are not intended for most users. There may be bugs, and other instabilities. You have been warned.
There are development builds available through:
- [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions) (includes builds from pull requests opened by contribuitors)
- [nightly.link](https://nightly.link/PrismLauncher/PrismLauncher/workflows/trigger_builds/develop) (this will always point only to the latest version of develop)
These have debug information in the binaries, so their file sizes are relatively larger.
Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS**. Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS**.
@ -30,7 +37,7 @@ For **Arch**, **Debian**, **Fedora**, **OpenSUSE (Tumbleweed)** and **Gentoo**,
[![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) [![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)
These packages are also availiable to all the distributions based on the ones mentioned above. These packages are also available to all the distributions based on the ones mentioned above.
## Community & Support ## Community & Support
@ -50,7 +57,7 @@ Feel free to create a GitHub issue if you find a bug or want to suggest a new fe
## Translations ## Translations
The translation effort for Prism Launcher 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> The translation effort for Prism Launcher 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 ## Building

View File

@ -33,6 +33,7 @@
* limitations under the License. * limitations under the License.
*/ */
#include <qstringliteral.h>
#include "BuildConfig.h" #include "BuildConfig.h"
#include <QObject> #include <QObject>
@ -59,8 +60,16 @@ Config::Config()
VERSION_MINOR = @Launcher_VERSION_MINOR@; VERSION_MINOR = @Launcher_VERSION_MINOR@;
BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@";
BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@";
BUILD_DATE = "@Launcher_BUILD_TIMESTAMP@"; BUILD_DATE = "@Launcher_BUILD_TIMESTAMP@";
UPDATER_BASE = "@Launcher_UPDATER_BASE@"; UPDATER_GITHUB_REPO = "@Launcher_UPDATER_GITHUB_REPO@";
COMPILER_NAME = "@Launcher_COMPILER_NAME@";
COMPILER_VERSION = "@Launcher_COMPILER_VERSION@";
COMPILER_TARGET_SYSTEM = "@Launcher_COMPILER_TARGET_SYSTEM@";
COMPILER_TARGET_SYSTEM_VERSION = "@Launcher_COMPILER_TARGET_SYSTEM_VERSION@";
COMPILER_TARGET_SYSTEM_PROCESSOR = "@Launcher_COMPILER_TARGET_PROCESSOR@";
MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@";
MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@";
@ -68,6 +77,8 @@ Config::Config()
if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty())
{ {
UPDATER_ENABLED = true; UPDATER_ENABLED = true;
} else if(!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) {
UPDATER_ENABLED = true;
} }
GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_COMMIT = "@Launcher_GIT_COMMIT@";
@ -89,9 +100,6 @@ Config::Config()
{ {
VERSION_CHANNEL = GIT_REFSPEC; VERSION_CHANNEL = GIT_REFSPEC;
VERSION_CHANNEL.remove("refs/heads/"); VERSION_CHANNEL.remove("refs/heads/");
if(!UPDATER_BASE.isEmpty() && !BUILD_PLATFORM.isEmpty()) {
UPDATER_ENABLED = true;
}
} }
else if (!GIT_COMMIT.isEmpty()) else if (!GIT_COMMIT.isEmpty())
{ {
@ -136,3 +144,16 @@ QString Config::printableVersionString() const
} }
return vstr; return vstr;
} }
QString Config::compilerID() const
{
if (COMPILER_VERSION.isEmpty())
return COMPILER_NAME;
return QStringLiteral("%1 - %2").arg(COMPILER_NAME).arg(COMPILER_VERSION);
}
QString Config::systemID() const
{
return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR);
}

View File

@ -71,11 +71,29 @@ class Config {
/// A short string identifying this build's platform or distribution. /// A short string identifying this build's platform or distribution.
QString BUILD_PLATFORM; QString BUILD_PLATFORM;
/// A short string identifying this build's valid artifacts int he updater. For example, "lin64" or "win32".
QString BUILD_ARTIFACT;
/// A string containing the build timestamp /// A string containing the build timestamp
QString BUILD_DATE; QString BUILD_DATE;
/// A string identifying the compiler use to build
QString COMPILER_NAME;
/// A string identifying the compiler version used to build
QString COMPILER_VERSION;
/// A string identifying the compiler target system os
QString COMPILER_TARGET_SYSTEM;
/// A String identifying the compiler target system version
QString COMPILER_TARGET_SYSTEM_VERSION;
/// A String identifying the compiler target processor
QString COMPILER_TARGET_SYSTEM_PROCESSOR;
/// URL for the updater's channel /// URL for the updater's channel
QString UPDATER_BASE; QString UPDATER_GITHUB_REPO;
/// The public key used to sign releases for the Sparkle updater appcast /// The public key used to sign releases for the Sparkle updater appcast
QString MAC_SPARKLE_PUB_KEY; QString MAC_SPARKLE_PUB_KEY;
@ -175,6 +193,18 @@ class Config {
* \return The version number in string format (major.minor.revision.build). * \return The version number in string format (major.minor.revision.build).
*/ */
QString printableVersionString() const; QString printableVersionString() const;
/**
* \brief Compiler ID String
* \return a string of the form "Name - Version" of just "Name" if the version is empty
*/
QString compilerID() const;
/**
* \brief System ID String
* \return a string of the form "OS Verison Processor"
*/
QString systemID() const;
}; };
extern const Config BuildConfig; extern const Config BuildConfig;

View File

@ -68,6 +68,8 @@ function(
/w14906 # string literal cast to 'LPWSTR' /w14906 # string literal cast to 'LPWSTR'
/w14928 # illegal copy-initialization; more than one user-defined conversion has been implicitly applied /w14928 # illegal copy-initialization; more than one user-defined conversion has been implicitly applied
/permissive- # standards conformance mode for MSVC compiler. /permissive- # standards conformance mode for MSVC compiler.
/we4062 # forbid omitting a possible value of an enum in a switch statement
) )
endif() endif()
@ -75,7 +77,6 @@ function(
set(CLANG_WARNINGS set(CLANG_WARNINGS
-Wall -Wall
-Wextra # reasonable and standard -Wextra # reasonable and standard
-Wextra-semi # Warn about semicolon after in-class function definition.
-Wshadow # warn the user if a variable declaration shadows one from a parent context -Wshadow # warn the user if a variable declaration shadows one from a parent context
-Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps
# catch hard to track down memory errors # catch hard to track down memory errors
@ -90,6 +91,12 @@ function(
-Wdouble-promotion # warn if float is implicit promoted to double -Wdouble-promotion # warn if float is implicit promoted to double
-Wformat=2 # warn on security issues around functions that format output (ie printf) -Wformat=2 # warn on security issues around functions that format output (ie printf)
-Wimplicit-fallthrough # warn on statements that fallthrough without an explicit annotation -Wimplicit-fallthrough # warn on statements that fallthrough without an explicit annotation
# -Wgnu-zero-variadic-macro-arguments (part of -pedantic) is triggered by every qCDebug() call and therefore results
# in a lot of noise. This warning is only notifying us that clang is emulating the GCC behaviour
# instead of the exact standard wording so we can safely ignore it
-Wno-gnu-zero-variadic-macro-arguments
-Werror=switch # forbid omitting a possible value of an enum in a switch statement
) )
endif() endif()
@ -101,6 +108,8 @@ function(
-Wduplicated-branches # warn if if / else branches have duplicated code -Wduplicated-branches # warn if if / else branches have duplicated code
-Wlogical-op # warn about logical operations being used where bitwise were probably wanted -Wlogical-op # warn about logical operations being used where bitwise were probably wanted
-Wuseless-cast # warn if you perform a cast to the same type -Wuseless-cast # warn if you perform a cast to the same type
-Werror=switch # forbid omitting a possible value of an enum in a switch statement
) )
endif() endif()
@ -125,6 +134,8 @@ function(
-Woverloaded-virtual -Woverloaded-virtual
-Wuseless-cast -Wuseless-cast
-Wextra-semi -Wextra-semi
-Werror=switch # forbid omitting a possible value of an enum in a switch statement
) )
target_compile_options( target_compile_options(

52
flake.lock generated
View File

@ -3,11 +3,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1673956053, "lastModified": 1696426674,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -21,11 +21,11 @@
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": "nixpkgs-lib"
}, },
"locked": { "locked": {
"lastModified": 1690933134, "lastModified": 1698882062,
"narHash": "sha256-ab989mN63fQZBFrkk4Q8bYxQCktuHmBIBqUG1jl6/FQ=", "narHash": "sha256-HkhafUayIqxXyHH1X8d9RDl1M2CkFgZLjKD3MzabiEo=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "59cf3f1447cfc75087e7273b04b31e689a8599fb", "rev": "8c9fa2545007b49a5db5f650ae91f227672c3877",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -76,11 +76,11 @@
"libnbtplusplus": { "libnbtplusplus": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1690036783, "lastModified": 1699286814,
"narHash": "sha256-A5kTgICnx+Qdq3Fir/bKTfdTt/T1NQP2SC+nhN1ENug=", "narHash": "sha256-yy0q+bky80LtK1GWzz7qpM+aAGrOqLuewbid8WT1ilk=",
"owner": "PrismLauncher", "owner": "PrismLauncher",
"repo": "libnbtplusplus", "repo": "libnbtplusplus",
"rev": "a5e8fd52b8bf4ab5d5bcc042b2a247867589985f", "rev": "23b955121b8217c1c348a9ed2483167a6f3ff4ad",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -89,13 +89,28 @@
"type": "github" "type": "github"
} }
}, },
"nix-filter": {
"locked": {
"lastModified": 1694857738,
"narHash": "sha256-bxxNyLHjhu0N8T3REINXQ2ZkJco0ABFPn6PIe2QUfqo=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "41fd48e00c22b4ced525af521ead8792402de0ea",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1692463654, "lastModified": 1700108881,
"narHash": "sha256-F8hZmsQINI+S6UROM4jyxAMbQLtzE44pI8Nk6NtMdao=", "narHash": "sha256-+Lqybl8kj0+nD/IlAWPPG/RDTa47gff9nbei0u7BntE=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "ca3c9ac9f4cdd4bea19f592b32bb59b74ab7d783", "rev": "7414e9ee0b3e9903c24d3379f577a417f0aae5f1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -108,11 +123,11 @@
"nixpkgs-lib": { "nixpkgs-lib": {
"locked": { "locked": {
"dir": "lib", "dir": "lib",
"lastModified": 1690881714, "lastModified": 1698611440,
"narHash": "sha256-h/nXluEqdiQHs1oSgkOOWF+j8gcJMWhwnZ9PFabN6q0=", "narHash": "sha256-jPjHjrerhYDy3q9+s5EAsuhyhuknNfowY6yt6pjn9pc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9e1960bc196baf6881340d53dccb203a951745a2", "rev": "0cbe9f69c234a7700596e943bfae7ef27a31b735",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -138,11 +153,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1692274144, "lastModified": 1700064067,
"narHash": "sha256-BxTQuRUANQ81u8DJznQyPmRsg63t4Yc+0kcyq6OLz8s=", "narHash": "sha256-1ZWNDzhu8UlVCK7+DUN9dVQfiHX1bv6OQP9VxstY/gs=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "7e3517c03d46159fdbf8c0e5c97f82d5d4b0c8fa", "rev": "e558068cba67b23b4fbc5537173dbb43748a17e8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -156,6 +171,7 @@
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"libnbtplusplus": "libnbtplusplus", "libnbtplusplus": "libnbtplusplus",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks" "pre-commit-hooks": "pre-commit-hooks"
} }

View File

@ -4,6 +4,7 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts.url = "github:hercules-ci/flake-parts";
nix-filter.url = "github:numtide/nix-filter";
pre-commit-hooks = { pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix"; url = "github:cachix/pre-commit-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
@ -20,8 +21,24 @@
}; };
}; };
outputs = inputs: outputs = {
inputs.flake-parts.lib.mkFlake flake-parts,
{inherit inputs;} pre-commit-hooks,
{imports = [./nix];}; ...
} @ inputs:
flake-parts.lib.mkFlake {inherit inputs;} {
imports = [
pre-commit-hooks.flakeModule
./nix/dev.nix
./nix/distribution.nix
];
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
};
} }

View File

@ -1,6 +1,6 @@
id: org.prismlauncher.PrismLauncher id: org.prismlauncher.PrismLauncher
runtime: org.kde.Platform runtime: org.kde.Platform
runtime-version: "5.15-22.08" runtime-version: "5.15-23.08"
sdk: org.kde.Sdk sdk: org.kde.Sdk
sdk-extensions: sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk17 - org.freedesktop.Sdk.Extension.openjdk17
@ -113,6 +113,9 @@ modules:
version-query: .tag_name version-query: .tag_name
url-query: .tarball_url url-query: .tarball_url
timestamp-query: .published_at timestamp-query: .published_at
# from https://github.com/flathub/net.gaijin.WarThunder/blob/7ea6f7a9f84b9c77150c003a7059dc03f8dcbc7f/gamemode.patch
- type: patch
path: patches/gamemode.patch
cleanup: cleanup:
- /include - /include
- /lib/pkgconfig - /lib/pkgconfig

View File

@ -0,0 +1,12 @@
diff -ruN a/common/common-pidfds.c b/common/common-pidfds.c
--- a/common/common-pidfds.c 2021-02-18 20:00:12.000000000 +0100
+++ b/common/common-pidfds.c 2023-09-07 08:57:42.954362763 +0200
@@ -58,6 +58,8 @@
{
return (int)syscall(__NR_pidfd_open, pid, flags);
}
+#else
+#include <sys/pidfd.h>
#endif
/* pidfd functions */

View File

@ -1,5 +1,6 @@
builds: builds:
exclude: [] exclude:
- "*.x86_64-darwin.*"
include: include:
- "checks.x86_64-linux.*" - "checks.x86_64-linux.*"
- "devShells.*.*" - "devShells.*.*"

View File

@ -58,6 +58,7 @@
#include "ui/pages/global/APIPage.h" #include "ui/pages/global/APIPage.h"
#include "ui/pages/global/AccountListPage.h" #include "ui/pages/global/AccountListPage.h"
#include "ui/pages/global/CustomCommandsPage.h" #include "ui/pages/global/CustomCommandsPage.h"
#include "ui/pages/global/EnvironmentVariablesPage.h"
#include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/ExternalToolsPage.h"
#include "ui/pages/global/JavaPage.h" #include "ui/pages/global/JavaPage.h"
#include "ui/pages/global/LanguagePage.h" #include "ui/pages/global/LanguagePage.h"
@ -122,6 +123,7 @@
#include <FileSystem.h> #include <FileSystem.h>
#include <LocalPeer.h> #include <LocalPeer.h>
#include <stdlib.h>
#include <sys.h> #include <sys.h>
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
@ -130,9 +132,13 @@
#include "gamemode_client.h" #include "gamemode_client.h"
#endif #endif
#if defined(Q_OS_MAC) && defined(SPARKLE_ENABLED) #if defined(Q_OS_MAC)
#if defined(SPARKLE_ENABLED)
#include "updater/MacSparkleUpdater.h" #include "updater/MacSparkleUpdater.h"
#endif #endif
#else
#include "updater/PrismExternalUpdater.h"
#endif
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
#include "WindowsConsole.h" #include "WindowsConsole.h"
@ -164,6 +170,34 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt
} // namespace } // namespace
std::tuple<QDateTime, QString, QString, QString, QString> read_lock_File(const QString& path)
{
auto contents = QString(FS::read(path));
auto lines = contents.split('\n');
QDateTime timestamp;
QString from, to, target, data_path;
for (auto line : lines) {
auto index = line.indexOf("=");
if (index < 0)
continue;
auto left = line.left(index);
auto right = line.mid(index + 1);
if (left.toLower() == "timestamp") {
timestamp = QDateTime::fromString(right, Qt::ISODate);
} else if (left.toLower() == "from") {
from = right;
} else if (left.toLower() == "to") {
to = right;
} else if (left.toLower() == "target") {
target = right;
} else if (left.toLower() == "data_path") {
data_path = right;
}
}
return std::make_tuple(timestamp, from, to, target, data_path);
}
Application::Application(int& argc, char** argv) : QApplication(argc, argv) Application::Application(int& argc, char** argv) : QApplication(argc, argv)
{ {
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
@ -296,6 +330,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
.arg(dataPath)); .arg(dataPath));
return; return;
} }
m_dataPath = dataPath;
/* /*
* Establish the mechanism for communication with an already running PrismLauncher that uses the same data path. * Establish the mechanism for communication with an already running PrismLauncher that uses the same data path.
@ -450,11 +485,16 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
} }
{ {
qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT; qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME) << ", (c) 2022-2023 "
<< qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
qDebug() << "Version : " << BuildConfig.printableVersionString(); qDebug() << "Version : " << BuildConfig.printableVersionString();
qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM; qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM;
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
qDebug() << "Compiled for : " << BuildConfig.systemID();
qDebug() << "Compiled by : " << BuildConfig.compilerID();
qDebug() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT;
qDebug() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No");
if (adjustedBy.size()) { if (adjustedBy.size()) {
qDebug() << "Work dir before adjustment : " << origcwdPath; qDebug() << "Work dir before adjustment : " << origcwdPath;
qDebug() << "Work dir after adjustment : " << QDir::currentPath(); qDebug() << "Work dir after adjustment : " << QDir::currentPath();
@ -503,6 +543,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("MenuBarInsteadOfToolBar", false); m_settings->registerSetting("MenuBarInsteadOfToolBar", false);
m_settings->registerSetting("NumberOfConcurrentTasks", 10);
m_settings->registerSetting("NumberOfConcurrentDownloads", 6);
QString defaultMonospace; QString defaultMonospace;
int defaultSize = 11; int defaultSize = 11;
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
@ -579,6 +622,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("IgnoreJavaCompatibility", false); m_settings->registerSetting("IgnoreJavaCompatibility", false);
m_settings->registerSetting("IgnoreJavaWizard", false); m_settings->registerSetting("IgnoreJavaWizard", false);
// Legacy settings
m_settings->registerSetting("OnlineFixes", false);
// Native library workarounds // Native library workarounds
m_settings->registerSetting("UseNativeOpenAL", false); m_settings->registerSetting("UseNativeOpenAL", false);
m_settings->registerSetting("CustomOpenALPath", ""); m_settings->registerSetting("CustomOpenALPath", "");
@ -594,9 +640,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ShowGameTime", true); m_settings->registerSetting("ShowGameTime", true);
m_settings->registerSetting("ShowGlobalGameTime", true); m_settings->registerSetting("ShowGlobalGameTime", true);
m_settings->registerSetting("RecordGameTime", true); m_settings->registerSetting("RecordGameTime", true);
m_settings->registerSetting("ShowGameTimeWithoutDays", false);
// Minecraft mods // Minecraft mods
m_settings->registerSetting("ModMetadataDisabled", false); m_settings->registerSetting("ModMetadataDisabled", false);
m_settings->registerSetting("ModDependenciesDisabled", false);
// Minecraft offline player name // Minecraft offline player name
m_settings->registerSetting("LastOfflinePlayerName", ""); m_settings->registerSetting("LastOfflinePlayerName", "");
@ -673,6 +721,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false); m_settings->registerSetting("QuitAfterGameStop", false);
m_settings->registerSetting("Env", QVariant(QMap<QString, QVariant>()));
// Custom Microsoft Authentication Client ID // Custom Microsoft Authentication Client ID
m_settings->registerSetting("MSAClientIDOverride", ""); m_settings->registerSetting("MSAClientIDOverride", "");
@ -698,6 +748,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_globalSettingsProvider->addPage<JavaPage>(); m_globalSettingsProvider->addPage<JavaPage>();
m_globalSettingsProvider->addPage<LanguagePage>(); m_globalSettingsProvider->addPage<LanguagePage>();
m_globalSettingsProvider->addPage<CustomCommandsPage>(); m_globalSettingsProvider->addPage<CustomCommandsPage>();
m_globalSettingsProvider->addPage<EnvironmentVariablesPage>();
m_globalSettingsProvider->addPage<ProxyPage>(); m_globalSettingsProvider->addPage<ProxyPage>();
m_globalSettingsProvider->addPage<ExternalToolsPage>(); m_globalSettingsProvider->addPage<ExternalToolsPage>();
m_globalSettingsProvider->addPage<AccountListPage>(); m_globalSettingsProvider->addPage<AccountListPage>();
@ -734,15 +785,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
qDebug() << "<> Translations loaded."; qDebug() << "<> Translations loaded.";
} }
// initialize the updater
if (BuildConfig.UPDATER_ENABLED) {
qDebug() << "Initializing updater";
#if defined(Q_OS_MAC) && defined(SPARKLE_ENABLED)
m_updater.reset(new MacSparkleUpdater());
#endif
qDebug() << "<> Updater started.";
}
// Instance icons // Instance icons
{ {
auto setting = APPLICATION->settings()->getSetting("IconsDir"); auto setting = APPLICATION->settings()->getSetting("IconsDir");
@ -845,6 +887,107 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
detectLibraries(); detectLibraries();
// check update locks
{
auto update_log_path = FS::PathCombine(m_dataPath, "logs", "prism_launcher_update.log");
auto update_lock = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.lock"));
if (update_lock.exists()) {
auto [timestamp, from, to, target, data_path] = read_lock_File(update_lock.absoluteFilePath());
auto infoMsg = tr("This installation has a update lock file present at: %1\n"
"\n"
"Timestamp: %2\n"
"Updating from version %3 to %4\n"
"Target install path: %5\n"
"Data Path: %6"
"\n"
"This likely means that a update attempt failed. Please ensure your installation is in working order before "
"proceeding.\n"
"Check the Prism Launcher updater log at: \n"
"%7\n"
"for details on the last update attempt.\n"
"\n"
"To delete this lock and proceed select \"Ignore\" below.")
.arg(update_lock.absoluteFilePath())
.arg(timestamp.toString(Qt::ISODate), from, to, target, data_path)
.arg(update_log_path);
auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update In Progress"), infoMsg, QMessageBox::Ignore | QMessageBox::Abort);
msgBox.setDefaultButton(QMessageBox::Abort);
msgBox.setModal(true);
msgBox.setDetailedText(FS::read(update_log_path));
msgBox.setMinimumWidth(460);
msgBox.adjustSize();
auto res = msgBox.exec();
switch (res) {
case QMessageBox::Ignore: {
FS::deletePath(update_lock.absoluteFilePath());
break;
}
case QMessageBox::Abort:
[[fallthrough]];
default: {
qDebug() << "Exiting because update lockfile is present";
QMetaObject::invokeMethod(
this, []() { exit(1); }, Qt::QueuedConnection);
return;
}
}
}
auto update_fail_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.fail"));
if (update_fail_marker.exists()) {
auto infoMsg = tr("An update attempt failed\n"
"\n"
"Please ensure your installation is in working order before "
"proceeding.\n"
"Check the Prism Launcher updater log at: \n"
"%1\n"
"for details on the last update attempt.")
.arg(update_log_path);
auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update Failed"), infoMsg, QMessageBox::Ignore | QMessageBox::Abort);
msgBox.setDefaultButton(QMessageBox::Abort);
msgBox.setModal(true);
msgBox.setDetailedText(FS::read(update_log_path));
msgBox.setMinimumWidth(460);
msgBox.adjustSize();
auto res = msgBox.exec();
switch (res) {
case QMessageBox::Ignore: {
FS::deletePath(update_fail_marker.absoluteFilePath());
break;
}
case QMessageBox::Abort:
[[fallthrough]];
default: {
qDebug() << "Exiting because update lockfile is present";
QMetaObject::invokeMethod(
this, []() { exit(1); }, Qt::QueuedConnection);
return;
}
}
}
auto update_success_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.success"));
if (update_success_marker.exists()) {
auto infoMsg = tr("Update succeeded\n"
"\n"
"You are now running %1 .\n"
"Check the Prism Launcher updater log at: \n"
"%1\n"
"for details.")
.arg(BuildConfig.printableVersionString())
.arg(update_log_path);
auto msgBox = new QMessageBox(QMessageBox::Information, tr("Update Succeeded"), infoMsg, QMessageBox::Ok);
msgBox->setDefaultButton(QMessageBox::Ok);
msgBox->setDetailedText(FS::read(update_log_path));
msgBox->setAttribute(Qt::WA_DeleteOnClose);
msgBox->setMinimumWidth(460);
msgBox->adjustSize();
msgBox->open();
FS::deletePath(update_success_marker.absoluteFilePath());
}
}
if (createSetupWizard()) { if (createSetupWizard()) {
return; return;
} }
@ -913,6 +1056,26 @@ bool Application::createSetupWizard()
return false; return false;
} }
bool Application::updaterEnabled()
{
#if defined(Q_OS_MAC)
return BuildConfig.UPDATER_ENABLED;
#else
return BuildConfig.UPDATER_ENABLED && QFileInfo(FS::PathCombine(m_rootPath, updaterBinaryName())).isFile();
#endif
}
QString Application::updaterBinaryName()
{
auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME);
#if defined Q_OS_WIN32
exe_name.append(".exe");
#else
exe_name.prepend("bin/");
#endif
return exe_name;
}
bool Application::event(QEvent* event) bool Application::event(QEvent* event)
{ {
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
@ -981,6 +1144,20 @@ void Application::performMainStartupAction()
showMainWindow(false); showMainWindow(false);
qDebug() << "<> Main window shown."; qDebug() << "<> Main window shown.";
} }
// initialize the updater
if (updaterEnabled()) {
qDebug() << "Initializing updater";
#ifdef Q_OS_MAC
#if defined(SPARKLE_ENABLED)
m_updater.reset(new MacSparkleUpdater());
#endif
#else
m_updater.reset(new PrismExternalUpdater(m_mainWindow, m_rootPath, m_dataPath));
#endif
qDebug() << "<> Updater started.";
}
if (!m_urlsToImport.isEmpty()) { if (!m_urlsToImport.isEmpty()) {
qDebug() << "<> Importing from url:" << m_urlsToImport; qDebug() << "<> Importing from url:" << m_urlsToImport;
m_mainWindow->processURLs(m_urlsToImport); m_mainWindow->processURLs(m_urlsToImport);

View File

@ -159,6 +159,9 @@ class Application : public QApplication {
/// this is the root of the 'installation'. Used for automatic updates /// this is the root of the 'installation'. Used for automatic updates
const QString& root() { return m_rootPath; } const QString& root() { return m_rootPath; }
/// the data path the application is using
const QString& dataRoot() { return m_dataPath; }
bool isPortable() { return m_portable; } bool isPortable() { return m_portable; }
const Capabilities capabilities() { return m_capabilities; } const Capabilities capabilities() { return m_capabilities; }
@ -179,6 +182,9 @@ class Application : public QApplication {
int suitableMaxMem(); int suitableMaxMem();
bool updaterEnabled();
QString updaterBinaryName();
QUrl normalizeImportUrl(QString const& url); QUrl normalizeImportUrl(QString const& url);
signals: signals:
@ -244,6 +250,7 @@ class Application : public QApplication {
QMap<QString, std::shared_ptr<BaseProfilerFactory>> m_profilers; QMap<QString, std::shared_ptr<BaseProfilerFactory>> m_profilers;
QString m_rootPath; QString m_rootPath;
QString m_dataPath;
Status m_status = Application::StartingUp; Status m_status = Application::StartingUp;
Capabilities m_capabilities; Capabilities m_capabilities;
bool m_portable = false; bool m_portable = false;

View File

@ -388,7 +388,7 @@ QString BaseInstance::name() const
QString BaseInstance::windowTitle() const QString BaseInstance::windowTitle() const
{ {
return BuildConfig.LAUNCHER_DISPLAYNAME + ": " + name().replace(QRegularExpression("\\s+"), " "); return BuildConfig.LAUNCHER_DISPLAYNAME + ": " + name();
} }
// FIXME: why is this here? move it to MinecraftInstance!!! // FIXME: why is this here? move it to MinecraftInstance!!!

View File

@ -64,7 +64,7 @@ class LaunchTask;
class BaseInstance; class BaseInstance;
// pointer for lazy people // pointer for lazy people
typedef std::shared_ptr<BaseInstance> InstancePtr; using InstancePtr = std::shared_ptr<BaseInstance>;
/*! /*!
* \brief Base class for instances. * \brief Base class for instances.

View File

@ -51,7 +51,7 @@ class BaseVersionList : public QAbstractListModel {
ArchitectureRole, ArchitectureRole,
SortRole SortRole
}; };
typedef QList<int> RoleList; using RoleList = QList<int>;
explicit BaseVersionList(QObject* parent = 0); explicit BaseVersionList(QObject* parent = 0);

View File

@ -139,6 +139,7 @@ set(NET_SOURCES
net/HeaderProxy.h net/HeaderProxy.h
net/RawHeaderProxy.h net/RawHeaderProxy.h
net/ApiHeaderProxy.h net/ApiHeaderProxy.h
net/StaticHeaderProxy.h
net/ApiDownload.h net/ApiDownload.h
net/ApiDownload.cpp net/ApiDownload.cpp
net/ApiUpload.cpp net/ApiUpload.cpp
@ -181,6 +182,11 @@ set(MAC_UPDATE_SOURCES
updater/MacSparkleUpdater.mm updater/MacSparkleUpdater.mm
) )
set(PRISM_UPDATE_SOURCES
updater/PrismExternalUpdater.h
updater/PrismExternalUpdater.cpp
)
# Backend for the news bar... there's usually no news. # Backend for the news bar... there's usually no news.
set(NEWS_SOURCES set(NEWS_SOURCES
# News System # News System
@ -216,13 +222,9 @@ set(MINECRAFT_SOURCES
minecraft/auth/MinecraftAccount.h minecraft/auth/MinecraftAccount.h
minecraft/auth/Parsers.cpp minecraft/auth/Parsers.cpp
minecraft/auth/Parsers.h minecraft/auth/Parsers.h
minecraft/auth/Yggdrasil.cpp
minecraft/auth/Yggdrasil.h
minecraft/auth/flows/AuthFlow.cpp minecraft/auth/flows/AuthFlow.cpp
minecraft/auth/flows/AuthFlow.h minecraft/auth/flows/AuthFlow.h
minecraft/auth/flows/Mojang.cpp
minecraft/auth/flows/Mojang.h
minecraft/auth/flows/MSA.cpp minecraft/auth/flows/MSA.cpp
minecraft/auth/flows/MSA.h minecraft/auth/flows/MSA.h
minecraft/auth/flows/Offline.cpp minecraft/auth/flows/Offline.cpp
@ -236,12 +238,8 @@ set(MINECRAFT_SOURCES
minecraft/auth/steps/GetSkinStep.h minecraft/auth/steps/GetSkinStep.h
minecraft/auth/steps/LauncherLoginStep.cpp minecraft/auth/steps/LauncherLoginStep.cpp
minecraft/auth/steps/LauncherLoginStep.h minecraft/auth/steps/LauncherLoginStep.h
minecraft/auth/steps/MigrationEligibilityStep.cpp
minecraft/auth/steps/MigrationEligibilityStep.h
minecraft/auth/steps/MinecraftProfileStep.cpp minecraft/auth/steps/MinecraftProfileStep.cpp
minecraft/auth/steps/MinecraftProfileStep.h minecraft/auth/steps/MinecraftProfileStep.h
minecraft/auth/steps/MinecraftProfileStepMojang.cpp
minecraft/auth/steps/MinecraftProfileStepMojang.h
minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.cpp
minecraft/auth/steps/MSAStep.h minecraft/auth/steps/MSAStep.h
minecraft/auth/steps/XboxAuthorizationStep.cpp minecraft/auth/steps/XboxAuthorizationStep.cpp
@ -250,8 +248,6 @@ set(MINECRAFT_SOURCES
minecraft/auth/steps/XboxProfileStep.h minecraft/auth/steps/XboxProfileStep.h
minecraft/auth/steps/XboxUserStep.cpp minecraft/auth/steps/XboxUserStep.cpp
minecraft/auth/steps/XboxUserStep.h minecraft/auth/steps/XboxUserStep.h
minecraft/auth/steps/YggdrasilStep.cpp
minecraft/auth/steps/YggdrasilStep.h
minecraft/gameoptions/GameOptions.h minecraft/gameoptions/GameOptions.h
minecraft/gameoptions/GameOptions.cpp minecraft/gameoptions/GameOptions.cpp
@ -589,6 +585,63 @@ set(LINKEXE_SOURCES
DesktopServices.cpp DesktopServices.cpp
) )
set(PRISMUPDATER_SOURCES
updater/prismupdater/PrismUpdater.h
updater/prismupdater/PrismUpdater.cpp
updater/prismupdater/UpdaterDialogs.h
updater/prismupdater/UpdaterDialogs.cpp
updater/prismupdater/GitHubRelease.h
updater/prismupdater/GitHubRelease.cpp
Json.h
Json.cpp
FileSystem.h
FileSystem.cpp
StringUtils.h
StringUtils.cpp
DesktopServices.h
DesktopServices.cpp
Version.h
Version.cpp
Markdown.h
Markdown.cpp
# Zip
MMCZip.h
MMCZip.cpp
# Time
MMCTime.h
MMCTime.cpp
net/ByteArraySink.h
net/ChecksumValidator.h
net/Download.cpp
net/Download.h
net/FileSink.cpp
net/FileSink.h
net/HttpMetaCache.cpp
net/HttpMetaCache.h
net/Logging.h
net/Logging.cpp
net/NetAction.h
net/NetRequest.cpp
net/NetRequest.h
net/NetJob.cpp
net/NetJob.h
net/NetUtils.h
net/Sink.h
net/Validator.h
net/HeaderProxy.h
net/RawHeaderProxy.h
ui/dialogs/ProgressDialog.cpp
ui/dialogs/ProgressDialog.h
ui/widgets/SubTaskProgressBar.h
ui/widgets/SubTaskProgressBar.cpp
)
######## Logging categories ######## ######## Logging categories ########
ecm_qt_declare_logging_category(CORE_SOURCES ecm_qt_declare_logging_category(CORE_SOURCES
@ -685,6 +738,8 @@ set(LOGIC_SOURCES
if(APPLE AND Launcher_ENABLE_UPDATER) if(APPLE AND Launcher_ENABLE_UPDATER)
set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES}) set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES})
else()
set (LOGIC_SOURCES ${LOGIC_SOURCES} ${PRISM_UPDATE_SOURCES})
endif() endif()
SET(LAUNCHER_SOURCES SET(LAUNCHER_SOURCES
@ -834,6 +889,8 @@ SET(LAUNCHER_SOURCES
ui/pages/global/AccountListPage.h ui/pages/global/AccountListPage.h
ui/pages/global/CustomCommandsPage.cpp ui/pages/global/CustomCommandsPage.cpp
ui/pages/global/CustomCommandsPage.h ui/pages/global/CustomCommandsPage.h
ui/pages/global/EnvironmentVariablesPage.cpp
ui/pages/global/EnvironmentVariablesPage.h
ui/pages/global/ExternalToolsPage.cpp ui/pages/global/ExternalToolsPage.cpp
ui/pages/global/ExternalToolsPage.h ui/pages/global/ExternalToolsPage.h
ui/pages/global/JavaPage.cpp ui/pages/global/JavaPage.cpp
@ -916,6 +973,9 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/ImportPage.cpp ui/pages/modplatform/ImportPage.cpp
ui/pages/modplatform/ImportPage.h ui/pages/modplatform/ImportPage.h
ui/pages/modplatform/OptionalModDialog.cpp
ui/pages/modplatform/OptionalModDialog.h
ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
ui/pages/modplatform/modrinth/ModrinthResourceModels.h ui/pages/modplatform/modrinth/ModrinthResourceModels.h
ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
@ -944,8 +1004,6 @@ SET(LAUNCHER_SOURCES
ui/dialogs/IconPickerDialog.h ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourceDialog.cpp ui/dialogs/ImportResourceDialog.cpp
ui/dialogs/ImportResourceDialog.h ui/dialogs/ImportResourceDialog.h
ui/dialogs/LoginDialog.cpp
ui/dialogs/LoginDialog.h
ui/dialogs/MSALoginDialog.cpp ui/dialogs/MSALoginDialog.cpp
ui/dialogs/MSALoginDialog.h ui/dialogs/MSALoginDialog.h
ui/dialogs/OfflineLoginDialog.cpp ui/dialogs/OfflineLoginDialog.cpp
@ -984,6 +1042,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/Common.h ui/widgets/Common.h
ui/widgets/CustomCommands.cpp ui/widgets/CustomCommands.cpp
ui/widgets/CustomCommands.h ui/widgets/CustomCommands.h
ui/widgets/EnvironmentVariables.cpp
ui/widgets/EnvironmentVariables.h
ui/widgets/DropLabel.cpp ui/widgets/DropLabel.cpp
ui/widgets/DropLabel.h ui/widgets/DropLabel.h
ui/widgets/FocusLineEdit.cpp ui/widgets/FocusLineEdit.cpp
@ -1042,6 +1102,15 @@ SET(LAUNCHER_SOURCES
ui/instanceview/VisualGroup.h ui/instanceview/VisualGroup.h
) )
if (NOT Apple)
set(LAUNCHER_SOURCES
${LAUNCHER_SOURCES}
ui/dialogs/UpdateAvailableDialog.h
ui/dialogs/UpdateAvailableDialog.cpp
)
endif()
if(WIN32) if(WIN32)
set(LAUNCHER_SOURCES set(LAUNCHER_SOURCES
WindowsConsole.cpp WindowsConsole.cpp
@ -1080,10 +1149,12 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/legacy_ftb/Page.ui
ui/pages/modplatform/import_ftb/ImportFTBPage.ui ui/pages/modplatform/import_ftb/ImportFTBPage.ui
ui/pages/modplatform/ImportPage.ui ui/pages/modplatform/ImportPage.ui
ui/pages/modplatform/OptionalModDialog.ui
ui/pages/modplatform/modrinth/ModrinthPage.ui ui/pages/modplatform/modrinth/ModrinthPage.ui
ui/pages/modplatform/technic/TechnicPage.ui ui/pages/modplatform/technic/TechnicPage.ui
ui/widgets/InstanceCardWidget.ui ui/widgets/InstanceCardWidget.ui
ui/widgets/CustomCommands.ui ui/widgets/CustomCommands.ui
ui/widgets/EnvironmentVariables.ui
ui/widgets/InfoFrame.ui ui/widgets/InfoFrame.ui
ui/widgets/ModFilterWidget.ui ui/widgets/ModFilterWidget.ui
ui/widgets/SubTaskProgressBar.ui ui/widgets/SubTaskProgressBar.ui
@ -1104,7 +1175,6 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/MSALoginDialog.ui ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui ui/dialogs/AboutDialog.ui
ui/dialogs/LoginDialog.ui
ui/dialogs/EditAccountDialog.ui ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui ui/dialogs/ScrollMessageBox.ui
@ -1112,6 +1182,14 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ChooseProviderDialog.ui ui/dialogs/ChooseProviderDialog.ui
) )
qt_wrap_ui(PRISM_UPDATE_UI
ui/dialogs/UpdateAvailableDialog.ui
)
if (NOT Apple)
set (LAUNCHER_UI ${LAUNCHER_UI} ${PRISM_UPDATE_UI})
endif()
qt_add_resources(LAUNCHER_RESOURCES qt_add_resources(LAUNCHER_RESOURCES
resources/backgrounds/backgrounds.qrc resources/backgrounds/backgrounds.qrc
resources/multimc/multimc.qrc resources/multimc/multimc.qrc
@ -1128,6 +1206,12 @@ qt_add_resources(LAUNCHER_RESOURCES
../${Launcher_Branding_LogoQRC} ../${Launcher_Branding_LogoQRC}
) )
qt_wrap_ui(PRISMUPDATER_UI
updater/prismupdater/SelectReleaseDialog.ui
ui/widgets/SubTaskProgressBar.ui
ui/dialogs/ProgressDialog.ui
)
######## Windows resource files ######## ######## Windows resource files ########
if(WIN32) if(WIN32)
set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC})
@ -1137,12 +1221,16 @@ include(CompilerWarnings)
# Add executable # Add executable
add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES})
if(BUILD_TESTING)
target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_TEST)
endif()
set_project_warnings(Launcher_logic set_project_warnings(Launcher_logic
"${Launcher_MSVC_WARNINGS}" "${Launcher_MSVC_WARNINGS}"
"${Launcher_CLANG_WARNINGS}" "${Launcher_CLANG_WARNINGS}"
"${Launcher_GCC_WARNINGS}") "${Launcher_GCC_WARNINGS}")
target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION)
target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION)
target_link_libraries(Launcher_logic target_link_libraries(Launcher_logic
systeminfo systeminfo
Launcher_murmur2 Launcher_murmur2
@ -1224,7 +1312,45 @@ install(TARGETS ${Launcher_Name}
FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
) )
if(WIN32) if(Launcher_BUILD_UPDATER)
# Updater
add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI})
target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(prism_updater_logic
QuaZip::QuaZip
${ZLIB_LIBRARIES}
systeminfo
BuildConfig
ghcFilesystem::ghc_filesystem
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
${Launcher_QT_LIBS}
cmark::cmark
Katabasis
)
add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp)
target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest)
target_link_libraries("${Launcher_Name}_updater" prism_updater_logic)
if(DEFINED Launcher_APP_BINARY_NAME)
set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_updater")
endif()
if(DEFINED Launcher_BINARY_RPATH)
SET_TARGET_PROPERTIES("${Launcher_Name}_updater" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}")
endif()
install(TARGETS "${Launcher_Name}_updater"
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(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER))
# File link
add_library(filelink_logic STATIC ${LINKEXE_SOURCES}) add_library(filelink_logic STATIC ${LINKEXE_SOURCES})
set_project_warnings(filelink_logic set_project_warnings(filelink_logic
"${Launcher_MSVC_WARNINGS}" "${Launcher_MSVC_WARNINGS}"
@ -1243,7 +1369,7 @@ if(WIN32)
${Launcher_QT_LIBS} ${Launcher_QT_LIBS}
) )
add_executable("${Launcher_Name}_filelink" WIN32 filelink/main.cpp) add_executable("${Launcher_Name}_filelink" WIN32 filelink/filelink_main.cpp)
target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest) target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest)

View File

@ -18,7 +18,7 @@
class DataMigrationTask : public Task { class DataMigrationTask : public Task {
Q_OBJECT Q_OBJECT
public: public:
explicit DataMigrationTask(QObject* parent, const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathmatcher); explicit DataMigrationTask(QObject* parent, const QString& sourcePath, const QString& targetPath, IPathMatcher::Ptr pathmatcher);
~DataMigrationTask() override = default; ~DataMigrationTask() override = default;
protected: protected:

View File

@ -108,12 +108,9 @@ bool openDirectory(const QString& path, [[maybe_unused]] bool ensureExists)
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) { if (!isSandbox()) {
return IndirectOpen(f); return IndirectOpen(f);
} else {
return f();
} }
#else
return f();
#endif #endif
return f();
} }
bool openFile(const QString& path) bool openFile(const QString& path)

View File

@ -267,10 +267,7 @@ bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceP
bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const
{ {
auto fileName = fileInfo.fileName(); return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath()));
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 bool FileIgnoreProxy::filterFile(const QString& fileName) const

View File

@ -123,26 +123,35 @@ namespace fs = ghc::filesystem;
#if defined(__MINGW32__) #if defined(__MINGW32__)
typedef struct _DUPLICATE_EXTENTS_DATA { struct _DUPLICATE_EXTENTS_DATA {
HANDLE FileHandle; HANDLE FileHandle;
LARGE_INTEGER SourceFileOffset; LARGE_INTEGER SourceFileOffset;
LARGE_INTEGER TargetFileOffset; LARGE_INTEGER TargetFileOffset;
LARGE_INTEGER ByteCount; LARGE_INTEGER ByteCount;
} DUPLICATE_EXTENTS_DATA, *PDUPLICATE_EXTENTS_DATA; };
typedef struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER { using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA;
using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*;
struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER {
WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32
WORD Reserved; // Must be 0 WORD Reserved; // Must be 0
DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx
DWORD ChecksumChunkSizeInBytes; DWORD ChecksumChunkSizeInBytes;
DWORD ClusterSizeInBytes; DWORD ClusterSizeInBytes;
} FSCTL_GET_INTEGRITY_INFORMATION_BUFFER, *PFSCTL_GET_INTEGRITY_INFORMATION_BUFFER; };
typedef struct _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER { using FSCTL_GET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER;
using PFSCTL_GET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER*;
struct _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER {
WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32
WORD Reserved; // Must be 0 WORD Reserved; // Must be 0
DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx
} FSCTL_SET_INTEGRITY_INFORMATION_BUFFER, *PFSCTL_SET_INTEGRITY_INFORMATION_BUFFER; };
using FSCTL_SET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER;
using PFSCTL_SET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER*;
#endif #endif
@ -194,6 +203,40 @@ void write(const QString& filename, const QByteArray& data)
} }
} }
void appendSafe(const QString& filename, const QByteArray& data)
{
ensureExists(QFileInfo(filename).dir());
QByteArray buffer;
try {
buffer = read(filename);
} catch (FileSystemException&) {
buffer = QByteArray();
}
buffer.append(data);
QSaveFile file(filename);
if (!file.open(QSaveFile::WriteOnly)) {
throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString());
}
if (buffer.size() != file.write(buffer)) {
throw FileSystemException("Error writing data to " + filename + ": " + file.errorString());
}
if (!file.commit()) {
throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString());
}
}
void append(const QString& filename, const QByteArray& data)
{
ensureExists(QFileInfo(filename).dir());
QFile file(filename);
if (!file.open(QFile::Append)) {
throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString());
}
if (data.size() != file.write(data)) {
throw FileSystemException("Error writing data to " + filename + ": " + file.errorString());
}
}
QByteArray read(const QString& filename) QByteArray read(const QString& filename)
{ {
QFile file(filename); QFile file(filename);
@ -238,6 +281,28 @@ bool ensureFolderPathExists(QString foldernamepath)
return success; return success;
} }
bool copyFileAttributes(QString src, QString dst)
{
#ifdef Q_OS_WIN32
auto attrs = GetFileAttributesW(src.toStdWString().c_str());
if (attrs == INVALID_FILE_ATTRIBUTES)
return false;
return SetFileAttributesW(dst.toStdWString().c_str(), attrs);
#endif
return true;
}
// needs folders to exists
void copyFolderAttributes(QString src, QString dst, QString relative)
{
auto path = PathCombine(src, relative);
QDir dsrc(src);
while ((path = QFileInfo(path).path()).length() >= src.length()) {
auto dst_path = PathCombine(dst, dsrc.relativeFilePath(path));
copyFileAttributes(path, dst_path);
}
}
/** /**
* @brief Copies a directory and it's contents from src to dest * @brief Copies a directory and it's contents from src to dest
* @param offset subdirectory form src to copy to dest * @param offset subdirectory form src to copy to dest
@ -265,6 +330,9 @@ bool copy::operator()(const QString& offset, bool dryRun)
if (!m_followSymlinks) if (!m_followSymlinks)
opt |= copy_opts::copy_symlinks; opt |= copy_opts::copy_symlinks;
if (m_overwrite)
opt |= copy_opts::overwrite_existing;
// Function that'll do the actual copying // Function that'll do the actual copying
auto copy_file = [&](QString src_path, QString relative_dst_path) { auto copy_file = [&](QString src_path, QString relative_dst_path) {
if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist))
@ -273,6 +341,9 @@ bool copy::operator()(const QString& offset, bool dryRun)
auto dst_path = PathCombine(dst, relative_dst_path); auto dst_path = PathCombine(dst, relative_dst_path);
if (!dryRun) { if (!dryRun) {
ensureFilePathExists(dst_path); ensureFilePathExists(dst_path);
#ifdef Q_OS_WIN32
copyFolderAttributes(src, dst, relative_dst_path);
#endif
fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err); fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err);
} }
if (err) { if (err) {
@ -872,6 +943,8 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
<< "\n"; << "\n";
stream << "Type=Application" stream << "Type=Application"
<< "\n"; << "\n";
stream << "Categories=Game;ActionGame;AdventureGame;Simulation"
<< "\n";
stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n"; stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n";
stream << "Name=" << name.toLocal8Bit() << "\n"; stream << "Name=" << name.toLocal8Bit() << "\n";
if (!icon.isEmpty()) { if (!icon.isEmpty()) {

View File

@ -61,6 +61,16 @@ class FileSystemException : public ::Exception {
*/ */
void write(const QString& filename, const QByteArray& data); void write(const QString& filename, const QByteArray& data);
/**
* append data to a file safely
*/
void appendSafe(const QString& filename, const QByteArray& data);
/**
* append data to a file
*/
void append(const QString& filename, const QByteArray& data);
/** /**
* read data from a file safely\ * read data from a file safely\
*/ */
@ -109,6 +119,11 @@ class copy : public QObject {
m_whitelist = whitelist; m_whitelist = whitelist;
return *this; return *this;
} }
copy& overwrite(const bool overwrite)
{
m_overwrite = overwrite;
return *this;
}
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
@ -128,6 +143,7 @@ class copy : public QObject {
bool m_followSymlinks = true; bool m_followSymlinks = true;
const IPathMatcher* m_matcher = nullptr; const IPathMatcher* m_matcher = nullptr;
bool m_whitelist = false; bool m_whitelist = false;
bool m_overwrite = false;
QDir m_src; QDir m_src;
QDir m_dst; QDir m_dst;
qsizetype m_copied; qsizetype m_copied;

View File

@ -59,7 +59,7 @@
#include <quazip/quazipdir.h> #include <quazip/quazipdir.h>
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent, QMap<QString, QString>&& extra_info) InstanceImportTask::InstanceImportTask(const QUrl& sourceUrl, QWidget* parent, QMap<QString, QString>&& extra_info)
: m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent) : m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent)
{} {}

View File

@ -54,7 +54,7 @@ class FileResolvingTask;
class InstanceImportTask : public InstanceTask { class InstanceImportTask : public InstanceTask {
Q_OBJECT Q_OBJECT
public: public:
explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {}); explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {});
bool abort() override; bool abort() override;
const QVector<Flame::File>& getBlockedFiles() const { return m_blockedMods; } const QVector<Flame::File>& getBlockedFiles() const { return m_blockedMods; }

View File

@ -2,6 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * 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 * 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 * it under the terms of the GNU General Public License as published by
@ -237,8 +238,11 @@ GroupId InstanceList::getInstanceGroup(const InstanceId& id) const
return GroupId(); return GroupId();
} }
void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) void InstanceList::setInstanceGroup(const InstanceId& id, GroupId name)
{ {
if (name.isEmpty() && !name.isNull())
name = QString();
auto inst = getInstanceById(id); auto inst = getInstanceById(id);
if (!inst) { if (!inst) {
qDebug() << "Attempt to set a null instance's group"; qDebug() << "Attempt to set a null instance's group";
@ -249,6 +253,7 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name)
auto iter = m_instanceGroupIndex.find(inst->id()); auto iter = m_instanceGroupIndex.find(inst->id());
if (iter != m_instanceGroupIndex.end()) { if (iter != m_instanceGroupIndex.end()) {
if (*iter != name) { if (*iter != name) {
decreaseGroupCount(*iter);
*iter = name; *iter = name;
changed = true; changed = true;
} }
@ -258,7 +263,7 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name)
} }
if (changed) { if (changed) {
m_groupNameCache.insert(name); increaseGroupCount(name);
auto idx = getInstIndex(inst.get()); auto idx = getInstIndex(inst.get());
emit dataChanged(index(idx), index(idx), { GroupRole }); emit dataChanged(index(idx), index(idx), { GroupRole });
saveGroupList(); saveGroupList();
@ -267,29 +272,55 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name)
QStringList InstanceList::getGroups() QStringList InstanceList::getGroups()
{ {
return m_groupNameCache.values(); return m_groupNameCache.keys();
} }
void InstanceList::deleteGroup(const QString& name) void InstanceList::deleteGroup(const GroupId& name)
{ {
m_groupNameCache.remove(name);
m_collapsedGroups.remove(name);
bool removed = false; bool removed = false;
qDebug() << "Delete group" << name; qDebug() << "Delete group" << name;
for (auto& instance : m_instances) { for (auto& instance : m_instances) {
const auto& instID = instance->id(); const QString& instID = instance->id();
auto instGroupName = getInstanceGroup(instID); const QString instGroupName = getInstanceGroup(instID);
if (instGroupName == name) { if (instGroupName == name) {
m_instanceGroupIndex.remove(instID); m_instanceGroupIndex.remove(instID);
qDebug() << "Remove" << instID << "from group" << name; qDebug() << "Remove" << instID << "from group" << name;
removed = true; removed = true;
auto idx = getInstIndex(instance.get()); auto idx = getInstIndex(instance.get());
if (idx > 0) { if (idx >= 0)
emit dataChanged(index(idx), index(idx), { GroupRole }); emit dataChanged(index(idx), index(idx), { GroupRole });
} }
} }
} if (removed)
if (removed) {
saveGroupList(); saveGroupList();
} }
void InstanceList::renameGroup(const QString& src, const QString& dst)
{
m_groupNameCache.remove(src);
if (m_collapsedGroups.remove(src))
m_collapsedGroups.insert(dst);
bool modified = false;
qDebug() << "Rename group" << src << "to" << dst;
for (auto& instance : m_instances) {
const QString& instID = instance->id();
const QString instGroupName = getInstanceGroup(instID);
if (instGroupName == src) {
m_instanceGroupIndex[instID] = dst;
increaseGroupCount(dst);
qDebug() << "Set" << instID << "group to" << dst;
modified = true;
auto idx = getInstIndex(instance.get());
if (idx >= 0)
emit dataChanged(index(idx), index(idx), { GroupRole });
}
}
if (modified)
saveGroupList();
} }
bool InstanceList::isGroupCollapsed(const QString& group) bool InstanceList::isGroupCollapsed(const QString& group)
@ -305,12 +336,13 @@ bool InstanceList::trashInstance(const InstanceId& id)
return false; return false;
} }
auto cachedGroupId = m_instanceGroupIndex[id]; QString cachedGroupId = m_instanceGroupIndex[id];
qDebug() << "Will trash instance" << id; qDebug() << "Will trash instance" << id;
QString trashedLoc; QString trashedLoc;
if (m_instanceGroupIndex.remove(id)) { if (m_instanceGroupIndex.remove(id)) {
decreaseGroupCount(cachedGroupId);
saveGroupList(); saveGroupList();
} }
@ -348,7 +380,7 @@ void InstanceList::undoTrashInstance()
QFile(top.trashPath).rename(top.polyPath); QFile(top.trashPath).rename(top.polyPath);
m_instanceGroupIndex[top.id] = top.groupName; m_instanceGroupIndex[top.id] = top.groupName;
m_groupNameCache.insert(top.groupName); increaseGroupCount(top.groupName);
saveGroupList(); saveGroupList();
emit instancesChanged(); emit instancesChanged();
@ -362,7 +394,10 @@ void InstanceList::deleteInstance(const InstanceId& id)
return; return;
} }
QString cachedGroupId = m_instanceGroupIndex[id];
if (m_instanceGroupIndex.remove(id)) { if (m_instanceGroupIndex.remove(id)) {
decreaseGroupCount(cachedGroupId);
saveGroupList(); saveGroupList();
} }
@ -610,6 +645,25 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id)
return inst; return inst;
} }
void InstanceList::increaseGroupCount(const QString& group)
{
if (group.isEmpty())
return;
++m_groupNameCache[group];
}
void InstanceList::decreaseGroupCount(const QString& group)
{
if (group.isEmpty())
return;
if (--m_groupNameCache[group] < 1) {
m_groupNameCache.remove(group);
m_collapsedGroups.remove(group);
}
}
void InstanceList::saveGroupList() void InstanceList::saveGroupList()
{ {
qDebug() << "Will save group list now."; qDebug() << "Will save group list now.";
@ -621,7 +675,7 @@ void InstanceList::saveGroupList()
QString groupFileName = m_instDir + "/instgroups.json"; QString groupFileName = m_instDir + "/instgroups.json";
QMap<QString, QSet<QString>> reverseGroupMap; QMap<QString, QSet<QString>> reverseGroupMap;
for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) { for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) {
QString id = iter.key(); const QString& id = iter.key();
QString group = iter.value(); QString group = iter.value();
if (group.isEmpty()) if (group.isEmpty())
continue; continue;
@ -711,17 +765,22 @@ void InstanceList::loadGroupList()
return; return;
} }
QSet<QString> groupSet;
m_instanceGroupIndex.clear(); m_instanceGroupIndex.clear();
m_groupNameCache.clear();
// Iterate through all the groups. // Iterate through all the groups.
QJsonObject groupMapping = rootObj.value("groups").toObject(); QJsonObject groupMapping = rootObj.value("groups").toObject();
for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) { for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) {
QString groupName = iter.key(); QString groupName = iter.key();
if (iter.key().isEmpty()) {
qWarning() << "Redundant empty group found";
continue;
}
// If not an object, complain and skip to the next one. // If not an object, complain and skip to the next one.
if (!iter.value().isObject()) { if (!iter.value().isObject()) {
qWarning() << QString("Group '%1' in the group list should be an object.").arg(groupName).toUtf8(); qWarning() << QString("Group '%1' in the group list should be an object").arg(groupName).toUtf8();
continue; continue;
} }
@ -733,23 +792,19 @@ void InstanceList::loadGroupList()
continue; continue;
} }
// keep a list/set of groups for choosing
groupSet.insert(groupName);
auto hidden = groupObj.value("hidden").toBool(false); auto hidden = groupObj.value("hidden").toBool(false);
if (hidden) { if (hidden)
m_collapsedGroups.insert(groupName); m_collapsedGroups.insert(groupName);
}
// Iterate through the list of instances in the group. // Iterate through the list of instances in the group.
QJsonArray instancesArray = groupObj.value("instances").toArray(); QJsonArray instancesArray = groupObj.value("instances").toArray();
for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++) { for (auto value : instancesArray) {
m_instanceGroupIndex[(*iter2).toString()] = groupName; m_instanceGroupIndex[value.toString()] = groupName;
increaseGroupCount(groupName);
} }
} }
m_groupsLoaded = true; m_groupsLoaded = true;
m_groupNameCache.unite(groupSet);
qDebug() << "Group list loaded."; qDebug() << "Group list loaded.";
} }
@ -768,6 +823,9 @@ void InstanceList::on_InstFolderChanged([[maybe_unused]] const Setting& setting,
} }
m_instDir = newInstDir; m_instDir = newInstDir;
m_groupsLoaded = false; m_groupsLoaded = false;
beginRemoveRows(QModelIndex(), 0, count());
m_instances.erase(m_instances.begin(), m_instances.end());
endRemoveRows();
emit instancesChanged(); emit instancesChanged();
} }
} }
@ -892,9 +950,12 @@ QString InstanceList::getStagedInstancePath()
bool InstanceList::commitStagedInstance(const QString& path, bool InstanceList::commitStagedInstance(const QString& path,
InstanceName const& instanceName, InstanceName const& instanceName,
const QString& groupName, QString groupName,
InstanceTask const& commiting) InstanceTask const& commiting)
{ {
if (groupName.isEmpty() && !groupName.isNull())
groupName = QString();
QDir dir; QDir dir;
QString instID; QString instID;
InstancePtr inst; InstancePtr inst;
@ -925,7 +986,7 @@ bool InstanceList::commitStagedInstance(const QString& path,
} }
m_instanceGroupIndex[instID] = groupName; m_instanceGroupIndex[instID] = groupName;
m_groupNameCache.insert(groupName); increaseGroupCount(groupName);
} }
instanceSet.insert(instID); instanceSet.insert(instID);

View File

@ -1,4 +1,24 @@
/* Copyright 2013-2021 MultiMC Contributors // 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/>.
*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -86,9 +106,10 @@ class InstanceList : public QAbstractListModel {
bool isGroupCollapsed(const QString& groupName); bool isGroupCollapsed(const QString& groupName);
GroupId getInstanceGroup(const InstanceId& id) const; GroupId getInstanceGroup(const InstanceId& id) const;
void setInstanceGroup(const InstanceId& id, const GroupId& name); void setInstanceGroup(const InstanceId& id, GroupId name);
void deleteGroup(const GroupId& name); void deleteGroup(const GroupId& name);
void renameGroup(const GroupId& src, const GroupId& dst);
bool trashInstance(const InstanceId& id); bool trashInstance(const InstanceId& id);
bool trashedSomething(); bool trashedSomething();
void undoTrashInstance(); void undoTrashInstance();
@ -109,7 +130,7 @@ class InstanceList : public QAbstractListModel {
* should_override is used when another similar instance already exists, and we want to override it * should_override is used when another similar instance already exists, and we want to override it
* - for instance, when updating it. * - for instance, when updating it.
*/ */
bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, const InstanceTask&); bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, QString groupName, const InstanceTask&);
/** /**
* Destroy a previously created staging area given by @keyPath - used when creation fails. * Destroy a previously created staging area given by @keyPath - used when creation fails.
@ -158,12 +179,16 @@ class InstanceList : public QAbstractListModel {
QList<InstanceId> discoverInstances(); QList<InstanceId> discoverInstances();
InstancePtr loadInstance(const InstanceId& id); InstancePtr loadInstance(const InstanceId& id);
void increaseGroupCount(const QString& group);
void decreaseGroupCount(const QString& group);
private: private:
int m_watchLevel = 0; int m_watchLevel = 0;
int totalPlayTime = 0; int totalPlayTime = 0;
bool m_dirty = false; bool m_dirty = false;
QList<InstancePtr> m_instances; QList<InstancePtr> m_instances;
QSet<QString> m_groupNameCache; // id -> refs
QMap<QString, int> m_groupNameCache;
SettingsObjectPtr m_globalSettings; SettingsObjectPtr m_globalSettings;
QString m_instDir; QString m_instDir;

View File

@ -36,6 +36,7 @@
#include "LaunchController.h" #include "LaunchController.h"
#include "Application.h" #include "Application.h"
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AccountList.h" #include "minecraft/auth/AccountList.h"
#include "ui/InstanceWindow.h" #include "ui/InstanceWindow.h"
@ -88,8 +89,8 @@ void LaunchController::decideAccount()
if (accounts->count() <= 0) { if (accounts->count() <= 0) {
// Tell the user they need to log in at least one account in order to play. // Tell the user they need to log in at least one account in order to play.
auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"), auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"),
tr("In order to play Minecraft, you must have at least one Microsoft or Mojang " tr("In order to play Minecraft, you must have at least one Microsoft "
"account logged in. Mojang accounts can only be used offline. " "account which owns Minecraft logged in. "
"Would you like to open the account manager to add an account now?"), "Would you like to open the account manager to add an account now?"),
QMessageBox::Information, QMessageBox::Yes | QMessageBox::No) QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)
->exec(); ->exec();
@ -106,7 +107,7 @@ void LaunchController::decideAccount()
// Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used // 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 instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString();
auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId); auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId);
if (instanceAccountIndex == -1) { if (instanceAccountIndex == -1 || instanceAccountId.isEmpty()) {
m_accountToUse = accounts->defaultAccount(); m_accountToUse = accounts->defaultAccount();
} else { } else {
m_accountToUse = accounts->at(instanceAccountIndex); m_accountToUse = accounts->at(instanceAccountIndex);
@ -161,7 +162,7 @@ void LaunchController::login()
m_accountToUse->fillSession(m_session); m_accountToUse->fillSession(m_session);
// Launch immediately in true offline mode // Launch immediately in true offline mode
if (m_accountToUse->isOffline()) { if (m_accountToUse->accountType() == AccountType::Offline) {
launchInstance(); launchInstance();
return; return;
} }

View File

@ -16,19 +16,20 @@
*/ */
#include <MMCTime.h> #include <MMCTime.h>
#include <qobject.h>
#include <QDateTime> #include <QDateTime>
#include <QObject> #include <QObject>
#include <QTextStream> #include <QTextStream>
QString Time::prettifyDuration(int64_t duration) QString Time::prettifyDuration(int64_t duration, bool noDays)
{ {
int seconds = (int)(duration % 60); int seconds = (int)(duration % 60);
duration /= 60; duration /= 60;
int minutes = (int)(duration % 60); int minutes = (int)(duration % 60);
duration /= 60; duration /= 60;
int hours = (int)(duration % 24); int hours = (int)(noDays ? duration : (duration % 24));
int days = (int)(duration / 24); int days = (int)(noDays ? 0 : (duration / 24));
if ((hours == 0) && (days == 0)) { if ((hours == 0) && (days == 0)) {
return QObject::tr("%1min %2s").arg(minutes).arg(seconds); return QObject::tr("%1min %2s").arg(minutes).arg(seconds);
} }

View File

@ -20,7 +20,7 @@
namespace Time { namespace Time {
QString prettifyDuration(int64_t duration); QString prettifyDuration(int64_t duration, bool noDays = false);
/** /**
* @brief Returns a string with short form time duration ie. `2days 1h3m4s56.0ms`. * @brief Returns a string with short form time duration ie. `2days 1h3m4s56.0ms`.

View File

@ -42,11 +42,15 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QUrl>
#if defined(LAUNCHER_APPLICATION)
#include <QtConcurrentRun> #include <QtConcurrentRun>
#endif
namespace MMCZip { namespace MMCZip {
// ours // ours
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction filter) bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction& filter)
{ {
QuaZip modZip(from.filePath()); QuaZip modZip(from.filePath());
modZip.open(QuaZip::mdUnzip); modZip.open(QuaZip::mdUnzip);
@ -132,6 +136,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
return result; return result;
} }
#if defined(LAUNCHER_APPLICATION)
// ours // ours
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods) bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods)
{ {
@ -217,6 +222,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
} }
return true; return true;
} }
#endif
// ours // ours
QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root) QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root)
@ -422,6 +428,7 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q
return true; return true;
} }
#if defined(LAUNCHER_APPLICATION)
void ExportToZipTask::executeTask() void ExportToZipTask::executeTask()
{ {
setStatus("Adding files..."); setStatus("Adding files...");
@ -500,5 +507,6 @@ bool ExportToZipTask::abort()
} }
return false; return false;
} }
#endif
} // namespace MMCZip } // namespace MMCZip

View File

@ -48,7 +48,10 @@
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <optional> #include <optional>
#if defined(LAUNCHER_APPLICATION)
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
#endif
#include "tasks/Task.h" #include "tasks/Task.h"
namespace MMCZip { namespace MMCZip {
@ -57,7 +60,7 @@ using FilterFunction = std::function<bool(const QString&)>;
/** /**
* Merge two zip files, using a filter function * Merge two zip files, using a filter function
*/ */
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction filter = nullptr); bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction& filter = nullptr);
/** /**
* Compress directory, by providing a list of files to compress * Compress directory, by providing a list of files to compress
@ -79,11 +82,12 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow
*/ */
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false); bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false);
#if defined(LAUNCHER_APPLICATION)
/** /**
* take a source jar, add mods to it, resulting in target jar * take a source jar, add mods to it, resulting in target jar
*/ */
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods); bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods);
#endif
/** /**
* Find a single file in archive by file name (not path) * Find a single file in archive by file name (not path)
* *
@ -147,6 +151,7 @@ bool extractFile(QString fileCompressed, QString file, QString dir);
*/ */
bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter); bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter);
#if defined(LAUNCHER_APPLICATION)
class ExportToZipTask : public Task { class ExportToZipTask : public Task {
public: public:
ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
@ -167,7 +172,7 @@ class ExportToZipTask : public Task {
void setExcludeFiles(QStringList excludeFiles) { m_exclude_files = excludeFiles; } void setExcludeFiles(QStringList excludeFiles) { m_exclude_files = excludeFiles; }
void addExtraFile(QString fileName, QByteArray data) { m_extra_files.insert(fileName, data); } void addExtraFile(QString fileName, QByteArray data) { m_extra_files.insert(fileName, data); }
typedef std::optional<QString> ZipResult; using ZipResult = std::optional<QString>;
protected: protected:
virtual void executeTask() override; virtual void executeTask() override;
@ -189,4 +194,5 @@ class ExportToZipTask : public Task {
QFuture<ZipResult> m_build_zip_future; QFuture<ZipResult> m_build_zip_future;
QFutureWatcher<ZipResult> m_build_zip_watcher; QFutureWatcher<ZipResult> m_build_zip_watcher;
}; };
#endif
} // namespace MMCZip } // namespace MMCZip

View File

@ -5,6 +5,7 @@
#include <QPixmapCache> #include <QPixmapCache>
#include <QThread> #include <QThread>
#include <QTime> #include <QTime>
#include <limits>
#define GET_TYPE() \ #define GET_TYPE() \
Qt::ConnectionType type; \ Qt::ConnectionType type; \
@ -100,10 +101,14 @@ class PixmapCache final : public QObject {
*/ */
bool _markCacheMissByEviciton() bool _markCacheMissByEviciton()
{ {
static constexpr uint maxInt = static_cast<uint>(std::numeric_limits<int>::max());
static constexpr uint step = 10240;
static constexpr int oneSecond = 1000;
auto now = QTime::currentTime(); auto now = QTime::currentTime();
if (!m_last_cache_miss_by_eviciton.isNull()) { if (!m_last_cache_miss_by_eviciton.isNull()) {
auto diff = m_last_cache_miss_by_eviciton.msecsTo(now); auto diff = m_last_cache_miss_by_eviciton.msecsTo(now);
if (diff < 1000) { // less than a second ago if (diff < oneSecond) { // less than a second ago
++m_consecutive_fast_evicitons; ++m_consecutive_fast_evicitons;
} else { } else {
m_consecutive_fast_evicitons = 0; m_consecutive_fast_evicitons = 0;
@ -111,11 +116,17 @@ class PixmapCache final : public QObject {
} }
m_last_cache_miss_by_eviciton = now; m_last_cache_miss_by_eviciton = now;
if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) { if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) {
// double the cache size // increase the cache size
auto newSize = _cacheLimit() * 2; uint newSize = _cacheLimit() + step;
qDebug() << m_consecutive_fast_evicitons << "pixmap cache misses by eviction happened too fast, doubling cache size to" if (newSize >= maxInt) { // increase it until you overflow :D
<< newSize; newSize = maxInt;
_setCacheLimit(newSize); qDebug() << m_consecutive_fast_evicitons
<< tr("pixmap cache misses by eviction happened too fast, doing nothing as the cache size reached it's limit");
} else {
qDebug() << m_consecutive_fast_evicitons
<< tr("pixmap cache misses by eviction happened too fast, increasing cache size to") << static_cast<int>(newSize);
}
_setCacheLimit(static_cast<int>(newSize));
m_consecutive_fast_evicitons = 0; m_consecutive_fast_evicitons = 0;
return true; return true;
} }

View File

@ -13,7 +13,7 @@ class RecursiveFileSystemWatcher : public QObject {
QDir rootDir() const { return m_root; } QDir rootDir() const { return m_root; }
// WARNING: setting this to true may be bad for performance // WARNING: setting this to true may be bad for performance
void setWatchFiles(const bool watchFiles); void setWatchFiles(bool watchFiles);
bool watchFiles() const { return m_watchFiles; } bool watchFiles() const { return m_watchFiles; }
void setMatcher(IPathMatcher::Ptr matcher) { m_matcher = matcher; } void setMatcher(IPathMatcher::Ptr matcher) { m_matcher = matcher; }

View File

@ -32,7 +32,7 @@ class ResourceDownloadTask : public SequentialTask {
public: public:
explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion version, ModPlatform::IndexedVersion version,
const std::shared_ptr<ResourceFolderModel> packs, std::shared_ptr<ResourceFolderModel> packs,
bool is_indexed = true, bool is_indexed = true,
QString custom_target_folder = {}); QString custom_target_folder = {});
const QString& getFilename() const { return m_pack_version.fileName; } const QString& getFilename() const { return m_pack_version.fileName; }

View File

@ -35,6 +35,7 @@
*/ */
#include "StringUtils.h" #include "StringUtils.h"
#include <qpair.h>
#include <QRegularExpression> #include <QRegularExpression>
#include <QUuid> #include <QUuid>
@ -149,7 +150,7 @@ QString StringUtils::truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_
} }
if ((url_compact.length() >= max_len) && hard_limit) { if ((url_compact.length() >= max_len) && hard_limit) {
// still too long, truncate normaly // still too long, truncate normally
url_compact = QString(str_url); url_compact = QString(str_url);
auto to_remove = url_compact.length() - max_len + 3; auto to_remove = url_compact.length() - max_len + 3;
url_compact.remove(url_compact.length() - to_remove - 1, to_remove); url_compact.remove(url_compact.length() - to_remove - 1, to_remove);
@ -182,3 +183,32 @@ QString StringUtils::getRandomAlphaNumeric()
{ {
return QUuid::createUuid().toString(QUuid::Id128); return QUuid::createUuid().toString(QUuid::Id128);
} }
QPair<QString, QString> StringUtils::splitFirst(const QString& s, const QString& sep, Qt::CaseSensitivity cs)
{
QString left, right;
auto index = s.indexOf(sep, 0, cs);
left = s.mid(0, index);
right = s.mid(index + sep.length());
return qMakePair(left, right);
}
QPair<QString, QString> StringUtils::splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs)
{
QString left, right;
auto index = s.indexOf(sep, 0, cs);
left = s.mid(0, index);
right = s.mid(left.length() + 1);
return qMakePair(left, right);
}
QPair<QString, QString> StringUtils::splitFirst(const QString& s, const QRegularExpression& re)
{
QString left, right;
QRegularExpressionMatch match;
auto index = s.indexOf(re, 0, &match);
left = s.mid(0, index);
auto end = match.hasMatch() ? left.length() + match.capturedLength() : left.length() + 1;
right = s.mid(end);
return qMakePair(left, right);
}

View File

@ -36,8 +36,10 @@
#pragma once #pragma once
#include <QPair>
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
#include <utility>
namespace StringUtils { namespace StringUtils {
@ -70,12 +72,17 @@ int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs)
/** /**
* @brief Truncate a url while keeping its readability py placing the `...` in the middle of the path * @brief Truncate a url while keeping its readability py placing the `...` in the middle of the path
* @param url Url to truncate * @param url Url to truncate
* @param max_len max lenght of url in charaters * @param max_len max length of url in characters
* @param hard_limit if truncating the path can't get the url short enough, truncate it normaly. * @param hard_limit if truncating the path can't get the url short enough, truncate it normally.
*/ */
QString truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit = false); QString truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit = false);
QString humanReadableFileSize(double bytes, bool use_si = false, int decimal_points = 1); QString humanReadableFileSize(double bytes, bool use_si = false, int decimal_points = 1);
QString getRandomAlphaNumeric(); QString getRandomAlphaNumeric();
QPair<QString, QString> splitFirst(const QString& s, const QString& sep, Qt::CaseSensitivity cs = Qt::CaseSensitive);
QPair<QString, QString> splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive);
QPair<QString, QString> splitFirst(const QString& s, const QRegularExpression& re);
} // namespace StringUtils } // namespace StringUtils

View File

@ -56,6 +56,7 @@ class Version {
bool operator!=(const Version& other) const; bool operator!=(const Version& other) const;
QString toString() const { return m_string; } QString toString() const { return m_string; }
bool isEmpty() const { return m_string.isEmpty(); }
friend QDebug operator<<(QDebug debug, const Version& v); friend QDebug operator<<(QDebug debug, const Version& v);
@ -103,14 +104,8 @@ class Version {
QString m_fullString; QString m_fullString;
[[nodiscard]] inline bool isAppendix() const [[nodiscard]] inline bool isAppendix() const { return m_stringPart.startsWith('+'); }
{ [[nodiscard]] inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; }
return m_stringPart.startsWith('+');
}
[[nodiscard]] inline bool isPreRelease() const
{
return m_stringPart.startsWith('-') && m_stringPart.length() > 1;
}
inline bool operator==(const Section& other) const inline bool operator==(const Section& other) const
{ {
@ -156,14 +151,8 @@ class Version {
return m_fullString < other.m_fullString; return m_fullString < other.m_fullString;
} }
inline bool operator!=(const Section& other) const inline bool operator!=(const Section& other) const { return !(*this == other); }
{ inline bool operator>(const Section& other) const { return !(*this < other || *this == other); }
return !(*this == other);
}
inline bool operator>(const Section& other) const
{
return !(*this < other || *this == other);
}
}; };
private: private:

View File

@ -10,7 +10,7 @@ class VersionProxyModel : public QAbstractProxyModel {
Q_OBJECT Q_OBJECT
public: public:
enum Column { Name, ParentVersion, Branch, Type, Architecture, Path, Time }; enum Column { Name, ParentVersion, Branch, Type, Architecture, Path, Time };
typedef QHash<BaseVersionList::ModelRoles, std::shared_ptr<Filter>> FilterMap; using FilterMap = QHash<BaseVersionList::ModelRoles, std::shared_ptr<Filter>>;
public: public:
VersionProxyModel(QObject* parent = 0); VersionProxyModel(QObject* parent = 0);
@ -28,7 +28,7 @@ class VersionProxyModel : public QAbstractProxyModel {
const FilterMap& filters() const; const FilterMap& filters() const;
const QString& search() const; const QString& search() const;
void setFilter(const BaseVersionList::ModelRoles column, Filter* filter); void setFilter(BaseVersionList::ModelRoles column, Filter* filter);
void setSearch(const QString& search); void setSearch(const QString& search);
void clearFilters(); void clearFilters();
QModelIndex getRecommended() const; QModelIndex getRecommended() const;

View File

@ -93,6 +93,7 @@ FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv),
joinServer(serverToJoin); joinServer(serverToJoin);
} else { } else {
qDebug() << "no server to join"; qDebug() << "no server to join";
m_status = Failed;
exit(); exit();
} }
} }
@ -108,6 +109,7 @@ void FileLinkApp::joinServer(QString server)
connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs); connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs);
connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) { connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) {
m_status = Failed;
switch (socketError) { switch (socketError) {
case QLocalSocket::ServerNotFoundError: case QLocalSocket::ServerNotFoundError:
qDebug() qDebug()
@ -132,6 +134,7 @@ void FileLinkApp::joinServer(QString server)
connect(&socket, &QLocalSocket::disconnected, this, [&]() { connect(&socket, &QLocalSocket::disconnected, this, [&]() {
qDebug() << "disconnected from server, should exit"; qDebug() << "disconnected from server, should exit";
m_status = Succeeded;
exit(); exit();
}); });

View File

@ -41,8 +41,10 @@ class FileLinkApp : public QCoreApplication {
// friends for the purpose of limiting access to deprecated stuff // friends for the purpose of limiting access to deprecated stuff
Q_OBJECT Q_OBJECT
public: public:
enum Status { Starting, Failed, Succeeded, Initialized };
FileLinkApp(int& argc, char** argv); FileLinkApp(int& argc, char** argv);
virtual ~FileLinkApp(); virtual ~FileLinkApp();
Status status() const { return m_status; }
private: private:
void joinServer(QString server); void joinServer(QString server);
@ -50,6 +52,8 @@ class FileLinkApp : public QCoreApplication {
void runLink(); void runLink();
void sendResults(); void sendResults();
Status m_status = Status::Starting;
bool m_useHardLinks = false; bool m_useHardLinks = false;
QDateTime m_startTime; QDateTime m_startTime;

View File

@ -26,5 +26,16 @@ int main(int argc, char* argv[])
{ {
FileLinkApp ldh(argc, argv); FileLinkApp ldh(argc, argv);
switch (ldh.status()) {
case FileLinkApp::Starting:
case FileLinkApp::Initialized: {
return ldh.exec(); return ldh.exec();
} }
case FileLinkApp::Failed:
return 1;
case FileLinkApp::Succeeded:
return 0;
default:
return -1;
}
}

View File

@ -67,7 +67,7 @@ class IconList : public QAbstractListModel {
virtual Qt::ItemFlags flags(const QModelIndex& index) const override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
bool addThemeIcon(const QString& key); bool addThemeIcon(const QString& key);
bool addIcon(const QString& key, const QString& name, const QString& path, const IconType type); bool addIcon(const QString& key, const QString& name, const QString& path, IconType type);
void saveIcon(const QString& key, const QString& path, const char* format) const; void saveIcon(const QString& key, const QString& path, const char* format) const;
bool deleteIcon(const QString& key); bool deleteIcon(const QString& key);
bool trashIcon(const QString& key); bool trashIcon(const QString& key);

View File

@ -22,8 +22,8 @@ struct JavaCheckResult {
enum class Validity { Errored, ReturnedInvalidData, Valid } validity = Validity::Errored; enum class Validity { Errored, ReturnedInvalidData, Valid } validity = Validity::Errored;
}; };
typedef shared_qobject_ptr<QProcess> QProcessPtr; using QProcessPtr = shared_qobject_ptr<QProcess>;
typedef shared_qobject_ptr<JavaChecker> JavaCheckerPtr; using JavaCheckerPtr = shared_qobject_ptr<JavaChecker>;
class JavaChecker : public QObject { class JavaChecker : public QObject {
Q_OBJECT Q_OBJECT
public: public:

View File

@ -20,7 +20,7 @@
#include "tasks/Task.h" #include "tasks/Task.h"
class JavaCheckerJob; class JavaCheckerJob;
typedef shared_qobject_ptr<JavaCheckerJob> JavaCheckerJobPtr; using JavaCheckerJobPtr = shared_qobject_ptr<JavaCheckerJob>;
// FIXME: this just seems horribly redundant // FIXME: this just seems horribly redundant
class JavaCheckerJob : public Task { class JavaCheckerJob : public Task {

View File

@ -24,11 +24,11 @@
struct JavaInstall : public BaseVersion { struct JavaInstall : public BaseVersion {
JavaInstall() {} JavaInstall() {}
JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {} JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {}
virtual QString descriptor() { return id.toString(); } virtual QString descriptor() override { return id.toString(); }
virtual QString name() { return id.toString(); } virtual QString name() override { return id.toString(); }
virtual QString typeString() const { return arch; } virtual QString typeString() const override { return arch; }
virtual bool operator<(BaseVersion& a) override; virtual bool operator<(BaseVersion& a) override;
virtual bool operator>(BaseVersion& a) override; virtual bool operator>(BaseVersion& a) override;
@ -42,4 +42,4 @@ struct JavaInstall : public BaseVersion {
bool recommended = false; bool recommended = false;
}; };
typedef std::shared_ptr<JavaInstall> JavaInstallPtr; using JavaInstallPtr = std::shared_ptr<JavaInstall>;

View File

@ -34,6 +34,7 @@
*/ */
#include <QDir> #include <QDir>
#include <QFileInfo>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
@ -335,6 +336,7 @@ QList<QString> JavaUtils::FindJavaPaths()
} }
} }
candidates.append(getMinecraftJavaBundle());
candidates = addJavasFromEnv(candidates); candidates = addJavasFromEnv(candidates);
candidates.removeDuplicates(); candidates.removeDuplicates();
return candidates; return candidates;
@ -360,6 +362,7 @@ QList<QString> JavaUtils::FindJavaPaths()
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
} }
javas.append(getMinecraftJavaBundle());
javas = addJavasFromEnv(javas); javas = addJavasFromEnv(javas);
javas.removeDuplicates(); javas.removeDuplicates();
return javas; return javas;
@ -403,6 +406,15 @@ QList<QString> JavaUtils::FindJavaPaths()
scanJavaDirs("/opt/jdks"); scanJavaDirs("/opt/jdks");
// flatpak // flatpak
scanJavaDirs("/app/jdk"); scanJavaDirs("/app/jdk");
auto home = qEnvironmentVariable("HOME");
// javas downloaded by IntelliJ
scanJavaDirs(FS::PathCombine(home, ".jdks"));
// javas downloaded by sdkman
scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java"));
javas.append(getMinecraftJavaBundle());
javas = addJavasFromEnv(javas); javas = addJavasFromEnv(javas);
javas.removeDuplicates(); javas.removeDuplicates();
return javas; return javas;
@ -415,6 +427,7 @@ QList<QString> JavaUtils::FindJavaPaths()
QList<QString> javas; QList<QString> javas;
javas.append(this->GetDefaultJava()->path); javas.append(this->GetDefaultJava()->path);
javas.append(getMinecraftJavaBundle());
return addJavasFromEnv(javas); return addJavasFromEnv(javas);
} }
#endif #endif
@ -423,3 +436,50 @@ QString JavaUtils::getJavaCheckPath()
{ {
return APPLICATION->getJarPath("JavaCheck.jar"); return APPLICATION->getJarPath("JavaCheck.jar");
} }
QStringList getMinecraftJavaBundle()
{
QString partialPath;
QString executable = "java";
QStringList processpaths;
#if defined(Q_OS_OSX)
partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support");
#elif defined(Q_OS_WIN32)
partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
executable += "w.exe";
// add the microsoft store version of the launcher to the search. the current path is:
// C:\Users\USERNAME\AppData\Local\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime
auto minecraftMSStorePath =
FS::PathCombine(QFileInfo(partialPath).absolutePath(), "Local", "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe");
minecraftMSStorePath = FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime");
processpaths << minecraftMSStorePath;
#else
partialPath = QDir::homePath();
#endif
auto minecraftDataPath = FS::PathCombine(partialPath, ".minecraft", "runtime");
processpaths << minecraftDataPath;
QStringList javas;
while (!processpaths.isEmpty()) {
auto dirPath = processpaths.takeFirst();
QDir dir(dirPath);
if (!dir.exists())
continue;
auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
auto binFound = false;
for (auto& entry : entries) {
if (entry.baseName() == "bin") {
javas.append(FS::PathCombine(entry.canonicalFilePath(), executable));
binFound = true;
break;
}
}
if (!binFound) {
for (auto& entry : entries) {
processpaths << entry.canonicalFilePath();
}
}
}
return javas;
}

View File

@ -26,6 +26,7 @@
QString stripVariableEntries(QString name, QString target, QString remove); QString stripVariableEntries(QString name, QString target, QString remove);
QProcessEnvironment CleanEnviroment(); QProcessEnvironment CleanEnviroment();
QStringList getMinecraftJavaBundle();
class JavaUtils : public QObject { class JavaUtils : public QObject {
Q_OBJECT Q_OBJECT

View File

@ -45,10 +45,12 @@ QString JavaVersion::toString() const
bool JavaVersion::requiresPermGen() bool JavaVersion::requiresPermGen()
{ {
if (m_parseable) { return !m_parseable || m_major < 8;
return m_major < 8;
} }
return true;
bool JavaVersion::isModular()
{
return m_parseable && m_major >= 9;
} }
bool JavaVersion::operator<(const JavaVersion& rhs) bool JavaVersion::operator<(const JavaVersion& rhs)

View File

@ -25,6 +25,8 @@ class JavaVersion {
bool requiresPermGen(); bool requiresPermGen();
bool isModular();
QString toString() const; QString toString() const;
int major() { return m_major; } int major() { return m_major; }

View File

@ -55,6 +55,6 @@ class Index : public QAbstractListModel, public BaseEntity {
QVector<VersionList::Ptr> m_lists; QVector<VersionList::Ptr> m_lists;
QHash<QString, VersionList::Ptr> m_uids; QHash<QString, VersionList::Ptr> m_uids;
void connectVersionList(const int row, const VersionList::Ptr& list); void connectVersionList(int row, const VersionList::Ptr& list);
}; };
} // namespace Meta } // namespace Meta

View File

@ -64,7 +64,7 @@ class Version : public QObject, public BaseVersion, public BaseEntity {
public: // for usage by format parsers only public: // for usage by format parsers only
void setType(const QString& type); void setType(const QString& type);
void setTime(const qint64 time); void setTime(qint64 time);
void setRequires(const Meta::RequireSet& reqs, const Meta::RequireSet& conflicts); void setRequires(const Meta::RequireSet& reqs, const Meta::RequireSet& conflicts);
void setVolatile(bool volatile_); void setVolatile(bool volatile_);
void setRecommended(bool recommended); void setRecommended(bool recommended);

View File

@ -79,7 +79,7 @@ class VersionList : public BaseVersionList, public BaseEntity {
Version::Ptr m_recommended; Version::Ptr m_recommended;
void setupAddedVersion(const int row, const Version::Ptr& version); void setupAddedVersion(int row, const Version::Ptr& version);
}; };
} // namespace Meta } // namespace Meta
Q_DECLARE_METATYPE(Meta::VersionList::Ptr) Q_DECLARE_METATYPE(Meta::VersionList::Ptr)

View File

@ -6,7 +6,7 @@
class Agent; class Agent;
typedef std::shared_ptr<Agent> AgentPtr; using AgentPtr = std::shared_ptr<Agent>;
class Agent { class Agent {
public: public:

View File

@ -48,6 +48,7 @@
#include "FileSystem.h" #include "FileSystem.h"
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
#include "net/ChecksumValidator.h" #include "net/ChecksumValidator.h"
#include "net/Download.h"
#include "Application.h" #include "Application.h"

View File

@ -105,4 +105,4 @@ class Component : public QObject, public ProblemProvider {
bool m_loaded = false; bool m_loaded = false;
}; };
typedef shared_qobject_ptr<Component> ComponentPtr; using ComponentPtr = shared_qobject_ptr<Component>;

View File

@ -2,14 +2,14 @@
#include "Component.h" #include "Component.h"
#include "ComponentUpdateTask_p.h" #include "ComponentUpdateTask_p.h"
#include "OneSixVersionFormat.h"
#include "PackProfile.h" #include "PackProfile.h"
#include "PackProfile_p.h" #include "PackProfile_p.h"
#include "Version.h" #include "Version.h"
#include "cassert" #include "cassert"
#include "meta/Index.h" #include "meta/Index.h"
#include "meta/Version.h" #include "meta/Version.h"
#include "meta/VersionList.h" #include "minecraft/OneSixVersionFormat.h"
#include "minecraft/ProfileUtils.h"
#include "net/Mode.h" #include "net/Mode.h"
#include "Application.h" #include "Application.h"

View File

@ -52,7 +52,7 @@
class Library; class Library;
class MinecraftInstance; class MinecraftInstance;
typedef std::shared_ptr<Library> LibraryPtr; using LibraryPtr = std::shared_ptr<Library>;
class Library { class Library {
friend class OneSixVersionFormat; friend class OneSixVersionFormat;

View File

@ -184,6 +184,13 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride); m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride);
m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride); m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride);
// Legacy-related options
auto legacySettings = m_settings->registerSetting("OverrideLegacySettings", false);
m_settings->registerOverride(global_settings->getSetting("OnlineFixes"), legacySettings);
auto envSetting = m_settings->registerSetting("OverrideEnv", false);
m_settings->registerOverride(global_settings->getSetting("Env"), envSetting);
m_settings->set("InstanceType", "OneSix"); m_settings->set("InstanceType", "OneSix");
} }
@ -195,6 +202,12 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerSetting("UseAccountForInstance", false); m_settings->registerSetting("UseAccountForInstance", false);
m_settings->registerSetting("InstanceAccountId", ""); m_settings->registerSetting("InstanceAccountId", "");
m_settings->registerSetting("ExportName", "");
m_settings->registerSetting("ExportVersion", "1.0.0");
m_settings->registerSetting("ExportSummary", "");
m_settings->registerSetting("ExportAuthor", "");
m_settings->registerSetting("ExportOptionalFiles", true);
qDebug() << "Instance-type specific settings were loaded!"; qDebug() << "Instance-type specific settings were loaded!";
setSpecificSettingsLoaded(true); setSpecificSettingsLoaded(true);
@ -305,7 +318,7 @@ QString MinecraftInstance::getLocalLibraryPath() const
bool MinecraftInstance::supportsDemo() const bool MinecraftInstance::supportsDemo() const
{ {
Version instance_ver{ getPackProfile()->getComponentVersion("net.minecraft") }; Version instance_ver{ getPackProfile()->getComponentVersion("net.minecraft") };
// Demo mode was introduced in 1.3.1: https://minecraft.fandom.com/wiki/Demo_mode#History // Demo mode was introduced in 1.3.1: https://minecraft.wiki/w/Demo_mode#History
// FIXME: Due to Version constraints atm, this can't handle well non-release versions // FIXME: Due to Version constraints atm, this can't handle well non-release versions
return instance_ver >= Version("1.3.1"); return instance_ver >= Version("1.3.1");
} }
@ -507,20 +520,28 @@ QStringList MinecraftInstance::javaArguments()
args << "-Duser.language=en"; args << "-Duser.language=en";
if (javaVersion.isModular() && shouldApplyOnlineFixes())
// allow reflective access to java.net - required by the skin fix
args << "--add-opens"
<< "java.base/java.net=ALL-UNNAMED";
return args; return args;
} }
QString MinecraftInstance::getLauncher() QString MinecraftInstance::getLauncher()
{ {
auto profile = m_components->getProfile();
// use legacy launcher if the traits are set // use legacy launcher if the traits are set
if (profile->getTraits().contains("legacyLaunch") || profile->getTraits().contains("alphaLaunch")) if (traits().contains("legacyLaunch") || traits().contains("alphaLaunch"))
return "legacy"; return "legacy";
return "standard"; return "standard";
} }
bool MinecraftInstance::shouldApplyOnlineFixes()
{
return traits().contains("legacyServices") && settings()->get("OnlineFixes").toBool();
}
QMap<QString, QString> MinecraftInstance::getVariables() QMap<QString, QString> MinecraftInstance::getVariables()
{ {
QMap<QString, QString> out; QMap<QString, QString> out;
@ -530,6 +551,7 @@ QMap<QString, QString> MinecraftInstance::getVariables()
out.insert("INST_MC_DIR", QDir::toNativeSeparators(QDir(gameRoot()).absolutePath())); out.insert("INST_MC_DIR", QDir::toNativeSeparators(QDir(gameRoot()).absolutePath()));
out.insert("INST_JAVA", settings()->get("JavaPath").toString()); out.insert("INST_JAVA", settings()->get("JavaPath").toString());
out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); out.insert("INST_JAVA_ARGS", javaArguments().join(' '));
out.insert("NO_COLOR", "1");
return out; return out;
} }
@ -543,6 +565,22 @@ QProcessEnvironment MinecraftInstance::createEnvironment()
for (auto it = variables.begin(); it != variables.end(); ++it) { for (auto it = variables.begin(); it != variables.end(); ++it) {
env.insert(it.key(), it.value()); env.insert(it.key(), it.value());
} }
// custom env
auto insertEnv = [&env](QMap<QString, QVariant> envMap) {
if (envMap.isEmpty())
return;
for (auto iter = envMap.begin(); iter != envMap.end(); iter++)
env.insert(iter.key(), iter.value().toString());
};
bool overrideEnv = settings()->get("OverrideEnv").toBool();
if (!overrideEnv)
insertEnv(APPLICATION->settings()->get("Env").toMap());
else
insertEnv(settings()->get("Env").toMap());
return env; return env;
} }
@ -553,16 +591,27 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
if (settings()->get("EnableMangoHud").toBool() && APPLICATION->capabilities() & Application::SupportsMangoHud) { if (settings()->get("EnableMangoHud").toBool() && APPLICATION->capabilities() & Application::SupportsMangoHud) {
auto preloadList = env.value("LD_PRELOAD").split(QLatin1String(":")); QStringList preloadList;
auto libPaths = env.value("LD_LIBRARY_PATH").split(QLatin1String(":")); if (auto value = env.value("LD_PRELOAD"); !value.isEmpty())
preloadList = value.split(QLatin1String(":"));
QStringList libPaths;
if (auto value = env.value("LD_LIBRARY_PATH"); !value.isEmpty())
libPaths = value.split(QLatin1String(":"));
auto mangoHudLibString = MangoHud::getLibraryString(); auto mangoHudLibString = MangoHud::getLibraryString();
if (!mangoHudLibString.isEmpty()) { if (!mangoHudLibString.isEmpty()) {
QFileInfo mangoHudLib(mangoHudLibString); QFileInfo mangoHudLib(mangoHudLibString);
QString libPath = mangoHudLib.absolutePath();
auto appendLib = [libPath, &preloadList](QString fileName) {
if (QFileInfo(FS::PathCombine(libPath, fileName)).exists())
preloadList << fileName;
};
// dlsym variant is only needed for OpenGL and not included in the vulkan layer // dlsym variant is only needed for OpenGL and not included in the vulkan layer
preloadList << "libMangoHud_dlsym.so" << mangoHudLib.fileName(); appendLib("libMangoHud_dlsym.so");
libPaths << mangoHudLib.absolutePath(); appendLib("libMangoHud_opengl.so");
appendLib(mangoHudLib.fileName());
libPaths << libPath;
} }
env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":"))); env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":")));
@ -579,7 +628,6 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
} }
#endif #endif
return env; return env;
} }
@ -692,7 +740,7 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
{ {
QString windowParams; QString windowParams;
if (settings()->get("LaunchMaximized").toBool()) if (settings()->get("LaunchMaximized").toBool())
windowParams = "max"; windowParams = "maximized";
else else
windowParams = windowParams =
QString("%1x%2").arg(settings()->get("MinecraftWinWidth").toInt()).arg(settings()->get("MinecraftWinHeight").toInt()); QString("%1x%2").arg(settings()->get("MinecraftWinWidth").toInt()).arg(settings()->get("MinecraftWinHeight").toInt());
@ -700,6 +748,19 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
launchScript += "windowParams " + windowParams + "\n"; launchScript += "windowParams " + windowParams + "\n";
} }
// launcher info
{
launchScript += "launcherBrand " + BuildConfig.LAUNCHER_NAME + "\n";
launchScript += "launcherVersion " + BuildConfig.printableVersionString() + "\n";
}
// instance info
{
launchScript += "instanceName " + name() + "\n";
launchScript += "instanceIconKey " + name() + "\n";
launchScript += "instanceIconPath icon.png\n"; // we already save a copy here
}
// legacy auth // legacy auth
if (session) { if (session) {
launchScript += "userName " + session->player_name + "\n"; launchScript += "userName " + session->player_name + "\n";
@ -710,6 +771,9 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
launchScript += "traits " + trait + "\n"; launchScript += "traits " + trait + "\n";
} }
if (shouldApplyOnlineFixes())
launchScript += "onlineFixes true\n";
launchScript += "launcher " + getLauncher() + "\n"; launchScript += "launcher " + getLauncher() + "\n";
// qDebug() << "Generated launch script:" << launchScript; // qDebug() << "Generated launch script:" << launchScript;
@ -850,9 +914,6 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess
if (sessionRef.access_token != "0") { if (sessionRef.access_token != "0") {
addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>")); addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>"));
} }
if (sessionRef.client_token.size()) {
addToFilter(sessionRef.client_token, tr("<CLIENT TOKEN>"));
}
addToFilter(sessionRef.uuid, tr("<PROFILE ID>")); addToFilter(sessionRef.uuid, tr("<PROFILE ID>"));
return filter; return filter;
@ -934,13 +995,16 @@ QString MinecraftInstance::getStatusbarDescription()
if (m_settings->get("ShowGameTime").toBool()) { if (m_settings->get("ShowGameTime").toBool()) {
if (lastTimePlayed() > 0) { if (lastTimePlayed() > 0) {
QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch());
description.append(tr(", last played on %1 for %2") description.append(
tr(", last played on %1 for %2")
.arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat)) .arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat))
.arg(Time::prettifyDuration(lastTimePlayed()))); .arg(Time::prettifyDuration(lastTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool())));
} }
if (totalTimePlayed() > 0) { if (totalTimePlayed() > 0) {
description.append(tr(", total played for %1").arg(Time::prettifyDuration(totalTimePlayed()))); description.append(
tr(", total played for %1")
.arg(Time::prettifyDuration(totalTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool())));
} }
} }
if (hasCrashed()) { if (hasCrashed()) {

View File

@ -129,6 +129,7 @@ class MinecraftInstance : public BaseInstance {
/// get arguments passed to java /// get arguments passed to java
QStringList javaArguments(); QStringList javaArguments();
QString getLauncher(); QString getLauncher();
bool shouldApplyOnlineFixes();
/// get variables for launch command variable substitution/environment /// get variables for launch command variable substitution/environment
QMap<QString, QString> getVariables() override; QMap<QString, QString> getVariables() override;
@ -173,4 +174,4 @@ class MinecraftInstance : public BaseInstance {
mutable std::shared_ptr<GameOptions> m_game_options; mutable std::shared_ptr<GameOptions> m_game_options;
}; };
typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr; using MinecraftInstancePtr = std::shared_ptr<MinecraftInstance>;

View File

@ -5,7 +5,7 @@
struct MojangDownloadInfo { struct MojangDownloadInfo {
// types // types
typedef std::shared_ptr<MojangDownloadInfo> Ptr; using Ptr = std::shared_ptr<MojangDownloadInfo>;
// data // data
/// Local filesystem path. WARNING: not used, only here so we can pass through mojang files unmolested! /// Local filesystem path. WARNING: not used, only here so we can pass through mojang files unmolested!
@ -23,7 +23,7 @@ struct MojangLibraryDownloadInfo {
MojangLibraryDownloadInfo() {} MojangLibraryDownloadInfo() {}
// types // types
typedef std::shared_ptr<MojangLibraryDownloadInfo> Ptr; using Ptr = std::shared_ptr<MojangLibraryDownloadInfo>;
// methods // methods
MojangDownloadInfo* getDownloadInfo(QString classifier) MojangDownloadInfo* getDownloadInfo(QString classifier)
@ -42,7 +42,7 @@ struct MojangLibraryDownloadInfo {
struct MojangAssetIndexInfo : public MojangDownloadInfo { struct MojangAssetIndexInfo : public MojangDownloadInfo {
// types // types
typedef std::shared_ptr<MojangAssetIndexInfo> Ptr; using Ptr = std::shared_ptr<MojangAssetIndexInfo>;
// methods // methods
MojangAssetIndexInfo() {} MojangAssetIndexInfo() {}

View File

@ -9,7 +9,7 @@
class OneSixVersionFormat { class OneSixVersionFormat {
public: public:
// version files / profile patches // version files / profile patches
static VersionFilePtr versionFileFromJson(const QJsonDocument& doc, const QString& filename, const bool requireOrder); static VersionFilePtr versionFileFromJson(const QJsonDocument& doc, const QString& filename, bool requireOrder);
static QJsonDocument versionFileToJson(const VersionFilePtr& patch); static QJsonDocument versionFileToJson(const VersionFilePtr& patch);
// libraries // libraries

View File

@ -58,15 +58,14 @@
#include "ComponentUpdateTask.h" #include "ComponentUpdateTask.h"
#include "PackProfile.h" #include "PackProfile.h"
#include "PackProfile_p.h" #include "PackProfile_p.h"
#include "minecraft/mod/Mod.h"
#include "modplatform/ModIndex.h"
#include "Application.h" static const QMap<QString, ModPlatform::ModLoaderType> modloaderMapping{ { "net.neoforged", ModPlatform::NeoForge },
#include "modplatform/ResourceAPI.h" { "net.minecraftforge", ModPlatform::Forge },
{ "net.fabricmc.fabric-loader", ModPlatform::Fabric },
static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{ { "net.neoforged", ResourceAPI::NeoForge }, { "org.quiltmc.quilt-loader", ModPlatform::Quilt },
{ "net.minecraftforge", ResourceAPI::Forge }, { "com.mumfrey.liteloader", ModPlatform::LiteLoader } };
{ "net.fabricmc.fabric-loader", ResourceAPI::Fabric },
{ "org.quiltmc.quilt-loader", ResourceAPI::Quilt },
{ "com.mumfrey.liteloader", ResourceAPI::LiteLoader } };
PackProfile::PackProfile(MinecraftInstance* instance) : QAbstractListModel() PackProfile::PackProfile(MinecraftInstance* instance) : QAbstractListModel()
{ {
@ -431,7 +430,7 @@ bool PackProfile::remove(const int index)
return true; return true;
} }
bool PackProfile::remove(const QString id) bool PackProfile::remove(const QString& id)
{ {
int i = 0; int i = 0;
for (auto patch : d->components) { for (auto patch : d->components) {
@ -990,12 +989,12 @@ void PackProfile::disableInteraction(bool disable)
} }
} }
std::optional<ResourceAPI::ModLoaderTypes> PackProfile::getModLoaders() std::optional<ModPlatform::ModLoaderTypes> PackProfile::getModLoaders()
{ {
ResourceAPI::ModLoaderTypes result; ModPlatform::ModLoaderTypes result;
bool has_any_loader = false; bool has_any_loader = false;
QMapIterator<QString, ResourceAPI::ModLoaderType> i(modloaderMapping); QMapIterator<QString, ModPlatform::ModLoaderType> i(modloaderMapping);
while (i.hasNext()) { while (i.hasNext()) {
i.next(); i.next();
@ -1009,3 +1008,17 @@ std::optional<ResourceAPI::ModLoaderTypes> PackProfile::getModLoaders()
return {}; return {};
return result; return result;
} }
std::optional<ModPlatform::ModLoaderTypes> PackProfile::getSupportedModLoaders()
{
auto loadersOpt = getModLoaders();
if (!loadersOpt.has_value())
return loadersOpt;
auto loaders = loadersOpt.value();
// TODO: remove this or add version condition once Quilt drops official Fabric support
if (loaders & ModPlatform::Quilt)
loaders |= ModPlatform::Fabric;
if (getComponentVersion("net.minecraft") == "1.20.1" && (loaders & ModPlatform::NeoForge))
loaders |= ModPlatform::Forge;
return loaders;
}

View File

@ -44,14 +44,11 @@
#include <QList> #include <QList>
#include <QString> #include <QString>
#include <memory> #include <memory>
#include <optional>
#include "BaseVersion.h"
#include "Component.h" #include "Component.h"
#include "LaunchProfile.h" #include "LaunchProfile.h"
#include "Library.h" #include "modplatform/ModIndex.h"
#include "MojangDownloadInfo.h"
#include "ProfileUtils.h"
#include "modplatform/ResourceAPI.h"
#include "net/Mode.h" #include "net/Mode.h"
class MinecraftInstance; class MinecraftInstance;
@ -92,13 +89,13 @@ class PackProfile : public QAbstractListModel {
enum MoveDirection { MoveUp, MoveDown }; enum MoveDirection { MoveUp, MoveDown };
/// move component file # up or down the list /// move component file # up or down the list
void move(const int index, const MoveDirection direction); void move(int index, MoveDirection direction);
/// remove component file # - including files/records /// remove component file # - including files/records
bool remove(const int index); bool remove(int index);
/// remove component file by id - including files/records /// remove component file by id - including files/records
bool remove(const QString id); bool remove(const QString& id);
bool customize(int index); bool customize(int index);
@ -146,7 +143,9 @@ class PackProfile : public QAbstractListModel {
// todo(merged): is this the best approach // todo(merged): is this the best approach
void appendComponent(ComponentPtr component); void appendComponent(ComponentPtr component);
std::optional<ResourceAPI::ModLoaderTypes> getModLoaders(); std::optional<ModPlatform::ModLoaderTypes> getModLoaders();
// this returns aditional loaders(Quilt supports fabric and NeoForge supports Forge)
std::optional<ModPlatform::ModLoaderTypes> getSupportedModLoaders();
private: private:
void scheduleSave(); void scheduleSave();

View File

@ -140,7 +140,7 @@ VersionFilePtr parseJsonFile(const QFileInfo& fileInfo, const bool requireOrder)
return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), requireOrder); return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), requireOrder);
} }
bool saveJsonFile(const QJsonDocument doc, const QString& filename) bool saveJsonFile(const QJsonDocument& doc, const QString& filename)
{ {
auto data = doc.toJson(); auto data = doc.toJson();
QSaveFile jsonFile(filename); QSaveFile jsonFile(filename);

View File

@ -38,7 +38,7 @@
#include "VersionFile.h" #include "VersionFile.h"
namespace ProfileUtils { namespace ProfileUtils {
typedef QStringList PatchOrder; using PatchOrder = QStringList;
/// Read and parse a OneSix format order file /// Read and parse a OneSix format order file
bool readOverrideOrders(QString path, PatchOrder& order); bool readOverrideOrders(QString path, PatchOrder& order);
@ -47,10 +47,10 @@ bool readOverrideOrders(QString path, PatchOrder& order);
bool writeOverrideOrders(QString path, const PatchOrder& order); bool writeOverrideOrders(QString path, const PatchOrder& order);
/// Parse a version file in JSON format /// Parse a version file in JSON format
VersionFilePtr parseJsonFile(const QFileInfo& fileInfo, const bool requireOrder); VersionFilePtr parseJsonFile(const QFileInfo& fileInfo, bool requireOrder);
/// Save a JSON file (in any format) /// Save a JSON file (in any format)
bool saveJsonFile(const QJsonDocument doc, const QString& filename); bool saveJsonFile(const QJsonDocument& doc, const QString& filename);
/// Remove LWJGL from a patch file. This is applied to all Mojang-like profile files. /// Remove LWJGL from a patch file. This is applied to all Mojang-like profile files.
void removeLwjglFromPatch(VersionFilePtr patch); void removeLwjglFromPatch(VersionFilePtr patch);

View File

@ -278,67 +278,6 @@ bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out)
} // namespace } // namespace
bool AccountData::resumeStateFromV2(QJsonObject data)
{
// The JSON object must at least have a username for it to be valid.
if (!data.value("username").isString()) {
qCritical() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type.";
return false;
}
QString userName = data.value("username").toString("");
QString clientToken = data.value("clientToken").toString("");
QString accessToken = data.value("accessToken").toString("");
QJsonArray profileArray = data.value("profiles").toArray();
if (profileArray.size() < 1) {
qCritical() << "Can't load Mojang account with username \"" << userName << "\". No profiles found.";
return false;
}
struct AccountProfile {
QString id;
QString name;
bool legacy;
};
QList<AccountProfile> profiles;
int currentProfileIndex = 0;
int index = -1;
QString currentProfile = data.value("activeProfile").toString("");
for (QJsonValue profileVal : profileArray) {
index++;
QJsonObject profileObject = profileVal.toObject();
QString id = profileObject.value("id").toString("");
QString name = profileObject.value("name").toString("");
bool legacy_ = profileObject.value("legacy").toBool(false);
if (id.isEmpty() || name.isEmpty()) {
qWarning() << "Unable to load a profile" << name << "because it was missing an ID or a name.";
continue;
}
if (id == currentProfile) {
currentProfileIndex = index;
}
profiles.append({ id, name, legacy_ });
}
auto& profile = profiles[currentProfileIndex];
type = AccountType::Mojang;
legacy = profile.legacy;
minecraftProfile.id = profile.id;
minecraftProfile.name = profile.name;
minecraftProfile.validity = Katabasis::Validity::Assumed;
yggdrasilToken.token = accessToken;
yggdrasilToken.extra["clientToken"] = clientToken;
yggdrasilToken.extra["userName"] = userName;
yggdrasilToken.validity = Katabasis::Validity::Assumed;
validity_ = minecraftProfile.validity;
return true;
}
bool AccountData::resumeStateFromV3(QJsonObject data) bool AccountData::resumeStateFromV3(QJsonObject data)
{ {
auto typeV = data.value("type"); auto typeV = data.value("type");
@ -349,8 +288,6 @@ bool AccountData::resumeStateFromV3(QJsonObject data)
auto typeS = typeV.toString(); auto typeS = typeV.toString();
if (typeS == "MSA") { if (typeS == "MSA") {
type = AccountType::MSA; type = AccountType::MSA;
} else if (typeS == "Mojang") {
type = AccountType::Mojang;
} else if (typeS == "Offline") { } else if (typeS == "Offline") {
type = AccountType::Offline; type = AccountType::Offline;
} else { } else {
@ -358,11 +295,6 @@ bool AccountData::resumeStateFromV3(QJsonObject data)
return false; return false;
} }
if (type == AccountType::Mojang) {
legacy = data.value("legacy").toBool(false);
canMigrateToMSA = data.value("canMigrateToMSA").toBool(false);
}
if (type == AccountType::MSA) { if (type == AccountType::MSA) {
auto clientIDV = data.value("msa-client-id"); auto clientIDV = data.value("msa-client-id");
if (clientIDV.isString()) { if (clientIDV.isString()) {
@ -395,15 +327,7 @@ bool AccountData::resumeStateFromV3(QJsonObject data)
QJsonObject AccountData::saveState() const QJsonObject AccountData::saveState() const
{ {
QJsonObject output; QJsonObject output;
if (type == AccountType::Mojang) { if (type == AccountType::MSA) {
output["type"] = "Mojang";
if (legacy) {
output["legacy"] = true;
}
if (canMigrateToMSA) {
output["canMigrateToMSA"] = true;
}
} else if (type == AccountType::MSA) {
output["type"] = "MSA"; output["type"] = "MSA";
output["msa-client-id"] = msaClientID; output["msa-client-id"] = msaClientID;
tokenToJSONV3(output, msaToken, "msa"); tokenToJSONV3(output, msaToken, "msa");
@ -420,51 +344,11 @@ QJsonObject AccountData::saveState() const
return output; return output;
} }
QString AccountData::userName() const
{
if (type == AccountType::MSA) {
return QString();
}
return yggdrasilToken.extra["userName"].toString();
}
QString AccountData::accessToken() const QString AccountData::accessToken() const
{ {
return yggdrasilToken.token; return yggdrasilToken.token;
} }
QString AccountData::clientToken() const
{
if (type != AccountType::Mojang) {
return QString();
}
return yggdrasilToken.extra["clientToken"].toString();
}
void AccountData::setClientToken(QString clientToken)
{
if (type != AccountType::Mojang) {
return;
}
yggdrasilToken.extra["clientToken"] = clientToken;
}
void AccountData::generateClientTokenIfMissing()
{
if (yggdrasilToken.extra.contains("clientToken")) {
return;
}
invalidateClientToken();
}
void AccountData::invalidateClientToken()
{
if (type != AccountType::Mojang) {
return;
}
yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]"));
}
QString AccountData::profileId() const QString AccountData::profileId() const
{ {
return minecraftProfile.id; return minecraftProfile.id;
@ -482,9 +366,6 @@ QString AccountData::profileName() const
QString AccountData::accountDisplayString() const QString AccountData::accountDisplayString() const
{ {
switch (type) { switch (type) {
case AccountType::Mojang: {
return userName();
}
case AccountType::Offline: { case AccountType::Offline: {
return QObject::tr("<Offline>"); return QObject::tr("<Offline>");
} }

View File

@ -71,27 +71,17 @@ struct MinecraftProfile {
Katabasis::Validity validity = Katabasis::Validity::None; Katabasis::Validity validity = Katabasis::Validity::None;
}; };
enum class AccountType { MSA, Mojang, Offline }; enum class AccountType { MSA, Offline };
enum class AccountState { Unchecked, Offline, Working, Online, Disabled, Errored, Expired, Gone }; enum class AccountState { Unchecked, Offline, Working, Online, Disabled, Errored, Expired, Gone };
struct AccountData { struct AccountData {
QJsonObject saveState() const; QJsonObject saveState() const;
bool resumeStateFromV2(QJsonObject data);
bool resumeStateFromV3(QJsonObject data); bool resumeStateFromV3(QJsonObject data);
//! userName for Mojang accounts, gamertag for MSA //! userName for Mojang accounts, gamertag for MSA
QString accountDisplayString() const; QString accountDisplayString() const;
//! Only valid for Mojang accounts. MSA does not preserve this information
QString userName() const;
//! Only valid for Mojang accounts.
QString clientToken() const;
void setClientToken(QString clientToken);
void invalidateClientToken();
void generateClientTokenIfMissing();
//! Yggdrasil access token, as passed to the game. //! Yggdrasil access token, as passed to the game.
QString accessToken() const; QString accessToken() const;
@ -101,8 +91,6 @@ struct AccountData {
QString lastError() const; QString lastError() const;
AccountType type = AccountType::MSA; AccountType type = AccountType::MSA;
bool legacy = false;
bool canMigrateToMSA = false;
QString msaClientID; QString msaClientID;
Katabasis::Token msaToken; Katabasis::Token msaToken;

View File

@ -54,7 +54,7 @@
#include <chrono> #include <chrono>
enum AccountListVersion { MojangOnly = 2, MojangMSA = 3 }; enum AccountListVersion { MojangMSA = 3 };
AccountList::AccountList(QObject* parent) : QAbstractListModel(parent) AccountList::AccountList(QObject* parent) : QAbstractListModel(parent)
{ {
@ -283,9 +283,15 @@ QVariant AccountList::data(const QModelIndex& index, int role) const
return account->accountDisplayString(); return account->accountDisplayString();
case TypeColumn: { case TypeColumn: {
auto typeStr = account->typeString(); switch (account->accountType()) {
typeStr[0] = typeStr[0].toUpper(); case AccountType::MSA: {
return typeStr; return tr("MSA", "Account type");
}
case AccountType::Offline: {
return tr("Offline", "Account type");
}
}
return tr("Unknown", "Account type");
} }
case StatusColumn: { case StatusColumn: {
@ -320,17 +326,6 @@ QVariant AccountList::data(const QModelIndex& index, int role) const
} }
} }
case MigrationColumn: {
if (account->isMSA() || account->isOffline()) {
return tr("N/A", "Can Migrate");
}
if (account->canMigrate()) {
return tr("Yes", "Can Migrate");
} else {
return tr("No", "Can Migrate");
}
}
default: default:
return QVariant(); return QVariant();
} }
@ -366,8 +361,6 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o
return tr("Type"); return tr("Type");
case StatusColumn: case StatusColumn:
return tr("Status"); return tr("Status");
case MigrationColumn:
return tr("Can Migrate?");
default: default:
return QVariant(); return QVariant();
} }
@ -379,11 +372,9 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o
case NameColumn: case NameColumn:
return tr("User name of the account."); return tr("User name of the account.");
case TypeColumn: case TypeColumn:
return tr("Type of the account - Mojang or MSA."); return tr("Type of the account (MSA or Offline)");
case StatusColumn: case StatusColumn:
return tr("Current status of the account."); return tr("Current status of the account.");
case MigrationColumn:
return tr("Can this account migrate to a Microsoft account?");
default: default:
return QVariant(); return QVariant();
} }
@ -415,7 +406,7 @@ Qt::ItemFlags AccountList::flags(const QModelIndex& index) const
bool AccountList::setData(const QModelIndex& idx, const QVariant& value, int role) bool AccountList::setData(const QModelIndex& idx, const QVariant& value, int role)
{ {
if (idx.row() < 0 || idx.row() >= rowCount(idx) || !idx.isValid()) { if (idx.row() < 0 || idx.row() >= rowCount(idx.parent()) || !idx.isValid()) {
return false; return false;
} }
@ -423,7 +414,8 @@ bool AccountList::setData(const QModelIndex& idx, const QVariant& value, int rol
if (value == Qt::Checked) { if (value == Qt::Checked) {
MinecraftAccountPtr account = at(idx.row()); MinecraftAccountPtr account = at(idx.row());
setDefaultAccount(account); setDefaultAccount(account);
} } else if (m_defaultAccount == at(idx.row()))
setDefaultAccount(nullptr);
} }
emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1)); emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1));
@ -472,9 +464,6 @@ bool AccountList::loadList()
// Make sure the format version matches. // Make sure the format version matches.
auto listVersion = root.value("formatVersion").toVariant().toInt(); auto listVersion = root.value("formatVersion").toVariant().toInt();
switch (listVersion) { switch (listVersion) {
case AccountListVersion::MojangOnly: {
return loadV2(root);
} break;
case AccountListVersion::MojangMSA: { case AccountListVersion::MojangMSA: {
return loadV3(root); return loadV3(root);
} break; } break;
@ -488,36 +477,6 @@ bool AccountList::loadList()
} }
} }
bool AccountList::loadV2(QJsonObject& root)
{
beginResetModel();
auto defaultUserName = root.value("activeAccount").toString("");
QJsonArray accounts = root.value("accounts").toArray();
for (QJsonValue accountVal : accounts) {
QJsonObject accountObj = accountVal.toObject();
MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV2(accountObj);
if (account.get() != nullptr) {
auto profileId = account->profileId();
if (!profileId.size()) {
continue;
}
if (findAccountByProfileId(profileId) != -1) {
continue;
}
connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged);
m_accounts.append(account);
if (defaultUserName.size() && account->mojangUserName() == defaultUserName) {
m_defaultAccount = account;
}
} else {
qWarning() << "Failed to load an account.";
}
}
endResetModel();
return true;
}
bool AccountList::loadV3(QJsonObject& root) bool AccountList::loadV3(QJsonObject& root)
{ {
beginResetModel(); beginResetModel();

View File

@ -55,7 +55,6 @@ class AccountList : public QAbstractListModel {
// TODO: Add icon column. // TODO: Add icon column.
ProfileNameColumn = 0, ProfileNameColumn = 0,
NameColumn, NameColumn,
MigrationColumn,
TypeColumn, TypeColumn,
StatusColumn, StatusColumn,
@ -76,7 +75,7 @@ class AccountList : public QAbstractListModel {
virtual Qt::ItemFlags flags(const QModelIndex& index) const override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
virtual bool setData(const QModelIndex& index, const QVariant& value, int role) override; virtual bool setData(const QModelIndex& index, const QVariant& value, int role) override;
void addAccount(const MinecraftAccountPtr account); void addAccount(MinecraftAccountPtr account);
void removeAccount(QModelIndex index); void removeAccount(QModelIndex index);
int findAccountByProfileId(const QString& profileId) const; int findAccountByProfileId(const QString& profileId) const;
MinecraftAccountPtr getAccountByProfileName(const QString& profileName) const; MinecraftAccountPtr getAccountByProfileName(const QString& profileName) const;
@ -97,7 +96,6 @@ class AccountList : public QAbstractListModel {
void setListFilePath(QString path, bool autosave = false); void setListFilePath(QString path, bool autosave = false);
bool loadList(); bool loadList();
bool loadV2(QJsonObject& root);
bool loadV3(QJsonObject& root); bool loadV3(QJsonObject& root);
bool saveList(); bool saveList();

View File

@ -24,10 +24,6 @@ struct AuthSession {
GoneOrMigrated GoneOrMigrated
} status = Undetermined; } status = Undetermined;
// client token
QString client_token;
// account user name
QString username;
// combined session ID // combined session ID
QString session; QString session;
// volatile auth token // volatile auth token
@ -47,4 +43,4 @@ struct AuthSession {
bool demo = false; bool demo = false;
}; };
typedef std::shared_ptr<AuthSession> AuthSessionPtr; using AuthSessionPtr = std::shared_ptr<AuthSession>;

View File

@ -51,23 +51,14 @@
#include <QPainter> #include <QPainter>
#include "flows/MSA.h" #include "flows/MSA.h"
#include "flows/Mojang.h"
#include "flows/Offline.h" #include "flows/Offline.h"
#include "minecraft/auth/AccountData.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent)
{ {
data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
} }
MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json)
{
MinecraftAccountPtr account(new MinecraftAccount());
if (account->data.resumeStateFromV2(json)) {
return account;
}
return nullptr;
}
MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json)
{ {
MinecraftAccountPtr account(new MinecraftAccount()); MinecraftAccountPtr account(new MinecraftAccount());
@ -77,15 +68,6 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json)
return nullptr; return nullptr;
} }
MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString& username)
{
auto account = makeShared<MinecraftAccount>();
account->data.type = AccountType::Mojang;
account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
return account;
}
MinecraftAccountPtr MinecraftAccount::createBlankMSA() MinecraftAccountPtr MinecraftAccount::createBlankMSA()
{ {
MinecraftAccountPtr account(new MinecraftAccount()); MinecraftAccountPtr account(new MinecraftAccount());
@ -138,18 +120,6 @@ QPixmap MinecraftAccount::getFace() const
return skin.scaled(64, 64, Qt::KeepAspectRatio); return skin.scaled(64, 64, Qt::KeepAspectRatio);
} }
shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password)
{
Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new MojangLogin(&data, password));
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); });
emit activityChanged(true);
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA()
{ {
Q_ASSERT(m_currentTask.get() == nullptr); Q_ASSERT(m_currentTask.get() == nullptr);
@ -182,10 +152,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh()
if (data.type == AccountType::MSA) { if (data.type == AccountType::MSA) {
m_currentTask.reset(new MSASilent(&data)); m_currentTask.reset(new MSASilent(&data));
} else if (data.type == AccountType::Offline) {
m_currentTask.reset(new OfflineRefresh(&data));
} else { } else {
m_currentTask.reset(new MojangRefresh(&data)); m_currentTask.reset(new OfflineRefresh(&data));
} }
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
@ -218,7 +186,7 @@ void MinecraftAccount::authFailed(QString reason)
// NOTE: this doesn't do much. There was an error of some sort. // NOTE: this doesn't do much. There was an error of some sort.
} break; } break;
case AccountTaskState::STATE_FAILED_HARD: { case AccountTaskState::STATE_FAILED_HARD: {
if (isMSA()) { if (accountType() == AccountType::MSA) {
data.msaToken.token = QString(); data.msaToken.token = QString();
data.msaToken.refresh_token = QString(); data.msaToken.refresh_token = QString();
data.msaToken.validity = Katabasis::Validity::None; data.msaToken.validity = Katabasis::Validity::None;
@ -296,13 +264,8 @@ void MinecraftAccount::fillSession(AuthSessionPtr session)
} }
} }
// the user name. you have to have an user name
// FIXME: not with MSA
session->username = data.userName();
// volatile auth token // volatile auth token
session->access_token = data.accessToken(); session->access_token = data.accessToken();
// the semi-permanent client token
session->client_token = data.clientToken();
// profile name // profile name
session->player_name = data.profileName(); session->player_name = data.profileName();
// profile ID // profile ID

View File

@ -54,7 +54,7 @@ class Task;
class AccountTask; class AccountTask;
class MinecraftAccount; class MinecraftAccount;
typedef shared_qobject_ptr<MinecraftAccount> MinecraftAccountPtr; using MinecraftAccountPtr = shared_qobject_ptr<MinecraftAccount>;
Q_DECLARE_METATYPE(MinecraftAccountPtr) Q_DECLARE_METATYPE(MinecraftAccountPtr)
/** /**
@ -85,13 +85,10 @@ class MinecraftAccount : public QObject, public Usable {
//! Default constructor //! Default constructor
explicit MinecraftAccount(QObject* parent = 0); explicit MinecraftAccount(QObject* parent = 0);
static MinecraftAccountPtr createFromUsername(const QString& username);
static MinecraftAccountPtr createBlankMSA(); static MinecraftAccountPtr createBlankMSA();
static MinecraftAccountPtr createOffline(const QString& username); static MinecraftAccountPtr createOffline(const QString& username);
static MinecraftAccountPtr loadFromJsonV2(const QJsonObject& json);
static MinecraftAccountPtr loadFromJsonV3(const QJsonObject& json); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject& json);
static QUuid uuidFromUsername(QString username); static QUuid uuidFromUsername(QString username);
@ -100,12 +97,6 @@ class MinecraftAccount : public QObject, public Usable {
QJsonObject saveToJson() const; QJsonObject saveToJson() const;
public: /* manipulation */ public: /* manipulation */
/**
* Attempt to login. Empty password means we use the token.
* If the attempt fails because we already are performing some task, it returns false.
*/
shared_qobject_ptr<AccountTask> login(QString password);
shared_qobject_ptr<AccountTask> loginMSA(); shared_qobject_ptr<AccountTask> loginMSA();
shared_qobject_ptr<AccountTask> loginOffline(); shared_qobject_ptr<AccountTask> loginOffline();
@ -119,8 +110,6 @@ class MinecraftAccount : public QObject, public Usable {
QString accountDisplayString() const { return data.accountDisplayString(); } QString accountDisplayString() const { return data.accountDisplayString(); }
QString mojangUserName() const { return data.userName(); }
QString accessToken() const { return data.accessToken(); } QString accessToken() const { return data.accessToken(); }
QString profileId() const { return data.profileId(); } QString profileId() const { return data.profileId(); }
@ -129,11 +118,7 @@ class MinecraftAccount : public QObject, public Usable {
bool isActive() const; bool isActive() const;
bool canMigrate() const { return data.canMigrateToMSA; } [[nodiscard]] AccountType accountType() const noexcept { return data.type; }
bool isMSA() const { return data.type == AccountType::MSA; }
bool isOffline() const { return data.type == AccountType::Offline; }
bool ownsMinecraft() const { return data.minecraftEntitlement.ownsMinecraft; } bool ownsMinecraft() const { return data.minecraftEntitlement.ownsMinecraft; }
@ -142,12 +127,6 @@ class MinecraftAccount : public QObject, public Usable {
QString typeString() const QString typeString() const
{ {
switch (data.type) { switch (data.type) {
case AccountType::Mojang: {
if (data.legacy) {
return "legacy";
}
return "mojang";
} break;
case AccountType::MSA: { case AccountType::MSA: {
return "msa"; return "msa";
} break; } break;

View File

@ -1,342 +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.
*/
#include "Yggdrasil.h"
#include "AccountData.h"
#include <QByteArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QObject>
#include <QString>
#include <QDebug>
#include "Application.h"
Yggdrasil::Yggdrasil(AccountData* data, QObject* parent) : AccountTask(data, parent)
{
changeState(AccountTaskState::STATE_CREATED);
}
void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content)
{
changeState(AccountTaskState::STATE_WORKING);
QNetworkRequest netRequest(endpoint);
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
m_netReply = APPLICATION->network()->post(netRequest, content);
connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply);
connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers);
connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers);
connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors);
timeout_keeper.setSingleShot(true);
timeout_keeper.start(timeout_max);
counter.setSingleShot(false);
counter.start(time_step);
progress(0, timeout_max);
connect(&timeout_keeper, &QTimer::timeout, this, &Yggdrasil::abortByTimeout);
connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat);
}
void Yggdrasil::executeTask() {}
void Yggdrasil::refresh()
{
start();
/*
* {
* "clientToken": "client identifier"
* "accessToken": "current access token to be refreshed"
* "selectedProfile": // specifying this causes errors
* {
* "id": "profile ID"
* "name": "profile name"
* }
* "requestUser": true/false // request the user structure
* }
*/
QJsonObject req;
req.insert("clientToken", m_data->clientToken());
req.insert("accessToken", m_data->accessToken());
/*
{
auto currentProfile = m_account->currentProfile();
QJsonObject profile;
profile.insert("id", currentProfile->id());
profile.insert("name", currentProfile->name());
req.insert("selectedProfile", profile);
}
*/
req.insert("requestUser", false);
QJsonDocument doc(req);
QUrl reqUrl("https://authserver.mojang.com/refresh");
QByteArray requestData = doc.toJson();
sendRequest(reqUrl, requestData);
}
void Yggdrasil::login(QString password)
{
start();
/*
* {
* "agent": { // optional
* "name": "Minecraft", // So far this is the only encountered value
* "version": 1 // This number might be increased
* // by the vanilla client in the future
* },
* "username": "mojang account name", // Can be an email address or player name for
* // unmigrated accounts
* "password": "mojang account password",
* "clientToken": "client identifier", // optional
* "requestUser": true/false // request the user structure
* }
*/
QJsonObject req;
{
QJsonObject agent;
// C++ makes string literals void* for some stupid reason, so we have to tell it
// QString... Thanks Obama.
agent.insert("name", QString("Minecraft"));
agent.insert("version", 1);
req.insert("agent", agent);
}
req.insert("username", m_data->userName());
req.insert("password", password);
req.insert("requestUser", false);
// If we already have a client token, give it to the server.
// Otherwise, let the server give us one.
m_data->generateClientTokenIfMissing();
req.insert("clientToken", m_data->clientToken());
QJsonDocument doc(req);
QUrl reqUrl("https://authserver.mojang.com/authenticate");
QNetworkRequest netRequest(reqUrl);
QByteArray requestData = doc.toJson();
sendRequest(reqUrl, requestData);
}
void Yggdrasil::refreshTimers(qint64, qint64)
{
timeout_keeper.stop();
timeout_keeper.start(timeout_max);
progress(count = 0, timeout_max);
}
void Yggdrasil::heartbeat()
{
count += time_step;
progress(count, timeout_max);
}
bool Yggdrasil::abort()
{
progress(timeout_max, timeout_max);
// TODO: actually use this in a meaningful way
m_aborted = Yggdrasil::BY_USER;
m_netReply->abort();
return true;
}
void Yggdrasil::abortByTimeout()
{
progress(timeout_max, timeout_max);
// TODO: actually use this in a meaningful way
m_aborted = Yggdrasil::BY_TIMEOUT;
m_netReply->abort();
}
void Yggdrasil::sslErrors(QList<QSslError> errors)
{
int i = 1;
for (auto error : errors) {
qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
auto cert = error.certificate();
qCritical() << "Certificate in question:\n" << cert.toText();
i++;
}
}
void Yggdrasil::processResponse(QJsonObject responseData)
{
// Read the response data. We need to get the client token, access token, and the selected
// profile.
qDebug() << "Processing authentication response.";
// qDebug() << responseData;
// If we already have a client token, make sure the one the server gave us matches our
// existing one.
QString clientToken = responseData.value("clientToken").toString("");
if (clientToken.isEmpty()) {
// Fail if the server gave us an empty client token
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
return;
}
if (m_data->clientToken().isEmpty()) {
m_data->setClientToken(clientToken);
} else if (clientToken != m_data->clientToken()) {
changeState(AccountTaskState::STATE_FAILED_HARD,
tr("Authentication server attempted to change the client token. This isn't supported."));
return;
}
// Now, we set the access token.
qDebug() << "Getting access token.";
QString accessToken = responseData.value("accessToken").toString("");
if (accessToken.isEmpty()) {
// Fail if the server didn't give us an access token.
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
return;
}
// Set the access token.
m_data->yggdrasilToken.token = accessToken;
m_data->yggdrasilToken.validity = Katabasis::Validity::Certain;
m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
// Get UUID here since we need it for later
auto profile = responseData.value("selectedProfile");
if (!profile.isObject()) {
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a selected profile."));
return;
}
auto profileObj = profile.toObject();
for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) {
if (i.key() == "name" && i.value().isString()) {
m_data->minecraftProfile.name = i->toString();
} else if (i.key() == "id" && i.value().isString()) {
m_data->minecraftProfile.id = i->toString();
}
}
if (m_data->minecraftProfile.id.isEmpty()) {
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile."));
return;
}
// We've made it through the minefield of possible errors. Return true to indicate that
// we've succeeded.
qDebug() << "Finished reading authentication response.";
changeState(AccountTaskState::STATE_SUCCEEDED);
}
void Yggdrasil::processReply()
{
changeState(AccountTaskState::STATE_WORKING);
switch (m_netReply->error()) {
case QNetworkReply::NoError:
break;
case QNetworkReply::TimeoutError:
changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out."));
return;
case QNetworkReply::OperationCanceledError:
changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
return;
case QNetworkReply::SslHandshakeFailedError:
changeState(AccountTaskState::STATE_FAILED_SOFT,
tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
"<ul>"
"<li>You use Windows and need to update your root certificates, please install any outstanding updates.</li>"
"<li>Some device on your network is interfering with SSL traffic. In that case, "
"you have bigger worries than Minecraft not starting.</li>"
"<li>Possibly something else. Check the log file for details</li>"
"</ul>"));
return;
// used for invalid credentials and similar errors. Fall through.
case QNetworkReply::ContentAccessDenied:
case QNetworkReply::ContentOperationNotPermittedError:
break;
case QNetworkReply::ContentGoneError: {
changeState(AccountTaskState::STATE_FAILED_GONE,
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account."));
return;
}
default:
changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation failed due to a network error: %1 (%2)")
.arg(m_netReply->errorString())
.arg(m_netReply->error()));
return;
}
// Try to parse the response regardless of the response code.
// Sometimes the auth server will give more information and an error code.
QJsonParseError jsonError;
QByteArray replyData = m_netReply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
// Check the response code.
int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (responseCode == 200) {
// If the response code was 200, then there shouldn't be an error. Make sure
// anyways.
// Also, sometimes an empty reply indicates success. If there was no data received,
// pass an empty json object to the processResponse function.
if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) {
processResponse(replyData.size() > 0 ? doc.object() : QJsonObject());
return;
} else {
changeState(AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to parse authentication server response JSON response: %1 at offset %2.")
.arg(jsonError.errorString())
.arg(jsonError.offset));
qCritical() << replyData;
}
return;
}
// If the response code was not 200, then Yggdrasil may have given us information
// about the error.
// If we can parse the response, then get information from it. Otherwise just say
// there was an unknown error.
if (jsonError.error == QJsonParseError::NoError) {
// We were able to parse the server's response. Woo!
// Call processError. If a subclass has overridden it then they'll handle their
// stuff there.
qDebug() << "The request failed, but the server gave us an error message. Processing error.";
processError(doc.object());
} else {
// The server didn't say anything regarding the error. Give the user an unknown
// error.
qDebug() << "The request failed and the server gave no error message. Unknown error.";
changeState(
AccountTaskState::STATE_FAILED_SOFT,
tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString()));
}
}
void Yggdrasil::processError(QJsonObject responseData)
{
QJsonValue errorVal = responseData.value("error");
QJsonValue errorMessageValue = responseData.value("errorMessage");
QJsonValue causeVal = responseData.value("cause");
if (errorVal.isString() && errorMessageValue.isString()) {
m_error = std::shared_ptr<Error>(new Error{ errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("") });
changeState(AccountTaskState::STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
} else {
// Error is not in standard format. Don't set m_error and return unknown error.
changeState(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
}
}

View File

@ -1,92 +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 "AccountTask.h"
#include <qsslerror.h>
#include <QJsonObject>
#include <QString>
#include <QTimer>
#include "MinecraftAccount.h"
class QNetworkAccessManager;
class QNetworkReply;
/**
* A Yggdrasil task is a task that performs an operation on a given mojang account.
*/
class Yggdrasil : public AccountTask {
Q_OBJECT
public:
explicit Yggdrasil(AccountData* data, QObject* parent = 0);
virtual ~Yggdrasil() = default;
void refresh();
void login(QString password);
struct Error {
QString m_errorMessageShort;
QString m_errorMessageVerbose;
QString m_cause;
};
std::shared_ptr<Error> m_error;
enum AbortedBy { BY_NOTHING, BY_USER, BY_TIMEOUT } m_aborted = BY_NOTHING;
protected:
void executeTask() override;
/**
* Processes the response received from the server.
* If an error occurred, this should emit a failed signal.
* If Yggdrasil gave an error response, it should call setError() first, and then return false.
* Otherwise, it should return true.
* Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
* an empty QJsonObject.
*/
void processResponse(QJsonObject responseData);
/**
* Processes an error response received from the server.
* The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
* \returns a QString error message that will be passed to emitFailed.
*/
virtual void processError(QJsonObject responseData);
protected slots:
void processReply();
void refreshTimers(qint64, qint64);
void heartbeat();
void sslErrors(QList<QSslError>);
void abortByTimeout();
public slots:
virtual bool abort() override;
private:
void sendRequest(QUrl endpoint, QByteArray content);
protected:
QNetworkReply* m_netReply = nullptr;
QTimer timeout_keeper;
QTimer counter;
int count = 0; // num msec since time reset
const int timeout_max = 30000;
const int time_step = 50;
};

View File

@ -12,7 +12,6 @@
#include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AccountTask.h" #include "minecraft/auth/AccountTask.h"
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "minecraft/auth/Yggdrasil.h"
class AuthFlow : public AccountTask { class AuthFlow : public AccountTask {
Q_OBJECT Q_OBJECT

View File

@ -1,22 +0,0 @@
#include "Mojang.h"
#include "minecraft/auth/steps/GetSkinStep.h"
#include "minecraft/auth/steps/MigrationEligibilityStep.h"
#include "minecraft/auth/steps/MinecraftProfileStepMojang.h"
#include "minecraft/auth/steps/YggdrasilStep.h"
MojangRefresh::MojangRefresh(AccountData* data, QObject* parent) : AuthFlow(data, parent)
{
m_steps.append(makeShared<YggdrasilStep>(m_data, QString()));
m_steps.append(makeShared<MinecraftProfileStepMojang>(m_data));
m_steps.append(makeShared<MigrationEligibilityStep>(m_data));
m_steps.append(makeShared<GetSkinStep>(m_data));
}
MojangLogin::MojangLogin(AccountData* data, QString password, QObject* parent) : AuthFlow(data, parent), m_password(password)
{
m_steps.append(makeShared<YggdrasilStep>(m_data, m_password));
m_steps.append(makeShared<MinecraftProfileStepMojang>(m_data));
m_steps.append(makeShared<MigrationEligibilityStep>(m_data));
m_steps.append(makeShared<GetSkinStep>(m_data));
}

View File

@ -1,17 +0,0 @@
#pragma once
#include "AuthFlow.h"
class MojangRefresh : public AuthFlow {
Q_OBJECT
public:
explicit MojangRefresh(AccountData* data, QObject* parent = 0);
};
class MojangLogin : public AuthFlow {
Q_OBJECT
public:
explicit MojangLogin(AccountData* data, QString password, QObject* parent = 0);
private:
QString m_password;
};

View File

@ -1,45 +0,0 @@
#include "MigrationEligibilityStep.h"
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
MigrationEligibilityStep::MigrationEligibilityStep(AccountData* data) : AuthStep(data) {}
MigrationEligibilityStep::~MigrationEligibilityStep() noexcept = default;
QString MigrationEligibilityStep::describe()
{
return tr("Checking for migration eligibility.");
}
void MigrationEligibilityStep::perform()
{
auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigration");
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
AuthRequest* requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &MigrationEligibilityStep::onRequestDone);
requestor->get(request);
}
void MigrationEligibilityStep::rehydrate()
{
// NOOP, for now. We only save bools and there's nothing to check.
}
void MigrationEligibilityStep::onRequestDone(QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers)
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
if (error == QNetworkReply::NoError) {
Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA);
}
emit finished(AccountTaskState::STATE_WORKING, tr("Got migration flags"));
}

View File

@ -1,21 +0,0 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class MigrationEligibilityStep : public AuthStep {
Q_OBJECT
public:
explicit MigrationEligibilityStep(AccountData* data);
virtual ~MigrationEligibilityStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
};

View File

@ -41,10 +41,6 @@ void MinecraftProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByt
qCDebug(authCredentials()) << data; qCDebug(authCredentials()) << data;
if (error == QNetworkReply::ContentNotFoundError) { if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state. // NOTE: Succeed even if we do not have a profile. This is a valid account state.
if (m_data->type == AccountType::Mojang) {
m_data->minecraftEntitlement.canPlayMinecraft = false;
m_data->minecraftEntitlement.ownsMinecraft = false;
}
m_data->minecraftProfile = MinecraftProfile(); m_data->minecraftProfile = MinecraftProfile();
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile.")); emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile."));
return; return;
@ -73,10 +69,5 @@ void MinecraftProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByt
return; return;
} }
if (m_data->type == AccountType::Mojang) {
auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
m_data->minecraftEntitlement.ownsMinecraft = validProfile;
}
emit finished(AccountTaskState::STATE_WORKING, tr("Minecraft Java profile acquisition succeeded.")); emit finished(AccountTaskState::STATE_WORKING, tr("Minecraft Java profile acquisition succeeded."));
} }

View File

@ -1,87 +0,0 @@
#include "MinecraftProfileStepMojang.h"
#include <QNetworkRequest>
#include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) {}
MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default;
QString MinecraftProfileStepMojang::describe()
{
return tr("Fetching the Minecraft profile.");
}
void MinecraftProfileStepMojang::perform()
{
if (m_data->minecraftProfile.id.isEmpty()) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile."));
return;
}
// use session server instead of profile due to profile endpoint being locked for locked Mojang accounts
QUrl url = QUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + m_data->minecraftProfile.id);
QNetworkRequest req = QNetworkRequest(url);
AuthRequest* request = new AuthRequest(this);
connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone);
request->get(req);
}
void MinecraftProfileStepMojang::rehydrate()
{
// NOOP, for now. We only save bools and there's nothing to check.
}
void MinecraftProfileStepMojang::onRequestDone(QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers)
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
qCDebug(authCredentials()) << data;
if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
if (m_data->type == AccountType::Mojang) {
m_data->minecraftEntitlement.canPlayMinecraft = false;
m_data->minecraftEntitlement.ownsMinecraft = false;
}
m_data->minecraftProfile = MinecraftProfile();
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile."));
return;
}
if (error != QNetworkReply::NoError) {
qWarning() << "Error getting profile:";
qWarning() << " HTTP Status: " << requestor->httpStatus_;
qWarning() << " Internal error no.: " << error;
qWarning() << " Error string: " << requestor->errorString_;
qWarning() << " Response:";
qWarning() << QString::fromUtf8(data);
if (Net::isApplicationError(error)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_));
} else {
emit finished(AccountTaskState::STATE_OFFLINE,
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_));
}
return;
}
if (!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) {
m_data->minecraftProfile = MinecraftProfile();
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed"));
return;
}
if (m_data->type == AccountType::Mojang) {
auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
m_data->minecraftEntitlement.ownsMinecraft = validProfile;
}
emit finished(AccountTaskState::STATE_WORKING, tr("Minecraft Java profile acquisition succeeded."));
}

View File

@ -1,21 +0,0 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class MinecraftProfileStepMojang : public AuthStep {
Q_OBJECT
public:
explicit MinecraftProfileStepMojang(AccountData* data);
virtual ~MinecraftProfileStepMojang() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
};

View File

@ -38,7 +38,7 @@ void XboxUserStep::perform()
QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json"); request.setRawHeader("Accept", "application/json");
// set contract-verison header (prevent err 400 bad-request?) // set contract-version header (prevent err 400 bad-request?)
// https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders
request.setRawHeader("x-xbl-contract-version", "1"); request.setRawHeader("x-xbl-contract-version", "1");

View File

@ -1,57 +0,0 @@
#include "YggdrasilStep.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "minecraft/auth/Yggdrasil.h"
YggdrasilStep::YggdrasilStep(AccountData* data, QString password) : AuthStep(data), m_password(password)
{
m_yggdrasil = new Yggdrasil(m_data, this);
connect(m_yggdrasil, &Task::failed, this, &YggdrasilStep::onAuthFailed);
connect(m_yggdrasil, &Task::succeeded, this, &YggdrasilStep::onAuthSucceeded);
connect(m_yggdrasil, &Task::aborted, this, &YggdrasilStep::onAuthFailed);
}
YggdrasilStep::~YggdrasilStep() noexcept = default;
QString YggdrasilStep::describe()
{
return tr("Logging in with Mojang account.");
}
void YggdrasilStep::rehydrate()
{
// NOOP, for now.
}
void YggdrasilStep::perform()
{
if (m_password.size()) {
m_yggdrasil->login(m_password);
} else {
m_yggdrasil->refresh();
}
}
void YggdrasilStep::onAuthSucceeded()
{
emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang"));
}
void YggdrasilStep::onAuthFailed()
{
// TODO: hook these in again, expand to MSA
// m_error = m_yggdrasil->m_error;
// m_aborted = m_yggdrasil->m_aborted;
auto state = m_yggdrasil->taskState();
QString errorMessage = tr("Mojang user authentication failed.");
// NOTE: soft error in the first step means 'offline'
if (state == AccountTaskState::STATE_FAILED_SOFT) {
state = AccountTaskState::STATE_OFFLINE;
errorMessage = tr("Mojang user authentication ended with a network error.");
}
emit finished(state, errorMessage);
}

View File

@ -1,28 +0,0 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class Yggdrasil;
class YggdrasilStep : public AuthStep {
Q_OBJECT
public:
explicit YggdrasilStep(AccountData* data, QString password);
virtual ~YggdrasilStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onAuthSucceeded();
void onAuthFailed();
private:
Yggdrasil* m_yggdrasil = nullptr;
QString m_password;
};

View File

@ -105,6 +105,17 @@ void LauncherPartLaunch::executeTask()
auto instance = m_parent->instance(); auto instance = m_parent->instance();
std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance);
QString legacyJarPath;
if (minecraftInstance->getLauncher() == "legacy" || minecraftInstance->shouldApplyOnlineFixes()) {
legacyJarPath = APPLICATION->getJarPath("NewLaunchLegacy.jar");
if (legacyJarPath.isEmpty()) {
const char* reason = QT_TR_NOOP("Legacy launcher library could not be found. Please check your installation.");
emit logLine(tr(reason), MessageLevel::Fatal);
emitFailed(tr(reason));
return;
}
}
m_launchScript = minecraftInstance->createLaunchScript(m_session, m_serverToJoin); m_launchScript = minecraftInstance->createLaunchScript(m_session, m_serverToJoin);
QStringList args = minecraftInstance->javaArguments(); QStringList args = minecraftInstance->javaArguments();
QString allArgs = args.join(", "); QString allArgs = args.join(", ");
@ -120,6 +131,9 @@ void LauncherPartLaunch::executeTask()
auto classPath = minecraftInstance->getClassPath(); auto classPath = minecraftInstance->getClassPath();
classPath.prepend(jarPath); classPath.prepend(jarPath);
if (!legacyJarPath.isEmpty())
classPath.prepend(legacyJarPath);
auto natPath = minecraftInstance->getNativePath(); auto natPath = minecraftInstance->getNativePath();
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
if (!fitsInLocal8bit(natPath)) { if (!fitsInLocal8bit(natPath)) {

View File

@ -26,4 +26,4 @@ struct MinecraftServerTarget {
static MinecraftServerTarget parse(const QString& fullAddress); static MinecraftServerTarget parse(const QString& fullAddress);
}; };
typedef std::shared_ptr<MinecraftServerTarget> MinecraftServerTargetPtr; using MinecraftServerTargetPtr = std::shared_ptr<MinecraftServerTarget>;

View File

@ -28,7 +28,7 @@
#include "Version.h" #include "Version.h"
// Values taken from: // Values taken from:
// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_data_pack#%22pack_format%22 // https://minecraft.wiki/w/Tutorials/Creating_a_data_pack#%22pack_format%22
static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = { static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } }, { 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } },
{ 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } },

View File

@ -63,7 +63,7 @@ class DataPack : public Resource {
mutable QMutex m_data_lock; mutable QMutex m_data_lock;
/* The 'version' of a data pack, as defined in the pack.mcmeta file. /* The 'version' of a data pack, as defined in the pack.mcmeta file.
* See https://minecraft.fandom.com/wiki/Data_pack#pack.mcmeta * See https://minecraft.wiki/w/Data_pack#pack.mcmeta
*/ */
int m_pack_format = 0; int m_pack_format = 0;

View File

@ -31,6 +31,7 @@ class Mod;
class Metadata { class Metadata {
public: public:
using ModStruct = Packwiz::V1::Mod; using ModStruct = Packwiz::V1::Mod;
using ModSide = Packwiz::V1::Side;
static auto create(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> ModStruct static auto create(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> ModStruct
{ {

View File

@ -132,15 +132,21 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) -
if (!preserve_metadata) { if (!preserve_metadata) {
qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name());
destroyMetadata(index_dir);
}
return Resource::destroy(attempt_trash);
}
void Mod::destroyMetadata(QDir& index_dir)
{
if (metadata()) { if (metadata()) {
Metadata::remove(index_dir, metadata()->slug); Metadata::remove(index_dir, metadata()->slug);
} else { } else {
auto n = name(); auto n = name();
Metadata::remove(index_dir, n); Metadata::remove(index_dir, n);
} }
} m_local_details.metadata = nullptr;
return Resource::destroy(attempt_trash);
} }
auto Mod::details() const -> const ModDetails& auto Mod::details() const -> const ModDetails&
@ -246,7 +252,8 @@ void Mod::setIcon(QImage new_image) const
PixmapCache::remove(m_pack_image_cache_key.key); PixmapCache::remove(m_pack_image_cache_key.key);
// scale the image to avoid flooding the pixmapcache // scale the image to avoid flooding the pixmapcache
auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); auto pixmap =
QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
m_pack_image_cache_key.key = PixmapCache::insert(pixmap); m_pack_image_cache_key.key = PixmapCache::insert(pixmap);
m_pack_image_cache_key.was_ever_used = true; m_pack_image_cache_key.was_ever_used = true;
@ -259,7 +266,7 @@ QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const
if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {
if (size.isNull()) if (size.isNull())
return cached_image; return cached_image;
return cached_image.scaled(size, mode); return cached_image.scaled(size, mode, Qt::SmoothTransformation);
} }
// No valid image we can get // No valid image we can get

View File

@ -93,6 +93,8 @@ class Mod : public Resource {
// Delete all the files of this mod // Delete all the files of this mod
auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool;
// Delete the metadata only
void destroyMetadata(QDir& index_dir);
void finishResolvingWithDetails(ModDetails&& details); void finishResolvingWithDetails(ModDetails&& details);

View File

@ -37,9 +37,9 @@
#include "ModFolderModel.h" #include "ModFolderModel.h"
#include <FileSystem.h> #include <FileSystem.h>
#include <qheaderview.h>
#include <QDebug> #include <QDebug>
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QHeaderView>
#include <QIcon> #include <QIcon>
#include <QMimeData> #include <QMimeData>
#include <QString> #include <QString>
@ -51,8 +51,13 @@
#include "Application.h" #include "Application.h"
#include "Json.h"
#include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir) ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir)
: ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed) : ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed)
@ -60,8 +65,8 @@ ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool
m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider" }); m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider") }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER }; m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER };
m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch,
QHeaderView::ResizeToContents, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents }; QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true }; m_columnsHideable = { false, true, false, true, true, true };
} }
@ -126,6 +131,11 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
} }
return {}; return {};
} }
case Qt::SizeHintRole:
if (column == ImageColumn) {
return QSize(32, 32);
}
return {};
case Qt::CheckStateRole: case Qt::CheckStateRole:
switch (column) { switch (column) {
case ActiveColumn: case ActiveColumn:
@ -228,6 +238,25 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
return true; return true;
} }
bool ModFolderModel::deleteModsMetadata(const QModelIndexList& indexes)
{
if (indexes.isEmpty())
return true;
for (auto i : indexes) {
if (i.column() != 0) {
continue;
}
auto m = at(i.row());
auto index_dir = indexDir();
m->destroyMetadata(index_dir);
}
update();
return true;
}
bool ModFolderModel::isValid() bool ModFolderModel::isValid()
{ {
return m_dir.exists() && m_dir.isReadable(); return m_dir.exists() && m_dir.isReadable();
@ -309,3 +338,47 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id)
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
} }
static const FlameAPI flameAPI;
bool ModFolderModel::installMod(QString file_path, ModPlatform::IndexedVersion& vers)
{
if (vers.addonId.isValid()) {
ModPlatform::IndexedPack pack{
vers.addonId,
ModPlatform::ResourceProvider::FLAME,
};
QEventLoop loop;
auto response = std::make_shared<QByteArray>();
auto job = flameAPI.getProject(vers.addonId.toString(), response);
QObject::connect(job.get(), &Task::failed, [&loop] { loop.quit(); });
QObject::connect(job.get(), &Task::aborted, &loop, &QEventLoop::quit);
QObject::connect(job.get(), &Task::succeeded, [response, this, &vers, &loop, &pack] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qDebug() << *response;
return;
}
try {
auto obj = Json::requireObject(Json::requireObject(doc), "data");
FlameMod::loadIndexedPack(pack, obj);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading mod info: " << e.cause();
}
LocalModUpdateTask update_metadata(indexDir(), pack, vers);
QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
update_metadata.start();
});
job->start();
loop.exec();
}
return ResourceFolderModel::installResource(file_path);
}

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