Merge branch 'develop' into desysteminfo

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2025-11-23 16:23:27 +00:00 committed by GitHub
commit 19aab36a4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
121 changed files with 1494 additions and 1912 deletions

View file

@ -13,10 +13,6 @@ inputs:
description: Name of the uploaded artifact description: Name of the uploaded artifact
required: true required: true
default: Linux default: Linux
cmake-preset:
description: Base CMake preset previously used for the build
required: true
default: linux
qt-version: qt-version:
description: Version of Qt to use description: Version of Qt to use
required: true required: true
@ -62,7 +58,7 @@ runs:
GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }} GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }}
run: | run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr
mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml
export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated
@ -107,12 +103,10 @@ runs:
env: env:
BUILD_DIR: build BUILD_DIR: build
CMAKE_PRESET: ${{ inputs.cmake-preset }}
INSTALL_PORTABLE_DIR: install-portable INSTALL_PORTABLE_DIR: install-portable
run: | run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }}
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
cd ${{ env.INSTALL_PORTABLE_DIR }} cd ${{ env.INSTALL_PORTABLE_DIR }}

View file

@ -59,7 +59,7 @@ runs:
BUILD_DIR: build BUILD_DIR: build
INSTALL_DIR: install INSTALL_DIR: install
run: | run: |
cmake --install ${{ env.BUILD_DIR }} cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }}
cd ${{ env.INSTALL_DIR }} cd ${{ env.INSTALL_DIR }}
chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher"

View file

@ -15,12 +15,15 @@ inputs:
msystem: msystem:
description: MSYS2 subsystem to use description: MSYS2 subsystem to use
required: false required: false
windows-codesign-cert: azure-client-id:
description: Certificate for signing Windows builds description: Client ID for the Azure Signer Application
required: false required: true
windows-codesign-password: azure-tenant-id:
description: Password for signing Windows builds description: Tenant ID for the Azure Signer Application
required: false required: true
azure-subscription-id:
description: Subscription ID for the Azure Signer Application
required: true
runs: runs:
using: composite using: composite
@ -33,7 +36,7 @@ runs:
BUILD_DIR: build BUILD_DIR: build
INSTALL_DIR: install INSTALL_DIR: install
run: | run: |
cmake --install ${{ env.BUILD_DIR }} cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }}
touch ${{ env.INSTALL_DIR }}/manifest.txt touch ${{ env.INSTALL_DIR }}/manifest.txt
for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt
@ -50,23 +53,45 @@ runs:
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 - name: Emit warning for unsigned builds
shell: bash # yes, we are not using MSYS2 or PowerShell here if: ${{ github.ref_name != 'develop' || inputs.azure-client-id == '' }}
run: |
echo '${{ inputs.windows-codesign-cert }}' | base64 --decode > codesign.pfx
- name: Sign executable
shell: pwsh shell: pwsh
env:
INSTALL_DIR: install
run: | run: |
if (Get-Content ./codesign.pfx){ ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
cd ${{ env.INSTALL_DIR }}
# We ship the exact same executable for portable and non-portable editions, so signing just once is fine - name: Login to Azure
SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe if: ${{ github.ref_name == 'develop' && inputs.azure-client-id != '' }}
} else { uses: azure/login@v2
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY with:
} client-id: ${{ inputs.azure-client-id }}
tenant-id: ${{ inputs.azure-tenant-id }}
subscription-id: ${{ inputs.azure-subscription-id }}
- name: Sign executables
if: ${{ github.ref_name == 'develop' && inputs.azure-client-id != '' }}
uses: azure/trusted-signing-action@v0
with:
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: PrismLauncher
certificate-profile-name: PrismLauncher
files: |
${{ github.workspace }}\install\prismlauncher.exe
${{ github.workspace }}\install\prismlauncher_filelink.exe
${{ github.workspace }}\install\prismlauncher_updater.exe
# TODO(@getchoo): Is this all really needed???
# https://github.com/Azure/trusted-signing-action/blob/fc390cf8ed0f14e248a542af1d838388a47c7a7c/docs/OIDC.md
exclude-environment-credential: true
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: false
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Package (MinGW, portable) - name: Package (MinGW, portable)
if: ${{ inputs.msystem != '' }} if: ${{ inputs.msystem != '' }}
@ -77,7 +102,7 @@ runs:
INSTALL_PORTABLE_DIR: install-portable INSTALL_PORTABLE_DIR: install-portable
run: | run: |
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
- name: Package (MSVC, portable) - name: Package (MSVC, portable)
@ -89,7 +114,7 @@ runs:
INSTALL_PORTABLE_DIR: install-portable INSTALL_PORTABLE_DIR: install-portable
run: | run: |
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
@ -115,13 +140,28 @@ runs:
makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi"
- name: Sign installer - name: Sign installer
shell: pwsh if: ${{ github.ref_name == 'develop' && inputs.azure-client-id != '' }}
run: | uses: azure/trusted-signing-action@v0
if (Get-Content ./codesign.pfx){ with:
SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe endpoint: https://eus.codesigning.azure.net/
} else { trusted-signing-account-name: PrismLauncher
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY certificate-profile-name: PrismLauncher
}
files: |
${{ github.workspace }}\PrismLauncher-Setup.exe
# TODO(@getchoo): Is this all really needed???
# https://github.com/Azure/trusted-signing-action/blob/fc390cf8ed0f14e248a542af1d838388a47c7a7c/docs/OIDC.md
exclude-environment-credential: true
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: false
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Upload binary zip - name: Upload binary zip
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5

View file

@ -11,6 +11,7 @@ runs:
sudo apt-get -y install \ sudo apt-get -y install \
dpkg-dev \ dpkg-dev \
ninja-build extra-cmake-modules scdoc \ ninja-build extra-cmake-modules scdoc \
libqrencode-dev \
appstream libxcb-cursor-dev appstream libxcb-cursor-dev
- name: Setup AppImage tooling - name: Setup AppImage tooling

View file

@ -79,6 +79,7 @@ runs:
qt6-5compat:p qt6-5compat:p
qt6-networkauth:p qt6-networkauth:p
cmark:p cmark:p
qrencode:p
tomlplusplus:p tomlplusplus:p
quazip-qt6:p quazip-qt6:p

View file

@ -70,6 +70,8 @@ jobs:
name: Build (${{ matrix.artifact-name }}) name: Build (${{ matrix.artifact-name }})
permissions: permissions:
# Required for Azure Trusted Signing
id-token: write
# Required for vcpkg binary cache # Required for vcpkg binary cache
packages: write packages: write
@ -79,7 +81,7 @@ jobs:
include: include:
- os: ubuntu-22.04 - os: ubuntu-22.04
artifact-name: Linux artifact-name: Linux
base-cmake-preset: linux cmake-preset: linux
# NOTE(@getchoo): Yes, we're intentionally using 24.04 here!!! # NOTE(@getchoo): Yes, we're intentionally using 24.04 here!!!
# #
@ -87,34 +89,34 @@ jobs:
# *for the same version* are compiled against 24.04 on ARM, and *not* 22.04 like x64 # *for the same version* are compiled against 24.04 on ARM, and *not* 22.04 like x64
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: Linux-aarch64 artifact-name: Linux-aarch64
base-cmake-preset: linux cmake-preset: linux
- os: windows-2022 - os: windows-2022
artifact-name: Windows-MinGW-w64 artifact-name: Windows-MinGW-w64
base-cmake-preset: windows_mingw cmake-preset: windows_mingw
msystem: CLANG64 msystem: CLANG64
vcvars-arch: amd64_x86 vcvars-arch: amd64_x86
- os: windows-11-arm - os: windows-11-arm
artifact-name: Windows-MinGW-arm64 artifact-name: Windows-MinGW-arm64
base-cmake-preset: windows_mingw cmake-preset: windows_mingw
msystem: CLANGARM64 msystem: CLANGARM64
vcvars-arch: arm64 vcvars-arch: arm64
- os: windows-2022 - os: windows-2022
artifact-name: Windows-MSVC artifact-name: Windows-MSVC
base-cmake-preset: windows_msvc cmake-preset: windows_msvc
# TODO(@getchoo): This is the default in setup-dependencies/windows. Why isn't it working?!?! # TODO(@getchoo): This is the default in setup-dependencies/windows. Why isn't it working?!?!
vcvars-arch: amd64 vcvars-arch: amd64
- os: windows-11-arm - os: windows-11-arm
artifact-name: Windows-MSVC-arm64 artifact-name: Windows-MSVC-arm64
base-cmake-preset: windows_msvc cmake-preset: windows_msvc
vcvars-arch: arm64 vcvars-arch: arm64
- os: macos-14 - os: macos-14
artifact-name: macOS artifact-name: macOS
base-cmake-preset: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'macos_universal' || 'macos' }} cmake-preset: macos_universal
macosx-deployment-target: 12.0 macosx-deployment-target: 12.0
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -124,6 +126,11 @@ jobs:
shell: ${{ matrix.msystem != '' && 'msys2 {0}' || 'bash' }} shell: ${{ matrix.msystem != '' && 'msys2 {0}' || 'bash' }}
env: env:
ARTIFACT_NAME: ${{ matrix.artifact-name }}-Qt6
BUILD_PLATFORM: official
BUILD_TYPE: ${{ inputs.build-type || 'Debug' }}
CMAKE_PRESET: ${{ matrix.cmake-preset }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx-deployment-target }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx-deployment-target }}
steps: steps:
@ -140,7 +147,7 @@ jobs:
id: setup-dependencies id: setup-dependencies
uses: ./.github/actions/setup-dependencies uses: ./.github/actions/setup-dependencies
with: with:
build-type: ${{ inputs.build-type || 'Debug' }} build-type: ${{ env.BUILD_TYPE }}
artifact-name: ${{ matrix.artifact-name }} artifact-name: ${{ matrix.artifact-name }}
msystem: ${{ matrix.msystem }} msystem: ${{ matrix.msystem }}
vcvars-arch: ${{ matrix.vcvars-arch }} vcvars-arch: ${{ matrix.vcvars-arch }}
@ -150,22 +157,17 @@ jobs:
# BUILD # BUILD
## ##
- name: Get CMake preset - name: Configure project
id: cmake-preset
env:
BASE_CMAKE_PRESET: ${{ matrix.base-cmake-preset }}
PRESET_TYPE: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'debug' || 'ci' }}
run: | run: |
echo preset="$BASE_CMAKE_PRESET"_"$PRESET_TYPE" >> "$GITHUB_OUTPUT" cmake --preset "$CMAKE_PRESET"
- name: Run CMake workflow - name: Run build
env:
CMAKE_PRESET: ${{ steps.cmake-preset.outputs.preset }}
ARTIFACT_NAME: ${{ matrix.artifact-name }}-Qt6
BUILD_PLATFORM: official
run: | run: |
cmake --workflow --preset "$CMAKE_PRESET" cmake --build --preset "$CMAKE_PRESET" --config "$BUILD_TYPE"
- name: Run tests
run: |
ctest --preset "$CMAKE_PRESET" --build-config "$BUILD_TYPE"
## ##
# PACKAGE # PACKAGE
@ -184,7 +186,6 @@ jobs:
version: ${{ steps.short-version.outputs.version }} version: ${{ steps.short-version.outputs.version }}
build-type: ${{ steps.setup-dependencies.outputs.build-type }} build-type: ${{ steps.setup-dependencies.outputs.build-type }}
artifact-name: ${{ matrix.artifact-name }} artifact-name: ${{ matrix.artifact-name }}
cmake-preset: ${{ steps.cmake-preset.outputs.preset }}
qt-version: ${{ steps.setup-dependencies.outputs.qt-version }} qt-version: ${{ steps.setup-dependencies.outputs.qt-version }}
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
@ -198,13 +199,13 @@ jobs:
build-type: ${{ steps.setup-dependencies.outputs.build-type }} build-type: ${{ steps.setup-dependencies.outputs.build-type }}
artifact-name: ${{ matrix.artifact-name }} artifact-name: ${{ matrix.artifact-name }}
apple-codesign-cert: ${{ secrets.APPLE-CODESIGN-CERT }} apple-codesign-cert: ${{ secrets.APPLE_CODESIGN_CERT }}
apple-codesign-password: ${{ secrets.APPLE-CODESIGN_PASSWORD }} apple-codesign-password: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
apple-codesign-id: ${{ secrets.APPLE-CODESIGN_ID }} apple-codesign-id: ${{ secrets.APPLE_CODESIGN_ID }}
apple-notarize-apple-id: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} apple-notarize-apple-id: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
apple-notarize-team-id: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} apple-notarize-team-id: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
apple-notarize-password: ${{ secrets.APPLE-NOTARIZE_PASSWORD }} apple-notarize-password: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
sparkle-ed25519-key: ${{ secrets.SPARKLE-ED25519_KEY }} sparkle-ed25519-key: ${{ secrets.SPARKLE_ED25519_KEY }}
- name: Package (Windows) - name: Package (Windows)
if: ${{ runner.os == 'Windows' }} if: ${{ runner.os == 'Windows' }}
@ -215,5 +216,6 @@ jobs:
artifact-name: ${{ matrix.artifact-name }} artifact-name: ${{ matrix.artifact-name }}
msystem: ${{ matrix.msystem }} msystem: ${{ matrix.msystem }}
windows-codesign-cert: ${{ secrets.WINDOWS_CODESIGN_CERT }} azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
windows-codesign-password: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

View file

@ -78,8 +78,8 @@ jobs:
- name: Configure and Build - name: Configure and Build
run: | run: |
cmake --preset linux_debug cmake --preset linux
cmake --build --preset linux_debug cmake --build --preset linux --config Debug
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4 uses: github/codeql-action/analyze@v4

View file

@ -17,7 +17,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31 - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: DeterminateSystems/update-flake-lock@v27 - uses: DeterminateSystems/update-flake-lock@v27
with: with:

3
.gitmodules vendored
View file

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

View file

@ -112,7 +112,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}"
# Export compile commands for debug builds if we can (useful in LSPs like clangd) # Export compile commands for debug builds if we can (useful in LSPs like clangd)
# https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html # https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html
if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR STREQUAL "Ninja" AND CMAKE_BUILD_TYPE STREQUAL "Debug") if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR MATCHES "^Ninja")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
endif() endif()
@ -345,12 +345,24 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS}) set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS})
endif() endif()
# Find libqrencode
## NOTE(@getchoo): Never use pkg-config with MSVC since the vcpkg port makes our install bundle fail to find the dll
if(MSVC)
find_path(LIBQRENCODE_INCLUDE_DIR qrencode.h REQUIRED)
find_library(LIBQRENCODE_LIBRARY_RELEASE qrencode REQUIRED)
find_library(LIBQRENCODE_LIBRARY_DEBUG qrencoded)
set(LIBQRENCODE_LIBRARIES optimized ${LIBQRENCODE_LIBRARY_RELEASE} debug ${LIBQRENCODE_LIBRARY_DEBUG})
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(libqrencode REQUIRED IMPORTED_TARGET libqrencode)
endif()
if(NOT Launcher_FORCE_BUNDLED_LIBS) if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find toml++ # Find toml++
find_package(tomlplusplus 3.2.0 QUIET) find_package(tomlplusplus 3.2.0 QUIET)
# Fallback to pkg-config (if available) if CMake files aren't found # Fallback to pkg-config (if available) if CMake files aren't found
if(NOT tomlplusplus_FOUND) if(NOT tomlplusplus_FOUND)
find_package(PkgConfig) find_package(PkgConfig QUIET)
if(PkgConfig_FOUND) if(PkgConfig_FOUND)
pkg_check_modules(tomlplusplus IMPORTED_TARGET tomlplusplus>=3.2.0) pkg_check_modules(tomlplusplus IMPORTED_TARGET tomlplusplus>=3.2.0)
endif() endif()
@ -359,9 +371,6 @@ if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find cmark # Find cmark
find_package(cmark QUIET) find_package(cmark QUIET)
# Find qrcodegencpp-cmake
find_package(qrcodegencpp QUIET)
endif() endif()
include(ECMQtDeclareLoggingCategory) include(ECMQtDeclareLoggingCategory)
@ -405,8 +414,8 @@ if(UNIX AND APPLE)
set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed")
set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed")
set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz" CACHE STRING "URL to Sparkle release archive") set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.8.0/Sparkle-2.8.0.tar.xz" CACHE STRING "URL to Sparkle release archive")
set(MACOSX_SPARKLE_SHA256 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" CACHE STRING "SHA256 checksum for Sparkle release archive") set(MACOSX_SPARKLE_SHA256 "fd5681ee92bf238aaac2d08214ceaf0cc8976e452d7f882d80bac1e61581f3b1" CACHE STRING "SHA256 checksum for Sparkle release archive")
set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle")
if(NOT MACOSX_SPARKLE_UPDATE_PUBLIC_KEY STREQUAL "" AND NOT MACOSX_SPARKLE_UPDATE_FEED_URL STREQUAL "") if(NOT MACOSX_SPARKLE_UPDATE_PUBLIC_KEY STREQUAL "" AND NOT MACOSX_SPARKLE_UPDATE_FEED_URL STREQUAL "")
@ -522,18 +531,6 @@ if(NOT cmark_FOUND)
else() else()
message(STATUS "Using system cmark") message(STATUS "Using system cmark")
endif() endif()
if(NOT qrcodegencpp_FOUND)
set(QRCODE_SOURCES
libraries/qrcodegenerator/cpp/qrcodegen.cpp
libraries/qrcodegenerator/cpp/qrcodegen.hpp
)
add_library(qrcodegenerator STATIC ${QRCODE_SOURCES})
target_include_directories(qrcodegenerator PUBLIC "libraries/qrcodegenerator/cpp/" )
generate_export_header(qrcodegenerator)
else()
add_library(qrcodegenerator ALIAS qrcodegencpp::qrcodegencpp)
message(STATUS "Using system qrcodegencpp-cmake")
endif()
add_subdirectory(libraries/gamemode) add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
add_subdirectory(libraries/qdcss) # css parser add_subdirectory(libraries/qdcss) # css parser

View file

@ -5,10 +5,210 @@
"major": 3, "major": 3,
"minor": 28 "minor": 28
}, },
"include": [ "configurePresets": [
"cmake/linuxPreset.json", {
"cmake/macosPreset.json", "name": "base",
"cmake/windowsMinGWPreset.json", "hidden": true,
"cmake/windowsMSVCPreset.json" "binaryDir": "build",
"installDir": "install",
"generator": "Ninja Multi-Config",
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}",
"Launcher_BUILD_PLATFORM": "$penv{BUILD_PLATFORM}",
"Launcher_ENABLE_JAVA_DOWNLOADER": "ON",
"ENABLE_LTO": "ON"
}
},
{
"name": "linux",
"displayName": "Linux",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "macos",
"displayName": "macOS",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
},
{
"name": "macos_universal",
"displayName": "macOS (Universal Binary)",
"inherits": [
"macos"
],
"cacheVariables": {
"CMAKE_OSX_ARCHITECTURES": "x86_64;arm64",
"VCPKG_TARGET_TRIPLET": "universal-osx"
}
},
{
"name": "windows_mingw",
"displayName": "Windows (MinGW)",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "windows_msvc",
"displayName": "Windows (MSVC)",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
}
],
"buildPresets": [
{
"name": "linux",
"displayName": "Linux",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"configurePreset": "linux"
},
{
"name": "macos",
"displayName": "macOS",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
},
"configurePreset": "macos"
},
{
"name": "macos_universal",
"displayName": "macOS (Universal Binary)",
"inherits": [
"macos"
],
"configurePreset": "macos_universal"
},
{
"name": "windows_mingw",
"displayName": "Windows (MinGW)",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"configurePreset": "windows_mingw"
},
{
"name": "windows_msvc",
"displayName": "Windows (MSVC)",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"configurePreset": "windows_msvc"
}
],
"testPresets": [
{
"name": "base",
"hidden": true,
"output": {
"outputOnFailure": true
},
"execution": {
"noTestsAction": "error"
},
"filter": {
"exclude": {
"name": "^example64|example$"
}
}
},
{
"name": "linux",
"displayName": "Linux",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"configurePreset": "linux"
},
{
"name": "macos",
"displayName": "macOS",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
},
"configurePreset": "macos"
},
{
"name": "macos_universal",
"displayName": "macOS (Universal Binary)",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
},
"configurePreset": "macos_universal"
},
{
"name": "windows_mingw",
"displayName": "Windows (MinGW)",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"configurePreset": "windows_mingw"
},
{
"name": "windows_msvc",
"displayName": "Windows (MSVC)",
"inherits": [
"base"
],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"configurePreset": "windows_msvc"
}
] ]
} }

