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

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97 2023-10-16 23:13:53 +03:00
commit b6f48f6fe0
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
163 changed files with 5973 additions and 1781 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ Feel free to create a GitHub issue if you find a bug or want to suggest a new fe
## Translations
The translation effort for Prism Launcher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at <https://github.com/PrismLauncher/Translations>
The translation effort for Prism Launcher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at <https://github.com/PrismLauncher/Translations>.
## Building

View File

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

View File

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

View File

@ -89,6 +89,10 @@ function(
-Wdouble-promotion # warn if float is implicit promoted to double
-Wformat=2 # warn on security issues around functions that format output (ie printf)
-Wimplicit-fallthrough # warn on statements that fallthrough without an explicit annotation
# -Wgnu-zero-variadic-macro-arguments (part of -pedantic) is triggered by every qCDebug() call and therefore results
# in a lot of noise. This warning is only notifying us that clang is emulating the GCC behaviour
# instead of the exact standard wording so we can safely ignore it
-Wno-gnu-zero-variadic-macro-arguments
)
endif()

30
flake.lock generated
View File

@ -3,11 +3,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
@ -21,11 +21,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1693611461,
"narHash": "sha256-aPODl8vAgGQ0ZYFIRisxYG5MOGSkIczvu2Cd8Gb9+1Y=",
"lastModified": 1696343447,
"narHash": "sha256-B2xAZKLkkeRFG5XcHHSXXcP7To9Xzr59KXeZiRf4vdQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "7f53fdb7bdc5bb237da7fefef12d099e4fd611ca",
"rev": "c9afaba3dfa4085dbd2ccb38dfade5141e33d9d4",
"type": "github"
},
"original": {
@ -106,11 +106,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1695318763,
"narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=",
"lastModified": 1697009197,
"narHash": "sha256-viVRhBTFT8fPJTb1N3brQIpFZnttmwo3JVKNuWRVc3s=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e12483116b3b51a185a33a272bf351e357ba9a99",
"rev": "01441e14af5e29c9d27ace398e6dd0b293e25a54",
"type": "github"
},
"original": {
@ -123,11 +123,11 @@
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1693471703,
"narHash": "sha256-0l03ZBL8P1P6z8MaSDS/MvuU8E75rVxe5eE1N6gxeTo=",
"lastModified": 1696019113,
"narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3e52e76b70d5508f3cec70b882a29199f4d1ee85",
"rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a",
"type": "github"
},
"original": {
@ -153,11 +153,11 @@
]
},
"locked": {
"lastModified": 1694364351,
"narHash": "sha256-oadhSCqopYXxURwIA6/Anpe5IAG11q2LhvTJNP5zE6o=",
"lastModified": 1696846637,
"narHash": "sha256-0hv4kbXxci2+pxhuXlVgftj/Jq79VSmtAyvfabCCtYk=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "4f883a76282bc28eb952570afc3d8a1bf6f481d7",
"rev": "42e1b6095ef80a51f79595d9951eb38e91c4e6ca",
"type": "github"
},
"original": {

View File

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

View File

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

View File

@ -181,6 +181,11 @@ set(MAC_UPDATE_SOURCES
updater/MacSparkleUpdater.mm
)
set(PRISM_UPDATE_SOURCES
updater/PrismExternalUpdater.h
updater/PrismExternalUpdater.cpp
)
# Backend for the news bar... there's usually no news.
set(NEWS_SOURCES
# News System
@ -216,13 +221,9 @@ set(MINECRAFT_SOURCES
minecraft/auth/MinecraftAccount.h
minecraft/auth/Parsers.cpp
minecraft/auth/Parsers.h
minecraft/auth/Yggdrasil.cpp
minecraft/auth/Yggdrasil.h
minecraft/auth/flows/AuthFlow.cpp
minecraft/auth/flows/AuthFlow.h
minecraft/auth/flows/Mojang.cpp
minecraft/auth/flows/Mojang.h
minecraft/auth/flows/MSA.cpp
minecraft/auth/flows/MSA.h
minecraft/auth/flows/Offline.cpp
@ -236,12 +237,8 @@ set(MINECRAFT_SOURCES
minecraft/auth/steps/GetSkinStep.h
minecraft/auth/steps/LauncherLoginStep.cpp
minecraft/auth/steps/LauncherLoginStep.h
minecraft/auth/steps/MigrationEligibilityStep.cpp
minecraft/auth/steps/MigrationEligibilityStep.h
minecraft/auth/steps/MinecraftProfileStep.cpp
minecraft/auth/steps/MinecraftProfileStep.h
minecraft/auth/steps/MinecraftProfileStepMojang.cpp
minecraft/auth/steps/MinecraftProfileStepMojang.h
minecraft/auth/steps/MSAStep.cpp
minecraft/auth/steps/MSAStep.h
minecraft/auth/steps/XboxAuthorizationStep.cpp
@ -250,8 +247,6 @@ set(MINECRAFT_SOURCES
minecraft/auth/steps/XboxProfileStep.h
minecraft/auth/steps/XboxUserStep.cpp
minecraft/auth/steps/XboxUserStep.h
minecraft/auth/steps/YggdrasilStep.cpp
minecraft/auth/steps/YggdrasilStep.h
minecraft/gameoptions/GameOptions.h
minecraft/gameoptions/GameOptions.cpp
@ -589,6 +584,63 @@ set(LINKEXE_SOURCES
DesktopServices.cpp
)
set(PRISMUPDATER_SOURCES
updater/prismupdater/PrismUpdater.h
updater/prismupdater/PrismUpdater.cpp
updater/prismupdater/UpdaterDialogs.h
updater/prismupdater/UpdaterDialogs.cpp
updater/prismupdater/GitHubRelease.h
updater/prismupdater/GitHubRelease.cpp
Json.h
Json.cpp
FileSystem.h
FileSystem.cpp
StringUtils.h
StringUtils.cpp
DesktopServices.h
DesktopServices.cpp
Version.h
Version.cpp
Markdown.h
Markdown.cpp
# Zip
MMCZip.h
MMCZip.cpp
# Time
MMCTime.h
MMCTime.cpp
net/ByteArraySink.h
net/ChecksumValidator.h
net/Download.cpp
net/Download.h
net/FileSink.cpp
net/FileSink.h
net/HttpMetaCache.cpp
net/HttpMetaCache.h
net/Logging.h
net/Logging.cpp
net/NetAction.h
net/NetRequest.cpp
net/NetRequest.h
net/NetJob.cpp
net/NetJob.h
net/NetUtils.h
net/Sink.h
net/Validator.h
net/HeaderProxy.h
net/RawHeaderProxy.h
ui/dialogs/ProgressDialog.cpp
ui/dialogs/ProgressDialog.h
ui/widgets/SubTaskProgressBar.h
ui/widgets/SubTaskProgressBar.cpp
)
######## Logging categories ########
ecm_qt_declare_logging_category(CORE_SOURCES
@ -685,6 +737,8 @@ set(LOGIC_SOURCES
if(APPLE AND Launcher_ENABLE_UPDATER)
set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES})
else()
set (LOGIC_SOURCES ${LOGIC_SOURCES} ${PRISM_UPDATE_SOURCES})
endif()
SET(LAUNCHER_SOURCES
@ -916,6 +970,9 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/ImportPage.cpp
ui/pages/modplatform/ImportPage.h
ui/pages/modplatform/OptionalModDialog.cpp
ui/pages/modplatform/OptionalModDialog.h
ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
ui/pages/modplatform/modrinth/ModrinthResourceModels.h
ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
@ -944,8 +1001,6 @@ SET(LAUNCHER_SOURCES
ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourceDialog.cpp
ui/dialogs/ImportResourceDialog.h
ui/dialogs/LoginDialog.cpp
ui/dialogs/LoginDialog.h
ui/dialogs/MSALoginDialog.cpp
ui/dialogs/MSALoginDialog.h
ui/dialogs/OfflineLoginDialog.cpp
@ -1042,6 +1097,15 @@ SET(LAUNCHER_SOURCES
ui/instanceview/VisualGroup.h
)
if (NOT Apple)
set(LAUNCHER_SOURCES
${LAUNCHER_SOURCES}
ui/dialogs/UpdateAvailableDialog.h
ui/dialogs/UpdateAvailableDialog.cpp
)
endif()
if(WIN32)
set(LAUNCHER_SOURCES
WindowsConsole.cpp
@ -1080,6 +1144,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/modplatform/legacy_ftb/Page.ui
ui/pages/modplatform/import_ftb/ImportFTBPage.ui
ui/pages/modplatform/ImportPage.ui
ui/pages/modplatform/OptionalModDialog.ui
ui/pages/modplatform/modrinth/ModrinthPage.ui
ui/pages/modplatform/technic/TechnicPage.ui
ui/widgets/InstanceCardWidget.ui
@ -1104,7 +1169,6 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui
ui/dialogs/LoginDialog.ui
ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui
@ -1112,6 +1176,14 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ChooseProviderDialog.ui
)
qt_wrap_ui(PRISM_UPDATE_UI
ui/dialogs/UpdateAvailableDialog.ui
)
if (NOT Apple)
set (LAUNCHER_UI ${LAUNCHER_UI} ${PRISM_UPDATE_UI})
endif()
qt_add_resources(LAUNCHER_RESOURCES
resources/backgrounds/backgrounds.qrc
resources/multimc/multimc.qrc
@ -1128,6 +1200,12 @@ qt_add_resources(LAUNCHER_RESOURCES
../${Launcher_Branding_LogoQRC}
)
qt_wrap_ui(PRISMUPDATER_UI
updater/prismupdater/SelectReleaseDialog.ui
ui/widgets/SubTaskProgressBar.ui
ui/dialogs/ProgressDialog.ui
)
######## Windows resource files ########
if(WIN32)
set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC})
@ -1146,6 +1224,7 @@ set_project_warnings(Launcher_logic
"${Launcher_GCC_WARNINGS}")
target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION)
target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION)
target_link_libraries(Launcher_logic
systeminfo
Launcher_murmur2
@ -1227,7 +1306,45 @@ install(TARGETS ${Launcher_Name}
FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
)
if(WIN32)
if(NOT APPLE OR (DEFINED Launcher_BUILD_UPDATER AND Launcher_BUILD_UPDATER))
# Updater
add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI})
target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(prism_updater_logic
QuaZip::QuaZip
${ZLIB_LIBRARIES}
systeminfo
BuildConfig
ghcFilesystem::ghc_filesystem
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
${Launcher_QT_LIBS}
cmark::cmark
Katabasis
)
add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp)
target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest)
target_link_libraries("${Launcher_Name}_updater" prism_updater_logic)
if(DEFINED Launcher_APP_BINARY_NAME)
set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_updater")
endif()
if(DEFINED Launcher_BINARY_RPATH)
SET_TARGET_PROPERTIES("${Launcher_Name}_updater" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}")
endif()
install(TARGETS "${Launcher_Name}_updater"
BUNDLE DESTINATION "." COMPONENT Runtime
LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime
RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime
FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
)
endif()
if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER))
# File link
add_library(filelink_logic STATIC ${LINKEXE_SOURCES})
set_project_warnings(filelink_logic
"${Launcher_MSVC_WARNINGS}"
@ -1246,7 +1363,7 @@ if(WIN32)
${Launcher_QT_LIBS}
)
add_executable("${Launcher_Name}_filelink" WIN32 filelink/main.cpp)
add_executable("${Launcher_Name}_filelink" WIN32 filelink/filelink_main.cpp)
target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest)