View file

@ -404,14 +404,23 @@
You should have received a copy of the GNU Lesser General Public You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>. License along with this library. If not, see <http://www.gnu.org/licenses/>.
## QR-Code-generator (`libraries/qrcodegenerator`) ## libqrencode (`fukuchi/libqrencode`)
Copyright © 2024 Project Nayuki. (MIT License) Copyright (C) 2020 libqrencode Authors
https://www.nayuki.io/page/qr-code-generator-library
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: This library is free software; you can redistribute it and/or
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. modify it under the terms of the GNU Lesser General Public
- The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software. License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
## vcpkg (`cmake/vcpkg-ports`) ## vcpkg (`cmake/vcpkg-ports`)

View file

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

View file

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

View file

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

View file

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

View file

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

25
flake.lock generated
View file

@ -18,11 +18,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1761907660, "lastModified": 1762977756,
"narHash": "sha256-kJ8lIZsiPOmbkJypG+B5sReDXSD1KGu2VEPNqhRa/ew=", "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15", "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -32,27 +32,10 @@
"type": "github" "type": "github"
} }
}, },
"qrcodegenerator": {
"flake": false,
"locked": {
"lastModified": 1737616857,
"narHash": "sha256-6SugPt0lp1Gz7nV23FLmsmpfzgFItkSw7jpGftsDPWc=",
"owner": "nayuki",
"repo": "QR-Code-generator",
"rev": "2c9044de6b049ca25cb3cd1649ed7e27aa055138",
"type": "github"
},
"original": {
"owner": "nayuki",
"repo": "QR-Code-generator",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"libnbtplusplus": "libnbtplusplus", "libnbtplusplus": "libnbtplusplus",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs"
"qrcodegenerator": "qrcodegenerator"
} }
} }
}, },

View file

@ -15,11 +15,6 @@
url = "github:PrismLauncher/libnbtplusplus"; url = "github:PrismLauncher/libnbtplusplus";
flake = false; flake = false;
}; };
qrcodegenerator = {
url = "github:nayuki/QR-Code-generator";
flake = false;
};
}; };
outputs = outputs =
@ -27,7 +22,6 @@
self, self,
nixpkgs, nixpkgs,
libnbtplusplus, libnbtplusplus,
qrcodegenerator,
}: }:
let let
@ -175,7 +169,6 @@
prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix {
inherit inherit
libnbtplusplus libnbtplusplus
qrcodegenerator
self self
; ;
}; };

View file

@ -108,8 +108,6 @@
#include "icons/IconList.h" #include "icons/IconList.h"
#include "net/HttpMetaCache.h" #include "net/HttpMetaCache.h"
#include "java/JavaInstallList.h"
#include "updater/ExternalUpdater.h" #include "updater/ExternalUpdater.h"
#include "tools/JProfiler.h" #include "tools/JProfiler.h"
@ -127,7 +125,6 @@
#include <LocalPeer.h> #include <LocalPeer.h>
#include <stdlib.h> #include <stdlib.h>
#include <QStringLiteral>
#include "SysInfo.h" #include "SysInfo.h"
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
@ -708,6 +705,16 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("SkinsDir", "skins"); m_settings->registerSetting("SkinsDir", "skins");
m_settings->registerSetting("JavaDir", "java"); m_settings->registerSetting("JavaDir", "java");
#ifdef Q_OS_MACOS
// Folder security-scoped bookmarks
m_settings->registerSetting("InstanceDirBookmark", "");
m_settings->registerSetting("CentralModsDirBookmark", "");
m_settings->registerSetting("IconsDirBookmark", "");
m_settings->registerSetting("DownloadsDirBookmark", "");
m_settings->registerSetting("SkinsDirBookmark", "");
m_settings->registerSetting("JavaDirBookmark", "");
#endif
// Editors // Editors
m_settings->registerSetting("JsonEditor", QString()); m_settings->registerSetting("JsonEditor", QString());
@ -958,12 +965,27 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// Themes // Themes
m_themeManager = std::make_unique<ThemeManager>(); m_themeManager = std::make_unique<ThemeManager>();
#ifdef Q_OS_MACOS
// for macOS: getting directory settings will generate URL security-scoped bookmarks if needed and not present
// this facilitates a smooth transition from a non-sandboxed version of the launcher, that likely can access the directory,
// and a sandboxed version that can't access the directory without a bookmark
// this section can likely be removed once the sandboxed version has been released for a while and migrations aren't done anymore
{
m_settings->get("InstanceDir");
m_settings->get("CentralModsDir");
m_settings->get("IconsDir");
m_settings->get("DownloadsDir");
m_settings->get("SkinsDir");
m_settings->get("JavaDir");
}
#endif
// initialize and load all instances // initialize and load all instances
{ {
auto InstDirSetting = m_settings->getSetting("InstanceDir"); auto InstDirSetting = m_settings->getSetting("InstanceDir");
// instance path: check for problems with '!' in instance path and warn the user in the log // instance path: check for problems with '!' in instance path and warn the user in the log
// and remember that we have to show him a dialog when the gui starts (if it does so) // and remember that we have to show him a dialog when the gui starts (if it does so)
QString instDir = InstDirSetting->get().toString(); QString instDir = m_settings->get("InstanceDir").toString();
qInfo() << "Instance path : " << instDir; qInfo() << "Instance path : " << instDir;
if (FS::checkProblemticPathJava(QDir(instDir))) { if (FS::checkProblemticPathJava(QDir(instDir))) {
qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!"; qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!";

View file

@ -1053,8 +1053,6 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ImportResourceDialog.h ui/dialogs/ImportResourceDialog.h
ui/dialogs/MSALoginDialog.cpp ui/dialogs/MSALoginDialog.cpp
ui/dialogs/MSALoginDialog.h ui/dialogs/MSALoginDialog.h
ui/dialogs/OfflineLoginDialog.cpp
ui/dialogs/OfflineLoginDialog.h
ui/dialogs/NewComponentDialog.cpp ui/dialogs/NewComponentDialog.cpp
ui/dialogs/NewComponentDialog.h ui/dialogs/NewComponentDialog.h
ui/dialogs/NewInstanceDialog.cpp ui/dialogs/NewInstanceDialog.cpp
@ -1081,6 +1079,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ResourceUpdateDialog.h ui/dialogs/ResourceUpdateDialog.h
ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.cpp
ui/dialogs/InstallLoaderDialog.h ui/dialogs/InstallLoaderDialog.h
ui/dialogs/ChooseOfflineNameDialog.cpp
ui/dialogs/ChooseOfflineNameDialog.h
ui/dialogs/skins/SkinManageDialog.cpp ui/dialogs/skins/SkinManageDialog.cpp
ui/dialogs/skins/SkinManageDialog.h ui/dialogs/skins/SkinManageDialog.h
@ -1173,6 +1173,15 @@ if (NOT Apple)
) )
endif() endif()
if (APPLE)
set(LAUNCHER_SOURCES
${LAUNCHER_SOURCES}
macsandbox/SecurityBookmarkFileAccess.h
macsandbox/SecurityBookmarkFileAccess.mm
)
endif()
if(WIN32) if(WIN32)
set(LAUNCHER_SOURCES set(LAUNCHER_SOURCES
console/WindowsConsole.h console/WindowsConsole.h
@ -1234,13 +1243,13 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/IconPickerDialog.ui ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourceDialog.ui ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui ui/dialogs/AboutDialog.ui
ui/dialogs/ReviewMessageBox.ui ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui ui/dialogs/ScrollMessageBox.ui
ui/dialogs/BlockedModsDialog.ui ui/dialogs/BlockedModsDialog.ui
ui/dialogs/ChooseProviderDialog.ui ui/dialogs/ChooseProviderDialog.ui
ui/dialogs/skins/SkinManageDialog.ui ui/dialogs/skins/SkinManageDialog.ui
ui/dialogs/ChooseOfflineNameDialog.ui
) )
qt_wrap_ui(PRISM_UPDATE_UI qt_wrap_ui(PRISM_UPDATE_UI
@ -1296,8 +1305,15 @@ target_link_libraries(Launcher_logic
qdcss qdcss
BuildConfig BuildConfig
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
qrcodegenerator
) )
if(TARGET PkgConfig::libqrencode)
target_link_libraries(Launcher_logic PkgConfig::libqrencode)
else()
target_include_directories(Launcher_logic PRIVATE ${LIBQRENCODE_INCLUDE_DIR})
target_link_libraries(Launcher_logic ${LIBQRENCODE_LIBRARIES})
endif()
if(TARGET PkgConfig::tomlplusplus) if(TARGET PkgConfig::tomlplusplus)
target_link_libraries(Launcher_logic PkgConfig::tomlplusplus) target_link_libraries(Launcher_logic PkgConfig::tomlplusplus)
else() else()

View file

@ -59,10 +59,8 @@
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
#define NOMINMAX #define NOMINMAX
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#include <objbase.h>
#include <objidl.h> #include <objidl.h>
#include <shlguid.h> #include <shlguid.h>
#include <shlobj.h>
#include <shobjidl.h> #include <shobjidl.h>
#include <sys/utime.h> #include <sys/utime.h>
#include <versionhelpers.h> #include <versionhelpers.h>

View file

@ -153,7 +153,7 @@ QJsonValue toJson<QVariant>(const QVariant& variant)
template <> template <>
QByteArray requireIsType<QByteArray>(const QJsonValue& value, const QString& what) QByteArray requireIsType<QByteArray>(const QJsonValue& value, const QString& what)
{ {
const QString string = ensureIsType<QString>(value, what); const QString string = value.toString(what);
// ensure that the string can be safely cast to Latin1 // ensure that the string can be safely cast to Latin1
if (string != QString::fromLatin1(string.toLatin1())) { if (string != QString::fromLatin1(string.toLatin1())) {
throw JsonException(what + " is not encodable as Latin1"); throw JsonException(what + " is not encodable as Latin1");
@ -221,7 +221,7 @@ QDateTime requireIsType<QDateTime>(const QJsonValue& value, const QString& what)
template <> template <>
QUrl requireIsType<QUrl>(const QJsonValue& value, const QString& what) QUrl requireIsType<QUrl>(const QJsonValue& value, const QString& what)
{ {
const QString string = ensureIsType<QString>(value, what); const QString string = value.toString(what);
if (string.isEmpty()) { if (string.isEmpty()) {
return QUrl(); return QUrl();
} }
@ -287,7 +287,7 @@ QStringList toStringList(const QString& jsonString)
if (parseError.error != QJsonParseError::NoError || !doc.isArray()) if (parseError.error != QJsonParseError::NoError || !doc.isArray())
return {}; return {};
try { try {
return ensureIsArrayOf<QString>(doc.array(), ""); return requireIsArrayOf<QString>(doc);
} catch (Json::JsonException& e) { } catch (Json::JsonException& e) {
return {}; return {};
} }

View file

@ -153,18 +153,6 @@ QUrl requireIsType<QUrl>(const QJsonValue& value, const QString& what);
// the following functions are higher level functions, that make use of the above functions for // the following functions are higher level functions, that make use of the above functions for
// type conversion // type conversion
template <typename T>
T ensureIsType(const QJsonValue& value, const T default_ = T(), const QString& what = "Value")
{
if (value.isUndefined() || value.isNull()) {
return default_;
}
try {
return requireIsType<T>(value, what);
} catch (const JsonException&) {
return default_;
}
}
/// @throw JsonException /// @throw JsonException
template <typename T> template <typename T>
@ -177,16 +165,6 @@ T requireIsType(const QJsonObject& parent, const QString& key, const QString& wh
return requireIsType<T>(parent.value(key), localWhat); return requireIsType<T>(parent.value(key), localWhat);
} }
template <typename T>
T ensureIsType(const QJsonObject& parent, const QString& key, const T default_ = T(), const QString& what = "__placeholder__")
{
const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
if (!parent.contains(key)) {
return default_;
}
return ensureIsType<T>(parent.value(key), default_, localWhat);
}
template <typename T> template <typename T>
QList<T> requireIsArrayOf(const QJsonDocument& doc) QList<T> requireIsArrayOf(const QJsonDocument& doc)
{ {
@ -198,26 +176,6 @@ QList<T> requireIsArrayOf(const QJsonDocument& doc)
return out; return out;
} }
template <typename T>
QList<T> ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value")
{
const QJsonArray array = ensureIsType<QJsonArray>(value, QJsonArray(), what);
QList<T> out;
for (const QJsonValue val : array) {
out.append(requireIsType<T>(val, what));
}
return out;
}
template <typename T>
QList<T> ensureIsArrayOf(const QJsonValue& value, const QList<T> default_, const QString& what = "Value")
{
if (value.isUndefined()) {
return default_;
}
return ensureIsArrayOf<T>(value, what);
}
/// @throw JsonException /// @throw JsonException
template <typename T> template <typename T>
QList<T> requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") QList<T> requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__")
@ -226,20 +184,13 @@ QList<T> requireIsArrayOf(const QJsonObject& parent, const QString& key, const Q
if (!parent.contains(key)) { if (!parent.contains(key)) {
throw JsonException(localWhat + "s parent does not contain " + localWhat); throw JsonException(localWhat + "s parent does not contain " + localWhat);
} }
return ensureIsArrayOf<T>(parent.value(key), localWhat);
}
template <typename T> const QJsonArray array = parent[key].toArray();
QList<T> ensureIsArrayOf(const QJsonObject& parent, QList<T> out;
const QString& key, for (const QJsonValue val : array) {
const QList<T>& default_ = QList<T>(), out.append(requireIsType<T>(val, "Document"));
const QString& what = "__placeholder__")
{
const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
if (!parent.contains(key)) {
return default_;
} }
return ensureIsArrayOf<T>(parent.value(key), default_, localWhat); return out;
} }
// this macro part could be replaced by variadic functions that just pass on their arguments, but that wouldn't work well with IDE helpers // this macro part could be replaced by variadic functions that just pass on their arguments, but that wouldn't work well with IDE helpers
@ -248,18 +199,9 @@ QList<T> ensureIsArrayOf(const QJsonObject& parent,
{ \ { \
return requireIsType<TYPE>(value, what); \ return requireIsType<TYPE>(value, what); \
} \ } \
inline TYPE ensure##NAME(const QJsonValue& value, const TYPE default_ = TYPE(), const QString& what = "Value") \
{ \
return ensureIsType<TYPE>(value, default_, what); \
} \
inline TYPE require##NAME(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") \ inline TYPE require##NAME(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") \
{ \ { \
return requireIsType<TYPE>(parent, key, what); \ return requireIsType<TYPE>(parent, key, what); \
} \
inline TYPE ensure##NAME(const QJsonObject& parent, const QString& key, const TYPE default_ = TYPE(), \
const QString& what = "__placeholder") \
{ \
return ensureIsType<TYPE>(parent, key, default_, what); \
} }
JSON_HELPERFUNCTIONS(Array, QJsonArray) JSON_HELPERFUNCTIONS(Array, QJsonArray)

View file

@ -61,6 +61,7 @@
#include "JavaCommon.h" #include "JavaCommon.h"
#include "launch/steps/TextPrint.h" #include "launch/steps/TextPrint.h"
#include "tasks/Task.h" #include "tasks/Task.h"
#include "ui/dialogs/ChooseOfflineNameDialog.h"
LaunchController::LaunchController() : Task() {} LaunchController::LaunchController() : Task() {}
@ -157,10 +158,15 @@ QString LaunchController::askOfflineName(QString playerName, bool demo, bool& ok
QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString();
QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName; QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName;
QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedname, &ok);
if (!ok) ChooseOfflineNameDialog dialog(message, m_parentWidget);
dialog.setWindowTitle(tr("Player name"));
dialog.setUsername(usedname);
if (dialog.exec() != QDialog::Accepted) {
return {}; return {};
if (name.length()) { }
if (const QString name = dialog.getUsername(); !name.isEmpty()) {
usedname = name; usedname = name;
APPLICATION->settings()->set("LastOfflinePlayerName", usedname); APPLICATION->settings()->set("LastOfflinePlayerName", usedname);
} }

View file

@ -111,8 +111,8 @@ QString getLibraryString()
try { try {
auto conf = Json::requireDocument(filePath, vkLayer); auto conf = Json::requireDocument(filePath, vkLayer);
auto confObject = Json::requireObject(conf, vkLayer); auto confObject = Json::requireObject(conf, vkLayer);
auto layer = Json::ensureObject(confObject, "layer"); auto layer = confObject["layer"].toObject();
QString libraryName = Json::ensureString(layer, "library_path"); QString libraryName = layer["library_path"].toString();
if (libraryName.isEmpty()) { if (libraryName.isEmpty()) {
continue; continue;

View file

@ -52,27 +52,27 @@ MetadataPtr parseJavaMeta(const QJsonObject& in)
{ {
auto meta = std::make_shared<Metadata>(); auto meta = std::make_shared<Metadata>();
meta->m_name = Json::ensureString(in, "name", ""); meta->m_name = in["name"].toString("");
meta->vendor = Json::ensureString(in, "vendor", ""); meta->vendor = in["vendor"].toString("");
meta->url = Json::ensureString(in, "url", ""); meta->url = in["url"].toString("");
meta->releaseTime = timeFromS3Time(Json::ensureString(in, "releaseTime", "")); meta->releaseTime = timeFromS3Time(in["releaseTime"].toString(""));
meta->downloadType = parseDownloadType(Json::ensureString(in, "downloadType", "")); meta->downloadType = parseDownloadType(in["downloadType"].toString(""));
meta->packageType = Json::ensureString(in, "packageType", ""); meta->packageType = in["packageType"].toString("");
meta->runtimeOS = Json::ensureString(in, "runtimeOS", "unknown"); meta->runtimeOS = in["runtimeOS"].toString("unknown");
if (in.contains("checksum")) { if (in.contains("checksum")) {
auto obj = Json::requireObject(in, "checksum"); auto obj = Json::requireObject(in, "checksum");
meta->checksumHash = Json::ensureString(obj, "hash", ""); meta->checksumHash = obj["hash"].toString("");
meta->checksumType = Json::ensureString(obj, "type", ""); meta->checksumType = obj["type"].toString("");
} }
if (in.contains("version")) { if (in.contains("version")) {
auto obj = Json::requireObject(in, "version"); auto obj = Json::requireObject(in, "version");
auto name = Json::ensureString(obj, "name", ""); auto name = obj["name"].toString("");
auto major = Json::ensureInteger(obj, "major", 0); auto major = obj["major"].toInteger();
auto minor = Json::ensureInteger(obj, "minor", 0); auto minor = obj["minor"].toInteger();
auto security = Json::ensureInteger(obj, "security", 0); auto security = obj["security"].toInteger();
auto build = Json::ensureInteger(obj, "build", 0); auto build = obj["build"].toInteger();
meta->version = JavaVersion(major, minor, security, build, name); meta->version = JavaVersion(major, minor, security, build, name);
} }
return meta; return meta;

View file

@ -77,27 +77,27 @@ void ManifestDownloadTask::downloadJava(const QJsonDocument& doc)
// valid json doc, begin making jre spot // valid json doc, begin making jre spot
FS::ensureFolderPathExists(m_final_path); FS::ensureFolderPathExists(m_final_path);
std::vector<File> toDownload; std::vector<File> toDownload;
auto list = Json::ensureObject(Json::ensureObject(doc.object()), "files"); auto list = doc.object()["files"].toObject();
for (const auto& paths : list.keys()) { for (const auto& paths : list.keys()) {
auto file = FS::PathCombine(m_final_path, paths); auto file = FS::PathCombine(m_final_path, paths);
const QJsonObject& meta = Json::ensureObject(list, paths); const QJsonObject& meta = list[paths].toObject();
auto type = Json::ensureString(meta, "type"); auto type = meta["type"].toString();
if (type == "directory") { if (type == "directory") {
FS::ensureFolderPathExists(file); FS::ensureFolderPathExists(file);
} else if (type == "link") { } else if (type == "link") {
// this is *nix only ! // this is *nix only !
auto path = Json::ensureString(meta, "target"); auto path = meta["target"].toString();
if (!path.isEmpty()) { if (!path.isEmpty()) {
QFile::link(path, file); QFile::link(path, file);
} }
} else if (type == "file") { } else if (type == "file") {
// TODO download compressed version if it exists ? // TODO download compressed version if it exists ?
auto raw = Json::ensureObject(Json::ensureObject(meta, "downloads"), "raw"); auto raw = meta["downloads"].toObject()["raw"].toObject();
auto isExec = Json::ensureBoolean(meta, "executable", false); auto isExec = meta["executable"].toBool();
auto url = Json::ensureString(raw, "url"); auto url = raw["url"].toString();
if (!url.isEmpty() && QUrl(url).isValid()) { if (!url.isEmpty() && QUrl(url).isValid()) {
auto f = File{ file, url, QByteArray::fromHex(Json::ensureString(raw, "sha1").toLatin1()), isExec }; auto f = File{ file, url, QByteArray::fromHex(raw["sha1"].toString().toLatin1()), isExec };
toDownload.push_back(f); toDownload.push_back(f);
} }
} }
@ -134,4 +134,4 @@ bool ManifestDownloadTask::abort()
emitAborted(); emitAborted();
return aborted; return aborted;
}; };
} // namespace Java } // namespace Java

View file

@ -234,29 +234,40 @@ bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel::Enum level)
auto& model = *getLogModel(); auto& model = *getLogModel();
model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage)); model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage));
return false; return false;
} else { }
if (!items.isEmpty()) {
auto& model = *getLogModel(); if (items.isEmpty())
for (auto const& item : items) { return true;
if (std::holds_alternative<LogParser::LogEntry>(item)) {
auto entry = std::get<LogParser::LogEntry>(item); auto model = getLogModel();
auto msg = QString("[%1] [%2/%3] [%4]: %5") for (auto const& item : items) {
.arg(entry.timestamp.toString("HH:mm:ss")) if (std::holds_alternative<LogParser::LogEntry>(item)) {
.arg(entry.thread) auto entry = std::get<LogParser::LogEntry>(item);
.arg(entry.levelText) auto msg = QString("[%1] [%2/%3] [%4]: %5")
.arg(entry.logger) .arg(entry.timestamp.toString("HH:mm:ss"))
.arg(entry.message); .arg(entry.thread)
msg = censorPrivateInfo(msg); .arg(entry.levelText)
model.append(entry.level, msg); .arg(entry.logger)
} else if (std::holds_alternative<LogParser::PlainText>(item)) { .arg(entry.message);
auto msg = std::get<LogParser::PlainText>(item).message; msg = censorPrivateInfo(msg);
level = LogParser::guessLevel(msg, model.previousLevel()); model->append(entry.level, msg);
msg = censorPrivateInfo(msg); } else if (std::holds_alternative<LogParser::PlainText>(item)) {
model.append(level, msg); auto msg = std::get<LogParser::PlainText>(item).message;
}
} MessageLevel::Enum newLevel = MessageLevel::fromLine(msg);
if (newLevel == MessageLevel::Unknown)
newLevel = LogParser::guessLevel(line);
if (newLevel == MessageLevel::Unknown)
newLevel = model->previousLevel();
msg = censorPrivateInfo(msg);
model->append(newLevel, msg);
} }
} }
return true; return true;
} }
@ -273,23 +284,10 @@ void LaunchTask::onLogLine(QString line, MessageLevel::Enum level)
return; return;
} }
// if the launcher part set a log level, use it
auto innerLevel = MessageLevel::fromLine(line);
if (innerLevel != MessageLevel::Unknown) {
level = innerLevel;
}
auto& model = *getLogModel();
// If the level is still undetermined, guess level
if (level == MessageLevel::Unknown) {
level = LogParser::guessLevel(line, model.previousLevel());
}
// censor private user info // censor private user info
line = censorPrivateInfo(line); line = censorPrivateInfo(line);
model.append(level, line); getLogModel()->append(level, line);
} }
void LaunchTask::emitSucceeded() void LaunchTask::emitSucceeded()

View file

@ -169,8 +169,8 @@ bool LogModel::isOverFlow()
MessageLevel::Enum LogModel::previousLevel() MessageLevel::Enum LogModel::previousLevel()
{ {
if (!m_content.isEmpty()) { if (m_numLines > 0) {
return m_content.last().level; return m_content[m_numLines - 1].level;
} }
return MessageLevel::Unknown; return MessageLevel::Unknown;
} }

View file

@ -320,7 +320,7 @@ std::optional<LogParser::ParsedItem> LogParser::parseLog4J()
throw std::runtime_error("unreachable: already verified this was a complete log4j:Event"); throw std::runtime_error("unreachable: already verified this was a complete log4j:Event");
} }
MessageLevel::Enum LogParser::guessLevel(const QString& line, MessageLevel::Enum level) MessageLevel::Enum LogParser::guessLevel(const QString& line)
{ {
static const QRegularExpression LINE_WITH_LEVEL("^\\[(?<timestamp>[0-9:]+)\\] \\[[^/]+/(?<level>[^\\]]+)\\]"); static const QRegularExpression LINE_WITH_LEVEL("^\\[(?<timestamp>[0-9:]+)\\] \\[[^/]+/(?<level>[^\\]]+)\\]");
auto match = LINE_WITH_LEVEL.match(line); auto match = LINE_WITH_LEVEL.match(line);
@ -328,24 +328,26 @@ MessageLevel::Enum LogParser::guessLevel(const QString& line, MessageLevel::Enum
// New style logs from log4j // New style logs from log4j
QString timestamp = match.captured("timestamp"); QString timestamp = match.captured("timestamp");
QString levelStr = match.captured("level"); QString levelStr = match.captured("level");
level = MessageLevel::getLevel(levelStr);
return MessageLevel::getLevel(levelStr);
} else { } else {
// Old style forge logs // Old style forge logs
if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") ||
line.contains("[FINEST]")) line.contains("[FINEST]"))
level = MessageLevel::Info; return MessageLevel::Info;
if (line.contains("[SEVERE]") || line.contains("[STDERR]")) if (line.contains("[SEVERE]") || line.contains("[STDERR]"))
level = MessageLevel::Error; return MessageLevel::Error;
if (line.contains("[WARNING]")) if (line.contains("[WARNING]"))
level = MessageLevel::Warning; return MessageLevel::Warning;
if (line.contains("[DEBUG]")) if (line.contains("[DEBUG]"))
level = MessageLevel::Debug; return MessageLevel::Debug;
} }
if (level != MessageLevel::Unknown)
return level;
if (line.contains("overwriting existing")) if (line.contains("overwriting existing"))
return MessageLevel::Fatal; return MessageLevel::Fatal;
return MessageLevel::Info; if (line == "---- Minecraft Crash Report ----")
return MessageLevel::Error;
return MessageLevel::Unknown;
} }

View file

@ -59,7 +59,7 @@ class LogParser {
std::optional<Error> getError(); std::optional<Error> getError();
/// guess log level from a line of game log /// guess log level from a line of game log
static MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level); static MessageLevel::Enum guessLevel(const QString& line);
protected: protected:
std::optional<LogEntry> parseAttributes(); std::optional<LogEntry> parseAttributes();

View file

@ -0,0 +1,89 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef FILEACCESS_H
#define FILEACCESS_H
#include <QtCore/QMap>
#include <QtCore/QSet>
Q_FORWARD_DECLARE_OBJC_CLASS(NSData);
Q_FORWARD_DECLARE_OBJC_CLASS(NSURL);
Q_FORWARD_DECLARE_OBJC_CLASS(NSString);
Q_FORWARD_DECLARE_OBJC_CLASS(NSAutoreleasePool);
Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableDictionary);
Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableSet);
class QString;
class QByteArray;
class QUrl;
class SecurityBookmarkFileAccess {
/// The keys are bookmarks and the values are URLs.
NSMutableDictionary* m_bookmarks;
/// The keys are paths and the values are bookmarks.
NSMutableDictionary* m_paths;
/// Contains URLs that are currently being accessed.
NSMutableSet* m_activeURLs;
bool m_readOnly;
NSURL* securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale);
public:
/// \param readOnly A boolean indicating whether the bookmark should be read-only.
SecurityBookmarkFileAccess(bool readOnly = false);
~SecurityBookmarkFileAccess();
/// Get a security scoped bookmark from a URL.
///
/// The URL must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling
/// this function. Note that this is called implicitly if the user selects the directory from a file picker.
/// \param url The URL to get the security scoped bookmark from.
/// \return A QByteArray containing the security scoped bookmark.
QByteArray urlToSecurityScopedBookmark(const QUrl& url);
/// Get a security scoped bookmark from a path.
///
/// The path must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling
/// this function. Note that this is called implicitly if the user selects the directory from a file picker.
/// \param path The path to get the security scoped bookmark from.
/// \return A QByteArray containing the security scoped bookmark.
QByteArray pathToSecurityScopedBookmark(const QString& path);
/// Get a QUrl from a security scoped bookmark. If the bookmark is stale, isStale will be set to true and the bookmark will be updated.
///
/// You must check whether the URL is valid before using it.
/// \param bookmark The security scoped bookmark to get the URL from.
/// \param isStale A boolean that will be set to true if the bookmark is stale.
/// \return The URL from the security scoped bookmark.
QUrl securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale);
/// Makes the file or directory at the path pointed to by the bookmark accessible. Unlike `startAccessingSecurityScopedResource()`, this
/// class ensures that only one "access" is active at a time. Calling this function again after the security-scoped resource has
/// already been used will do nothing, and a single call to `stopUsingSecurityScopedBookmark()` will release the resource provided that
/// this is the only `SecurityBookmarkFileAccess` accessing the resource.
///
/// If the bookmark is stale, `isStale` will be set to true and the bookmark will be updated. Stored copies of the bookmark need to be
/// updated.
/// \param bookmark The security scoped bookmark to start accessing.
/// \param isStale A boolean that will be set to true if the bookmark is stale.
/// \return A boolean indicating whether the bookmark was successfully accessed.
bool startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale);
void stopUsingSecurityScopedBookmark(QByteArray& bookmark);
/// Returns true if access to the `path` is currently being maintained by this object.
bool isAccessingPath(const QString& path);
};
#endif //FILEACCESS_H

View file

@ -0,0 +1,172 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "SecurityBookmarkFileAccess.h"
#include <Foundation/Foundation.h>
#include <QByteArray>
#include <QUrl>
QByteArray SecurityBookmarkFileAccess::urlToSecurityScopedBookmark(const QUrl& url)
{
if (!url.isLocalFile())
return {};
NSError* error = nil;
NSURL* nsurl = [url.toNSURL() absoluteURL];
NSData* bookmark;
if ([m_paths objectForKey:[nsurl path]]) {
bookmark = m_paths[[nsurl path]];
} else {
bookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope |
(m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
}
if (error) {
return {};
}
// remove/reapply access to ensure that write access is immediately cut off for read-only bookmarks
// sometimes you need to call this twice to actually stop access (extra calls aren't harmful)
[nsurl stopAccessingSecurityScopedResource];
[nsurl stopAccessingSecurityScopedResource];
nsurl = [NSURL URLByResolvingBookmarkData:bookmark
options:NSURLBookmarkResolutionWithSecurityScope |
(m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
relativeToURL:nil
bookmarkDataIsStale:nil
error:&error];
m_paths[[nsurl path]] = bookmark;
m_bookmarks[bookmark] = nsurl;
QByteArray qBookmark = QByteArray::fromNSData(bookmark);
bool isStale = false;
startUsingSecurityScopedBookmark(qBookmark, isStale);
return qBookmark;
}
SecurityBookmarkFileAccess::SecurityBookmarkFileAccess(bool readOnly) : m_readOnly(readOnly)
{
m_bookmarks = [NSMutableDictionary new];
m_paths = [NSMutableDictionary new];
m_activeURLs = [NSMutableSet new];
}
SecurityBookmarkFileAccess::~SecurityBookmarkFileAccess()
{
for (NSURL* url : m_activeURLs) {
[url stopAccessingSecurityScopedResource];
}
}
QByteArray SecurityBookmarkFileAccess::pathToSecurityScopedBookmark(const QString& path)
{
return urlToSecurityScopedBookmark(QUrl::fromLocalFile(path));
}
NSURL* SecurityBookmarkFileAccess::securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale)
{
NSError* error = nil;
BOOL localStale = NO;
NSURL* nsurl = [NSURL URLByResolvingBookmarkData:bookmark.toNSData()
options:NSURLBookmarkResolutionWithSecurityScope |
(m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
relativeToURL:nil
bookmarkDataIsStale:&localStale
error:&error];
if (error) {
return nil;
}
isStale = localStale;
if (isStale) {
NSData* nsBookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope |
(m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0)
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
if (error) {
return nil;
}
bookmark = QByteArray::fromNSData(nsBookmark);
}
NSData* nsBookmark = bookmark.toNSData();
m_paths[[nsurl path]] = nsBookmark;
m_bookmarks[nsBookmark] = nsurl;
return nsurl;
}
QUrl SecurityBookmarkFileAccess::securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale)
{
if (bookmark.isEmpty())
return {};
NSURL* url = securityScopedBookmarkToNSURL(bookmark, isStale);
if (!url)
return {};
return QUrl::fromNSURL(url);
}
bool SecurityBookmarkFileAccess::startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale)
{
NSURL* url = [m_bookmarks objectForKey:bookmark.toNSData()] ? m_bookmarks[bookmark.toNSData()]
: securityScopedBookmarkToNSURL(bookmark, isStale);
if ([m_activeURLs containsObject:url])
return false;
[url stopAccessingSecurityScopedResource];
if ([url startAccessingSecurityScopedResource]) {
[m_activeURLs addObject:url];
return true;
}
return false;
}
void SecurityBookmarkFileAccess::stopUsingSecurityScopedBookmark(QByteArray& bookmark)
{
if (![m_bookmarks objectForKey:bookmark.toNSData()])
return;
NSURL* url = m_bookmarks[bookmark.toNSData()];
if ([m_activeURLs containsObject:url]) {
[url stopAccessingSecurityScopedResource];
[url stopAccessingSecurityScopedResource];
[m_activeURLs removeObject:url];
[m_paths removeObjectForKey:[url path]];
[m_bookmarks removeObjectForKey:bookmark.toNSData()];
}
}
bool SecurityBookmarkFileAccess::isAccessingPath(const QString& path)
{
NSData* bookmark = [m_paths objectForKey:path.toNSString()];
if (!bookmark && path.endsWith('/')) {
bookmark = [m_paths objectForKey:path.left(path.length() - 1).toNSString()];
}
if (!bookmark) {
return false;
}
NSURL* url = [m_bookmarks objectForKey:bookmark];
return [m_activeURLs containsObject:url];
}

View file

@ -35,34 +35,8 @@
#include "Application.h" #include "Application.h"
// #define BREAK_INFINITE_LOOP
// #define BREAK_EXCEPTION
// #define BREAK_RETURN
#ifdef BREAK_INFINITE_LOOP
#include <chrono>
#include <thread>
#endif
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
#ifdef BREAK_INFINITE_LOOP
while (true) {
std::this_thread::sleep_for(std::chrono::milliseconds(250));
}
#endif
#ifdef BREAK_EXCEPTION
throw 42;
#endif
#ifdef BREAK_RETURN
return 42;
#endif
#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0)
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif
// initialize Qt // initialize Qt
Application app(argc, argv); Application app(argc, argv);

View file

@ -40,8 +40,8 @@ static std::shared_ptr<Index> parseIndexInternal(const QJsonObject& obj)
lists.reserve(objects.size()); lists.reserve(objects.size());
std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) { std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) {
VersionList::Ptr list = std::make_shared<VersionList>(requireString(obj, "uid")); VersionList::Ptr list = std::make_shared<VersionList>(requireString(obj, "uid"));
list->setName(ensureString(obj, "name", QString())); list->setName(obj["name"].toString());
list->setSha256(ensureString(obj, "sha256", QString())); list->setSha256(obj["sha256"].toString());
return list; return list;
}); });
return std::make_shared<Index>(lists); return std::make_shared<Index>(lists);
@ -52,14 +52,14 @@ static Version::Ptr parseCommonVersion(const QString& uid, const QJsonObject& ob
{ {
Version::Ptr version = std::make_shared<Version>(uid, requireString(obj, "version")); Version::Ptr version = std::make_shared<Version>(uid, requireString(obj, "version"));
version->setTime(QDateTime::fromString(requireString(obj, "releaseTime"), Qt::ISODate).toMSecsSinceEpoch() / 1000); version->setTime(QDateTime::fromString(requireString(obj, "releaseTime"), Qt::ISODate).toMSecsSinceEpoch() / 1000);
version->setType(ensureString(obj, "type", QString())); version->setType(obj["type"].toString());
version->setRecommended(ensureBoolean(obj, QString("recommended"), false)); version->setRecommended(obj["recommended"].toBool());
version->setVolatile(ensureBoolean(obj, QString("volatile"), false)); version->setVolatile(obj["volatile"].toBool());
RequireSet reqs, conflicts; RequireSet reqs, conflicts;
parseRequires(obj, &reqs, "requires"); parseRequires(obj, &reqs, "requires");
parseRequires(obj, &conflicts, "conflicts"); parseRequires(obj, &conflicts, "conflicts");
version->setRequires(reqs, conflicts); version->setRequires(reqs, conflicts);
if (auto sha256 = ensureString(obj, "sha256", QString()); !sha256.isEmpty()) { if (auto sha256 = obj["sha256"].toString(); !sha256.isEmpty()) {
version->setSha256(sha256); version->setSha256(sha256);
} }
return version; return version;
@ -89,7 +89,7 @@ static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj)
}); });
VersionList::Ptr list = std::make_shared<VersionList>(uid); VersionList::Ptr list = std::make_shared<VersionList>(uid);
list->setName(ensureString(obj, "name", QString())); list->setName(obj["name"].toString());
list->setVersions(versions); list->setVersions(versions);
return list; return list;
} }
@ -171,8 +171,8 @@ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char* keyName)
while (iter != reqArray.end()) { while (iter != reqArray.end()) {
auto reqObject = requireObject(*iter); auto reqObject = requireObject(*iter);
auto uid = requireString(reqObject, "uid"); auto uid = requireString(reqObject, "uid");
auto equals = ensureString(reqObject, "equals", QString()); auto equals = reqObject["equals"].toString();
auto suggests = ensureString(reqObject, "suggests", QString()); auto suggests = reqObject["suggests"].toString();
ptr->insert({ uid, equals, suggests }); ptr->insert({ uid, equals, suggests });
iter++; iter++;
} }

View file

@ -259,8 +259,8 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc
if (root.contains("runtimes")) { if (root.contains("runtimes")) {
out->runtimes = {}; out->runtimes = {};
for (auto runtime : ensureArray(root, "runtimes")) { for (auto runtime : root["runtimes"].toArray()) {
out->runtimes.append(Java::parseJavaMeta(ensureObject(runtime))); out->runtimes.append(Java::parseJavaMeta(runtime.toObject()));
} }
} }

View file