View File

@ -194,6 +194,40 @@ void write(const QString& filename, const QByteArray& data)
}
}
void appendSafe(const QString& filename, const QByteArray& data)
{
ensureExists(QFileInfo(filename).dir());
QByteArray buffer;
try {
buffer = read(filename);
} catch (FileSystemException&) {
buffer = QByteArray();
}
buffer.append(data);
QSaveFile file(filename);
if (!file.open(QSaveFile::WriteOnly)) {
throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString());
}
if (buffer.size() != file.write(buffer)) {
throw FileSystemException("Error writing data to " + filename + ": " + file.errorString());
}
if (!file.commit()) {
throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString());
}
}
void append(const QString& filename, const QByteArray& data)
{
ensureExists(QFileInfo(filename).dir());
QFile file(filename);
if (!file.open(QFile::Append)) {
throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString());
}
if (data.size() != file.write(data)) {
throw FileSystemException("Error writing data to " + filename + ": " + file.errorString());
}
}
QByteArray read(const QString& filename)
{
QFile file(filename);
@ -238,6 +272,28 @@ bool ensureFolderPathExists(QString foldernamepath)
return success;
}
bool copyFileAttributes(QString src, QString dst)
{
#ifdef Q_OS_WIN32
auto attrs = GetFileAttributesW(src.toStdWString().c_str());
if (attrs == INVALID_FILE_ATTRIBUTES)
return false;
return SetFileAttributesW(dst.toStdWString().c_str(), attrs);
#endif
return true;
}
// needs folders to exists
void copyFolderAttributes(QString src, QString dst, QString relative)
{
auto path = PathCombine(src, relative);
QDir dsrc(src);
while ((path = QFileInfo(path).path()).length() >= src.length()) {
auto dst_path = PathCombine(dst, dsrc.relativeFilePath(path));
copyFileAttributes(path, dst_path);
}
}
/**
* @brief Copies a directory and it's contents from src to dest
* @param offset subdirectory form src to copy to dest
@ -265,6 +321,9 @@ bool copy::operator()(const QString& offset, bool dryRun)
if (!m_followSymlinks)
opt |= copy_opts::copy_symlinks;
if (m_overwrite)
opt |= copy_opts::overwrite_existing;
// Function that'll do the actual copying
auto copy_file = [&](QString src_path, QString relative_dst_path) {
if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist))
@ -273,6 +332,9 @@ bool copy::operator()(const QString& offset, bool dryRun)
auto dst_path = PathCombine(dst, relative_dst_path);
if (!dryRun) {
ensureFilePathExists(dst_path);
#ifdef Q_OS_WIN32
copyFolderAttributes(src, dst, relative_dst_path);
#endif
fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err);
}
if (err) {

View File

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

View File

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

View File

@ -1,4 +1,24 @@
/* Copyright 2013-2021 MultiMC Contributors
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -86,9 +106,10 @@ class InstanceList : public QAbstractListModel {
bool isGroupCollapsed(const QString& groupName);
GroupId getInstanceGroup(const InstanceId& id) const;
void setInstanceGroup(const InstanceId& id, const GroupId& name);
void setInstanceGroup(const InstanceId& id, GroupId name);
void deleteGroup(const GroupId& name);
void renameGroup(const GroupId& src, const GroupId& dst);
bool trashInstance(const InstanceId& id);
bool trashedSomething();
void undoTrashInstance();
@ -158,12 +179,16 @@ class InstanceList : public QAbstractListModel {
QList<InstanceId> discoverInstances();
InstancePtr loadInstance(const InstanceId& id);
void increaseGroupCount(const QString& group);
void decreaseGroupCount(const QString& group);
private:
int m_watchLevel = 0;
int totalPlayTime = 0;
bool m_dirty = false;
QList<InstancePtr> m_instances;
QSet<QString> m_groupNameCache;
// id -> refs
QMap<QString, int> m_groupNameCache;
SettingsObjectPtr m_globalSettings;
QString m_instDir;

View File

@ -88,8 +88,8 @@ void LaunchController::decideAccount()
if (accounts->count() <= 0) {
// Tell the user they need to log in at least one account in order to play.
auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"),
tr("In order to play Minecraft, you must have at least one Microsoft or Mojang "
"account logged in. Mojang accounts can only be used offline. "
tr("In order to play Minecraft, you must have at least one Microsoft "
"account which owns Minecraft logged in."
"Would you like to open the account manager to add an account now?"),
QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)
->exec();

View File

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

View File

@ -48,7 +48,10 @@
#include <functional>
#include <memory>
#include <optional>
#if defined(LAUNCHER_APPLICATION)
#include "minecraft/mod/Mod.h"
#endif
#include "tasks/Task.h"
namespace MMCZip {
@ -79,11 +82,12 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow
*/
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false);
#if defined(LAUNCHER_APPLICATION)
/**
* take a source jar, add mods to it, resulting in target jar
*/
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods);
#endif
/**
* Find a single file in archive by file name (not path)
*
@ -147,6 +151,7 @@ bool extractFile(QString fileCompressed, QString file, QString dir);
*/
bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter);
#if defined(LAUNCHER_APPLICATION)
class ExportToZipTask : public Task {
public:
ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
@ -189,4 +194,5 @@ class ExportToZipTask : public Task {
QFuture<ZipResult> m_build_zip_future;
QFutureWatcher<ZipResult> m_build_zip_watcher;
};
#endif
} // namespace MMCZip

View File

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

View File

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

View File

@ -56,6 +56,7 @@ class Version {
bool operator!=(const Version& other) const;
QString toString() const { return m_string; }
bool isEmpty() const { return m_string.isEmpty(); }
friend QDebug operator<<(QDebug debug, const Version& v);

View File

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

View File

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

View File

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

View File

@ -24,11 +24,11 @@
struct JavaInstall : public BaseVersion {
JavaInstall() {}
JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {}
virtual QString descriptor() { return id.toString(); }
virtual QString descriptor() override { return id.toString(); }
virtual QString name() { return id.toString(); }
virtual QString name() override { return id.toString(); }
virtual QString typeString() const { return arch; }
virtual QString typeString() const override { return arch; }
virtual bool operator<(BaseVersion& a) override;
virtual bool operator>(BaseVersion& a) override;

View File

@ -403,6 +403,14 @@ QList<QString> JavaUtils::FindJavaPaths()
scanJavaDirs("/opt/jdks");
// flatpak
scanJavaDirs("/app/jdk");
auto home = qEnvironmentVariable("HOME");
// javas downloaded by IntelliJ
scanJavaDirs(FS::PathCombine(home, ".jdks"));
// javas downloaded by sdkman
scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java"));
javas = addJavasFromEnv(javas);
javas.removeDuplicates();
return javas;

View File

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

View File

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

View File

@ -184,6 +184,10 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride);
m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride);
// Legacy-related options
auto legacySettings = m_settings->registerSetting("OverrideLegacySettings", false);
m_settings->registerOverride(global_settings->getSetting("OnlineFixes"), legacySettings);
m_settings->set("InstanceType", "OneSix");
}
@ -513,20 +517,28 @@ QStringList MinecraftInstance::javaArguments()
args << "-Duser.language=en";
if (javaVersion.isModular() && shouldApplyOnlineFixes())
// allow reflective access to java.net - required by the skin fix
args << "--add-opens"
<< "java.base/java.net=ALL-UNNAMED";
return args;
}
QString MinecraftInstance::getLauncher()
{
auto profile = m_components->getProfile();
// use legacy launcher if the traits are set
if (profile->getTraits().contains("legacyLaunch") || profile->getTraits().contains("alphaLaunch"))
if (traits().contains("legacyLaunch") || traits().contains("alphaLaunch"))
return "legacy";
return "standard";
}
bool MinecraftInstance::shouldApplyOnlineFixes()
{
return traits().contains("legacyServices") && settings()->get("OnlineFixes").toBool();
}
QMap<QString, QString> MinecraftInstance::getVariables()
{
QMap<QString, QString> out;
@ -716,6 +728,9 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
launchScript += "traits " + trait + "\n";
}
if (shouldApplyOnlineFixes())
launchScript += "onlineFixes true\n";
launchScript += "launcher " + getLauncher() + "\n";
// qDebug() << "Generated launch script:" << launchScript;
@ -856,9 +871,6 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess
if (sessionRef.access_token != "0") {
addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>"));
}
if (sessionRef.client_token.size()) {
addToFilter(sessionRef.client_token, tr("<CLIENT TOKEN>"));
}
addToFilter(sessionRef.uuid, tr("<PROFILE ID>"));
return filter;