@ -130,18 +130,18 @@ static ComponentPtr componentFromJsonV1(PackProfile* parent, const QString& comp
auto uid = Json::requireString(obj.value("uid")); auto uid = Json::requireString(obj.value("uid"));
auto filePath = componentJsonPattern.arg(uid); auto filePath = componentJsonPattern.arg(uid);
auto component = makeShared<Component>(parent, uid); auto component = makeShared<Component>(parent, uid);
component->m_version = Json::ensureString(obj.value("version")); component->m_version = obj.value("version").toString();
component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false); component->m_dependencyOnly = obj.value("dependencyOnly").toBool();
component->m_important = Json::ensureBoolean(obj.value("important"), false); component->m_important = obj.value("important").toBool();
// cached // cached
// TODO @RESILIENCE: ignore invalid values/structure here? // TODO @RESILIENCE: ignore invalid values/structure here?
component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion")); component->m_cachedVersion = obj.value("cachedVersion").toString();
component->m_cachedName = Json::ensureString(obj.value("cachedName")); component->m_cachedName = obj.value("cachedName").toString();
Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires"); Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires");
Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts"); Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts");
component->m_cachedVolatile = Json::ensureBoolean(obj.value("volatile"), false); component->m_cachedVolatile = obj.value("volatile").toBool();
bool disabled = Json::ensureBoolean(obj.value("disabled"), false); bool disabled = obj.value("disabled").toBool();
component->setEnabled(!disabled); component->setEnabled(!disabled);
return component; return component;
} }

View file

@ -41,7 +41,6 @@
#include <QDateTime> #include <QDateTime>
#include <QMap> #include <QMap>
#include <QString>
#include <QVariantMap> #include <QVariantMap>
enum class Validity { None, Assumed, Certain }; enum class Validity { None, Assumed, Certain };

View file

@ -105,9 +105,8 @@ DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& d
} }
auto obj = doc.object(); auto obj = doc.object();
return { return {
Json::ensureString(obj, "device_code"), Json::ensureString(obj, "user_code"), Json::ensureString(obj, "verification_uri"), obj["device_code"].toString(), obj["user_code"].toString(), obj["verification_uri"].toString(), obj["expires_in"].toInt(),
Json::ensureInteger(obj, "expires_in"), Json::ensureInteger(obj, "interval"), Json::ensureString(obj, "error"), obj["interval"].toInt(), obj["error"].toString(), obj["error_description"].toString(),
Json::ensureString(obj, "error_description"),
}; };
} }
@ -217,12 +216,12 @@ AuthenticationResponse parseAuthenticationResponse(const QByteArray& data)
return {}; return {};
} }
auto obj = doc.object(); auto obj = doc.object();
return { Json::ensureString(obj, "access_token"), return { obj["access_token"].toString(),
Json::ensureString(obj, "token_type"), obj["token_type"].toString(),
Json::ensureString(obj, "refresh_token"), obj["refresh_token"].toString(),
Json::ensureInteger(obj, "expires_in"), obj["expires_in"].toInt(),
Json::ensureString(obj, "error"), obj["error"].toString(),
Json::ensureString(obj, "error_description"), obj["error_description"].toString(),
obj.toVariantMap() }; obj.toVariantMap() };
} }

View file

@ -21,11 +21,11 @@ class QSortFilterProxyModel;
/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ /* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */
#define RESOURCE_HELPERS(T) \ #define RESOURCE_HELPERS(T) \
T& at(int index) \ T& at(int index) \
{ \ { \
return *static_cast<T*>(m_resources[index].get()); \ return *static_cast<T*>(m_resources[index].get()); \
} \ } \
const T& at(int index) const \ const T& at(int index) const \
{ \ { \
return *static_cast<const T*>(m_resources.at(index).get()); \ return *static_cast<const T*>(m_resources.at(index).get()); \
} \ } \

View file

@ -180,7 +180,7 @@ bool processMCMeta(DataPack* pack, QByteArray&& raw_data)
auto json_doc = QJsonDocument::fromJson(raw_data); auto json_doc = QJsonDocument::fromJson(raw_data);
auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); auto pack_obj = Json::requireObject(json_doc.object(), "pack", {});
pack->setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); pack->setPackFormat(pack_obj["pack_format"].toInt());
pack->setDescription(DataPackUtils::processComponent(pack_obj.value("description"))); pack->setDescription(DataPackUtils::processComponent(pack_obj.value("description")));
} catch (Json::JsonException& e) { } catch (Json::JsonException& e) {
qWarning() << "JsonException: " << e.what() << e.cause(); qWarning() << "JsonException: " << e.what() << e.cause();
@ -192,19 +192,19 @@ bool processMCMeta(DataPack* pack, QByteArray&& raw_data)
QString buildStyle(const QJsonObject& obj) QString buildStyle(const QJsonObject& obj)
{ {
QStringList styles; QStringList styles;
if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) { if (auto color = obj["color"].toString(); !color.isEmpty()) {
styles << QString("color: %1;").arg(color); styles << QString("color: %1;").arg(color);
} }
if (obj.contains("bold")) { if (obj.contains("bold")) {
QString weight = "normal"; QString weight = "normal";
if (Json::ensureBoolean(obj, "bold", false)) { if (obj["bold"].toBool()) {
weight = "bold"; weight = "bold";
} }
styles << QString("font-weight: %1;").arg(weight); styles << QString("font-weight: %1;").arg(weight);
} }
if (obj.contains("italic")) { if (obj.contains("italic")) {
QString style = "normal"; QString style = "normal";
if (Json::ensureBoolean(obj, "italic", false)) { if (obj["italic"].toBool()) {
style = "italic"; style = "italic";
} }
styles << QString("font-style: %1;").arg(style); styles << QString("font-style: %1;").arg(style);
@ -223,10 +223,10 @@ QString processComponent(const QJsonArray& value, bool strikethrough, bool under
QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline)
{ {
underline = Json::ensureBoolean(obj, "underlined", underline); underline = obj["underlined"].toBool(underline);
strikethrough = Json::ensureBoolean(obj, "strikethrough", strikethrough); strikethrough = obj["strikethrough"].toBool(strikethrough);
QString result = Json::ensureString(obj, "text"); QString result = obj["text"].toString();
if (underline) { if (underline) {
result = QString("<u>%1</u>").arg(result); result = QString("<u>%1</u>").arg(result);
} }
@ -234,14 +234,14 @@ QString processComponent(const QJsonObject& obj, bool strikethrough, bool underl
result = QString("<s>%1</s>").arg(result); result = QString("<s>%1</s>").arg(result);
} }
// the extra needs to be a array // the extra needs to be a array
result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline); result += processComponent(obj["extra"].toArray(), strikethrough, underline);
if (auto style = buildStyle(obj); !style.isEmpty()) { if (auto style = buildStyle(obj); !style.isEmpty()) {
result = QString("<span %1>%2</span>").arg(style, result); result = QString("<span %1>%2</span>").arg(style, result);
} }
if (obj.contains("clickEvent")) { if (obj.contains("clickEvent")) {
auto click_event = Json::ensureObject(obj, "clickEvent"); auto click_event = obj["clickEvent"].toObject();
auto action = Json::ensureString(click_event, "action"); auto action = click_event["action"].toString();
auto value = Json::ensureString(click_event, "value"); auto value = click_event["value"].toString();
if (action == "open_url" && !value.isEmpty()) { if (action == "open_url" && !value.isEmpty()) {
result = QString("<a href=\"%1\">%2</a>").arg(value, result); result = QString("<a href=\"%1\">%2</a>").arg(value, result);
} }
@ -366,4 +366,4 @@ void LocalDataPackParseTask::executeTask()
} }
emitSucceeded(); emitSucceeded();
} }

View file

@ -75,11 +75,11 @@ ModDetails ReadMCModInfo(QByteArray contents)
val = jsonDoc.object().value("modListVersion"); val = jsonDoc.object().value("modListVersion");
} }
int version = Json::ensureInteger(val, -1); int version = val.toInt(-1);
// Some mods set the number with "", so it's a String instead // Some mods set the number with "", so it's a String instead
if (version < 0) if (version < 0)
version = Json::ensureString(val, "").toInt(); version = val.toString("").toInt();
if (version != 2) { if (version != 2) {
qWarning() << QString(R"(The value of 'modListVersion' is "%1" (expected "2")! The file may be corrupted.)").arg(version); qWarning() << QString(R"(The value of 'modListVersion' is "%1" (expected "2")! The file may be corrupted.)").arg(version);
@ -298,7 +298,7 @@ ModDetails ReadQuiltModInfo(QByteArray contents)
QJsonParseError jsonError; QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = Json::requireObject(jsonDoc, "quilt.mod.json"); auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version"); auto schemaVersion = object.value("schema_version").toInt();
// https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md
if (schemaVersion == 1) { if (schemaVersion == 1) {
@ -307,17 +307,17 @@ ModDetails ReadQuiltModInfo(QByteArray contents)
details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
details.version = Json::requireString(modInfo.value("version"), "Mod version"); details.version = Json::requireString(modInfo.value("version"), "Mod version");
auto modMetadata = Json::ensureObject(modInfo.value("metadata")); auto modMetadata = modInfo.value("metadata").toObject();
details.name = Json::ensureString(modMetadata.value("name"), details.mod_id); details.name = modMetadata.value("name").toString(details.mod_id);
details.description = Json::ensureString(modMetadata.value("description")); details.description = modMetadata.value("description").toString();
auto modContributors = Json::ensureObject(modMetadata.value("contributors")); auto modContributors = modMetadata.value("contributors").toObject();
// We don't really care about the role of a contributor here // We don't really care about the role of a contributor here
details.authors += modContributors.keys(); details.authors += modContributors.keys();
auto modContact = Json::ensureObject(modMetadata.value("contact")); auto modContact = modMetadata.value("contact").toObject();
if (modContact.contains("homepage")) { if (modContact.contains("homepage")) {
details.homeurl = Json::requireString(modContact.value("homepage")); details.homeurl = Json::requireString(modContact.value("homepage"));

View file

@ -75,9 +75,9 @@ bool SkinList::update()
try { try {
auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file"); auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file");
const auto root = doc.object(); const auto root = doc.object();
auto skins = Json::ensureArray(root, "skins"); auto skins = root["skins"].toArray();
for (auto jSkin : skins) { for (auto jSkin : skins) {
SkinModel s(m_dir, Json::ensureObject(jSkin)); SkinModel s(m_dir, jSkin.toObject());
if (s.isValid()) { if (s.isValid()) {
newSkins << s; newSkins << s;
} }

View file

@ -21,12 +21,17 @@
#include <QPainter> #include <QPainter>
#include "FileSystem.h" #include "FileSystem.h"
#include "Json.h"
static QImage improveSkin(const QImage& skin) static QImage improveSkin(QImage skin)
{ {
// It seems some older skins may use this format, which can't be drawn onto
// https://github.com/PrismLauncher/PrismLauncher/issues/4032
// https://doc.qt.io/qt-6/qpainter.html#begin
if (skin.format() == QImage::Format_Indexed8) {
skin = skin.convertToFormat(QImage::Format_RGB32);
}
if (skin.size() == QSize(64, 32)) { // old format if (skin.size() == QSize(64, 32)) { // old format
QImage newSkin = QImage(QSize(64, 64), skin.format()); auto newSkin = QImage(QSize(64, 64), skin.format());
newSkin.fill(Qt::transparent); newSkin.fill(Qt::transparent);
QPainter p(&newSkin); QPainter p(&newSkin);
p.drawImage(QPoint(0, 0), skin.copy(QRect(0, 0, 64, 32))); // copy head p.drawImage(QPoint(0, 0), skin.copy(QRect(0, 0, 64, 32))); // copy head
@ -102,15 +107,15 @@ SkinModel::SkinModel(QString path) : m_path(path), m_texture(getSkin(path)), m_m
} }
SkinModel::SkinModel(QDir skinDir, QJsonObject obj) SkinModel::SkinModel(QDir skinDir, QJsonObject obj)
: m_capeId(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url")) : m_capeId(obj["capeId"].toString()), m_model(Model::CLASSIC), m_url(obj["url"].toString())
{ {
auto name = Json::ensureString(obj, "name"); auto name = obj["name"].toString();
if (auto model = Json::ensureString(obj, "model"); model == "SLIM") { if (auto model = obj["model"].toString(); model == "SLIM") {
m_model = Model::SLIM; m_model = Model::SLIM;
} }
m_path = skinDir.absoluteFilePath(name) + ".png"; m_path = skinDir.absoluteFilePath(name) + ".png";
m_texture = QImage(getSkin(m_path)); m_texture = getSkin(m_path);
m_preview = generatePreviews(m_texture, m_model == Model::SLIM); m_preview = generatePreviews(m_texture, m_model == Model::SLIM);
} }

View file

@ -374,8 +374,8 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask()
} }
for (auto match : data_arr) { for (auto match : data_arr) {
auto match_obj = Json::ensureObject(match, {}); auto match_obj = match.toObject();
auto file_obj = Json::ensureObject(match_obj, "file", {}); auto file_obj = match_obj["file"].toObject();
if (match_obj.isEmpty() || file_obj.isEmpty()) { if (match_obj.isEmpty() || file_obj.isEmpty()) {
qWarning() << "Fingerprint match is empty!"; qWarning() << "Fingerprint match is empty!";
@ -383,7 +383,7 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask()
return; return;
} }
auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt()); auto fingerprint = QString::number(file_obj["fileFingerprint"].toInteger());
auto resource = m_resources.find(fingerprint); auto resource = m_resources.find(fingerprint);
if (resource == m_resources.end()) { if (resource == m_resources.end()) {
qWarning() << "Invalid fingerprint from the API response."; qWarning() << "Invalid fingerprint from the API response.";

View file

@ -31,7 +31,8 @@ static const QMap<QString, IndexedVersionType::VersionType> s_indexed_version_ty
{ "alpha", IndexedVersionType::VersionType::Alpha } { "alpha", IndexedVersionType::VersionType::Alpha }
}; };
static const QList<ModLoaderType> loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric, Babric, BTA, LegacyFabric, Ornithe, Rift }; static const QList<ModLoaderType> loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric,
Babric, BTA, LegacyFabric, Ornithe, Rift };
QList<ModLoaderType> modLoaderTypesToList(ModLoaderTypes flags) QList<ModLoaderType> modLoaderTypesToList(ModLoaderTypes flags)
{ {

View file

@ -67,7 +67,10 @@ Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args, Callback<QList<ModPlatf
} }
callbacks.on_fail(reason, network_error_code); callbacks.on_fail(reason, network_error_code);
}); });
QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] {
if (callbacks.on_abort != nullptr)
callbacks.on_abort();
});
return netJob; return netJob;
} }
@ -80,7 +83,7 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVe
auto versions_url = versions_url_optional.value(); auto versions_url = versions_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::Versions").arg(args.pack.name), APPLICATION->network()); auto netJob = makeShared<NetJob>(QString("%1::Versions").arg(args.pack->name), APPLICATION->network());
auto response = std::make_shared<QByteArray>(); auto response = std::make_shared<QByteArray>();
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
@ -97,14 +100,14 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVe
QVector<ModPlatform::IndexedVersion> unsortedVersions; QVector<ModPlatform::IndexedVersion> unsortedVersions;
try { try {
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); auto arr = doc.isObject() ? doc.object()["data"].toArray() : doc.array();
for (auto versionIter : arr) { for (auto versionIter : arr) {
auto obj = versionIter.toObject(); auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj, args.resourceType); auto file = loadIndexedPackVersion(obj, args.resourceType);
if (!file.addonId.isValid()) if (!file.addonId.isValid())
file.addonId = args.pack.addonId; file.addonId = args.pack->addonId;
if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) // Heuristic to check if the returned value is valid if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) // Heuristic to check if the returned value is valid
unsortedVersions.append(file); unsortedVersions.append(file);
@ -135,15 +138,18 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVe
} }
callbacks.on_fail(reason, network_error_code); callbacks.on_fail(reason, network_error_code);
}); });
QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] {
if (callbacks.on_abort != nullptr)
callbacks.on_abort();
});
return netJob; return netJob;
} }
Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack>&& callbacks) const Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack::Ptr>&& callbacks) const
{ {
auto response = std::make_shared<QByteArray>(); auto response = std::make_shared<QByteArray>();
auto job = getProject(args.pack.addonId.toString(), response); auto job = getProject(args.pack->addonId.toString(), response);
QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] { QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] {
auto pack = args.pack; auto pack = args.pack;
@ -159,8 +165,8 @@ Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatfo
auto obj = Json::requireObject(doc); auto obj = Json::requireObject(doc);
if (obj.contains("data")) if (obj.contains("data"))
obj = Json::requireObject(obj, "data"); obj = Json::requireObject(obj, "data");
loadIndexedPack(pack, obj); loadIndexedPack(*pack, obj);
loadExtraPackInfo(pack, obj); loadExtraPackInfo(*pack, obj);
} catch (const JSONValidationError& e) { } catch (const JSONValidationError& e) {
qDebug() << doc; qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
@ -182,7 +188,10 @@ Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatfo
} }
callbacks.on_fail(reason, network_error_code); callbacks.on_fail(reason, network_error_code);
}); });
QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); QObject::connect(job.get(), &NetJob::aborted, [callbacks] {
if (callbacks.on_abort != nullptr)
callbacks.on_abort();
});
return job; return job;
} }
@ -213,7 +222,7 @@ Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args, Callbac
if (args.dependency.version.length() != 0 && doc.isObject()) { if (args.dependency.version.length() != 0 && doc.isObject()) {
arr.append(doc.object()); arr.append(doc.object());
} else { } else {
arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); arr = doc.isObject() ? doc.object()["data"].toArray() : doc.array();
} }
QVector<ModPlatform::IndexedVersion> versions; QVector<ModPlatform::IndexedVersion> versions;

View file

@ -88,7 +88,7 @@ class ResourceAPI {
}; };
struct VersionSearchArgs { struct VersionSearchArgs {
ModPlatform::IndexedPack pack; ModPlatform::IndexedPack::Ptr pack;
std::optional<std::list<Version>> mcVersions; std::optional<std::list<Version>> mcVersions;
std::optional<ModPlatform::ModLoaderTypes> loaders; std::optional<ModPlatform::ModLoaderTypes> loaders;
@ -96,7 +96,7 @@ class ResourceAPI {
}; };
struct ProjectInfoArgs { struct ProjectInfoArgs {
ModPlatform::IndexedPack pack; ModPlatform::IndexedPack::Ptr pack;
}; };
struct DependencySearchArgs { struct DependencySearchArgs {
@ -115,7 +115,7 @@ class ResourceAPI {
virtual Task::Ptr getProject(QString addonId, std::shared_ptr<QByteArray> response) const; virtual Task::Ptr getProject(QString addonId, std::shared_ptr<QByteArray> response) const;
virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const = 0; virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const = 0;
virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback<ModPlatform::IndexedPack>&&) const; virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback<ModPlatform::IndexedPack::Ptr>&&) const;
Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const; Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const;
virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback<ModPlatform::IndexedVersion>&&) const; virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback<ModPlatform::IndexedVersion>&&) const;

View file

@ -33,7 +33,7 @@ enum class ResourceType { Mod, ResourcePack, ShaderPack, Modpack, DataPack, Worl
namespace ResourceTypeUtils { namespace ResourceTypeUtils {
static const std::set<ResourceType> VALID_RESOURCES = { ResourceType::DataPack, ResourceType::ResourcePack, ResourceType::TexturePack, static const std::set<ResourceType> VALID_RESOURCES = { ResourceType::DataPack, ResourceType::ResourcePack, ResourceType::TexturePack,
ResourceType::ShaderPack, ResourceType::World, ResourceType::Mod }; ResourceType::ShaderPack, ResourceType::World, ResourceType::Mod };
QString getName(ResourceType type); QString getName(ResourceType type);
} // namespace ResourceTypeUtils } // namespace ResourceTypeUtils
} // namespace ModPlatform } // namespace ModPlatform

View file

@ -40,8 +40,8 @@ void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack& m, QJsonObject& obj)
loadIndexedVersion(version, versionObj); loadIndexedVersion(version, versionObj);
m.versions.append(version); m.versions.append(version);
} }
m.system = Json::ensureBoolean(obj, QString("system"), false); m.system = obj["system"].toBool();
m.description = Json::ensureString(obj, "description", ""); m.description = obj["description"].toString("");
static const QRegularExpression s_regex("[^A-Za-z0-9]"); static const QRegularExpression s_regex("[^A-Za-z0-9]");
m.safeName = Json::requireString(obj, "name").replace(s_regex, "").toLower() + ".png"; m.safeName = Json::requireString(obj, "name").replace(s_regex, "").toLower() + ".png";