View File

@ -129,6 +129,7 @@ class MinecraftInstance : public BaseInstance {
/// get arguments passed to java
QStringList javaArguments();
QString getLauncher();
bool shouldApplyOnlineFixes();
/// get variables for launch command variable substitution/environment
QMap<QString, QString> getVariables() override;

View File

@ -1018,8 +1018,7 @@ std::optional<ModPlatform::ModLoaderTypes> PackProfile::getSupportedModLoaders()
// TODO: remove this or add version condition once Quilt drops official Fabric support
if (loaders & ModPlatform::Quilt)
loaders |= ModPlatform::Fabric;
// TODO: remove this or add version condition once NeoForge drops official Forge support
if (loaders & ModPlatform::NeoForge)
if (getComponentVersion("net.minecraft") == "1.20.1" && (loaders & ModPlatform::NeoForge))
loaders |= ModPlatform::Forge;
return loaders;
}

View File

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

View File

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

View File

@ -54,7 +54,7 @@
#include <chrono>
enum AccountListVersion { MojangOnly = 2, MojangMSA = 3 };
enum AccountListVersion { MojangMSA = 3 };
AccountList::AccountList(QObject* parent) : QAbstractListModel(parent)
{
@ -320,17 +320,6 @@ QVariant AccountList::data(const QModelIndex& index, int role) const
}
}
case MigrationColumn: {
if (account->isMSA() || account->isOffline()) {
return tr("N/A", "Can Migrate");
}
if (account->canMigrate()) {
return tr("Yes", "Can Migrate");
} else {
return tr("No", "Can Migrate");
}
}
default:
return QVariant();
}
@ -366,8 +355,6 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o
return tr("Type");
case StatusColumn:
return tr("Status");
case MigrationColumn:
return tr("Can Migrate?");
default:
return QVariant();
}
@ -379,11 +366,9 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o
case NameColumn:
return tr("User name of the account.");
case TypeColumn:
return tr("Type of the account - Mojang or MSA.");
return tr("Type of the account (MSA or Offline)");
case StatusColumn:
return tr("Current status of the account.");
case MigrationColumn:
return tr("Can this account migrate to a Microsoft account?");
default:
return QVariant();
}
@ -473,9 +458,6 @@ bool AccountList::loadList()
// Make sure the format version matches.
auto listVersion = root.value("formatVersion").toVariant().toInt();
switch (listVersion) {
case AccountListVersion::MojangOnly: {
return loadV2(root);
} break;
case AccountListVersion::MojangMSA: {
return loadV3(root);
} break;
@ -489,36 +471,6 @@ bool AccountList::loadList()
}
}
bool AccountList::loadV2(QJsonObject& root)
{
beginResetModel();
auto defaultUserName = root.value("activeAccount").toString("");
QJsonArray accounts = root.value("accounts").toArray();
for (QJsonValue accountVal : accounts) {
QJsonObject accountObj = accountVal.toObject();
MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV2(accountObj);
if (account.get() != nullptr) {
auto profileId = account->profileId();
if (!profileId.size()) {
continue;
}
if (findAccountByProfileId(profileId) != -1) {
continue;
}
connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged);
m_accounts.append(account);
if (defaultUserName.size() && account->mojangUserName() == defaultUserName) {
m_defaultAccount = account;
}
} else {
qWarning() << "Failed to load an account.";
}
}
endResetModel();
return true;
}
bool AccountList::loadV3(QJsonObject& root)
{
beginResetModel();

View File

@ -55,7 +55,6 @@ class AccountList : public QAbstractListModel {
// TODO: Add icon column.
ProfileNameColumn = 0,
NameColumn,
MigrationColumn,
TypeColumn,
StatusColumn,
@ -97,7 +96,6 @@ class AccountList : public QAbstractListModel {
void setListFilePath(QString path, bool autosave = false);
bool loadList();
bool loadV2(QJsonObject& root);
bool loadV3(QJsonObject& root);
bool saveList();

View File

@ -24,10 +24,6 @@ struct AuthSession {
GoneOrMigrated
} status = Undetermined;
// client token
QString client_token;
// account user name
QString username;
// combined session ID
QString session;
// volatile auth token

View File

@ -51,7 +51,6 @@
#include <QPainter>
#include "flows/MSA.h"
#include "flows/Mojang.h"
#include "flows/Offline.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent)
@ -59,15 +58,6 @@ MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent)
data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
}
MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json)
{
MinecraftAccountPtr account(new MinecraftAccount());
if (account->data.resumeStateFromV2(json)) {
return account;
}
return nullptr;
}
MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json)
{
MinecraftAccountPtr account(new MinecraftAccount());
@ -77,15 +67,6 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json)
return nullptr;
}
MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString& username)
{
auto account = makeShared<MinecraftAccount>();
account->data.type = AccountType::Mojang;
account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
return account;
}
MinecraftAccountPtr MinecraftAccount::createBlankMSA()
{
MinecraftAccountPtr account(new MinecraftAccount());
@ -138,18 +119,6 @@ QPixmap MinecraftAccount::getFace() const
return skin.scaled(64, 64, Qt::KeepAspectRatio);
}
shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password)
{
Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new MojangLogin(&data, password));
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); });
emit activityChanged(true);
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA()
{
Q_ASSERT(m_currentTask.get() == nullptr);
@ -182,10 +151,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh()
if (data.type == AccountType::MSA) {
m_currentTask.reset(new MSASilent(&data));
} else if (data.type == AccountType::Offline) {
m_currentTask.reset(new OfflineRefresh(&data));
} else {
m_currentTask.reset(new MojangRefresh(&data));
m_currentTask.reset(new OfflineRefresh(&data));
}
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
@ -296,13 +263,8 @@ void MinecraftAccount::fillSession(AuthSessionPtr session)
}
}
// the user name. you have to have an user name
// FIXME: not with MSA
session->username = data.userName();
// volatile auth token
session->access_token = data.accessToken();
// the semi-permanent client token
session->client_token = data.clientToken();
// profile name
session->player_name = data.profileName();
// profile ID

View File

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

View File

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

View File

@ -1,92 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "AccountTask.h"
#include <qsslerror.h>
#include <QJsonObject>
#include <QString>
#include <QTimer>
#include "MinecraftAccount.h"
class QNetworkAccessManager;
class QNetworkReply;
/**
* A Yggdrasil task is a task that performs an operation on a given mojang account.
*/
class Yggdrasil : public AccountTask {
Q_OBJECT
public:
explicit Yggdrasil(AccountData* data, QObject* parent = 0);
virtual ~Yggdrasil() = default;
void refresh();
void login(QString password);
struct Error {
QString m_errorMessageShort;
QString m_errorMessageVerbose;
QString m_cause;
};
std::shared_ptr<Error> m_error;
enum AbortedBy { BY_NOTHING, BY_USER, BY_TIMEOUT } m_aborted = BY_NOTHING;
protected:
void executeTask() override;
/**
* Processes the response received from the server.
* If an error occurred, this should emit a failed signal.
* If Yggdrasil gave an error response, it should call setError() first, and then return false.
* Otherwise, it should return true.
* Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
* an empty QJsonObject.
*/
void processResponse(QJsonObject responseData);
/**
* Processes an error response received from the server.
* The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
* \returns a QString error message that will be passed to emitFailed.
*/
virtual void processError(QJsonObject responseData);
protected slots:
void processReply();
void refreshTimers(qint64, qint64);
void heartbeat();
void sslErrors(QList<QSslError>);
void abortByTimeout();
public slots:
virtual bool abort() override;
private:
void sendRequest(QUrl endpoint, QByteArray content);
protected:
QNetworkReply* m_netReply = nullptr;
QTimer timeout_keeper;
QTimer counter;
int count = 0; // num msec since time reset
const int timeout_max = 30000;
const int time_step = 50;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -330,7 +330,8 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
// When you have a Qt build with assertions turned on, proceeding here will abort the application
if (added_set.size() > 0) {
beginInsertRows(QModelIndex(), m_resources.size(), m_resources.size() + added_set.size() - 1);
beginInsertRows(QModelIndex(), static_cast<int>(m_resources.size()),
static_cast<int>(m_resources.size() + added_set.size() - 1));
for (auto& added : added_set) {
auto res = new_resources[added];

View File

@ -22,7 +22,7 @@
#include "ShaderPack.h"
#include "minecraft/mod/tasks/LocalShaderPackParseTask.h"
#include <QRegularExpression>
void ShaderPack::setPackFormat(ShaderPackFormat new_format)
{
@ -35,3 +35,8 @@ bool ShaderPack::valid() const
{
return m_pack_format != ShaderPackFormat::INVALID;
}
bool ShaderPack::applyFilter(QRegularExpression filter) const
{
return valid() && Resource::applyFilter(filter);
}

View File

@ -54,6 +54,7 @@ class ShaderPack : public Resource {
void setPackFormat(ShaderPackFormat new_format);
bool valid() const override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
protected:
mutable QMutex m_data_lock;

View File

@ -1,6 +1,9 @@
#pragma once
#include "ResourceFolderModel.h"
#include "minecraft/mod/ShaderPack.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalShaderPackParseTask.h"
class ShaderPackFolderModel : public ResourceFolderModel {
Q_OBJECT
@ -9,4 +12,14 @@ class ShaderPackFolderModel : public ResourceFolderModel {
explicit ShaderPackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) {}
virtual QString id() const override { return "shaderpacks"; }
[[nodiscard]] Task* createUpdateTask() override
{
return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared<ShaderPack>(entry); });
}
[[nodiscard]] Task* createParseTask(Resource& resource) override
{
return new LocalShaderPackParseTask(m_next_resolution_ticket, static_cast<ShaderPack&>(resource));
}
};

View File

@ -94,7 +94,7 @@ QList<ModPlatform::Dependency> GetModDependenciesTask::getDependenciesForVersion
for (auto ver_dep : version.dependencies) {
if (ver_dep.type != ModPlatform::DependencyType::REQUIRED)
continue;
ver_dep = getOverride(ver_dep, providerName);
auto isOnlyVersion = providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty();
if (auto dep = std::find_if(c_dependencies.begin(), c_dependencies.end(),
[&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) {
@ -127,7 +127,7 @@ QList<ModPlatform::Dependency> GetModDependenciesTask::getDependenciesForVersion
dep != m_pack_dependencies.end()) // check loaded dependencies
continue;
c_dependencies.append(getOverride(ver_dep, providerName));
c_dependencies.append(ver_dep);
}
return c_dependencies;
}

View File

@ -122,7 +122,7 @@ void ModFolderLoadTask::getFromMetadata()
auto metadata = Metadata::get(m_index_dir, entry);
if (!metadata.isValid()) {
return;
continue;
}
auto* mod = new Mod(m_mods_dir, metadata);

View File

@ -24,6 +24,7 @@ class CheckUpdateTask : public Task {
QString old_hash;
QString old_version;
QString new_version;
std::optional<ModPlatform::IndexedVersionType> new_version_type;
QString changelog;
ModPlatform::ResourceProvider provider;
shared_qobject_ptr<ResourceDownloadTask> download;
@ -33,10 +34,18 @@ class CheckUpdateTask : public Task {
QString old_h,
QString old_v,
QString new_v,
std::optional<ModPlatform::IndexedVersionType> new_v_type,
QString changelog,
ModPlatform::ResourceProvider p,
shared_qobject_ptr<ResourceDownloadTask> t)
: name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t)
: name(name)
, old_hash(old_h)
, old_version(old_v)
, new_version(new_v)
, new_version_type(new_v_type)
, changelog(changelog)
, provider(p)
, download(t)
{}
};

View File

@ -24,6 +24,40 @@
namespace ModPlatform {
static const QMap<QString, IndexedVersionType::VersionType> s_indexed_version_type_names = {
{ "release", IndexedVersionType::VersionType::Release },
{ "beta", IndexedVersionType::VersionType::Beta },
{ "alpha", IndexedVersionType::VersionType::Alpha }
};
IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {}
IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type)
{
m_type = type;
}
IndexedVersionType::IndexedVersionType(const IndexedVersionType& other)
{
m_type = other.m_type;
}
IndexedVersionType& IndexedVersionType::operator=(const IndexedVersionType& other)
{
m_type = other.m_type;
return *this;
}
const QString IndexedVersionType::toString(const IndexedVersionType::VersionType& type)
{
return s_indexed_version_type_names.key(type, "unknown");
}
IndexedVersionType::VersionType IndexedVersionType::enumFromString(const QString& type)
{
return s_indexed_version_type_names.value(type, IndexedVersionType::VersionType::Unknown);
}
auto ProviderCapabilities::name(ResourceProvider p) -> const char*
{
switch (p) {

View File

@ -25,6 +25,7 @@
#include <QVariant>
#include <QVector>
#include <memory>
#include <optional>
class QIODevice;
@ -58,6 +59,34 @@ struct DonationData {
QString url;
};
struct IndexedVersionType {
enum class VersionType { Release = 1, Beta, Alpha, Unknown };
IndexedVersionType(const QString& type);
IndexedVersionType(const IndexedVersionType::VersionType& type);
IndexedVersionType(const IndexedVersionType& type);
IndexedVersionType() : IndexedVersionType(IndexedVersionType::VersionType::Unknown) {}
static const QString toString(const IndexedVersionType::VersionType& type);
static IndexedVersionType::VersionType enumFromString(const QString& type);
bool isValid() const { return m_type != IndexedVersionType::VersionType::Unknown; }
IndexedVersionType& operator=(const IndexedVersionType& other);
bool operator==(const IndexedVersionType& other) const { return m_type == other.m_type; }
bool operator==(const IndexedVersionType::VersionType& type) const { return m_type == type; }
bool operator!=(const IndexedVersionType& other) const { return m_type != other.m_type; }
bool operator!=(const IndexedVersionType::VersionType& type) const { return m_type != type; }
bool operator<(const IndexedVersionType& other) const { return m_type < other.m_type; }
bool operator<(const IndexedVersionType::VersionType& type) const { return m_type < type; }
bool operator<=(const IndexedVersionType& other) const { return m_type <= other.m_type; }
bool operator<=(const IndexedVersionType::VersionType& type) const { return m_type <= type; }
bool operator>(const IndexedVersionType& other) const { return m_type > other.m_type; }
bool operator>(const IndexedVersionType::VersionType& type) const { return m_type > type; }
bool operator>=(const IndexedVersionType& other) const { return m_type >= other.m_type; }
bool operator>=(const IndexedVersionType::VersionType& type) const { return m_type >= type; }
QString toString() const { return toString(m_type); }
IndexedVersionType::VersionType m_type;
};
struct Dependency {
QVariant addonId;
DependencyType type;
@ -69,6 +98,7 @@ struct IndexedVersion {
QVariant fileId;
QString version;
QString version_number = {};
IndexedVersionType version_type;
QStringList mcVersion;
QString downloadUrl;
QString date;
@ -107,6 +137,7 @@ struct IndexedPack {
QString logoName;
QString logoUrl;
QString websiteUrl;
QString side;
bool versionsLoaded = false;
QVector<IndexedVersion> versions;

View File

@ -43,5 +43,5 @@ void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack& m, QJsonObject& obj)
m.system = Json::ensureBoolean(obj, QString("system"), false);
m.description = Json::ensureString(obj, "description", "");
m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), "");
m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), "").toLower() + ".png";
}

View File

@ -37,6 +37,7 @@
#include "ATLPackInstallTask.h"
#include <QtConcurrent>
#include <algorithm>
#include <quazip/quazip.h>
@ -50,6 +51,7 @@
#include "minecraft/MinecraftInstance.h"
#include "minecraft/OneSixVersionFormat.h"
#include "minecraft/PackProfile.h"
#include "modplatform/atlauncher/ATLPackManifest.h"
#include "net/ChecksumValidator.h"
#include "settings/INISettingsObject.h"
@ -57,6 +59,7 @@
#include "Application.h"
#include "BuildConfig.h"
#include "ui/dialogs/BlockedModsDialog.h"
namespace ATLauncher {
@ -717,6 +720,8 @@ void PackInstallTask::downloadMods()
jarmods.clear();
jobPtr.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
QList<VersionMod> blocked_mods;
for (const auto& mod : m_version.mods) {
// skip non-client mods
if (!mod.client)
@ -731,9 +736,10 @@ void PackInstallTask::downloadMods()
case DownloadType::Server:
url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url;
break;
case DownloadType::Browser:
emitFailed(tr("Unsupported download type: %1").arg(mod.download_raw));
return;
case DownloadType::Browser: {
blocked_mods.append(mod);
continue;
}
case DownloadType::Direct:
url = mod.url;
break;
@ -805,24 +811,86 @@ void PackInstallTask::downloadMods()
modsToCopy[entry->getFullPath()] = path;
}
}
if (!blocked_mods.isEmpty()) {
QList<BlockedMod> mods;
for (auto mod : blocked_mods) {
BlockedMod blocked_mod;
blocked_mod.name = mod.file;
blocked_mod.websiteUrl = mod.url;
blocked_mod.hash = mod.md5;
blocked_mod.matched = false;
blocked_mod.localPath = "";
mods.append(blocked_mod);
}
qWarning() << "Blocked mods found, displaying mod list";
BlockedModsDialog message_dialog(nullptr, tr("Blocked mods found"),
tr("The following files are not available for download in third party launchers.<br/>"
"You will need to manually download them and add them to the instance."),
mods, "md5");
message_dialog.setModal(true);
if (message_dialog.exec()) {
qDebug() << "Post dialog blocked mods list: " << mods;
for (auto blocked : mods) {
if (!blocked.matched) {
qDebug() << blocked.name << "was not matched to a local file, skipping copy";
continue;
}
auto modIter = std::find_if(blocked_mods.begin(), blocked_mods.end(),
[blocked](const VersionMod& mod) { return mod.url == blocked.websiteUrl; });
if (modIter == blocked_mods.end())
continue;
auto mod = *modIter;
if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) {
modsToExtract.insert(blocked.localPath, mod);
} else if (mod.type == ModType::Decomp) {
modsToDecomp.insert(blocked.localPath, mod);
} else {
auto relpath = getDirForModType(mod.type, mod.type_raw);
if (relpath == Q_NULLPTR)
continue;
auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file);
if (mod.type == ModType::Forge) {
auto ver = getComponentVersion("net.minecraftforge", mod.version);
if (ver) {
componentsToInstall.insert("net.minecraftforge", ver);
continue;
}
qDebug() << "Jarmod: " + path;
jarmods.push_back(path);
}
if (mod.type == ModType::Jar) {
qDebug() << "Jarmod: " + path;
jarmods.push_back(path);
}
modsToCopy[blocked.localPath] = path;
}
}
} else {
emitFailed(tr("Unknown download type: %1").arg("browser"));
return;
}
}
connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded);
connect(jobPtr.get(), &NetJob::failed, [&](QString reason) {
abortable = false;
jobPtr.reset();
emitFailed(reason);
});
connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
connect(jobPtr.get(), &NetJob::progress, [this](qint64 current, qint64 total) {
setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
abortable = true;
setProgress(current, total);
});
connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
connect(jobPtr.get(), &NetJob::aborted, [&] {
abortable = false;
jobPtr.reset();
emitAborted();
});
connect(jobPtr.get(), &NetJob::aborted, &PackInstallTask::emitAborted);
connect(jobPtr.get(), &NetJob::failed, &PackInstallTask::emitFailed);
jobPtr->start();
}
@ -843,7 +911,7 @@ void PackInstallTask::onModsDownloaded()
QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy);
#endif
connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onModsExtracted);
connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, [&]() { emitAborted(); });
connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::emitAborted);
m_modExtractFutureWatcher.setFuture(m_modExtractFuture);
} else {
install();

View File

@ -173,7 +173,7 @@ void FlameCheckUpdate::executeTask()
}
auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver, m_mods_folder);
m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver.version,
m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver.version, latest_ver.version_type,
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
ModPlatform::ResourceProvider::FLAME, download_task);
}