View file

@ -100,20 +100,20 @@ static ATLauncher::ModType parseModType(QString rawType)
static void loadVersionLoader(ATLauncher::VersionLoader& p, QJsonObject& obj) static void loadVersionLoader(ATLauncher::VersionLoader& p, QJsonObject& obj)
{ {
p.type = Json::requireString(obj, "type"); p.type = Json::requireString(obj, "type");
p.choose = Json::ensureBoolean(obj, QString("choose"), false); p.choose = obj["choose"].toBool();
auto metadata = Json::requireObject(obj, "metadata"); auto metadata = Json::requireObject(obj, "metadata");
p.latest = Json::ensureBoolean(metadata, QString("latest"), false); p.latest = metadata["latest"].toBool();
p.recommended = Json::ensureBoolean(metadata, QString("recommended"), false); p.recommended = metadata["recommended"].toBool();
// Minecraft Forge // Minecraft Forge
if (p.type == "forge") { if (p.type == "forge" || p.type == "neoforge") {
p.version = Json::ensureString(metadata, "version", ""); p.version = metadata["version"].toString("");
} }
// Fabric Loader // Fabric Loader
if (p.type == "fabric") { if (p.type == "fabric") {
p.version = Json::ensureString(metadata, "loader", ""); p.version = metadata["loader"].toString("");
} }
} }
@ -126,7 +126,7 @@ static void loadVersionLibrary(ATLauncher::VersionLibrary& p, QJsonObject& obj)
p.download_raw = Json::requireString(obj, "download"); p.download_raw = Json::requireString(obj, "download");
p.download = parseDownloadType(p.download_raw); p.download = parseDownloadType(p.download_raw);
p.server = Json::ensureString(obj, "server", ""); p.server = obj["server"].toString("");
} }
static void loadVersionConfigs(ATLauncher::VersionConfigs& p, QJsonObject& obj) static void loadVersionConfigs(ATLauncher::VersionConfigs& p, QJsonObject& obj)
@ -141,7 +141,7 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj)
p.version = Json::requireString(obj, "version"); p.version = Json::requireString(obj, "version");
p.url = Json::requireString(obj, "url"); p.url = Json::requireString(obj, "url");
p.file = Json::requireString(obj, "file"); p.file = Json::requireString(obj, "file");
p.md5 = Json::ensureString(obj, "md5", ""); p.md5 = obj["md5"].toString("");
p.download_raw = Json::requireString(obj, "download"); p.download_raw = Json::requireString(obj, "download");
p.download = parseDownloadType(p.download_raw); p.download = parseDownloadType(p.download_raw);
@ -161,7 +161,7 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj)
if (obj.contains("extractTo")) { if (obj.contains("extractTo")) {
p.extractTo_raw = Json::requireString(obj, "extractTo"); p.extractTo_raw = Json::requireString(obj, "extractTo");
p.extractTo = parseModType(p.extractTo_raw); p.extractTo = parseModType(p.extractTo_raw);
p.extractFolder = Json::ensureString(obj, "extractFolder", "").replace("%s%", "/"); p.extractFolder = obj["extractFolder"].toString("").replace("%s%", "/");
} }
if (obj.contains("decompType")) { if (obj.contains("decompType")) {
@ -170,23 +170,23 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj)
p.decompFile = Json::requireString(obj, "decompFile"); p.decompFile = Json::requireString(obj, "decompFile");
} }
p.description = Json::ensureString(obj, QString("description"), ""); p.description = obj["description"].toString("");
p.optional = Json::ensureBoolean(obj, QString("optional"), false); p.optional = obj["optional"].toBool();
p.recommended = Json::ensureBoolean(obj, QString("recommended"), false); p.recommended = obj["recommended"].toBool();
p.selected = Json::ensureBoolean(obj, QString("selected"), false); p.selected = obj["selected"].toBool();
p.hidden = Json::ensureBoolean(obj, QString("hidden"), false); p.hidden = obj["hidden"].toBool();
p.library = Json::ensureBoolean(obj, QString("library"), false); p.library = obj["library"].toBool();
p.group = Json::ensureString(obj, QString("group"), ""); p.group = obj["group"].toString("");
if (obj.contains("depends")) { if (obj.contains("depends")) {
auto dependsArr = Json::requireArray(obj, "depends"); auto dependsArr = Json::requireArray(obj, "depends");
for (const auto depends : dependsArr) { for (const auto depends : dependsArr) {
p.depends.append(Json::requireString(depends)); p.depends.append(Json::requireString(depends));
} }
} }
p.colour = Json::ensureString(obj, QString("colour"), ""); p.colour = obj["colour"].toString("");
p.warning = Json::ensureString(obj, QString("warning"), ""); p.warning = obj["warning"].toString("");
p.client = Json::ensureBoolean(obj, QString("client"), false); p.client = obj["client"].toBool();
// computed // computed
p.effectively_hidden = p.hidden || p.library; p.effectively_hidden = p.hidden || p.library;
@ -194,20 +194,20 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj)
static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj) static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj)
{ {
m.install = Json::ensureString(obj, "install", ""); m.install = obj["install"].toString("");
m.update = Json::ensureString(obj, "update", ""); m.update = obj["update"].toString("");
} }
static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj) static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj)
{ {
m.mainClass = Json::ensureString(obj, "mainClass", ""); m.mainClass = obj["mainClass"].toString("");
m.depends = Json::ensureString(obj, "depends", ""); m.depends = obj["depends"].toString("");
} }
static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj) static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj)
{ {
a.arguments = Json::ensureString(obj, "arguments", ""); a.arguments = obj["arguments"].toString("");
a.depends = Json::ensureString(obj, "depends", ""); a.depends = obj["depends"].toString("");
} }
static void loadVersionKeep(ATLauncher::VersionKeep& k, QJsonObject& obj) static void loadVersionKeep(ATLauncher::VersionKeep& k, QJsonObject& obj)
@ -272,7 +272,7 @@ void ATLauncher::loadVersion(PackVersion& v, QJsonObject& obj)
{ {
v.version = Json::requireString(obj, "version"); v.version = Json::requireString(obj, "version");
v.minecraft = Json::requireString(obj, "minecraft"); v.minecraft = Json::requireString(obj, "minecraft");
v.noConfigs = Json::ensureBoolean(obj, QString("noConfigs"), false); v.noConfigs = obj["noConfigs"].toBool();
if (obj.contains("mainClass")) { if (obj.contains("mainClass")) {
auto main = Json::requireObject(obj, "mainClass"); auto main = Json::requireObject(obj, "mainClass");
@ -314,22 +314,22 @@ void ATLauncher::loadVersion(PackVersion& v, QJsonObject& obj)
loadVersionConfigs(v.configs, configsObj); loadVersionConfigs(v.configs, configsObj);
} }
auto colourObj = Json::ensureObject(obj, "colours"); auto colourObj = obj["colours"].toObject();
for (const auto& key : colourObj.keys()) { for (const auto& key : colourObj.keys()) {
v.colours[key] = Json::requireString(colourObj.value(key), "colour"); v.colours[key] = Json::requireString(colourObj.value(key), "colour");
} }
auto warningsObj = Json::ensureObject(obj, "warnings"); auto warningsObj = obj["warnings"].toObject();
for (const auto& key : warningsObj.keys()) { for (const auto& key : warningsObj.keys()) {
v.warnings[key] = Json::requireString(warningsObj.value(key), "warning"); v.warnings[key] = Json::requireString(warningsObj.value(key), "warning");
} }
auto messages = Json::ensureObject(obj, "messages"); auto messages = obj["messages"].toObject();
loadVersionMessages(v.messages, messages); loadVersionMessages(v.messages, messages);
auto keeps = Json::ensureObject(obj, "keeps"); auto keeps = obj["keeps"].toObject();
loadVersionKeeps(v.keeps, keeps); loadVersionKeeps(v.keeps, keeps);
auto deletes = Json::ensureObject(obj, "deletes"); auto deletes = obj["deletes"].toObject();
loadVersionDeletes(v.deletes, deletes); loadVersionDeletes(v.deletes, deletes);
} }

View file

@ -59,7 +59,7 @@ QString FlameAPI::getModFileChangelog(int modId, int fileId)
return; return;
} }
changelog = Json::ensureString(doc.object(), "data"); changelog = doc.object()["data"].toString();
}); });
QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); }); QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); });
@ -92,7 +92,7 @@ QString FlameAPI::getModDescription(int modId)
return; return;
} }
description = Json::ensureString(doc.object(), "data"); description = doc.object()["data"].toString();
}); });
QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); }); QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); });

View file

@ -126,7 +126,7 @@ class FlameAPI : public ResourceAPI {
std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
{ {
auto addonId = args.pack.addonId.toString(); auto addonId = args.pack->addonId.toString();
QString url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000").arg(addonId); QString url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000").arg(addonId);
if (args.mcVersions.has_value()) if (args.mcVersions.has_value())
@ -140,7 +140,7 @@ class FlameAPI : public ResourceAPI {
return url; return url;
} }
QJsonArray documentToArray(QJsonDocument& obj) const override { return Json::ensureArray(obj.object(), "data"); } QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object()["data"].toArray(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { FlameMod::loadIndexedPack(m, obj); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { FlameMod::loadIndexedPack(m, obj); }
ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType resourceType) const override ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType resourceType) const override
{ {
@ -160,10 +160,7 @@ class FlameAPI : public ResourceAPI {
void loadExtraPackInfo(ModPlatform::IndexedPack& m, [[maybe_unused]] QJsonObject&) const override { FlameMod::loadBody(m); } void loadExtraPackInfo(ModPlatform::IndexedPack& m, [[maybe_unused]] QJsonObject&) const override { FlameMod::loadBody(m); }
private: private:
std::optional<QString> getInfoURL(QString const& id) const override std::optional<QString> getInfoURL(QString const& id) const override { return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id); }
{
return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id);
}
std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
{ {
auto addonId = args.dependency.addonId.toString(); auto addonId = args.dependency.addonId.toString();

View file

@ -46,7 +46,9 @@ void FlameCheckUpdate::executeTask()
connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress);
connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails); connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails);
for (auto* resource : m_resources) { for (auto* resource : m_resources) {
auto versionsUrlOptional = api.getVersionsURL({ { resource->metadata()->project_id.toString() }, m_gameVersions }); auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = resource->metadata()->project_id.toString();
auto versionsUrlOptional = api.getVersionsURL({ project, m_gameVersions });
if (!versionsUrlOptional.has_value()) if (!versionsUrlOptional.has_value())
continue; continue;

View file

@ -15,23 +15,26 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.provider = ModPlatform::ResourceProvider::FLAME; pack.provider = ModPlatform::ResourceProvider::FLAME;
pack.name = Json::requireString(obj, "name"); pack.name = Json::requireString(obj, "name");
pack.slug = Json::requireString(obj, "slug"); pack.slug = Json::requireString(obj, "slug");
pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.websiteUrl = obj["links"].toObject()["websiteUrl"].toString("");
pack.description = Json::ensureString(obj, "summary", ""); pack.description = obj["summary"].toString("");
QJsonObject logo = Json::ensureObject(obj, "logo"); QJsonObject logo = obj["logo"].toObject();
pack.logoName = Json::ensureString(logo, "title"); pack.logoName = logo["title"].toString();
pack.logoUrl = Json::ensureString(logo, "thumbnailUrl"); pack.logoUrl = logo["thumbnailUrl"].toString();
if (pack.logoUrl.isEmpty()) { if (pack.logoUrl.isEmpty()) {
pack.logoUrl = Json::ensureString(logo, "url"); pack.logoUrl = logo["url"].toString();
} }
auto authors = Json::ensureArray(obj, "authors"); auto authors = obj["authors"].toArray();
for (auto authorIter : authors) { if (!authors.isEmpty()) {
auto author = Json::requireObject(authorIter); pack.authors.clear();
ModPlatform::ModpackAuthor packAuthor; for (auto authorIter : authors) {
packAuthor.name = Json::requireString(author, "name"); auto author = Json::requireObject(authorIter);
packAuthor.url = Json::requireString(author, "url"); ModPlatform::ModpackAuthor packAuthor;
pack.authors.append(packAuthor); packAuthor.name = Json::requireString(author, "name");
packAuthor.url = Json::requireString(author, "url");
pack.authors.append(packAuthor);
}
} }
pack.extraDataLoaded = false; pack.extraDataLoaded = false;
@ -40,17 +43,17 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{ {
auto links_obj = Json::ensureObject(obj, "links"); auto links_obj = obj["links"].toObject();
pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); pack.extraData.issuesUrl = links_obj["issuesUrl"].toString();
if (pack.extraData.issuesUrl.endsWith('/')) if (pack.extraData.issuesUrl.endsWith('/'))
pack.extraData.issuesUrl.chop(1); pack.extraData.issuesUrl.chop(1);
pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); pack.extraData.sourceUrl = links_obj["sourceUrl"].toString();
if (pack.extraData.sourceUrl.endsWith('/')) if (pack.extraData.sourceUrl.endsWith('/'))
pack.extraData.sourceUrl.chop(1); pack.extraData.sourceUrl.chop(1);
pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); pack.extraData.wikiUrl = links_obj["wikiUrl"].toString();
if (pack.extraData.wikiUrl.endsWith('/')) if (pack.extraData.wikiUrl.endsWith('/'))
pack.extraData.wikiUrl.chop(1); pack.extraData.wikiUrl.chop(1);
@ -136,7 +139,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
file.fileId = Json::requireInteger(obj, "id"); file.fileId = Json::requireInteger(obj, "id");
file.date = Json::requireString(obj, "fileDate"); file.date = Json::requireString(obj, "fileDate");
file.version = Json::requireString(obj, "displayName"); file.version = Json::requireString(obj, "displayName");
file.downloadUrl = Json::ensureString(obj, "downloadUrl"); file.downloadUrl = obj["downloadUrl"].toString();
file.fileName = Json::requireString(obj, "fileName"); file.fileName = Json::requireString(obj, "fileName");
file.fileName = FS::RemoveInvalidPathChars(file.fileName); file.fileName = FS::RemoveInvalidPathChars(file.fileName);
@ -156,11 +159,11 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
} }
file.version_type = ModPlatform::IndexedVersionType(ver_type); file.version_type = ModPlatform::IndexedVersionType(ver_type);
auto hash_list = Json::ensureArray(obj, "hashes"); auto hash_list = obj["hashes"].toArray();
for (auto h : hash_list) { for (auto h : hash_list) {
auto hash_entry = Json::ensureObject(h); auto hash_entry = h.toObject();
auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::FLAME); auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::FLAME);
auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm")); auto hash_algo = enumToString(hash_entry["algo"].toInt(1));
if (hash_types.contains(hash_algo)) { if (hash_types.contains(hash_algo)) {
file.hash = Json::requireString(hash_entry, "value"); file.hash = Json::requireString(hash_entry, "value");
file.hash_type = hash_algo; file.hash_type = hash_algo;
@ -168,9 +171,9 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
} }
} }
auto dependencies = Json::ensureArray(obj, "dependencies"); auto dependencies = obj["dependencies"].toArray();
for (auto d : dependencies) { for (auto d : dependencies) {
auto dep = Json::ensureObject(d); auto dep = d.toObject();
ModPlatform::Dependency dependency; ModPlatform::Dependency dependency;
dependency.addonId = Json::requireInteger(dep, "modId"); dependency.addonId = Json::requireInteger(dep, "modId");
switch (Json::requireInteger(dep, "relationType")) { switch (Json::requireInteger(dep, "relationType")) {

View file

@ -199,8 +199,8 @@ void FlamePackExportTask::makeApiRequest()
return; return;
} }
for (auto match : dataArr) { for (auto match : dataArr) {
auto matchObj = Json::ensureObject(match, {}); auto matchObj = match.toObject();
auto fileObj = Json::ensureObject(matchObj, "file", {}); auto fileObj = matchObj["file"].toObject();
if (matchObj.isEmpty() || fileObj.isEmpty()) { if (matchObj.isEmpty() || fileObj.isEmpty()) {
qWarning() << "Fingerprint match is empty!"; qWarning() << "Fingerprint match is empty!";
@ -208,7 +208,7 @@ void FlamePackExportTask::makeApiRequest()
return; return;
} }
auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt()); auto fingerprint = QString::number(fileObj["fileFingerprint"].toInteger());
auto mod = pendingHashes.find(fingerprint); auto mod = pendingHashes.find(fingerprint);
if (mod == pendingHashes.end()) { if (mod == pendingHashes.end()) {
qWarning() << "Invalid fingerprint from the API response."; qWarning() << "Invalid fingerprint from the API response.";
@ -216,7 +216,7 @@ void FlamePackExportTask::makeApiRequest()
} }
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name)); setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name));
if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable")) if (fileObj["isAvailable"].toBool())
resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"), resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"),
mod->enabled, mod->isMod }); mod->enabled, mod->isMod });
} }
@ -429,4 +429,4 @@ QByteArray FlamePackExportTask::generateHTML()
} }
content = "<ul>" + content + "</ul>"; content = "<ul>" + content + "</ul>";
return content.toUtf8(); return content.toUtf8();
} }

View file

@ -5,13 +5,13 @@ static void loadFileV1(Flame::File& f, QJsonObject& file)
{ {
f.projectId = Json::requireInteger(file, "projectID"); f.projectId = Json::requireInteger(file, "projectID");
f.fileId = Json::requireInteger(file, "fileID"); f.fileId = Json::requireInteger(file, "fileID");
f.required = Json::ensureBoolean(file, QString("required"), true); f.required = file["required"].toBool(true);
} }
static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader) static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader)
{ {
m.id = Json::requireString(modLoader, "id"); m.id = Json::requireString(modLoader, "id");
m.primary = Json::ensureBoolean(modLoader, QString("primary"), false); m.primary = modLoader["primary"].toBool();
} }
static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft) static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft)
@ -19,15 +19,15 @@ static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft)
m.version = Json::requireString(minecraft, "version"); m.version = Json::requireString(minecraft, "version");
// extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack // extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack
// intended use is likely hardcoded in the 'Flame' client, the manifest says nothing // intended use is likely hardcoded in the 'Flame' client, the manifest says nothing
m.libraries = Json::ensureString(minecraft, QString("libraries"), QString()); m.libraries = minecraft["libraries"].toString();
auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray()); auto arr = minecraft["modLoaders"].toArray();
for (QJsonValueRef item : arr) { for (QJsonValueRef item : arr) {
auto obj = Json::requireObject(item); auto obj = Json::requireObject(item);
Flame::Modloader loader; Flame::Modloader loader;
loadModloaderV1(loader, obj); loadModloaderV1(loader, obj);
m.modLoaders.append(loader); m.modLoaders.append(loader);
} }
m.recommendedRAM = Json::ensureInteger(minecraft, "recommendedRam", 0); m.recommendedRAM = minecraft["recommendedRam"].toInt();
} }
static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest) static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest)
@ -36,11 +36,11 @@ static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest)
loadMinecraftV1(pack.minecraft, mc); loadMinecraftV1(pack.minecraft, mc);
pack.name = Json::ensureString(manifest, QString("name"), "Unnamed"); pack.name = manifest["name"].toString("Unnamed");
pack.version = Json::ensureString(manifest, QString("version"), QString()); pack.version = manifest["version"].toString();
pack.author = Json::ensureString(manifest, QString("author"), "Anonymous"); pack.author = manifest["author"].toString("Anonymous");
auto arr = Json::ensureArray(manifest, "files", QJsonArray()); auto arr = manifest["files"].toArray();
for (auto item : arr) { for (auto item : arr) {
auto obj = Json::requireObject(item); auto obj = Json::requireObject(item);
@ -50,7 +50,7 @@ static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest)
pack.files.insert(file.fileId, file); pack.files.insert(file.fileId, file);
} }
pack.overrides = Json::ensureString(manifest, "overrides", "overrides"); pack.overrides = manifest["overrides"].toString("overrides");
pack.is_loaded = true; pack.is_loaded = true;
} }