View File

@ -62,6 +62,7 @@
#include "minecraft/World.h"
#include "minecraft/mod/tasks/LocalResourceParse.h"
#include "net/ApiDownload.h"
#include "ui/pages/modplatform/OptionalModDialog.h"
static const FlameAPI api;
@ -509,13 +510,33 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
{
m_files_job.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network()));
for (const auto& result : m_mod_id_resolver->getResults().files) {
QString filename = result.fileName;
auto results = m_mod_id_resolver->getResults().files;
QStringList optionalFiles;
for (auto& result : results) {
if (!result.required) {
filename += ".disabled";
optionalFiles << FS::PathCombine(result.targetFolder, result.fileName);
}
}
auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
QStringList selectedOptionalMods;
if (!optionalFiles.empty()) {
OptionalModDialog optionalModDialog(m_parent, optionalFiles);
if (optionalModDialog.exec() == QDialog::Rejected) {
emitAborted();
loop.quit();
return;
}
selectedOptionalMods = optionalModDialog.getResult();
}
for (const auto& result : results) {
auto relpath = FS::PathCombine(result.targetFolder, result.fileName);
if (!result.required && !selectedOptionalMods.contains(relpath)) {
relpath += ".disabled";
}
relpath = FS::PathCombine("minecraft", relpath);
auto path = FS::PathCombine(m_stagingPath, relpath);
switch (result.type) {
@ -547,7 +568,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
m_mod_id_resolver.reset();
connect(m_files_job.get(), &NetJob::succeeded, this, [&]() {
m_files_job.reset();
validateZIPResouces();
validateZIPResources();
});
connect(m_files_job.get(), &NetJob::failed, [&](QString reason) {
m_files_job.reset();
@ -596,7 +617,7 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
setAbortable(true);
}
void FlameCreationTask::validateZIPResouces()
void FlameCreationTask::validateZIPResources()
{
qDebug() << "Validating whether resources stored as .zip are in the right place";
for (auto [fileName, targetFolder] : m_ZIP_resources) {
@ -649,8 +670,8 @@ void FlameCreationTask::validateZIPResouces()
validatePath(fileName, targetFolder, "datapacks");
break;
case PackedResourceType::ShaderPack:
// in theroy flame API can't do this but who knows, that *may* change ?
// better to handle it if it *does* occure in the future
// in theory flame API can't do this but who knows, that *may* change ?
// better to handle it if it *does* occur in the future
validatePath(fileName, targetFolder, "shaderpacks");
break;
case PackedResourceType::WorldSave:

View File

@ -74,7 +74,7 @@ class FlameCreationTask final : public InstanceCreationTask {
void idResolverSucceeded(QEventLoop&);
void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
void validateZIPResouces();
void validateZIPResources();
QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
private:

View File

@ -139,6 +139,22 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
file.downloadUrl = Json::ensureString(obj, "downloadUrl");
file.fileName = Json::requireString(obj, "fileName");
ModPlatform::IndexedVersionType::VersionType ver_type;
switch (Json::requireInteger(obj, "releaseType")) {
case 1:
ver_type = ModPlatform::IndexedVersionType::VersionType::Release;
break;
case 2:
ver_type = ModPlatform::IndexedVersionType::VersionType::Beta;
break;
case 3:
ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha;
break;
default:
ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown;
}
file.version_type = ModPlatform::IndexedVersionType(ver_type);
auto hash_list = Json::ensureArray(obj, "hashes");
for (auto h : hash_list) {
auto hash_entry = Json::ensureObject(h);

View File

@ -1,4 +1,6 @@
#include "FlamePackIndex.h"
#include <QFileInfo>
#include <QUrl>
#include "Json.h"
@ -9,8 +11,8 @@ void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj)
pack.description = Json::ensureString(obj, "summary", "");
auto logo = Json::requireObject(obj, "logo");
pack.logoName = Json::requireString(logo, "title");
pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
pack.logoName = Json::requireString(obj, "slug") + "." + QFileInfo(QUrl(pack.logoUrl).fileName()).suffix();
auto authors = Json::requireArray(obj, "authors");
for (auto authorIter : authors) {
@ -89,6 +91,22 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr)
// pick the latest version supported
file.mcVersion = versionArray[0].toString();
file.version = Json::requireString(version, "displayName");
ModPlatform::IndexedVersionType::VersionType ver_type;
switch (Json::requireInteger(version, "releaseType")) {
case 1:
ver_type = ModPlatform::IndexedVersionType::VersionType::Release;
break;
case 2:
ver_type = ModPlatform::IndexedVersionType::VersionType::Beta;
break;
case 3:
ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha;
break;
default:
ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown;
}
file.version_type = ModPlatform::IndexedVersionType(ver_type);
file.downloadUrl = Json::ensureString(version, "downloadUrl");
// only add if we have a download URL (third party distribution is enabled)

View File

@ -17,6 +17,7 @@ struct IndexedVersion {
int addonId;
int fileId;
QString version;
ModPlatform::IndexedVersionType version_type;
QString mcVersion;
QString downloadUrl;
};

View File

@ -48,7 +48,7 @@ struct File {
int projectId = 0;
int fileId = 0;
// NOTE: the opposite to 'optional'. This is at the time of writing unused.
// NOTE: the opposite to 'optional'
bool required = true;
QString hash;
// NOTE: only set on blocked files ! Empty otherwise.

View File

@ -70,16 +70,18 @@ void PackInstallTask::downloadPack()
setProgress(1, 4);
setAbortable(false);
archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
auto path = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
auto entry = APPLICATION->metacache()->resolveEntry("FTBPacks", path);
entry->setStale(true);
archivePath = entry->getFullPath();
netJobContainer.reset(new NetJob("Download FTB Pack", m_network));
QString url;
if (m_pack.type == PackType::Private) {
url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(archivePath);
url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(path);
} else {
url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(archivePath);
url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(path);
}
netJobContainer->addNetAction(Net::ApiDownload::makeFile(url, archivePath));
netJobContainer->addNetAction(Net::ApiDownload::makeCached(url, entry));
connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip);
connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed);

View File

@ -161,8 +161,8 @@ void ModrinthCheckUpdate::executeTask()
auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_mods_folder);
m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
ModPlatform::ResourceProvider::MODRINTH, download_task);
m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.version_type,
project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task);
}
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, project_ver));
}

View File

@ -9,6 +9,7 @@
#include "modplatform/helpers/OverrideUtils.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "net/ChecksumValidator.h"
#include "net/ApiDownload.h"
@ -16,8 +17,10 @@
#include "settings/INISettingsObject.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/pages/modplatform/OptionalModDialog.h"
#include <QAbstractButton>
#include <vector>
bool ModrinthCreationTask::abort()
{
@ -319,10 +322,10 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
}
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
bool had_optional = false;
std::vector<Modrinth::File> optionalFiles;
for (const auto& modInfo : jsonFiles) {
Modrinth::File file;
file.path = Json::requireString(modInfo, "path");
file.path = Json::requireString(modInfo, "path").replace("\\", "/");
auto env = Json::ensureObject(modInfo, "env");
// 'env' field is optional
@ -331,18 +334,7 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
if (support == "unsupported") {
continue;
} else if (support == "optional") {
// TODO: Make a review dialog for choosing which ones the user wants!
if (!had_optional && show_optional_dialog) {
had_optional = true;
auto info = CustomMessageBox::selectable(
m_parent, tr("Optional mod detected!"),
tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"),
QMessageBox::Information);
info->exec();
}
if (file.path.endsWith(".jar"))
file.path += ".disabled";
file.required = false;
}
}
@ -385,9 +377,29 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
}
}
files.push_back(file);
(file.required ? files : optionalFiles).push_back(file);
}
if (!optionalFiles.empty()) {
QStringList oFiles;
for (auto file : optionalFiles)
oFiles.push_back(file.path);
OptionalModDialog optionalModDialog(m_parent, oFiles);
if (optionalModDialog.exec() == QDialog::Rejected) {
emitAborted();
return false;
}
auto selectedMods = optionalModDialog.getResult();
for (auto file : optionalFiles) {
if (selectedMods.contains(file.path)) {
file.required = true;
} else {
file.path += ".disabled";
}
files.push_back(file);
}
}
if (set_internal_data) {
auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json");
for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) {

View File

@ -25,6 +25,7 @@
#include "Json.h"
#include "MMCZip.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/MetadataHandler.h"
#include "minecraft/mod/ModFolderModel.h"
const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" });
@ -129,7 +130,8 @@ void ModrinthPackExportTask::collectHashes()
QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1);
sha1.addData(data);
ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size() };
ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size(),
mod->metadata()->side };
resolvedFiles[relative] = resolvedFile;
// nice! we've managed to resolve based on local metadata!
@ -272,22 +274,33 @@ QByteArray ModrinthPackExportTask::generateIndex()
QString path = iterator.key();
const ResolvedFile& value = iterator.value();
if (optionalFiles) {
QJsonObject env;
// detect disabled mod
const QFileInfo pathInfo(path);
if (pathInfo.suffix() == "disabled") {
if (optionalFiles && pathInfo.suffix() == "disabled") {
// rename it
path = pathInfo.dir().filePath(pathInfo.completeBaseName());
// ...and make it optional
QJsonObject env;
env["client"] = "optional";
env["server"] = "optional";
} else {
env["client"] = "required";
env["server"] = "required";
}
switch (iterator->side) {
case Metadata::ModSide::ClientSide:
env["server"] = "unsupported";
break;
case Metadata::ModSide::ServerSide:
env["client"] = "unsupported";
break;
case Metadata::ModSide::UniversalSide:
break;
}
fileOut["env"] = env;
}
}
fileOut["path"] = path;
fileOut["downloads"] = QJsonArray{ iterator.value().url };
fileOut["downloads"] = QJsonArray{ iterator->url };
QJsonObject hashes;
hashes["sha1"] = value.sha1;

View File

@ -44,6 +44,7 @@ class ModrinthPackExportTask : public Task {
struct ResolvedFile {
QString sha1, sha512, url;
qint64 size;
Metadata::ModSide side;
};
static const QStringList PREFIXES;

View File

@ -27,6 +27,11 @@
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
bool shouldDownloadOnSide(QString side)
{
return side == "required" || side == "optional";
}
// https://docs.modrinth.com/api-spec/#tag/projects/operation/getProject
void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
@ -53,6 +58,17 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
modAuthor.url = api.getAuthorURL(modAuthor.name);
pack.authors.append(modAuthor);
auto client = shouldDownloadOnSide(Json::ensureString(obj, "client_side"));
auto server = shouldDownloadOnSide(Json::ensureString(obj, "server_side"));
if (server && client) {
pack.side = "both";
} else if (server) {
pack.side = "server";
} else if (client) {
pack.side = "client";
}
// Modrinth can have more data than what's provided by the basic search :)
pack.extraDataLoaded = false;
}
@ -149,6 +165,8 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
}
file.version = Json::requireString(obj, "name");
file.version_number = Json::requireString(obj, "version_number");
file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type"));
file.changelog = Json::requireString(obj, "changelog");
auto dependencies = Json::ensureArray(obj, "dependencies");

View File

@ -35,6 +35,7 @@
*/
#include "ModrinthPackManifest.h"
#include <QFileInfo>
#include "Json.h"
#include "modplatform/modrinth/ModrinthAPI.h"
@ -56,8 +57,8 @@ void loadIndexedPack(Modpack& pack, QJsonObject& obj)
pack.description = Json::ensureString(obj, "description");
auto temp_author_name = Json::ensureString(obj, "author");
pack.author = std::make_tuple(temp_author_name, api.getAuthorURL(temp_author_name));
pack.iconName = QString("modrinth_%1").arg(Json::ensureString(obj, "slug"));
pack.iconUrl = Json::ensureString(obj, "icon_url");
pack.iconName = QString("modrinth_%1.%2").arg(Json::ensureString(obj, "slug"), QFileInfo(pack.iconUrl.fileName()).suffix());
}
void loadIndexedInfo(Modpack& pack, QJsonObject& obj)
@ -128,6 +129,7 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion
file.name = Json::requireString(obj, "name");
file.version = Json::requireString(obj, "version_number");
file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type"));
file.changelog = Json::ensureString(obj, "changelog");
file.id = Json::requireString(obj, "id");

View File

@ -45,6 +45,8 @@
#include <QUrl>
#include <QVector>
#include "modplatform/ModIndex.h"
class MinecraftInstance;
namespace Modrinth {
@ -55,6 +57,7 @@ struct File {
QCryptographicHash::Algorithm hashAlgorithm;
QByteArray hash;
QQueue<QUrl> downloads;
bool required = true;
};
struct DonationData {
@ -79,6 +82,7 @@ struct ModpackExtra {
struct ModpackVersion {
QString name;
QString version;
ModPlatform::IndexedVersionType version_type;
QString changelog;
QString id;

View File

@ -21,6 +21,8 @@
#include <QDebug>
#include <QDir>
#include <QObject>
#include <sstream>
#include <string>
#include "FileSystem.h"
#include "StringUtils.h"
@ -111,6 +113,7 @@ auto V1::createModFormat([[maybe_unused]] QDir& index_dir, ModPlatform::IndexedP
mod.provider = mod_pack.provider;
mod.file_id = mod_version.fileId;
mod.project_id = mod_pack.addonId;
mod.side = stringToSide(mod_pack.side);
return mod;
}
@ -154,38 +157,52 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
FS::ensureFilePathExists(index_file.fileName());
}
toml::table update;
switch (mod.provider) {
case (ModPlatform::ResourceProvider::FLAME):
if (mod.file_id.toInt() == 0 || mod.project_id.toInt() == 0) {
qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname);
return;
}
update = toml::table{
{ "file-id", mod.file_id.toInt() },
{ "project-id", mod.project_id.toInt() },
};
break;
case (ModPlatform::ResourceProvider::MODRINTH):
if (mod.mod_id().toString().isEmpty() || mod.version().toString().isEmpty()) {
qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname);
return;
}
update = toml::table{
{ "mod-id", mod.mod_id().toString().toStdString() },
{ "version", mod.version().toString().toStdString() },
};
break;
}
if (!index_file.open(QIODevice::ReadWrite)) {
qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name));
qCritical() << QString("Could not open file %1!").arg(normalized_fname);
return;
}
// Put TOML data into the file
QTextStream in_stream(&index_file);
auto addToStream = [&in_stream](QString&& key, QString value) { in_stream << QString("%1 = \"%2\"\n").arg(key, value); };
{
addToStream("name", mod.name);
addToStream("filename", mod.filename);
addToStream("side", mod.side);
in_stream << QString("\n[download]\n");
addToStream("mode", mod.mode);
addToStream("url", mod.url.toString());
addToStream("hash-format", mod.hash_format);
addToStream("hash", mod.hash);
in_stream << QString("\n[update]\n");
in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider));
switch (mod.provider) {
case (ModPlatform::ResourceProvider::FLAME):
in_stream << QString("file-id = %1\n").arg(mod.file_id.toString());
in_stream << QString("project-id = %1\n").arg(mod.project_id.toString());
break;
case (ModPlatform::ResourceProvider::MODRINTH):
addToStream("mod-id", mod.mod_id().toString());
addToStream("version", mod.version().toString());
break;
}
auto tbl = toml::table{ { "name", mod.name.toStdString() },
{ "filename", mod.filename.toStdString() },
{ "side", sideToString(mod.side).toStdString() },
{ "download",
toml::table{
{ "mode", mod.mode.toStdString() },
{ "url", mod.url.toString().toStdString() },
{ "hash-format", mod.hash_format.toStdString() },
{ "hash", mod.hash.toStdString() },
} },
{ "update", toml::table{ { ProviderCaps.name(mod.provider), update } } } };
std::stringstream ss;
ss << tbl;
in_stream << QString::fromStdString(ss.str());
}
index_file.flush();
@ -258,7 +275,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
{ // Basic info
mod.name = stringEntry(table, "name");
mod.filename = stringEntry(table, "filename");
mod.side = stringEntry(table, "side");
mod.side = stringToSide(stringEntry(table, "side"));
}
{ // [download] info
@ -313,4 +330,28 @@ auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod
return {};
}
auto V1::sideToString(Side side) -> QString
{
switch (side) {
case Side::ClientSide:
return "client";
case Side::ServerSide:
return "server";
case Side::UniversalSide:
return "both";
}
return {};
}
auto V1::stringToSide(QString side) -> Side
{
if (side == "client")
return Side::ClientSide;
if (side == "server")
return Side::ServerSide;
if (side == "both")
return Side::UniversalSide;
return Side::UniversalSide;
}
} // namespace Packwiz

View File