View file

@ -72,7 +72,7 @@ Modpack parseDirectory(QString path)
modpack.name = Json::requireString(root, "name", "name"); modpack.name = Json::requireString(root, "name", "name");
modpack.version = Json::requireString(root, "version", "version"); modpack.version = Json::requireString(root, "version", "version");
modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion"); modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion");
modpack.jvmArgs = Json::ensureVariant(root, "jvmArgs", {}, "jvmArgs"); modpack.jvmArgs = root["jvmArgs"].toVariant();
modpack.totalPlayTime = Json::requireInteger(root, "totalPlayTime", "totalPlayTime"); modpack.totalPlayTime = Json::requireInteger(root, "totalPlayTime", "totalPlayTime");
} catch (const Exception& e) { } catch (const Exception& e) {
qDebug() << "Couldn't load ftb instance json: " << e.cause(); qDebug() << "Couldn't load ftb instance json: " << e.cause();

View file

@ -8,8 +8,6 @@
#include "meta/VersionList.h" #include "meta/VersionList.h"
#include "net/NetJob.h" #include "net/NetJob.h"
#include "net/NetJob.h"
#include <optional> #include <optional>
namespace LegacyFTB { namespace LegacyFTB {

View file

@ -147,7 +147,7 @@ QList<ModPlatform::Category> ModrinthAPI::loadCategories(std::shared_ptr<QByteAr
for (auto val : arr) { for (auto val : arr) {
auto cat = Json::requireObject(val); auto cat = Json::requireObject(val);
auto name = Json::requireString(cat, "name"); auto name = Json::requireString(cat, "name");
if (Json::ensureString(cat, "project_type", "") == projectType) if (cat["project_type"].toString() == projectType)
categories.push_back({ name, name }); categories.push_back({ name, name });
} }

View file

@ -45,7 +45,8 @@ class ModrinthAPI : public ResourceAPI {
{ {
QStringList l; QStringList l;
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader, for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader,
ModPlatform::DataPack, ModPlatform::Babric, ModPlatform::BTA, ModPlatform::LegacyFabric, ModPlatform::Ornithe, ModPlatform::Rift }) { ModPlatform::DataPack, ModPlatform::Babric, ModPlatform::BTA, ModPlatform::LegacyFabric, ModPlatform::Ornithe,
ModPlatform::Rift }) {
if (types & loader) { if (types & loader) {
l << getModLoaderAsString(loader); l << getModLoaderAsString(loader);
} }
@ -188,7 +189,7 @@ class ModrinthAPI : public ResourceAPI {
get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\""))); get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\"")));
return QString("%1/project/%2/version%3%4") return QString("%1/project/%2/version%3%4")
.arg(BuildConfig.MODRINTH_PROD_URL, args.pack.addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); .arg(BuildConfig.MODRINTH_PROD_URL, args.pack->addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&'));
}; };
QString getGameVersionsArray(std::list<Version> mcVersions) const QString getGameVersionsArray(std::list<Version> mcVersions) const
@ -204,7 +205,8 @@ class ModrinthAPI : public ResourceAPI {
static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool
{ {
return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader | return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader |
ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA | ModPlatform::LegacyFabric | ModPlatform::Ornithe | ModPlatform::Rift); ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA | ModPlatform::LegacyFabric |
ModPlatform::Ornithe | ModPlatform::Rift);
} }
std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override

View file