@ -35,12 +35,12 @@ auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool shoul
class V1 {
public:
enum class Side { ClientSide = 1 << 0, ServerSide = 1 << 1, UniversalSide = ClientSide | ServerSide };
struct Mod {
QString slug{};
QString name{};
QString filename{};
// FIXME: make side an enum
QString side{ "both" };
Side side{ Side::UniversalSide };
// [download]
QString mode{};
@ -93,6 +93,9 @@ class V1 {
* If the mod doesn't have a metadata, it simply returns an empty Mod object.
* */
static auto getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod;
static auto sideToString(Side side) -> QString;
static auto stringToSide(QString side) -> Side;
};
} // namespace Packwiz

View File

@ -36,13 +36,18 @@
*/
#include "NetJob.h"
#include "Application.h"
#include "tasks/ConcurrentTask.h"
#if defined(LAUNCHER_APPLICATION)
#include "Application.h"
#include "ui/dialogs/CustomMessageBox.h"
#endif
NetJob::NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network)
: ConcurrentTask(nullptr, job_name, APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()), m_network(network)
{}
NetJob::NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network) : ConcurrentTask(nullptr, job_name), m_network(network)
{
#if defined(LAUNCHER_APPLICATION)
setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt());
#endif
}
auto NetJob::addNetAction(NetAction::Ptr action) -> bool
{
@ -140,6 +145,7 @@ void NetJob::updateState()
void NetJob::emitFailed(QString reason)
{
#if defined(LAUNCHER_APPLICATION)
auto response = CustomMessageBox::selectable(nullptr, "Confirm retry",
"The tasks failed\n"
"Failed urls\n" +
@ -155,5 +161,6 @@ void NetJob::emitFailed(QString reason)
startNext();
return;
}
#endif
ConcurrentTask::emitFailed(reason);
}

View File

@ -46,6 +46,7 @@
#if defined(LAUNCHER_APPLICATION)
#include "Application.h"
#endif
#include "BuildConfig.h"
#include "net/NetAction.h"

View File

@ -206,7 +206,7 @@ void TranslationsModel::indexReceived()
reloadLocalFiles();
auto language = d->m_system_locale;
if (!findLanguage(language)) {
if (!findLanguageAsOptional(language).has_value()) {
language = d->m_system_language;
}
selectLanguage(language);
@ -417,14 +417,17 @@ int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) c
return 2;
}
Language* TranslationsModel::findLanguage(const QString& key)
QVector<Language>::Iterator TranslationsModel::findLanguage(const QString& key)
{
auto found = std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language& lang) { return lang.key == key; });
if (found == d->m_languages.end()) {
return nullptr;
} else {
return found;
return std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language& lang) { return lang.key == key; });
}
std::optional<Language> TranslationsModel::findLanguageAsOptional(const QString& key)
{
auto found = findLanguage(key);
if (found != d->m_languages.end())
return *found;
return {};
}
void TranslationsModel::setUseSystemLocale(bool useSystemLocale)
@ -436,13 +439,13 @@ void TranslationsModel::setUseSystemLocale(bool useSystemLocale)
bool TranslationsModel::selectLanguage(QString key)
{
QString& langCode = key;
auto langPtr = findLanguage(key);
auto langPtr = findLanguageAsOptional(key);
if (langCode.isEmpty()) {
d->no_language_set = true;
}
if (!langPtr) {
if (!langPtr.has_value()) {
qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode;
langCode = defaultLangCode;
} else {
@ -527,9 +530,8 @@ bool TranslationsModel::selectLanguage(QString key)
QModelIndex TranslationsModel::selectedIndex()
{
auto found = findLanguage(d->m_selectedLanguage);
if (found) {
// QVector iterator freely converts to pointer to contained type
return index(found - d->m_languages.begin(), 0, QModelIndex());
if (found != d->m_languages.end()) {
return index(std::distance(d->m_languages.begin(), found), 0, QModelIndex());
}
return QModelIndex();
}
@ -562,8 +564,8 @@ void TranslationsModel::updateLanguage(QString key)
qWarning() << "Cannot update builtin language" << key;
return;
}
auto found = findLanguage(key);
if (!found) {
auto found = findLanguageAsOptional(key);
if (!found.has_value()) {
qWarning() << "Cannot update invalid language" << key;
return;
}
@ -578,8 +580,8 @@ void TranslationsModel::downloadTranslation(QString key)
d->m_nextDownload = key;
return;
}
auto lang = findLanguage(key);
if (!lang) {
auto lang = findLanguageAsOptional(key);
if (!lang.has_value()) {
qWarning() << "Will not download an unknown translation" << key;
return;
}

View File

@ -17,6 +17,7 @@
#include <QAbstractListModel>
#include <memory>
#include <optional>
struct Language;
@ -40,7 +41,8 @@ class TranslationsModel : public QAbstractListModel {
void setUseSystemLocale(bool useSystemLocale);
private:
Language* findLanguage(const QString& key);
QVector<Language>::Iterator findLanguage(const QString& key);
std::optional<Language> findLanguageAsOptional(const QString& key);
void reloadLocalFiles();
void downloadTranslation(QString key);
void downloadNext();

View File

@ -219,7 +219,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
ui->actionDISCORD->setVisible(!BuildConfig.DISCORD_URL.isEmpty());
ui->actionREDDIT->setVisible(!BuildConfig.SUBREDDIT_URL.isEmpty());
ui->actionCheckUpdate->setVisible(BuildConfig.UPDATER_ENABLED);
ui->actionCheckUpdate->setVisible(APPLICATION->updaterEnabled());
#ifndef Q_OS_MAC
ui->actionAddToPATH->setVisible(false);
@ -363,7 +363,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
// Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit.
// Template hell sucks...
connect(APPLICATION->accounts().get(), &AccountList::defaultAccountChanged, [this] { defaultAccountChanged(); });
connect(APPLICATION->accounts().get(), &AccountList::listChanged, [this] { repopulateAccountsMenu(); });
connect(APPLICATION->accounts().get(), &AccountList::listChanged, [this] { defaultAccountChanged(); });
// Show initial account
defaultAccountChanged();
@ -377,7 +377,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
updateNewsLabel();
}
if (BuildConfig.UPDATER_ENABLED) {
if (APPLICATION->updaterEnabled()) {
bool updatesAllowed = APPLICATION->updatesAreAllowed();
updatesAllowedChanged(updatesAllowed);
@ -514,10 +514,10 @@ void MainWindow::showInstanceContextMenu(const QPoint& pos)
} else {
auto group = view->groupNameAt(pos);
QAction* actionVoid = new QAction(BuildConfig.LAUNCHER_DISPLAYNAME, this);
QAction* actionVoid = new QAction(group.isNull() ? BuildConfig.LAUNCHER_DISPLAYNAME : group, this);
actionVoid->setEnabled(false);
QAction* actionCreateInstance = new QAction(tr("Create instance"), this);
QAction* actionCreateInstance = new QAction(tr("&Create instance"), this);
actionCreateInstance->setToolTip(ui->actionAddInstance->toolTip());
if (!group.isNull()) {
QVariantMap instance_action_data;
@ -531,12 +531,13 @@ void MainWindow::showInstanceContextMenu(const QPoint& pos)
actions.prepend(actionVoid);
actions.append(actionCreateInstance);
if (!group.isNull()) {
QAction* actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this);
QVariantMap delete_group_action_data;
delete_group_action_data["group"] = group;
actionDeleteGroup->setData(delete_group_action_data);
connect(actionDeleteGroup, SIGNAL(triggered(bool)), SLOT(deleteGroup()));
QAction* actionDeleteGroup = new QAction(tr("&Delete group"), this);
connect(actionDeleteGroup, &QAction::triggered, this, [this, group] { deleteGroup(group); });
actions.append(actionDeleteGroup);
QAction* actionRenameGroup = new QAction(tr("&Rename group"), this);
connect(actionRenameGroup, &QAction::triggered, this, [this, group] { renameGroup(group); });
actions.append(actionRenameGroup);
}
}
QMenu myMenu;
@ -676,7 +677,7 @@ void MainWindow::repopulateAccountsMenu()
void MainWindow::updatesAllowedChanged(bool allowed)
{
if (!BuildConfig.UPDATER_ENABLED) {
if (!APPLICATION->updaterEnabled()) {
return;
}
ui->actionCheckUpdate->setEnabled(allowed);
@ -872,7 +873,7 @@ void MainWindow::finalizeInstance(InstancePtr inst)
} else {
CustomMessageBox::selectable(this, tr("Error"),
tr("The launcher cannot download Minecraft or update instances unless you have at least "
"one account added.\nPlease add your Microsoft or Mojang account."),
"one account added.\nPlease add a Microsoft account."),
QMessageBox::Warning)
->show();
}
@ -1128,40 +1129,49 @@ void MainWindow::on_actionChangeInstGroup_triggered()
if (!m_selectedInstance)
return;
bool ok = false;
InstanceId instId = m_selectedInstance->id();
QString name(APPLICATION->instances()->getInstanceGroup(instId));
auto groups = APPLICATION->instances()->getGroups();
groups.insert(0, "");
groups.sort(Qt::CaseInsensitive);
int foo = groups.indexOf(name);
QString src(APPLICATION->instances()->getInstanceGroup(instId));
QStringList groups = APPLICATION->instances()->getGroups();
groups.prepend("");
int index = groups.indexOf(src);
bool ok = false;
QString dst = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, index, true, &ok);
dst = dst.simplified();
name = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, foo, true, &ok);
name = name.simplified();
if (ok) {
APPLICATION->instances()->setInstanceGroup(instId, name);
APPLICATION->instances()->setInstanceGroup(instId, dst);
}
}
void MainWindow::deleteGroup()
void MainWindow::deleteGroup(QString group)
{
QObject* obj = sender();
if (!obj)
return;
QAction* action = qobject_cast<QAction*>(obj);
if (!action)
return;
auto map = action->data().toMap();
if (!map.contains("group"))
return;
QString groupName = map["group"].toString();
if (!groupName.isEmpty()) {
auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1?").arg(groupName),
Q_ASSERT(!group.isEmpty());
const int reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group '%1'?").arg(group),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
APPLICATION->instances()->deleteGroup(groupName);
if (reply == QMessageBox::Yes)
APPLICATION->instances()->deleteGroup(group);
}
void MainWindow::renameGroup(QString group)
{
Q_ASSERT(!group.isEmpty());
QString name = QInputDialog::getText(this, tr("Rename group"), tr("Enter a new group name."), QLineEdit::Normal, group);
name = name.simplified();
if (name.isNull() || name == group)
return;
const bool empty = name.isEmpty();
const bool duplicate = APPLICATION->instances()->getGroups().contains(name, Qt::CaseInsensitive) && group.toLower() != name.toLower();
if (empty || duplicate) {
QMessageBox::warning(this, tr("Cannot rename group"), empty ? tr("Cannot set empty name.") : tr("Group already exists. :/"));
return;
}
APPLICATION->instances()->renameGroup(group, name);
}
void MainWindow::undoTrashInstance()
@ -1208,7 +1218,7 @@ void MainWindow::refreshInstances()
void MainWindow::checkForUpdates()
{
if (BuildConfig.UPDATER_ENABLED) {
if (APPLICATION->updaterEnabled()) {
APPLICATION->triggerUpdateCheck();
} else {
qWarning() << "Updater not set up. Cannot check for updates.";

View File

@ -148,7 +148,8 @@ class MainWindow : public QMainWindow {
void on_actionDeleteInstance_triggered();
void deleteGroup();
void deleteGroup(QString group);
void renameGroup(QString group);
void undoTrashInstance();
inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); }

View File

@ -41,8 +41,8 @@
#include <QPushButton>
#include <QStandardPaths>
BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods)
: QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods)
BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods, QString hash_type)
: QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hash_type(hash_type)
{
m_hashing_task = shared_qobject_ptr<ConcurrentTask>(
new ConcurrentTask(this, "MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
@ -255,7 +255,7 @@ void BlockedModsDialog::addHashTask(QString path)
/// @param path the path to the local file being hashed
void BlockedModsDialog::buildHashTask(QString path)
{
auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, "sha1");
auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, m_hash_type);
qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path;
@ -335,6 +335,13 @@ bool BlockedModsDialog::checkValidPath(QString path)
for (auto& mod : m_mods) {
if (compare(filename, mod.name)) {
// if the mod is not yet matched and doesn't have a hash then
// just match it with the file that has the exact same name
if (!mod.matched && mod.hash.isEmpty()) {
mod.matched = true;
mod.localPath = path;
return false;
}
qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path;
return true;
}

View File

@ -54,7 +54,7 @@ class BlockedModsDialog : public QDialog {
Q_OBJECT
public:
BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods);
BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods, QString hash_type = "sha1");
~BlockedModsDialog() override;
@ -73,6 +73,7 @@ class BlockedModsDialog : public QDialog {
QSet<QString> m_pending_hash_paths;
bool m_rehash_pending;
QPushButton* m_openMissingButton;
QString m_hash_type;
void openAll(bool missingOnly);
void addDownloadFolder();

View File

@ -2,6 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -61,22 +62,14 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent)
ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
ui->instNameTextBox->setText(original->name());
ui->instNameTextBox->setFocus();
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
auto groupList = APPLICATION->instances()->getGroups();
QSet<QString> groups(groupList.begin(), groupList.end());
groupList = QStringList(groups.values());
#else
auto groups = APPLICATION->instances()->getGroups().toSet();
auto groupList = QStringList(groups.toList());
#endif
groupList.sort(Qt::CaseInsensitive);
groupList.removeOne("");
groupList.push_front("");
ui->groupBox->addItems(groupList);
int index = groupList.indexOf(APPLICATION->instances()->getInstanceGroup(m_original->id()));
if (index == -1) {
QStringList groups = APPLICATION->instances()->getGroups();
groups.prepend("");
ui->groupBox->addItems(groups);
int index = groups.indexOf(APPLICATION->instances()->getInstanceGroup(m_original->id()));
if (index == -1)
index = 0;
}
ui->groupBox->setCurrentIndex(index);
ui->groupBox->lineEdit()->setPlaceholderText(tr("No group"));
ui->copySavesCheckbox->setChecked(m_selectedOptions.isCopySavesEnabled());

View File

@ -1,115 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "LoginDialog.h"
#include "ui_LoginDialog.h"
#include "minecraft/auth/AccountTask.h"
#include <QtWidgets/QPushButton>
LoginDialog::LoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::LoginDialog)
{
ui->setupUi(this);
ui->progressBar->setVisible(false);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
LoginDialog::~LoginDialog()
{
delete ui;
}
// Stage 1: User interaction
void LoginDialog::accept()
{
setUserInputsEnabled(false);
ui->progressBar->setVisible(true);
// Setup the login task and start it
m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text());
m_loginTask = m_account->login(ui->passTextBox->text());
connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus);
connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress);
m_loginTask->start();
}
void LoginDialog::setUserInputsEnabled(bool enable)
{
ui->userTextBox->setEnabled(enable);
ui->passTextBox->setEnabled(enable);
ui->buttonBox->setEnabled(enable);
}
// Enable the OK button only when both textboxes contain something.
void LoginDialog::on_userTextBox_textEdited(const QString& newText)
{
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty());
}
void LoginDialog::on_passTextBox_textEdited(const QString& newText)
{
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty());
}
void LoginDialog::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 LoginDialog::onTaskSucceeded()
{
QDialog::accept();
}
void LoginDialog::onTaskStatus(const QString& status)
{
ui->label->setText(status);
}
void LoginDialog::onTaskProgress(qint64 current, qint64 total)
{
ui->progressBar->setMaximum(total);
ui->progressBar->setValue(current);
}
// Public interface
MinecraftAccountPtr LoginDialog::newAccount(QWidget* parent, QString msg)
{
LoginDialog dlg(parent);
dlg.ui->label->setText(msg);
if (dlg.exec() == QDialog::Accepted) {
return dlg.m_account;
}
return nullptr;
}