@ -249,7 +249,7 @@ bool ModrinthCreationTask::createInstance()
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
// TODO make this work with other sorts of resource // TODO make this work with other sorts of resource
QHash<QString, Resource*> resources; QHash<QString, Resource*> resources;
for (auto file : m_files) { for (auto& file : m_files) {
auto fileName = file.path; auto fileName = file.path;
fileName = FS::RemoveInvalidPathChars(fileName); fileName = FS::RemoveInvalidPathChars(fileName);
auto file_path = FS::PathCombine(root_modpack_path, fileName); auto file_path = FS::PathCombine(root_modpack_path, fileName);
@ -371,8 +371,8 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
if (set_internal_data) { if (set_internal_data) {
if (m_managed_version_id.isEmpty()) if (m_managed_version_id.isEmpty())
m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID"); m_managed_version_id = obj["versionId"].toString();
m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name"); m_managed_name = obj["name"].toString();
} }
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json"); auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
@ -381,10 +381,10 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
File file; File file;
file.path = Json::requireString(modInfo, "path").replace("\\", "/"); file.path = Json::requireString(modInfo, "path").replace("\\", "/");
auto env = Json::ensureObject(modInfo, "env"); auto env = modInfo["env"].toObject();
// 'env' field is optional // 'env' field is optional
if (!env.isEmpty()) { if (!env.isEmpty()) {
QString support = Json::ensureString(env, "client", "unsupported"); QString support = env["client"].toString("unsupported");
if (support == "unsupported") { if (support == "unsupported") {
continue; continue;
} else if (support == "optional") { } else if (support == "optional") {
@ -399,7 +399,7 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
// (as Modrinth seems to incorrectly handle spaces) // (as Modrinth seems to incorrectly handle spaces)
auto download_arr = Json::ensureArray(modInfo, "downloads"); auto download_arr = modInfo["downloads"].toArray();
for (auto download : download_arr) { for (auto download : download_arr) {
qWarning() << download.toString(); qWarning() << download.toString();
bool is_last = download.toString() == download_arr.last().toString(); bool is_last = download.toString() == download_arr.last().toString();

View file

@ -33,34 +33,36 @@ bool shouldDownloadOnSide(QString side)
return side == "required" || side == "optional"; return side == "required" || side == "optional";
} }
// https://docs.modrinth.com/api-spec/#tag/projects/operation/getProject // https://docs.modrinth.com/api/operations/getproject/
void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{ {
pack.addonId = Json::ensureString(obj, "project_id"); pack.addonId = obj["project_id"].toString();
if (pack.addonId.toString().isEmpty()) if (pack.addonId.toString().isEmpty())
pack.addonId = Json::requireString(obj, "id"); pack.addonId = Json::requireString(obj, "id");
pack.provider = ModPlatform::ResourceProvider::MODRINTH; pack.provider = ModPlatform::ResourceProvider::MODRINTH;
pack.name = Json::requireString(obj, "title"); pack.name = Json::requireString(obj, "title");
pack.slug = Json::ensureString(obj, "slug", ""); pack.slug = obj["slug"].toString("");
if (!pack.slug.isEmpty()) if (!pack.slug.isEmpty())
pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug; pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug;
else else
pack.websiteUrl = ""; pack.websiteUrl = "";
pack.description = Json::ensureString(obj, "description", ""); pack.description = obj["description"].toString("");
pack.logoUrl = Json::ensureString(obj, "icon_url", ""); pack.logoUrl = obj["icon_url"].toString("");
pack.logoName = QString("%1.%2").arg(Json::ensureString(obj, "slug"), QFileInfo(QUrl(pack.logoUrl).fileName()).suffix()); pack.logoName = QString("%1.%2").arg(obj["slug"].toString(), QFileInfo(QUrl(pack.logoUrl).fileName()).suffix());
ModPlatform::ModpackAuthor modAuthor; if (obj.contains("author")) {
modAuthor.name = Json::ensureString(obj, "author", QObject::tr("No author(s)")); ModPlatform::ModpackAuthor modAuthor;
modAuthor.url = api.getAuthorURL(modAuthor.name); modAuthor.name = obj["author"].toString();
pack.authors.append(modAuthor); modAuthor.url = api.getAuthorURL(modAuthor.name);
pack.authors = { modAuthor };
}
auto client = shouldDownloadOnSide(Json::ensureString(obj, "client_side")); auto client = shouldDownloadOnSide(obj["client_side"].toString());
auto server = shouldDownloadOnSide(Json::ensureString(obj, "server_side")); auto server = shouldDownloadOnSide(obj["server_side"].toString());
if (server && client) { if (server && client) {
pack.side = ModPlatform::Side::UniversalSide; pack.side = ModPlatform::Side::UniversalSide;
@ -76,38 +78,38 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{ {
pack.extraData.issuesUrl = Json::ensureString(obj, "issues_url"); pack.extraData.issuesUrl = obj["issues_url"].toString();
if (pack.extraData.issuesUrl.endsWith('/')) if (pack.extraData.issuesUrl.endsWith('/'))
pack.extraData.issuesUrl.chop(1); pack.extraData.issuesUrl.chop(1);
pack.extraData.sourceUrl = Json::ensureString(obj, "source_url"); pack.extraData.sourceUrl = obj["source_url"].toString();
if (pack.extraData.sourceUrl.endsWith('/')) if (pack.extraData.sourceUrl.endsWith('/'))
pack.extraData.sourceUrl.chop(1); pack.extraData.sourceUrl.chop(1);
pack.extraData.wikiUrl = Json::ensureString(obj, "wiki_url"); pack.extraData.wikiUrl = obj["wiki_url"].toString();
if (pack.extraData.wikiUrl.endsWith('/')) if (pack.extraData.wikiUrl.endsWith('/'))
pack.extraData.wikiUrl.chop(1); pack.extraData.wikiUrl.chop(1);
pack.extraData.discordUrl = Json::ensureString(obj, "discord_url"); pack.extraData.discordUrl = obj["discord_url"].toString();
if (pack.extraData.discordUrl.endsWith('/')) if (pack.extraData.discordUrl.endsWith('/'))
pack.extraData.discordUrl.chop(1); pack.extraData.discordUrl.chop(1);
auto donate_arr = Json::ensureArray(obj, "donation_urls"); auto donate_arr = obj["donation_urls"].toArray();
for (auto d : donate_arr) { for (auto d : donate_arr) {
auto d_obj = Json::requireObject(d); auto d_obj = Json::requireObject(d);
ModPlatform::DonationData donate; ModPlatform::DonationData donate;
donate.id = Json::ensureString(d_obj, "id"); donate.id = d_obj["id"].toString();
donate.platform = Json::ensureString(d_obj, "platform"); donate.platform = d_obj["platform"].toString();
donate.url = Json::ensureString(d_obj, "url"); donate.url = d_obj["url"].toString();
pack.extraData.donate.append(donate); pack.extraData.donate.append(donate);
} }
pack.extraData.status = Json::ensureString(obj, "status"); pack.extraData.status = obj["status"].toString();
pack.extraData.body = Json::ensureString(obj, "body").remove("<br>"); pack.extraData.body = obj["body"].toString().remove("<br>");
pack.extraDataLoaded = true; pack.extraDataLoaded = true;
} }
@ -147,12 +149,12 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q
file.changelog = Json::requireString(obj, "changelog"); file.changelog = Json::requireString(obj, "changelog");
auto dependencies = Json::ensureArray(obj, "dependencies"); auto dependencies = obj["dependencies"].toArray();
for (auto d : dependencies) { for (auto d : dependencies) {
auto dep = Json::ensureObject(d); auto dep = d.toObject();
ModPlatform::Dependency dependency; ModPlatform::Dependency dependency;
dependency.addonId = Json::ensureString(dep, "project_id"); dependency.addonId = dep["project_id"].toString();
dependency.version = Json::ensureString(dep, "version_id"); dependency.version = dep["version_id"].toString();
auto depType = Json::requireString(dep, "dependency_type"); auto depType = Json::requireString(dep, "dependency_type");
if (depType == "required") if (depType == "required")

View file

@ -37,7 +37,7 @@ void loadPack(Pack& v, QJsonObject& obj)
static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj) static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj)
{ {
b.name = Json::requireString(obj, "name"); b.name = Json::requireString(obj, "name");
b.version = Json::ensureString(obj, "version", ""); b.version = obj["version"].toString("");
b.md5 = Json::requireString(obj, "md5"); b.md5 = Json::requireString(obj, "md5");
b.url = Json::requireString(obj, "url"); b.url = Json::requireString(obj, "url");
} }

View file

@ -142,7 +142,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
try { try {
QJsonDocument doc = Json::requireDocument(data); QJsonDocument doc = Json::requireDocument(data);
QJsonObject root = Json::requireObject(doc, "version.json"); QJsonObject root = Json::requireObject(doc, "version.json");
QString packMinecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), ""); QString packMinecraftVersion = root["inheritsFrom"].toString();
if (packMinecraftVersion.isEmpty()) { if (packMinecraftVersion.isEmpty()) {
if (fmlMinecraftVersion.isEmpty()) { if (fmlMinecraftVersion.isEmpty()) {
emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing")); emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing"));
@ -151,21 +151,21 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
packMinecraftVersion = fmlMinecraftVersion; packMinecraftVersion = fmlMinecraftVersion;
} }
components->setComponentVersion("net.minecraft", packMinecraftVersion, true); components->setComponentVersion("net.minecraft", packMinecraftVersion, true);
for (auto library : Json::ensureArray(root, "libraries", {})) { for (auto library : root["libraries"].toArray()) {
if (!library.isObject()) { if (!library.isObject()) {
continue; continue;
} }
auto libraryObject = Json::ensureObject(library, {}, ""); auto libraryObject = library.toObject();
auto libraryName = Json::ensureString(libraryObject, "name", "", ""); auto libraryName = libraryObject["name"].toString();
if (libraryName.startsWith("net.neoforged.fancymodloader:")) { // it is neoforge if (libraryName.startsWith("net.neoforged.fancymodloader:")) { // it is neoforge
// no easy way to get the version from the libs so use the arguments // no easy way to get the version from the libs so use the arguments
auto arguments = Json::ensureObject(root, "arguments", {}); auto arguments = root["arguments"].toObject();
bool isVersionArg = false; bool isVersionArg = false;
QString neoforgeVersion; QString neoforgeVersion;
for (auto arg : Json::ensureArray(arguments, "game", {})) { for (auto arg : arguments["game"].toArray()) {
auto argument = Json::ensureString(arg, ""); auto argument = arg.toString("");
if (isVersionArg) { if (isVersionArg) {
neoforgeVersion = argument; neoforgeVersion = argument;
break; break;

View file

@ -248,15 +248,15 @@ void HttpMetaCache::Load()
auto root = json.object(); auto root = json.object();
// check file version first // check file version first
auto version_val = Json::ensureString(root, "version"); auto version_val = root["version"].toString();
if (version_val != "1") if (version_val != "1")
return; return;
// read the entry array // read the entry array
auto array = Json::ensureArray(root, "entries"); auto array = root["entries"].toArray();
for (auto element : array) { for (auto element : array) {
auto element_obj = Json::ensureObject(element); auto element_obj = element.toObject();
auto base = Json::ensureString(element_obj, "base"); auto base = element_obj["base"].toString();
if (!m_entries.contains(base)) if (!m_entries.contains(base))
continue; continue;
@ -264,16 +264,16 @@ void HttpMetaCache::Load()
auto foo = new MetaEntry(); auto foo = new MetaEntry();
foo->m_baseId = base; foo->m_baseId = base;
foo->m_relativePath = Json::ensureString(element_obj, "path"); foo->m_relativePath = element_obj["path"].toString();
foo->m_md5sum = Json::ensureString(element_obj, "md5sum"); foo->m_md5sum = element_obj["md5sum"].toString();
foo->m_etag = Json::ensureString(element_obj, "etag"); foo->m_etag = element_obj["etag"].toString();
foo->m_local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp"); foo->m_local_changed_timestamp = element_obj["last_changed_timestamp"].toDouble();
foo->m_remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp"); foo->m_remote_changed_timestamp = element_obj["remote_changed_timestamp"].toString();
foo->makeEternal(Json::ensureBoolean(element_obj, (const QString)QStringLiteral("eternal"), false)); foo->makeEternal(element_obj[QStringLiteral("eternal")].toBool());
if (!foo->isEternal()) { if (!foo->isEternal()) {
foo->m_current_age = Json::ensureDouble(element_obj, "current_age"); foo->m_current_age = element_obj["current_age"].toDouble();
foo->m_max_age = Json::ensureDouble(element_obj, "max_age"); foo->m_max_age = element_obj["max_age"].toDouble();
} }
// presumed innocent until closer examination // presumed innocent until closer examination

View file

@ -252,7 +252,6 @@
<file>scalable/discord.svg</file> <file>scalable/discord.svg</file>
<!-- flat instance icons CC BY-SA 4.0, Santiago Cézar --> <!-- flat instance icons CC BY-SA 4.0, Santiago Cézar -->
<file alias="128x128/flame.png">scalable/instances/flame.svg</file>
<file>scalable/instances/chicken.svg</file> <file>scalable/instances/chicken.svg</file>
<file>scalable/instances/creeper.svg</file> <file>scalable/instances/creeper.svg</file>
<file>scalable/instances/enderpearl.svg</file> <file>scalable/instances/enderpearl.svg</file>

View file

@ -20,6 +20,12 @@
#include "settings/Setting.h" #include "settings/Setting.h"
#include <QVariant> #include <QVariant>
#include <QDir>
#include <utility>
#ifdef Q_OS_MACOS
#include "macsandbox/SecurityBookmarkFileAccess.h"
#endif
SettingsObject::SettingsObject(QObject* parent) : QObject(parent) {} SettingsObject::SettingsObject(QObject* parent) : QObject(parent) {}
@ -78,9 +84,17 @@ std::shared_ptr<Setting> SettingsObject::getSetting(const QString& id) const
return m_settings[id]; return m_settings[id];
} }
QVariant SettingsObject::get(const QString& id) const QVariant SettingsObject::get(const QString& id)
{ {
auto setting = getSetting(id); auto setting = getSetting(id);
#ifdef Q_OS_MACOS
// for macOS, use a security scoped bookmark for the paths
if (id.endsWith("Dir")) {
return { getPathFromBookmark(id) };
}
#endif
return (setting ? setting->get() : QVariant()); return (setting ? setting->get() : QVariant());
} }
@ -90,11 +104,105 @@ bool SettingsObject::set(const QString& id, QVariant value)
if (!setting) { if (!setting) {
qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id); qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id);
return false; return false;
} else { }
setting->set(value);
#ifdef Q_OS_MACOS
// for macOS, keep a security scoped bookmark for the paths
if (value.userType() == QMetaType::QString && id.endsWith("Dir")) {
setPathWithBookmark(id, value.toString());
}
#endif
setting->set(std::move(value));
return true;
}
#ifdef Q_OS_MACOS
QString SettingsObject::getPathFromBookmark(const QString& id)
{
auto setting = getSetting(id);
if (!setting) {
qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id);
return "";
}
// there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access)
if (setting->get() == setting->defValue() || QDir(setting->get().toString()).absolutePath().startsWith(QDir::current().absolutePath())) {
return setting->get().toString();
}
auto bookmarkId = id + "Bookmark";
auto bookmarkSetting = getSetting(bookmarkId);
if (!bookmarkSetting) {
qCritical() << QString("Error changing setting %1. Bookmark setting doesn't exist.").arg(id);
return "";
}
QByteArray bookmark = bookmarkSetting->get().toByteArray();
if (bookmark.isEmpty()) {
qDebug() << "Creating bookmark for" << id << "at" << setting->get().toString();
setPathWithBookmark(id, setting->get().toString());
return setting->get().toString();
}
bool stale;
QUrl url = m_sandboxedFileAccess.securityScopedBookmarkToURL(bookmark, stale);
if (url.isValid()) {
if (stale) {
setting->set(url.path());
bookmarkSetting->set(bookmark);
}
m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bookmark, stale);
// already did a stale check, no need to do it again
// convert to relative path to current directory if `url` is a descendant of the current directory
QDir currentDir = QDir::current().absolutePath();
return url.path().startsWith(currentDir.absolutePath()) ? currentDir.relativeFilePath(url.path()) : url.path();
}
return setting->get().toString();
}
bool SettingsObject::setPathWithBookmark(const QString& id, const QString& path)
{
auto setting = getSetting(id);
if (!setting) {
qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id);
return false;
}
QDir dir(path);
if (!dir.exists()) {
qCritical() << QString("Error changing setting %1. Path doesn't exist.").arg(id);
return false;
}
QString absolutePath = dir.absolutePath();
QString bookmarkId = id + "Bookmark";
std::shared_ptr<Setting> bookmarkSetting = getSetting(bookmarkId);
// there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access)
if (path == setting->defValue().toString() || absolutePath.startsWith(QDir::current().absolutePath())) {
bookmarkSetting->reset();
return true; return true;
} }
QByteArray bytes = m_sandboxedFileAccess.pathToSecurityScopedBookmark(absolutePath);
if (bytes.isEmpty()) {
qCritical() << QString("Failed to create bookmark for %1 - no access?").arg(id);
// TODO: show an alert to the user asking them to reselect the directory
return false;
}
auto oldBookmark = bookmarkSetting->get().toByteArray();
m_sandboxedFileAccess.stopUsingSecurityScopedBookmark(oldBookmark);
if (!bytes.isEmpty() && bookmarkSetting) {
bookmarkSetting->set(bytes);
bool stale;
m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bytes, stale);
// just created the bookmark, it shouldn't be stale
}
setting->set(path);
return true;
} }
#endif
void SettingsObject::reset(const QString& id) const void SettingsObject::reset(const QString& id) const
{ {

View file

@ -23,6 +23,10 @@
#include <QVariant> #include <QVariant>
#include <memory> #include <memory>
#ifdef Q_OS_MACOS
#include "macsandbox/SecurityBookmarkFileAccess.h"
#endif
class Setting; class Setting;
class SettingsObject; class SettingsObject;
@ -119,7 +123,27 @@ class SettingsObject : public QObject {
* \return The setting's value as a QVariant. * \return The setting's value as a QVariant.
* If no setting with the given ID exists, returns an invalid QVariant. * If no setting with the given ID exists, returns an invalid QVariant.
*/ */
QVariant get(const QString& id) const; QVariant get(const QString& id);
#ifdef Q_OS_MACOS
/*!
* \brief Get the path to the file or directory represented by the bookmark stored in the associated setting.
* \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end.
* \return A path to the file or directory represented by the bookmark.
* If a bookmark is not valid or stored, use default logic (directly return the stored path).
* This can attempt to create a bookmark if the path is accessible and the bookmark is not valid.
*/
QString getPathFromBookmark(const QString& id);
/*!
* \brief Set a security-scoped bookmark to the provided path for the associated setting.
* \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end.
* \param path The new desired path.
* \return A boolean indicating whether a bookmark was successfully set.
* The path needs to be accessible to the launcher before calling this function. For example,
* it could come from a user selection in an open panel.
*/
bool setPathWithBookmark(const QString& id, const QString& path);
#endif
/*! /*!
* \brief Sets the value of the setting with the given ID. * \brief Sets the value of the setting with the given ID.
@ -207,6 +231,9 @@ class SettingsObject : public QObject {
private: private:
QMap<QString, std::shared_ptr<Setting>> m_settings; QMap<QString, std::shared_ptr<Setting>> m_settings;
#ifdef Q_OS_MACOS
SecurityBookmarkFileAccess m_sandboxedFileAccess;
#endif
protected: protected:
bool m_suspendSave = false; bool m_suspendSave = false;

View file

@ -251,8 +251,8 @@ void readIndex(const QString& path, QMap<QString, Language>& languages)
Language lang(iter.key()); Language lang(iter.key());
auto langObj = Json::requireObject(iter.value()); auto langObj = Json::requireObject(iter.value());
lang.setTranslationStats(Json::ensureInteger(langObj, "translated", 0), Json::ensureInteger(langObj, "untranslated", 0), lang.setTranslationStats(langObj["translated"].toInt(), langObj["untranslated"].toInt(),
Json::ensureInteger(langObj, "fuzzy", 0)); langObj["fuzzy"].toInt());
lang.file_name = Json::requireString(langObj, "file"); lang.file_name = Json::requireString(langObj, "file");
lang.file_sha1 = Json::requireString(langObj, "sha1"); lang.file_sha1 = Json::requireString(langObj, "sha1");
lang.file_size = Json::requireInteger(langObj, "size"); lang.file_size = Json::requireInteger(langObj, "size");

View file

@ -967,7 +967,7 @@ void MainWindow::processURLs(QList<QUrl> urls)
connect(job.get(), &Task::succeeded, this, [this, array, addonId, fileId, &dl_url, &version] { connect(job.get(), &Task::succeeded, this, [this, array, addonId, fileId, &dl_url, &version] {
qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str(); qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str();
auto doc = Json::requireDocument(*array); auto doc = Json::requireDocument(*array);
auto data = Json::ensureObject(Json::ensureObject(doc.object()), "data"); auto data = doc.object()["data"].toObject();
// No way to find out if it's a mod or a modpack before here // No way to find out if it's a mod or a modpack before here
// And also we need to check if it ends with .zip, instead of any better way // And also we need to check if it ends with .zip, instead of any better way
version = FlameMod::loadIndexedPackVersion(data); version = FlameMod::loadIndexedPackVersion(data);

View file

@ -0,0 +1,74 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2025 Octol1ttle <l1ttleofficial@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ChooseOfflineNameDialog.h"
#include <QPushButton>
#include <QRegularExpression>
#include "ui_ChooseOfflineNameDialog.h"
ChooseOfflineNameDialog::ChooseOfflineNameDialog(const QString& message, QWidget* parent) : QDialog(parent), ui(new Ui::ChooseOfflineNameDialog)
{
ui->setupUi(this);
ui->label->setText(message);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
const QRegularExpression usernameRegExp("^[A-Za-z0-9_]{3,16}$");
m_usernameValidator = new QRegularExpressionValidator(usernameRegExp, this);
ui->usernameTextBox->setValidator(m_usernameValidator);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
ChooseOfflineNameDialog::~ChooseOfflineNameDialog()
{
delete ui;
}
QString ChooseOfflineNameDialog::getUsername() const
{
return ui->usernameTextBox->text();
}
void ChooseOfflineNameDialog::setUsername(const QString& username) const
{
ui->usernameTextBox->setText(username);
updateAcceptAllowed(username);
}
void ChooseOfflineNameDialog::updateAcceptAllowed(const QString& username) const
{
const bool allowed = ui->allowInvalidUsernames->isChecked() ? !username.isEmpty() : ui->usernameTextBox->hasAcceptableInput();
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowed);
}
void ChooseOfflineNameDialog::on_usernameTextBox_textEdited(const QString& newText) const
{
updateAcceptAllowed(newText);
}
void ChooseOfflineNameDialog::on_allowInvalidUsernames_checkStateChanged(const Qt::CheckState checkState) const
{
ui->usernameTextBox->setValidator(checkState == Qt::Checked ? nullptr : m_usernameValidator);
updateAcceptAllowed(getUsername());
}

View file

@ -0,0 +1,50 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2025 Octol1ttle <l1ttleofficial@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDialog>
#include <QRegularExpressionValidator>
QT_BEGIN_NAMESPACE
namespace Ui {
class ChooseOfflineNameDialog;
}
QT_END_NAMESPACE
class ChooseOfflineNameDialog final : public QDialog {
Q_OBJECT
public:
explicit ChooseOfflineNameDialog(const QString& message, QWidget* parent = nullptr);
~ChooseOfflineNameDialog() override;
QString getUsername() const;
void setUsername(const QString& username) const;
private:
void updateAcceptAllowed(const QString& username) const;
protected slots:
void on_usernameTextBox_textEdited(const QString& newText) const;
void on_allowInvalidUsernames_checkStateChanged(Qt::CheckState checkState) const;
private:
Ui::ChooseOfflineNameDialog* ui;
QRegularExpressionValidator* m_usernameValidator;
};

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ChooseOfflineNameDialog</class>
<widget class="QDialog" name="ChooseOfflineNameDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>158</height>
</rect>
</property>
<property name="windowTitle">
<string>Choose Offline Name</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Message label placeholder.</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="usernameTextBox">
<property name="placeholderText">
<string>Username</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="allowInvalidUsernames">
<property name="toolTip">
<string>A username is valid only if it is from 3 to 16 characters in length, uses English letters, numbers, and underscores. An invalid username may prevent joining servers and singleplayer worlds.</string>
</property>
<property name="text">
<string>Allow invalid usernames</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -50,7 +50,7 @@
#include <QUrl> #include <QUrl>
#include <QtWidgets/QPushButton> #include <QtWidgets/QPushButton>
#include "qrcodegen.hpp" #include "qrencode.h"
MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog) MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
{ {
@ -146,27 +146,32 @@ void MSALoginDialog::authorizeWithBrowser(const QUrl& url)
m_url = url; m_url = url;
} }
// https://stackoverflow.com/questions/21400254/how-to-draw-a-qr-code-with-qt-in-native-c-c void paintQR(QPainter& painter, const QSize canvasSize, const QString& data, QColor fg)
void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg)
{ {
// NOTE: At this point you will use the API to get the encoding and format you want, instead of my hardcoded stuff: const auto* qr = QRcode_encodeString(data.toUtf8().constData(), 0, QRecLevel::QR_ECLEVEL_M, QRencodeMode::QR_MODE_8, 1);
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW); if (!qr) {
const int s = qr.getSize() > 0 ? qr.getSize() : 1; qWarning() << "Unable to encode" << data << "as QR code";
const double w = sz.width(); return;
const double h = sz.height(); }
const double aspect = w / h;
const double size = ((aspect > 1.0) ? h : w);
const double scale = size / (s + 2);
// NOTE: For performance reasons my implementation only draws the foreground parts in supplied color.
// It expects background to be prepared already (in white or whatever is preferred).
painter.setPen(Qt::NoPen); painter.setPen(Qt::NoPen);
painter.setBrush(fg); painter.setBrush(fg);
for (int y = 0; y < s; y++) {
for (int x = 0; x < s; x++) { // Make sure the QR code fits in the canvas with some padding
const int color = qr.getModule(x, y); // 0 for white, 1 for black const auto qrSize = qr->width;
if (0 != color) { const auto canvasWidth = canvasSize.width();
const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale; const auto canvasHeight = canvasSize.height();
QRectF r(rx1, ry1, scale, scale); const auto scale = 0.8 * std::min(canvasWidth / qrSize, canvasHeight / qrSize);
// Find an offset to center it in the canvas
const auto offsetX = (canvasWidth - qrSize * scale) / 2;
const auto offsetY = (canvasHeight - qrSize * scale) / 2;
for (int y = 0; y < qrSize; y++) {
for (int x = 0; x < qrSize; x++) {
auto shouldFillIn = qr->data[y * qrSize + x] & 1;
if (shouldFillIn) {
QRectF r(offsetX + x * scale, offsetY + y * scale, scale, scale);
painter.drawRects(&r, 1); painter.drawRects(&r, 1);
} }
} }

View file

@ -1,105 +0,0 @@
#include "OfflineLoginDialog.h"
#include "ui_OfflineLoginDialog.h"
#include <QtWidgets/QPushButton>
OfflineLoginDialog::OfflineLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::OfflineLoginDialog)
{
ui->setupUi(this);
ui->progressBar->setVisible(false);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
OfflineLoginDialog::~OfflineLoginDialog()
{
delete ui;
}
// Stage 1: User interaction
void OfflineLoginDialog::accept()
{
setUserInputsEnabled(false);
ui->progressBar->setVisible(true);
// Setup the login task and start it
m_account = MinecraftAccount::createOffline(ui->userTextBox->text());
m_loginTask = m_account->login();
connect(m_loginTask.get(), &Task::failed, this, &OfflineLoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &OfflineLoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &OfflineLoginDialog::onTaskStatus);
connect(m_loginTask.get(), &Task::progress, this, &OfflineLoginDialog::onTaskProgress);
m_loginTask->start();
}
void OfflineLoginDialog::setUserInputsEnabled(bool enable)
{
ui->userTextBox->setEnabled(enable);
ui->buttonBox->setEnabled(enable);
}
void OfflineLoginDialog::on_allowLongUsernames_stateChanged(int value)
{
if (value == Qt::Checked) {
ui->userTextBox->setMaxLength(INT_MAX);
} else {
ui->userTextBox->setMaxLength(16);
}
}
// Enable the OK button only when the textbox contains something.
void OfflineLoginDialog::on_userTextBox_textEdited(const QString& newText)
{
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty());
}
void OfflineLoginDialog::onTaskFailed(const QString& reason)
{
// Set message
auto lines = reason.split('\n');
QString processed;
for (auto line : lines) {
if (line.size()) {
processed += "<font color='red'>" + line + "</font><br />";
} else {
processed += "<br />";
}
}
ui->label->setText(processed);
// Re-enable user-interaction
setUserInputsEnabled(true);
ui->progressBar->setVisible(false);
}
void OfflineLoginDialog::onTaskSucceeded()
{
QDialog::accept();
}
void OfflineLoginDialog::onTaskStatus(const QString& status)
{
ui->label->setText(status);
}
void OfflineLoginDialog::onTaskProgress(qint64 current, qint64 total)
{
ui->progressBar->setMaximum(total);
ui->progressBar->setValue(current);
}
// Public interface
MinecraftAccountPtr OfflineLoginDialog::newAccount(QWidget* parent, QString msg)
{
OfflineLoginDialog dlg(parent);
dlg.ui->label->setText(msg);
if (dlg.exec() == QDialog::Accepted) {
return dlg.m_account;
}
return nullptr;
}

View file

@ -1,40 +0,0 @@
#pragma once
#include <QtWidgets/QDialog>
#include "minecraft/auth/MinecraftAccount.h"
#include "tasks/Task.h"
namespace Ui {
class OfflineLoginDialog;
}
class OfflineLoginDialog : public QDialog {
Q_OBJECT
public:
~OfflineLoginDialog();
static MinecraftAccountPtr newAccount(QWidget* parent, QString message);
private:
explicit OfflineLoginDialog(QWidget* parent = 0);
void setUserInputsEnabled(bool enable);
protected slots:
void accept();
void onTaskFailed(const QString& reason);
void onTaskSucceeded();
void onTaskStatus(const QString& status);
void onTaskProgress(qint64 current, qint64 total);
void on_userTextBox_textEdited(const QString& newText);
void on_allowLongUsernames_stateChanged(int value);
private:
Ui::OfflineLoginDialog* ui;
MinecraftAccountPtr m_account;
Task::Ptr m_loginTask;
};

View file

@ -1,80 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OfflineLoginDialog</class>
<widget class="QDialog" name="OfflineLoginDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>150</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Add Account</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string notr="true">Message label placeholder.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="userTextBox">
<property name="maxLength">
<number>16</number>
</property>
<property name="placeholderText">
<string>Username</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="allowLongUsernames">
<property name="toolTip">
<string>Usernames longer than 16 characters cannot be used for LAN games or offline-mode servers.</string>
</property>
<property name="text">
<string>Allow long usernames</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>69</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -461,13 +461,12 @@ void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, Q
auto requiredByItem = new QTreeWidgetItem(item_top); auto requiredByItem = new QTreeWidgetItem(item_top);
if (requiredBy.length() == 1) { if (requiredBy.length() == 1) {
requiredByItem->setText(0, tr("Required by: %1").arg(requiredBy.back())); requiredByItem->setText(0, tr("Required by: %1").arg(requiredBy.back()));
requiredByItem->setData(0, Qt::UserRole, requiredBy.back());
} else { } else {
requiredByItem->setText(0, tr("Required by:")); requiredByItem->setText(0, tr("Required by:"));
auto i = 0;
for (auto req : requiredBy) { for (auto req : requiredBy) {
auto reqItem = new QTreeWidgetItem(requiredByItem); auto reqItem = new QTreeWidgetItem(requiredByItem);
reqItem->setText(0, req); reqItem->setText(0, req);
reqItem->insertChildren(i++, { reqItem });
} }
} }

View file

@ -68,18 +68,14 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info)
filenameItem->setText(0, tr("Filename: %1").arg(info.filename)); filenameItem->setText(0, tr("Filename: %1").arg(info.filename));
filenameItem->setData(0, Qt::UserRole, info.filename); filenameItem->setData(0, Qt::UserRole, info.filename);
auto childIndx = 0;
itemTop->insertChildren(childIndx++, { filenameItem });
if (!info.custom_file_path.isEmpty()) { if (!info.custom_file_path.isEmpty()) {
auto customPathItem = new QTreeWidgetItem(itemTop); auto customPathItem = new QTreeWidgetItem(itemTop);
customPathItem->setText(0, tr("This download will be placed in: %1").arg(info.custom_file_path)); customPathItem->setText(0, tr("This download will be placed in: %1").arg(info.custom_file_path));
customPathItem->setData(0, Qt::UserRole, info.custom_file_path);
itemTop->insertChildren(1, { customPathItem });
itemTop->setIcon(1, QIcon(QIcon::fromTheme("status-yellow"))); itemTop->setIcon(1, QIcon(QIcon::fromTheme("status-yellow")));
itemTop->setToolTip( itemTop->setToolTip(
childIndx++, 1,
tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it.")); tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it."));
} }
@ -87,23 +83,19 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info)
providerItem->setText(0, tr("Provider: %1").arg(info.provider)); providerItem->setText(0, tr("Provider: %1").arg(info.provider));
providerItem->setData(0, Qt::UserRole, info.provider); providerItem->setData(0, Qt::UserRole, info.provider);
itemTop->insertChildren(childIndx++, { providerItem });
if (!info.required_by.isEmpty()) { if (!info.required_by.isEmpty()) {
auto requiredByItem = new QTreeWidgetItem(itemTop); auto requiredByItem = new QTreeWidgetItem(itemTop);
if (info.required_by.length() == 1) { if (info.required_by.length() == 1) {
requiredByItem->setText(0, tr("Required by: %1").arg(info.required_by.back())); requiredByItem->setText(0, tr("Required by: %1").arg(info.required_by.back()));
requiredByItem->setData(0, Qt::UserRole, info.required_by.back());
} else { } else {
requiredByItem->setText(0, tr("Required by:")); requiredByItem->setText(0, tr("Required by:"));
auto i = 0;
for (auto req : info.required_by) { for (auto req : info.required_by) {
auto reqItem = new QTreeWidgetItem(requiredByItem); auto reqItem = new QTreeWidgetItem(requiredByItem);
reqItem->setText(0, req); reqItem->setText(0, req);
reqItem->insertChildren(i++, { reqItem });
} }
} }
itemTop->insertChildren(childIndx++, { requiredByItem });
ui->toggleDepsButton->show(); ui->toggleDepsButton->show();
m_deps << itemTop; m_deps << itemTop;
} }
@ -112,8 +104,6 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info)
versionTypeItem->setText(0, tr("Version Type: %1").arg(info.version_type)); versionTypeItem->setText(0, tr("Version Type: %1").arg(info.version_type));
versionTypeItem->setData(0, Qt::UserRole, info.version_type); versionTypeItem->setData(0, Qt::UserRole, info.version_type);
itemTop->insertChildren(childIndx++, { versionTypeItem });
ui->modTreeWidget->addTopLevelItem(itemTop); ui->modTreeWidget->addTopLevelItem(itemTop);
} }

View file

@ -481,7 +481,7 @@ void SkinManageDialog::on_userBtn_clicked()
return; return;
} }
const auto root = doc.object(); const auto root = doc.object();
auto id = Json::ensureString(root, "id"); auto id = root["id"].toString();
if (!id.isEmpty()) { if (!id.isEmpty()) {
getProfile->setUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + id); getProfile->setUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + id);
} else { } else {

View file

@ -202,6 +202,13 @@ void SkinOpenGLWindow::resizeGL(int w, int h)
void SkinOpenGLWindow::paintGL() void SkinOpenGLWindow::paintGL()
{ {
// Adjust the viewport to account for fractional scaling
qreal dpr = devicePixelRatio();
if (dpr != 1.f) {
QSize scaledSize = size() * dpr;
glViewport(0, 0, scaledSize.width(), scaledSize.height());
}
// Clear color and depth buffer // Clear color and depth buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
@ -235,6 +242,13 @@ void SkinOpenGLWindow::paintGL()
m_scene->draw(m_modelProgram); m_scene->draw(m_modelProgram);
m_modelProgram->release(); m_modelProgram->release();
// Redraw the first frame; this is necessary because the pixel ratio for Wayland fractional scaling is not negotiated properly on the
// first frame
if (m_isFirstFrame) {
m_isFirstFrame = false;
update();
}
} }
void SkinOpenGLWindow::updateScene(SkinModel* skin) void SkinOpenGLWindow::updateScene(SkinModel* skin)

View file

@ -74,6 +74,8 @@ class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions {
float m_yaw = 90; // Horizontal rotation angle float m_yaw = 90; // Horizontal rotation angle
float m_pitch = 0; // Vertical rotation angle float m_pitch = 0; // Vertical rotation angle
bool m_isFirstFrame = true;
opengl::BoxGeometry* m_background = nullptr; opengl::BoxGeometry* m_background = nullptr;
QOpenGLTexture* m_backgroundTexture = nullptr; QOpenGLTexture* m_backgroundTexture = nullptr;
QColor m_baseColor; QColor m_baseColor;

View file

@ -45,8 +45,8 @@
#include <QDebug> #include <QDebug>
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ChooseOfflineNameDialog.h"
#include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/MSALoginDialog.h"
#include "ui/dialogs/OfflineLoginDialog.h"
#include "Application.h" #include "Application.h"
@ -149,10 +149,13 @@ void AccountListPage::on_actionAddOffline_triggered()
return; return;
} }
MinecraftAccountPtr account = ChooseOfflineNameDialog dialog(tr("Please enter your desired username to add your offline account."), this);
OfflineLoginDialog::newAccount(this, tr("Please enter your desired username to add your offline account.")); if (dialog.exec() != QDialog::Accepted) {
return;
}
if (account) { if (const MinecraftAccountPtr account = MinecraftAccount::createOffline(dialog.getUsername())) {
account->login()->start(); // The task will complete here.
m_accounts->addAccount(account); m_accounts->addAccount(account);
if (m_accounts->count() == 1) { if (m_accounts->count() == 1) {
m_accounts->setDefaultAccount(account); m_accounts->setDefaultAccount(account);

View file

@ -6,12 +6,14 @@
#include <QDesktopServices> #include <QDesktopServices>
#include <QUrl> #include <QUrl>
#include <QUrlQuery> #include <QUrlQuery>
#include "modplatform/ModIndex.h"
#include "ui_ManagedPackPage.h" #include "ui_ManagedPackPage.h"
#include <QFileDialog> #include <QFileDialog>
#include <QListView> #include <QListView>
#include <QProxyStyle> #include <QProxyStyle>
#include <QStyleFactory> #include <QStyleFactory>
#include <memory>
#include "Application.h" #include "Application.h"
#include "BuildConfig.h" #include "BuildConfig.h"
@ -284,7 +286,8 @@ void ModrinthManagedPackPage::parseManagedPack()
}; };
callbacks.on_fail = [this](QString reason, int) { setFailState(); }; callbacks.on_fail = [this](QString reason, int) { setFailState(); };
callbacks.on_abort = [this]() { setFailState(); }; callbacks.on_abort = [this]() { setFailState(); };
m_fetch_job = m_api.getProjectVersions({ m_pack, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); m_fetch_job = m_api.getProjectVersions(
{ std::make_shared<ModPlatform::IndexedPack>(m_pack), {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
ui->changelogTextBrowser->setText(tr("Fetching changelogs...")); ui->changelogTextBrowser->setText(tr("Fetching changelogs..."));
@ -455,7 +458,8 @@ void FlameManagedPackPage::parseManagedPack()
}; };
callbacks.on_fail = [this](QString reason, int) { setFailState(); }; callbacks.on_fail = [this](QString reason, int) { setFailState(); };
callbacks.on_abort = [this]() { setFailState(); }; callbacks.on_abort = [this]() { setFailState(); };
m_fetch_job = m_api.getProjectVersions({ m_pack, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); m_fetch_job = m_api.getProjectVersions(
{ std::make_shared<ModPlatform::IndexedPack>(m_pack), {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_fetch_job->start(); m_fetch_job->start();
} }

View file

@ -287,16 +287,10 @@ void OtherLogsPage::reload()
if (!m_instance) { if (!m_instance) {
level = MessageLevel::fromLauncherLine(lineTemp); level = MessageLevel::fromLauncherLine(lineTemp);
} else { } else {
// if the launcher part set a log level, use it level = LogParser::guessLevel(line);
auto innerLevel = MessageLevel::fromLine(lineTemp);
if (innerLevel != MessageLevel::Unknown) {
level = innerLevel;
}
// If the level is still undetermined, guess level if (level == MessageLevel::Unknown)
if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { level = last;
level = LogParser::guessLevel(line, last);
}
} }
last = level; last = level;

View file

@ -23,14 +23,14 @@ ResourceAPI::SearchArgs DataPackResourceModel::createSearchArguments()
ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(const QModelIndex& entry) ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(const QModelIndex& entry)
{ {
auto& pack = m_packs[entry.row()]; auto pack = m_packs[entry.row()];
return { *pack, {}, ModPlatform::ModLoaderType::DataPack }; return { pack, {}, ModPlatform::ModLoaderType::DataPack };
} }
ResourceAPI::ProjectInfoArgs DataPackResourceModel::createInfoArguments(const QModelIndex& entry) ResourceAPI::ProjectInfoArgs DataPackResourceModel::createInfoArguments(const QModelIndex& entry)
{ {
auto& pack = m_packs[entry.row()]; auto pack = m_packs[entry.row()];
return { *pack }; return { pack };
} }
void DataPackResourceModel::searchWithTerm(const QString& term, unsigned int sort) void DataPackResourceModel::searchWithTerm(const QString& term, unsigned int sort)

View file

@ -141,13 +141,13 @@ void ImportPage::updateState()
connect(job.get(), &NetJob::succeeded, this, [this, array, addonId, fileId] { connect(job.get(), &NetJob::succeeded, this, [this, array, addonId, fileId] {
qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str(); qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str();
auto doc = Json::requireDocument(*array); auto doc = Json::requireDocument(*array);
auto data = Json::ensureObject(Json::ensureObject(doc.object()), "data"); auto data = doc.object()["data"].toObject();
// No way to find out if it's a mod or a modpack before here // No way to find out if it's a mod or a modpack before here
// And also we need to check if it ends with .zip, instead of any better way // And also we need to check if it ends with .zip, instead of any better way
auto fileName = Json::ensureString(data, "fileName"); auto fileName = data["fileName"].toString();
if (fileName.endsWith(".zip")) { if (fileName.endsWith(".zip")) {
// Have to use ensureString then use QUrl to get proper url encoding // Have to use ensureString then use QUrl to get proper url encoding
auto dl_url = QUrl(Json::ensureString(data, "downloadUrl", "", "downloadUrl")); auto dl_url = QUrl(data["downloadUrl"].toString(""));
if (!dl_url.isValid()) { if (!dl_url.isValid()) {
CustomMessageBox::selectable( CustomMessageBox::selectable(
this, tr("Error"), this, tr("Error"),
@ -158,7 +158,7 @@ void ImportPage::updateState()
} }
QFileInfo dl_file(dl_url.fileName()); QFileInfo dl_file(dl_url.fileName());
QString pack_name = Json::ensureString(data, "displayName", dl_file.completeBaseName(), "displayName"); QString pack_name = data["displayName"].toString(dl_file.completeBaseName());
QMap<QString, QString> extra_info; QMap<QString, QString> extra_info;
extra_info.insert("pack_id", addonId); extra_info.insert("pack_id", addonId);

View file

@ -49,7 +49,7 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments()
ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelIndex& entry) ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelIndex& entry)
{ {
auto& pack = *m_packs[entry.row()]; auto pack = m_packs[entry.row()];
auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile(); auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile();
Q_ASSERT(profile); Q_ASSERT(profile);
@ -67,7 +67,7 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelInd
ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(const QModelIndex& entry) ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(const QModelIndex& entry)
{ {
auto& pack = *m_packs[entry.row()]; auto pack = m_packs[entry.row()];
return { pack }; return { pack };
} }

View file

@ -141,7 +141,7 @@ void ResourceModel::search()
if (m_search_term.startsWith("#")) { if (m_search_term.startsWith("#")) {
auto projectId = m_search_term.mid(1); auto projectId = m_search_term.mid(1);
if (!projectId.isEmpty()) { if (!projectId.isEmpty()) {
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks; ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks;
callbacks.on_fail = [this](QString reason, int) { callbacks.on_fail = [this](QString reason, int) {
if (!s_running_models.constFind(this).value()) if (!s_running_models.constFind(this).value())
@ -159,7 +159,9 @@ void ResourceModel::search()
return; return;
searchRequestForOneSucceeded(pack); searchRequestForOneSucceeded(pack);
}; };
if (auto job = m_api->getProjectInfo({ projectId }, std::move(callbacks)); job) auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = projectId;
if (auto job = m_api->getProjectInfo({ project }, std::move(callbacks)); job)
runSearchJob(job); runSearchJob(job);
return; return;
} }
@ -219,7 +221,7 @@ void ResourceModel::loadEntry(const QModelIndex& entry)
if (!pack->extraDataLoaded) { if (!pack->extraDataLoaded) {
auto args{ createInfoArguments(entry) }; auto args{ createInfoArguments(entry) };
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks{}; ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks{};
callbacks.on_succeed = [this, entry](auto& newpack) { callbacks.on_succeed = [this, entry](auto& newpack) {
if (!s_running_models.constFind(this).value()) if (!s_running_models.constFind(this).value())
@ -388,12 +390,12 @@ void ResourceModel::searchRequestSucceeded(QList<ModPlatform::IndexedPack::Ptr>&
endInsertRows(); endInsertRows();
} }
void ResourceModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack) void ResourceModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack)
{ {
m_search_state = SearchState::Finished; m_search_state = SearchState::Finished;
beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + 1); beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + 1);
m_packs.append(std::make_shared<ModPlatform::IndexedPack>(pack)); m_packs.append(pack);
endInsertRows(); endInsertRows();
} }
@ -448,18 +450,17 @@ void ResourceModel::versionRequestSucceeded(QVector<ModPlatform::IndexedVersion>
emit versionListUpdated(index); emit versionListUpdated(index);
} }
void ResourceModel::infoRequestSucceeded(ModPlatform::IndexedPack& pack, const QModelIndex& index) void ResourceModel::infoRequestSucceeded(ModPlatform::IndexedPack::Ptr pack, const QModelIndex& index)
{ {
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>(); auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
// Check if the index is still valid for this resource or not // Check if the index is still valid for this resource or not
if (pack.addonId != current_pack->addonId) if (pack->addonId != current_pack->addonId)
return; return;
*current_pack = pack;
// Cache info :^) // Cache info :^)
QVariant new_pack; QVariant new_pack;
new_pack.setValue(current_pack); new_pack.setValue(pack);
if (!setData(index, new_pack, Qt::UserRole)) { if (!setData(index, new_pack, Qt::UserRole)) {
qWarning() << "Failed to cache resource info!"; qWarning() << "Failed to cache resource info!";
return; return;

View file

@ -138,13 +138,13 @@ class ResourceModel : public QAbstractListModel {
private: private:
/* Default search request callbacks */ /* Default search request callbacks */
void searchRequestSucceeded(QList<ModPlatform::IndexedPack::Ptr>&); void searchRequestSucceeded(QList<ModPlatform::IndexedPack::Ptr>&);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack&); void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr);
void searchRequestFailed(QString reason, int network_error_code); void searchRequestFailed(QString reason, int network_error_code);
void searchRequestAborted(); void searchRequestAborted();
void versionRequestSucceeded(QVector<ModPlatform::IndexedVersion>&, QVariant, const QModelIndex&); void versionRequestSucceeded(QVector<ModPlatform::IndexedVersion>&, QVariant, const QModelIndex&);
void infoRequestSucceeded(ModPlatform::IndexedPack&, const QModelIndex&); void infoRequestSucceeded(ModPlatform::IndexedPack::Ptr, const QModelIndex&);
signals: signals:
void versionListUpdated(const QModelIndex& index); void versionListUpdated(const QModelIndex& index);

View file

@ -25,14 +25,14 @@ ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments()
ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(const QModelIndex& entry) ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(const QModelIndex& entry)
{ {
auto& pack = m_packs[entry.row()]; auto pack = m_packs[entry.row()];
return { *pack, {}, {}, ModPlatform::ResourceType::ResourcePack }; return { pack, {}, {}, ModPlatform::ResourceType::ResourcePack };
} }
ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(const QModelIndex& entry) ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(const QModelIndex& entry)
{ {
auto& pack = m_packs[entry.row()]; auto pack = m_packs[entry.row()];
return { *pack }; return { pack };
} }
void ResourcePackResourceModel::searchWithTerm(const QString& term, unsigned int sort) void ResourcePackResourceModel::searchWithTerm(const QString& term, unsigned int sort)

View file

@ -22,14 +22,14 @@ ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments()
ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(const QModelIndex& entry) ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(const QModelIndex& entry)
{ {
auto& pack = m_packs[entry.row()]; auto pack = m_packs[entry.row()];
return { *pack, {}, {}, ModPlatform::ResourceType::ShaderPack }; return { pack, {}, {}, ModPlatform::ResourceType::ShaderPack };
} }
ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(const QModelIndex& entry) ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(const QModelIndex& entry)
{ {
auto& pack = m_packs[entry.row()]; auto pack = m_packs[entry.row()];
return { *pack }; return { pack };
} }
void ShaderPackResourceModel::searchWithTerm(const QString& term, unsigned int sort) void ShaderPackResourceModel::searchWithTerm(const QString& term, unsigned int sort)

View file

@ -11,6 +11,7 @@
#include <Version.h> #include <Version.h>
#include <QtMath> #include <QtMath>
#include <memory>
namespace Flame { namespace Flame {
@ -167,7 +168,7 @@ void ListModel::performPaginatedSearch()
if (m_currentSearchTerm.startsWith("#")) { if (m_currentSearchTerm.startsWith("#")) {
auto projectId = m_currentSearchTerm.mid(1); auto projectId = m_currentSearchTerm.mid(1);
if (!projectId.isEmpty()) { if (!projectId.isEmpty()) {
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks; ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks;
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); }; callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); };
@ -175,7 +176,9 @@ void ListModel::performPaginatedSearch()
qCritical() << "Search task aborted by an unknown reason!"; qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted"); searchRequestFailed("Aborted");
}; };
if (auto job = api.getProjectInfo({ { projectId } }, std::move(callbacks)); job) { auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = projectId;
if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) {
m_jobPtr = job; m_jobPtr = job;
m_jobPtr->start(); m_jobPtr->start();
} }
@ -189,6 +192,10 @@ void ListModel::performPaginatedSearch()
callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); }; callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); };
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_abort = [this] {
qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted");
};
auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders, auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders,
m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }, m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource },
@ -241,12 +248,12 @@ void Flame::ListModel::searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr
endInsertRows(); endInsertRows();
} }
void Flame::ListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack) void Flame::ListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack)
{ {
m_jobPtr.reset(); m_jobPtr.reset();
beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1); beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1);
m_modpacks.append(std::make_shared<ModPlatform::IndexedPack>(pack)); m_modpacks.append(pack);
endInsertRows(); endInsertRows();
} }

View file

@ -50,7 +50,7 @@ class ListModel : public QAbstractListModel {
void searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr>&); void searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr>&);
void searchRequestFailed(QString reason); void searchRequestFailed(QString reason);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack&); void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr);
private: private:
void requestLogo(QString file, QString url); void requestLogo(QString file, QString url);

View file

@ -203,7 +203,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec();
}; };
auto netJob = api.getProjectVersions({ *m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); auto netJob = api.getProjectVersions({ m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_job = netJob; m_job = netJob;
netJob->start(); netJob->start();

View file

@ -47,6 +47,7 @@
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
#include <QMessageBox> #include <QMessageBox>
#include <memory>
namespace Modrinth { namespace Modrinth {
@ -137,7 +138,7 @@ void ModpackListModel::performPaginatedSearch()
if (m_currentSearchTerm.startsWith("#")) { if (m_currentSearchTerm.startsWith("#")) {
auto projectId = m_currentSearchTerm.mid(1); auto projectId = m_currentSearchTerm.mid(1);
if (!projectId.isEmpty()) { if (!projectId.isEmpty()) {
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks; ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks;
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); }; callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); };
@ -145,7 +146,9 @@ void ModpackListModel::performPaginatedSearch()
qCritical() << "Search task aborted by an unknown reason!"; qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted"); searchRequestFailed("Aborted");
}; };
if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = projectId;
if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) {
m_jobPtr = job; m_jobPtr = job;
m_jobPtr->start(); m_jobPtr->start();
} }
@ -159,6 +162,10 @@ void ModpackListModel::performPaginatedSearch()
callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); }; callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); };
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_abort = [this] {
qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted");
};
auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders, auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders,
m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }, m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource },
@ -300,12 +307,12 @@ void ModpackListModel::searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr
endInsertRows(); endInsertRows();
} }
void ModpackListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack) void ModpackListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack)
{ {
m_jobPtr.reset(); m_jobPtr.reset();
beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1); beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1);
m_modpacks.append(std::make_shared<ModPlatform::IndexedPack>(pack)); m_modpacks.append(pack);
endInsertRows(); endInsertRows();
} }

View file

@ -86,7 +86,7 @@ class ModpackListModel : public QAbstractListModel {
public slots: public slots:
void searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr>& doc_all); void searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr>& doc_all);
void searchRequestFailed(QString reason); void searchRequestFailed(QString reason);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack&); void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr);
protected slots: protected slots:

View file

@ -146,7 +146,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI
if (!m_current->extraDataLoaded) { if (!m_current->extraDataLoaded) {
qDebug() << "Loading modrinth modpack information"; qDebug() << "Loading modrinth modpack information";
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks; ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks;
auto id = m_current->addonId; auto id = m_current->addonId;
callbacks.on_fail = [this](QString reason, int) { callbacks.on_fail = [this](QString reason, int) {
@ -157,10 +157,8 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI
return; // wrong request? return; // wrong request?
} }
*m_current = pack;
QVariant current_updated; QVariant current_updated;
current_updated.setValue(m_current); current_updated.setValue(pack);
if (!m_model->setData(curr, current_updated, Qt::UserRole)) if (!m_model->setData(curr, current_updated, Qt::UserRole))
qWarning() << "Failed to cache extra info for the current pack!"; qWarning() << "Failed to cache extra info for the current pack!";
@ -168,7 +166,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI
suggestCurrent(); suggestCurrent();
updateUI(); updateUI();
}; };
if (auto netJob = m_api.getProjectInfo({ { m_current->addonId } }, std::move(callbacks)); netJob) { if (auto netJob = m_api.getProjectInfo({ m_current }, std::move(callbacks)); netJob) {
m_job = netJob; m_job = netJob;
m_job->start(); m_job->start();
} }
@ -220,7 +218,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec();
}; };
auto netJob = m_api.getProjectVersions({ *m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); auto netJob = m_api.getProjectVersions({ m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_job2 = netJob; m_job2 = netJob;
m_job2->start(); m_job2->start();

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