View File

@ -1,56 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QtCore/QEventLoop>
#include <QtWidgets/QDialog>
#include "minecraft/auth/MinecraftAccount.h"
#include "tasks/Task.h"
namespace Ui {
class LoginDialog;
}
class LoginDialog : public QDialog {
Q_OBJECT
public:
~LoginDialog();
static MinecraftAccountPtr newAccount(QWidget* parent, QString message);
private:
explicit LoginDialog(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_passTextBox_textEdited(const QString& newText);
private:
Ui::LoginDialog* ui;
MinecraftAccountPtr m_account;
Task::Ptr m_loginTask;
};

View File

@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LoginDialog</class>
<widget class="QDialog" name="LoginDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>421</width>
<height>198</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="placeholderText">
<string>Email</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="passTextBox">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Password</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</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

@ -219,8 +219,10 @@ void ModUpdateDialog::checkCandidates()
if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME)
changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt());
auto download_task = makeShared<ResourceDownloadTask>(dep->pack, dep->version, m_mod_model);
CheckUpdateTask::UpdatableMod updatable = { dep->pack->name, dep->version.hash, "", dep->version.version,
changelog, dep->pack->provider, download_task };
CheckUpdateTask::UpdatableMod updatable = {
dep->pack->name, dep->version.hash, "", dep->version.version, dep->version.version_type,
changelog, dep->pack->provider, download_task
};
appendMod(updatable, getRequiredBy.value(dep->version.addonId.toString()));
m_tasks.insert(updatable.name, updatable.download);
@ -415,6 +417,11 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri
auto new_version_item = new QTreeWidgetItem(item_top);
new_version_item->setText(0, tr("New version: %1").arg(info.new_version));
if (info.new_version_type.has_value()) {
auto new_version_type_itme = new QTreeWidgetItem(item_top);
new_version_type_itme->setText(0, tr("New Version Type: %1").arg(info.new_version_type.value().toString()));
}
if (!requiredBy.isEmpty()) {
auto requiredByItem = new QTreeWidgetItem(item_top);
if (requiredBy.length() == 1) {

View File

@ -2,6 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -75,23 +76,14 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
InstIconKey = "default";
ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
auto groupList = APPLICATION->instances()->getGroups();
auto groups = QSet<QString>(groupList.begin(), groupList.end());
groupList = groups.values();
#else
auto groups = APPLICATION->instances()->getGroups().toSet();
auto groupList = QStringList(groups.toList());
#endif
groupList.sort(Qt::CaseInsensitive);
groupList.removeOne("");
groupList.push_front(initialGroup);
groupList.push_front("");
ui->groupBox->addItems(groupList);
int index = groupList.indexOf(initialGroup);
QStringList groups = APPLICATION->instances()->getGroups();
groups.prepend("");
int index = groups.indexOf(initialGroup);
if (index == -1) {
index = 0;
index = 1;
groups.insert(index, initialGroup);
}
ui->groupBox->addItems(groups);
ui->groupBox->setCurrentIndex(index);
ui->groupBox->lineEdit()->setPlaceholderText(tr("No group"));
@ -237,8 +229,7 @@ void NewInstanceDialog::setSuggestedIcon(const QString& key)
InstanceTask* NewInstanceDialog::extractTask()
{
InstanceTask* extracted = creationTask.get();
creationTask.release();
InstanceTask* extracted = creationTask.release();
InstanceName inst_name(ui->instNameTextBox->placeholderText().trimmed(), importVersion);
inst_name.setName(ui->instNameTextBox->text().trimmed());

View File

@ -48,6 +48,9 @@
<property name="text">
<string>Global Task Status...</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
@ -109,8 +112,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>464</width>
<height>96</height>
<width>460</width>
<height>108</height>
</rect>
</property>
<layout class="QVBoxLayout" name="taskProgressLayout">

View File

@ -167,8 +167,8 @@ void ResourceDownloadDialog::confirm()
});
for (auto& task : selected) {
confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath(),
ProviderCaps.name(task->getProvider()),
getRequiredBy.value(task->getPack()->addonId.toString()) });
ProviderCaps.name(task->getProvider()), getRequiredBy.value(task->getPack()->addonId.toString()),
task->getVersion().version_type.toString() });
}
if (confirm_dialog->exec()) {

View File

@ -77,6 +77,10 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info)
itemTop->insertChildren(childIndx++, { requiredByItem });
}
auto versionTypeItem = new QTreeWidgetItem(itemTop);
versionTypeItem->setText(0, tr("Version Type: %1").arg(info.version_type));
itemTop->insertChildren(childIndx++, { versionTypeItem });
ui->modTreeWidget->addTopLevelItem(itemTop);
}

View File

@ -18,6 +18,7 @@ class ReviewMessageBox : public QDialog {
QString custom_file_path{};
QString provider;
QStringList required_by;
QString version_type;
};
void appendResource(ResourceInformation&& info);

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