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

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97 2023-10-16 23:32:24 +03:00
commit 9400879242
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
95 changed files with 5559 additions and 413 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:
@ -169,7 +156,7 @@ jobs:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
restore-keys: |
${{ matrix.os }}-mingw-w64-ccache
${{ matrix.os }}-mingw-w64-ccache
- name: Setup ccache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
@ -214,35 +201,35 @@ jobs:
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
uses: jurplel/install-qt-action@v3
with:
aqtversion: '==3.1.*'
py7zrversion: '>=0.20.2'
version: ${{ matrix.qt_version }}
host: 'windows'
target: 'desktop'
arch: ''
modules: ${{ matrix.qt_modules }}
tools: ${{ matrix.qt_tools }}
cache: ${{ inputs.is_qt_cached }}
cache-key-prefix: host-qt-arm64-windows
dir: ${{ github.workspace }}\HostQt
set-env: false
aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }}
host: "windows"
target: "desktop"
arch: ""
modules: ${{ matrix.qt_modules }}
tools: ${{ matrix.qt_tools }}
cache: ${{ inputs.is_qt_cached }}
cache-key-prefix: host-qt-arm64-windows
dir: ${{ github.workspace }}\HostQt
set-env: false
- name: Install Qt (macOS, Linux, Qt 6 & Windows MSVC)
if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '')
uses: jurplel/install-qt-action@v3
with:
aqtversion: '==3.1.*'
py7zrversion: '>=0.20.2'
version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }}
target: 'desktop'
arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }}
tools: ${{ matrix.qt_tools }}
cache: ${{ inputs.is_qt_cached }}
aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }}
target: "desktop"
arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }}
tools: ${{ matrix.qt_tools }}
cache: ${{ inputs.is_qt_cached }}
- name: Install MSVC (Windows MSVC)
if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool
if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool
uses: ilammy/msvc-dev-cmd@v1
with:
vsversion: 2022
@ -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
@ -343,7 +330,7 @@ jobs:
- name: Test (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64'
run: |
ctest -E "^example64|example$" --test-dir build --output-on-failure -C ${{ inputs.build_type }}
ctest -E "^example64|example$" --test-dir build --output-on-failure -C ${{ inputs.build_type }}
##
# PACKAGE BUILDS
@ -385,7 +372,7 @@ jobs:
run: |
cmake --install ${{ env.BUILD_DIR }}
touch ${{ env.INSTALL_DIR }}/manifest.txt
for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt
for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt
- name: Package (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == ''
@ -402,10 +389,9 @@ 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
shell: bash # yes, we are not using MSYS2 or PowerShell here
run: |
echo '${{ secrets.WINDOWS_CODESIGN_CERT }}' | base64 --decode > codesign.pfx
@ -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)
@ -599,7 +577,7 @@ jobs:
with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
- name: Upload AppImage Zsync (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
uses: actions/upload-artifact@v3
@ -623,10 +601,10 @@ 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
with:
bundle: "Prism Launcher.flatpak"
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml

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
@ -40,9 +41,9 @@ jobs:
run: |
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-Qt6*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt6-${{ 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

@ -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@";
@ -88,10 +99,7 @@ Config::Config()
if (GIT_REFSPEC.startsWith("refs/heads/"))
{
VERSION_CHANNEL = GIT_REFSPEC;
VERSION_CHANNEL.remove("refs/heads/");
if(!UPDATER_BASE.isEmpty() && !BUILD_PLATFORM.isEmpty()) {
UPDATER_ENABLED = true;
}
VERSION_CHANNEL.remove("refs/heads/");
}
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

@ -106,11 +106,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1696661029,
"narHash": "sha256-GIB5VTkvsDIqfMpdtuetOzpm64P8wm8nBSv5Eo8XM3Y=",
"lastModified": 1697009197,
"narHash": "sha256-viVRhBTFT8fPJTb1N3brQIpFZnttmwo3JVKNuWRVc3s=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2de1be5b51c3d6fa833f1c1f222dc867dd054b31",
"rev": "01441e14af5e29c9d27ace398e6dd0b293e25a54",
"type": "github"
},
"original": {
@ -153,11 +153,11 @@
]
},
"locked": {
"lastModified": 1696516544,
"narHash": "sha256-8rKE8Je6twTNFRTGF63P9mE3lZIq917RAicdc4XJO80=",
"lastModified": 1696846637,
"narHash": "sha256-0hv4kbXxci2+pxhuXlVgftj/Jq79VSmtAyvfabCCtYk=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "66c352d33e0907239e4a69416334f64af2c685cc",
"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
@ -579,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
@ -675,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
@ -906,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
@ -1030,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
@ -1068,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
@ -1099,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
@ -1115,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})
@ -1133,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
@ -1214,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}"
@ -1233,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,16 +1,36 @@
/* Copyright 2013-2021 MultiMC Contributors
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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
* 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.
*
* http://www.apache.org/licenses/LICENSE-2.0
* 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.
*
* 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.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
@ -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

@ -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...");
@ -606,4 +613,5 @@ bool ExtractZipTask::abort()
return false;
}
} // namespace MMCZip
#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)
@ -214,4 +219,5 @@ class ExtractZipTask : public Task {
QFuture<ZipResult> m_zip_future;
QFutureWatcher<ZipResult> m_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);
return ldh.exec();
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

@ -45,10 +45,12 @@ QString JavaVersion::toString() const
bool JavaVersion::requiresPermGen()
{
if (m_parseable) {
return m_major < 8;
}
return true;
return !m_parseable || m_major < 8;
}
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;

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

@ -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

@ -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

@ -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);
}
}
QStringList selectedOptionalMods;
if (!optionalFiles.empty()) {
OptionalModDialog optionalModDialog(m_parent, optionalFiles);
if (optionalModDialog.exec() == QDialog::Rejected) {
emitAborted();
loop.quit();
return;
}
auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
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

@ -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) {

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

@ -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

@ -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)
@ -111,9 +112,8 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc)
unsortedVersions.append(file);
}
auto orderSortPredicate = [](const ModpackVersion& a, const ModpackVersion& b) -> bool {
bool a_better_release = a.version_type <= b.version_type;
// dates are in RFC 3339 format
return a.date > b.date && a_better_release;
return a.date > b.date;
};
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);

View File

@ -57,6 +57,7 @@ struct File {
QCryptographicHash::Algorithm hashAlgorithm;
QByteArray hash;
QQueue<QUrl> downloads;
bool required = true;
};
struct DonationData {

View File

@ -89,4 +89,4 @@ QNetworkReply* Download::getReply(QNetworkRequest& request)
{
return m_network->get(request);
}
} // namespace Net
} // namespace Net

View File

@ -36,11 +36,16 @@
*/
#include "NetJob.h"
#if defined(LAUNCHER_APPLICATION)
#include "Application.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
{

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);
@ -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);
@ -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)
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(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;
QAction* action = qobject_cast<QAction*>(obj);
if (!action)
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;
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),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
APPLICATION->instances()->deleteGroup(groupName);
}
}
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

@ -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

@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include "UpdateAvailableDialog.h"
#include <QPushButton>
#include "Application.h"
#include "BuildConfig.h"
#include "Markdown.h"
#include "ui_UpdateAvailableDialog.h"
UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
const QString& availableVersion,
const QString& releaseNotes,
QWidget* parent)
: QDialog(parent), ui(new Ui::UpdateAvailableDialog)
{
ui->setupUi(this);
QString launcherName = BuildConfig.LAUNCHER_DISPLAYNAME;
ui->headerLabel->setText(tr("A new version of %1 is available!").arg(launcherName));
ui->versionAvailableLabel->setText(
tr("Version %1 is now available - you have %2 . Would you like to download it now?").arg(availableVersion).arg(currentVersion));
ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64));
auto releaseNotesHtml = markdownToHTML(releaseNotes);
ui->releaseNotes->setHtml(releaseNotesHtml);
ui->releaseNotes->setOpenExternalLinks(true);
connect(ui->skipButton, &QPushButton::clicked, this, [this]() {
setResult(ResultCode::Skip);
done(ResultCode::Skip);
});
connect(ui->delayButton, &QPushButton::clicked, this, [this]() {
setResult(ResultCode::DontInstall);
done(ResultCode::DontInstall);
});
connect(ui->installButton, &QPushButton::clicked, this, [this]() {
setResult(ResultCode::Install);
done(ResultCode::Install);
});
}

View File

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#pragma once
#include <QDialog>
namespace Ui {
class UpdateAvailableDialog;
}
class UpdateAvailableDialog : public QDialog {
Q_OBJECT
public:
enum ResultCode {
Install = 10,
DontInstall = 11,
Skip = 12,
};
explicit UpdateAvailableDialog(const QString& currentVersion,
const QString& availableVersion,
const QString& releaseNotes,
QWidget* parent = 0);
~UpdateAvailableDialog() = default;
private:
Ui::UpdateAvailableDialog* ui;
};

View File

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>UpdateAvailableDialog</class>
<widget class="QDialog" name="UpdateAvailableDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>636</width>
<height>352</height>
</rect>
</property>
<property name="windowTitle">
<string>Update Available</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
<item>
<layout class="QVBoxLayout" name="leftsideLayout">
<item>
<widget class="QLabel" name="icon">
<property name="minimumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="mainLayout">
<property name="leftMargin">
<number>9</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<item>
<widget class="QLabel" name="headerLabel">
<property name="font">
<font>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>A new version is available!</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="versionAvailableLabel">
<property name="text">
<string>Version %1 is now available - you have %2 . Would you like to download it now?</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="releaseNotesLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Release Notes:</string>
</property>
</widget>
</item>
<item>
<widget class="QTextBrowser" name="releaseNotes"/>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="skipButton">
<property name="text">
<string>Skip This Version</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="delayButton">
<property name="text">
<string>Remind Me Later</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="installButton">
<property name="text">
<string>Install Update</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -119,6 +119,9 @@ void MinecraftPage::applySettings()
// Miscellaneous
s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked());
s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked());
// Legacy settings
s->set("OnlineFixes", ui->onlineFixes->isChecked());
}
void MinecraftPage::loadSettings()
@ -170,6 +173,8 @@ void MinecraftPage::loadSettings()
ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool());
ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool());
ui->onlineFixes->setChecked(s->get("OnlineFixes").toBool());
}
void MinecraftPage::retranslate()

View File

@ -138,7 +138,7 @@
</property>
</widget>
</item>
<item>
<item>
<widget class="QCheckBox" name="showGameTimeWithoutDays">
<property name="text">
<string>Show time spent playing in hours</string>
@ -197,6 +197,25 @@
<string>Tweaks</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_12">
<item>
<widget class="QGroupBox" name="legacySettingsGroupBox">
<property name="title">
<string>Legacy settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="onlineFixes">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Emulates usages of old online services which are no longer operating.&lt;/p&gt;&lt;p&gt;This currently allows modern skins to be used.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Enable online fixes (experimental)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="nativeLibWorkaroundGroupBox">
<property name="title">

View File

@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 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
@ -253,6 +254,14 @@ void InstanceSettingsPage::applySettings()
m_settings->reset("InstanceAccountId");
}
bool overrideLegacySettings = ui->legacySettingsGroupBox->isChecked();
m_settings->set("OverrideLegacySettings", overrideLegacySettings);
if (overrideLegacySettings) {
m_settings->set("OnlineFixes", ui->onlineFixes->isChecked());
} else {
m_settings->reset("OnlineFixes");
}
// FIXME: This should probably be called by a signal instead
m_instance->updateRuntimeContext();
}
@ -356,6 +365,9 @@ void InstanceSettingsPage::loadSettings()
ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool());
updateAccountsMenu();
ui->legacySettingsGroupBox->setChecked(m_settings->get("OverrideLegacySettings").toBool());
ui->onlineFixes->setChecked(m_settings->get("OnlineFixes").toBool());
}
void InstanceSettingsPage::on_javaDetectBtn_clicked()

View File

@ -583,6 +583,31 @@
<string>Miscellaneous</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<widget class="QGroupBox" name="legacySettingsGroupBox">
<property name="title">
<string>Legacy settings</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_17">
<item>
<widget class="QCheckBox" name="onlineFixes">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Emulates usages of old online services which are no longer operating.&lt;/p&gt;&lt;p&gt;This currently allows modern skins to be used.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Enable online fixes (experimental)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gameTimeGroupBox">
<property name="enabled">

View File

@ -0,0 +1,63 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "OptionalModDialog.h"
#include "ui_OptionalModDialog.h"
OptionalModDialog::OptionalModDialog(QWidget* parent, const QStringList& mods) : QDialog(parent), ui(new Ui::OptionalModDialog)
{
ui->setupUi(this);
for (const QString& mod : mods) {
auto item = new QListWidgetItem(mod, ui->list);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(Qt::Unchecked);
item->setData(Qt::UserRole, mod);
}
connect(ui->selectAllButton, &QPushButton::clicked, ui->list, [this] {
for (int i = 0; i < ui->list->count(); i++)
ui->list->item(i)->setCheckState(Qt::Checked);
});
connect(ui->clearAllButton, &QPushButton::clicked, ui->list, [this] {
for (int i = 0; i < ui->list->count(); i++)
ui->list->item(i)->setCheckState(Qt::Unchecked);
});
connect(ui->list, &QListWidget::itemActivated, [](QListWidgetItem* item) {
if (item->checkState() == Qt::Checked)
item->setCheckState(Qt::Unchecked);
else
item->setCheckState(Qt::Checked);
});
}
OptionalModDialog::~OptionalModDialog()
{
delete ui;
}
QStringList OptionalModDialog::getResult()
{
QStringList result;
result.reserve(ui->list->count());
for (int i = 0; i < ui->list->count(); i++) {
auto item = ui->list->item(i);
if (item->checkState() == Qt::Checked)
result.append(item->data(Qt::UserRole).toString());
}
return result;
}

View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QAbstractListModel>
#include <QDialog>
namespace Ui {
class OptionalModDialog;
}
class OptionalModDialog : public QDialog {
Q_OBJECT
public:
OptionalModDialog(QWidget* parent, const QStringList& mods);
~OptionalModDialog() override;
QStringList getResult();
private:
Ui::OptionalModDialog* ui;
};

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OptionalModDialog</class>
<widget class="QDialog" name="OptionalModDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>550</width>
<height>310</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Optional Mods</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QListWidget" name="list">
<property name="defaultDropAction">
<enum>Qt::IgnoreAction</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="selectAllButton">
<property name="text">
<string>Select All</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clearAllButton">
<property name="text">
<string>Deselect All</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Unchecked mods will be disabled.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>OptionalModDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>274</x>
<y>284</y>
</hint>
<hint type="destinationlabel">
<x>274</x>
<y>154</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>OptionalModDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>274</x>
<y>284</y>
</hint>
<hint type="destinationlabel">
<x>274</x>
<y>154</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -63,7 +63,7 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
}
auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder");
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower());
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1").arg(pack.safeName);
((ListModel*)this)->requestLogo(pack.safeName, url);
return icon;

View File

@ -38,7 +38,7 @@
#include <QAbstractListModel>
#include <QDialog>
#include "modplatform/atlauncher/ATLPackIndex.h"
#include "modplatform/atlauncher/ATLPackManifest.h"
#include "net/NetJob.h"
namespace Ui {

View File

@ -114,8 +114,8 @@ void AtlPage::suggestCurrent()
auto uiSupport = new AtlUserInteractionSupportImpl(this);
dialog->setSuggestedPack(selected.name, selectedVersion, new ATLauncher::PackInstallTask(uiSupport, selected.name, selectedVersion));
auto editedLogoName = selected.safeName;
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower());
auto editedLogoName = "atl_" + selected.safeName;
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1").arg(selected.safeName);
listModel->getLogo(selected.safeName, url,
[this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); });
}

View File

@ -228,8 +228,7 @@ void FlamePage::suggestCurrent()
extra_info.insert("pack_version_id", QString::number(version.fileId));
dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)));
QString editedLogoName;
editedLogoName = "curseforge_" + current.logoName;
QString editedLogoName = "curseforge_" + current.logoName;
listModel->getLogo(current.logoName, current.logoUrl,
[this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); });
}

View File

@ -90,7 +90,7 @@ void ImportFTBPage::suggestCurrent()
}
dialog->setSuggestedPack(selected.name, new PackInstallTask(selected));
QString editedLogoName = QString("ftb_%1").arg(selected.id);
QString editedLogoName = QString("ftb_%1_%2,jpg").arg(selected.name, selected.id);
dialog->setSuggestedIconFromFile(FS::PathCombine(selected.path, "folder.jpg"), editedLogoName);
}

View File

@ -179,15 +179,11 @@ void Page::suggestCurrent()
}
dialog->setSuggestedPack(selected.name, selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion));
QString editedLogoName;
if (selected.logo.toLower().startsWith("ftb")) {
editedLogoName = selected.logo;
} else {
editedLogoName = "ftb_" + selected.logo;
QString editedLogoName = selected.logo;
if (!selected.logo.toLower().startsWith("ftb")) {
editedLogoName = "ftb_" + editedLogoName;
}
editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png"));
if (selected.type == PackType::Public) {
publicListModel->getLogo(selected.logo,
[this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); });

View File

@ -41,7 +41,9 @@
#include "net/ApiDownload.h"
#include "ui/widgets/ProjectItem.h"
#include <QFileInfo>
#include <QIcon>
#include <QUrl>
Technic::ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {}
@ -193,7 +195,7 @@ void Technic::ListModel::searchRequestFinished()
pack.logoName = "null";
} else {
pack.logoUrl = rawURL;
pack.logoName = rawURL.section(QLatin1Char('/'), -1);
pack.logoName = pack.slug + "." + QFileInfo(QUrl(rawURL).fileName()).suffix();
}
pack.broken = false;
newList.append(pack);
@ -215,7 +217,7 @@ void Technic::ListModel::searchRequestFinished()
auto iconUrl = Json::requireString(iconObj, "url");
pack.logoUrl = iconUrl;
pack.logoName = iconUrl.section(QLatin1Char('/'), -1);
pack.logoName = pack.slug + "." + QFileInfo(QUrl(iconUrl).fileName()).suffix();
} else {
pack.logoUrl = "null";
pack.logoName = "null";

View File

@ -0,0 +1,354 @@
// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include "PrismExternalUpdater.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QMessageBox>
#include <QProcess>
#include <QProgressDialog>
#include <QSettings>
#include <QTimer>
#include <memory>
#include "StringUtils.h"
#include "BuildConfig.h"
#include "ui/dialogs/UpdateAvailableDialog.h"
class PrismExternalUpdater::Private {
public:
QDir appDir;
QDir dataDir;
QTimer updateTimer;
bool allowBeta;
bool autoCheck;
double updateInterval;
QDateTime lastCheck;
std::unique_ptr<QSettings> settings;
QWidget* parent;
};
PrismExternalUpdater::PrismExternalUpdater(QWidget* parent, const QString& appDir, const QString& dataDir)
{
priv = new PrismExternalUpdater::Private();
priv->appDir = QDir(appDir);
priv->dataDir = QDir(dataDir);
auto settings_file = priv->dataDir.absoluteFilePath("prismlauncher_update.cfg");
priv->settings = std::make_unique<QSettings>(settings_file, QSettings::Format::IniFormat);
priv->allowBeta = priv->settings->value("allow_beta", false).toBool();
priv->autoCheck = priv->settings->value("auto_check", false).toBool();
bool interval_ok;
// default once per day
priv->updateInterval = priv->settings->value("update_interval", 86400).toInt(&interval_ok);
if (!interval_ok)
priv->updateInterval = 86400;
auto last_check = priv->settings->value("last_check");
if (!last_check.isNull() && last_check.isValid()) {
priv->lastCheck = QDateTime::fromString(last_check.toString(), Qt::ISODate);
}
priv->parent = parent;
connectTimer();
resetAutoCheckTimer();
}
PrismExternalUpdater::~PrismExternalUpdater()
{
if (priv->updateTimer.isActive())
priv->updateTimer.stop();
disconnectTimer();
priv->settings->sync();
delete priv;
}
void PrismExternalUpdater::checkForUpdates()
{
QProgressDialog progress(tr("Checking for updates..."), "", 0, 0, priv->parent);
progress.setCancelButton(nullptr);
progress.adjustSize();
progress.show();
QCoreApplication::processEvents();
QProcess proc;
auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME);
#if defined Q_OS_WIN32
exe_name.append(".exe");
auto env = QProcessEnvironment::systemEnvironment();
env.insert("__COMPAT_LAYER", "RUNASINVOKER");
proc.setProcessEnvironment(env);
#else
exe_name = QString("bin/%1").arg(exe_name);
#endif
QStringList args = { "--check-only", "--dir", priv->dataDir.absolutePath(), "--debug" };
if (priv->allowBeta)
args.append("--pre-release");
proc.start(priv->appDir.absoluteFilePath(exe_name), args);
auto result_start = proc.waitForStarted(5000);
if (!result_start) {
auto err = proc.error();
qDebug() << "Failed to start updater after 5 seconds."
<< "reason:" << err << proc.errorString();
auto msgBox =
QMessageBox(QMessageBox::Information, tr("Update Check Failed"),
tr("Failed to start after 5 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent);
msgBox.setMinimumWidth(460);
msgBox.adjustSize();
msgBox.exec();
priv->lastCheck = QDateTime::currentDateTime();
priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate));
priv->settings->sync();
resetAutoCheckTimer();
return;
}
QCoreApplication::processEvents();
auto result_finished = proc.waitForFinished(60000);
if (!result_finished) {
proc.kill();
auto err = proc.error();
auto output = proc.readAll();
qDebug() << "Updater failed to close after 60 seconds."
<< "reason:" << err << proc.errorString();
auto msgBox =
QMessageBox(QMessageBox::Information, tr("Update Check Failed"),
tr("Updater failed to close 60 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent);
msgBox.setDetailedText(output);
msgBox.setMinimumWidth(460);
msgBox.adjustSize();
msgBox.exec();
priv->lastCheck = QDateTime::currentDateTime();
priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate));
priv->settings->sync();
resetAutoCheckTimer();
return;
}
auto exit_code = proc.exitCode();
auto std_output = proc.readAllStandardOutput();
auto std_error = proc.readAllStandardError();
progress.hide();
QCoreApplication::processEvents();
switch (exit_code) {
case 0:
// no update available
{
qDebug() << "No update available";
auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("You are running the latest version."),
QMessageBox::Ok, priv->parent);
msgBox.setMinimumWidth(460);
msgBox.adjustSize();
msgBox.exec();
}
break;
case 1:
// there was an error
{
qDebug() << "Updater subprocess error" << qPrintable(std_error);
auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update Check Error"),
tr("There was an error running the update check."), QMessageBox::Ok, priv->parent);
msgBox.setDetailedText(QString(std_error));
msgBox.setMinimumWidth(460);
msgBox.adjustSize();
msgBox.exec();
}
break;
case 100:
// update available
{
auto [first_line, remainder1] = StringUtils::splitFirst(std_output, '\n');
auto [second_line, remainder2] = StringUtils::splitFirst(remainder1, '\n');
auto [third_line, release_notes] = StringUtils::splitFirst(remainder2, '\n');
auto version_name = StringUtils::splitFirst(first_line, ": ").second.trimmed();
auto version_tag = StringUtils::splitFirst(second_line, ": ").second.trimmed();
auto release_timestamp = QDateTime::fromString(StringUtils::splitFirst(third_line, ": ").second.trimmed(), Qt::ISODate);
qDebug() << "Update available:" << version_name << version_tag << release_timestamp;
qDebug() << "Update release notes:" << release_notes;
offerUpdate(version_name, version_tag, release_notes);
}
break;
default:
// unknown error code
{
qDebug() << "Updater exited with unknown code" << exit_code;
auto msgBox =
QMessageBox(QMessageBox::Information, tr("Unknown Update Error"),
tr("The updater exited with an unknown condition.\nExit Code: %1").arg(QString::number(exit_code)),
QMessageBox::Ok, priv->parent);
auto detail_txt = tr("StdOut: %1\nStdErr: %2").arg(QString(std_output)).arg(QString(std_error));
msgBox.setDetailedText(detail_txt);
msgBox.setMinimumWidth(460);
msgBox.adjustSize();
msgBox.exec();
}
}
priv->lastCheck = QDateTime::currentDateTime();
priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate));
priv->settings->sync();
resetAutoCheckTimer();
}
bool PrismExternalUpdater::getAutomaticallyChecksForUpdates()
{
return priv->autoCheck;
}
double PrismExternalUpdater::getUpdateCheckInterval()
{
return priv->updateInterval;
}
bool PrismExternalUpdater::getBetaAllowed()
{
return priv->allowBeta;
}
void PrismExternalUpdater::setAutomaticallyChecksForUpdates(bool check)
{
priv->autoCheck = check;
priv->settings->setValue("auto_check", check);
priv->settings->sync();
resetAutoCheckTimer();
}
void PrismExternalUpdater::setUpdateCheckInterval(double seconds)
{
priv->updateInterval = seconds;
priv->settings->setValue("update_interval", seconds);
priv->settings->sync();
resetAutoCheckTimer();
}
void PrismExternalUpdater::setBetaAllowed(bool allowed)
{
priv->allowBeta = allowed;
priv->settings->setValue("auto_beta", allowed);
priv->settings->sync();
}
void PrismExternalUpdater::resetAutoCheckTimer()
{
if (priv->autoCheck) {
int timeoutDuration = 0;
auto now = QDateTime::currentDateTime();
if (priv->lastCheck.isValid()) {
auto diff = priv->lastCheck.secsTo(now);
auto secs_left = priv->updateInterval - diff;
if (secs_left < 0)
secs_left = 0;
timeoutDuration = secs_left * 1000; // to msec
}
qDebug() << "Auto update timer starting," << timeoutDuration / 1000 << "seconds left";
priv->updateTimer.start(timeoutDuration);
} else {
if (priv->updateTimer.isActive())
priv->updateTimer.stop();
}
}
void PrismExternalUpdater::connectTimer()
{
connect(&priv->updateTimer, &QTimer::timeout, this, &PrismExternalUpdater::autoCheckTimerFired);
}
void PrismExternalUpdater::disconnectTimer()
{
disconnect(&priv->updateTimer, &QTimer::timeout, this, &PrismExternalUpdater::autoCheckTimerFired);
}
void PrismExternalUpdater::autoCheckTimerFired()
{
qDebug() << "Auto update Timer fired";
checkForUpdates();
}
void PrismExternalUpdater::offerUpdate(const QString& version_name, const QString& version_tag, const QString& release_notes)
{
priv->settings->beginGroup("skip");
auto should_skip = priv->settings->value(version_tag, false).toBool();
priv->settings->endGroup();
if (should_skip) {
auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("There are no new updates available."),
QMessageBox::Ok, priv->parent);
msgBox.setMinimumWidth(460);
msgBox.adjustSize();
msgBox.exec();
return;
}
UpdateAvailableDialog dlg(BuildConfig.printableVersionString(), version_name, release_notes);
auto result = dlg.exec();
qDebug() << "offer dlg result" << result;
switch (result) {
case UpdateAvailableDialog::Install: {
performUpdate(version_tag);
return;
}
case UpdateAvailableDialog::Skip: {
priv->settings->beginGroup("skip");
priv->settings->setValue(version_tag, true);
priv->settings->endGroup();
priv->settings->sync();
return;
}
case UpdateAvailableDialog::DontInstall: {
return;
}
}
}
void PrismExternalUpdater::performUpdate(const QString& version_tag)
{
QProcess proc;
auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME);
#if defined Q_OS_WIN32
exe_name.append(".exe");
auto env = QProcessEnvironment::systemEnvironment();
env.insert("__COMPAT_LAYER", "RUNASINVOKER");
proc.setProcessEnvironment(env);
#else
exe_name = QString("bin/%1").arg(exe_name);
#endif
QStringList args = { "--dir", priv->dataDir.absolutePath(), "--install-version", version_tag };
if (priv->allowBeta)
args.append("--pre-release");
auto result = proc.startDetached(priv->appDir.absoluteFilePath(exe_name), args);
if (!result) {
qDebug() << "Failed to start updater:" << proc.error() << proc.errorString();
}
QCoreApplication::exit();
}

View File

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#pragma once
#include <QObject>
#include "ExternalUpdater.h"
/*!
* An implementation for the updater on windows and linux that uses out external updater.
*/
class PrismExternalUpdater : public ExternalUpdater {
Q_OBJECT
public:
PrismExternalUpdater(QWidget* parent, const QString& appDir, const QString& dataDir);
~PrismExternalUpdater() override;
/*!
* Check for updates manually, showing the user a progress bar and an alert if no updates are found.
*/
void checkForUpdates() override;
/*!
* Indicates whether or not to check for updates automatically.
*/
bool getAutomaticallyChecksForUpdates() override;
/*!
* Indicates the current automatic update check interval in seconds.
*/
double getUpdateCheckInterval() override;
/*!
* Indicates whether or not beta updates should be checked for in addition to regular releases.
*/
bool getBetaAllowed() override;
/*!
* Set whether or not to check for updates automatically.
*
* The update schedule cycle will be reset in a short delay after the propertys new value is set. This is to allow
* reverting this property without kicking off a schedule change immediately."
*/
void setAutomaticallyChecksForUpdates(bool check) override;
/*!
* Set the current automatic update check interval in seconds.
*
* The update schedule cycle will be reset in a short delay after the propertys new value is set. This is to allow
* reverting this property without kicking off a schedule change immediately."
*/
void setUpdateCheckInterval(double seconds) override;
/*!
* Set whether or not beta updates should be checked for in addition to regular releases.
*/
void setBetaAllowed(bool allowed) override;
void resetAutoCheckTimer();
void disconnectTimer();
void connectTimer();
void offerUpdate(const QString& version_name, const QString& version_tag, const QString& release_notes);
void performUpdate(const QString& version_tag);
public slots:
void autoCheckTimerFired();
private:
class Private;
Private* priv;
};

View File

@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include "GitHubRelease.h"
QDebug operator<<(QDebug debug, const GitHubReleaseAsset& asset)
{
QDebugStateSaver saver(debug);
debug.nospace() << "GitHubReleaseAsset( "
"id: "
<< asset.id
<< ", "
"name "
<< asset.name
<< ", "
"label: "
<< asset.label
<< ", "
"content_type: "
<< asset.content_type
<< ", "
"size: "
<< asset.size
<< ", "
"created_at: "
<< asset.created_at
<< ", "
"updated_at: "
<< asset.updated_at
<< ", "
"browser_download_url: "
<< asset.browser_download_url
<< " "
")";
return debug;
}
QDebug operator<<(QDebug debug, const GitHubRelease& rls)
{
QDebugStateSaver saver(debug);
debug.nospace() << "GitHubRelease( "
"id: "
<< rls.id
<< ", "
"name "
<< rls.name
<< ", "
"tag_name: "
<< rls.tag_name
<< ", "
"created_at: "
<< rls.created_at
<< ", "
"published_at: "
<< rls.published_at
<< ", "
"prerelease: "
<< rls.prerelease
<< ", "
"draft: "
<< rls.draft
<< ", "
"version"
<< rls.version
<< ", "
"body: "
<< rls.body
<< ", "
"assets: "
<< rls.assets
<< " "
")";
return debug;
}

View File

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#pragma once
#include <QDateTime>
#include <QList>
#include <QString>
#include <QDebug>
#include "Version.h"
struct GitHubReleaseAsset {
int id = -1;
QString name;
QString label;
QString content_type;
int size;
QDateTime created_at;
QDateTime updated_at;
QString browser_download_url;
bool isValid() { return id > 0; }
};
struct GitHubRelease {
int id = -1;
QString name;
QString tag_name;
QDateTime created_at;
QDateTime published_at;
bool prerelease;
bool draft;
QString body;
QList<GitHubReleaseAsset> assets;
Version version;
bool isValid() const { return id > 0; }
};
QDebug operator<<(QDebug debug, const GitHubReleaseAsset& rls);
QDebug operator<<(QDebug debug, const GitHubRelease& rls);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,143 @@
// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#pragma once
#include <QtCore>
#include <QApplication>
#include <QDataStream>
#include <QDateTime>
#include <QDebug>
#include <QFlag>
#include <QIcon>
#include <QLocalSocket>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QUrl>
#include <memory>
#include <optional>
#include "QObjectPtr.h"
#include "net/Download.h"
#define PRISM_EXTERNAL_EXE
#include "FileSystem.h"
#include "GitHubRelease.h"
class PrismUpdaterApp : public QApplication {
// friends for the purpose of limiting access to deprecated stuff
Q_OBJECT
public:
enum Status { Starting, Failed, Succeeded, Initialized, Aborted };
PrismUpdaterApp(int& argc, char** argv);
virtual ~PrismUpdaterApp();
void loadReleaseList();
void run();
Status status() const { return m_status; }
private:
void fail(const QString& reason);
void abort(const QString& reason);
void showFatalErrorMessage(const QString& title, const QString& content);
bool loadPrismVersionFromExe(const QString& exe_path);
void downloadReleasePage(const QString& api_url, int page);
int parseReleasePage(const QByteArray* response);
bool needUpdate(const GitHubRelease& release);
GitHubRelease getLatestRelease();
GitHubRelease selectRelease();
QList<GitHubRelease> newerReleases();
QList<GitHubRelease> nonDraftReleases();
void printReleases();
QList<GitHubReleaseAsset> validReleaseArtifacts(const GitHubRelease& release);
GitHubReleaseAsset selectAsset(const QList<GitHubReleaseAsset>& assets);
void performUpdate(const GitHubRelease& release);
void performInstall(QFileInfo file);
void unpackAndInstall(QFileInfo file);
void backupAppDir();
std::optional<QDir> unpackArchive(QFileInfo file);
QFileInfo downloadAsset(const GitHubReleaseAsset& asset);
bool callAppImageUpdate();
void moveAndFinishUpdate(QDir target);
public slots:
void downloadError(QString reason);
private:
const QString& root() { return m_rootPath; }
bool isPortable() { return m_isPortable; }
void clearUpdateLog();
void logUpdate(const QString& msg);
QString m_rootPath;
QString m_dataPath;
bool m_isPortable = false;
bool m_isAppimage = false;
bool m_isFlatpak = false;
QString m_appimagePath;
QString m_prismExecutable;
QUrl m_prismRepoUrl;
Version m_userSelectedVersion;
bool m_checkOnly;
bool m_forceUpdate;
bool m_printOnly;
bool m_selectUI;
bool m_allowDowngrade;
bool m_allowPreRelease;
QString m_updateLogPath;
QString m_prismBinaryName;
QString m_prismVersion;
int m_prismVersionMajor = -1;
int m_prismVersionMinor = -1;
QString m_prsimVersionChannel;
QString m_prismGitCommit;
GitHubRelease m_install_release;
Status m_status = Status::Starting;
shared_qobject_ptr<QNetworkAccessManager> m_network;
QString m_current_url;
Task::Ptr m_current_task;
QList<GitHubRelease> m_releases;
public:
std::unique_ptr<QFile> logFile;
bool logToConsole = false;
#if defined Q_OS_WIN32
// used on Windows to attach the standard IO streams
bool consoleAttached = false;
#endif
};

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SelectReleaseDialog</class>
<widget class="QDialog" name="SelectReleaseDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>468</width>
<height>385</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Release to Install</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1,0">
<item>
<widget class="QLabel" name="eplainLabel">
<property name="text">
<string>Please select the release you wish to update to.</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="versionsTree">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QTextBrowser" name="changelogTextBrowser"/>
</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>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SelectReleaseDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SelectReleaseDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,168 @@
// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include "UpdaterDialogs.h"
#include "ui_SelectReleaseDialog.h"
#include <QTextBrowser>
#include "Markdown.h"
SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const QList<GitHubRelease>& releases, QWidget* parent)
: QDialog(parent), m_releases(releases), m_currentVersion(current_version), ui(new Ui::SelectReleaseDialog)
{
ui->setupUi(this);
ui->changelogTextBrowser->setOpenExternalLinks(true);
ui->changelogTextBrowser->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth);
ui->changelogTextBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
ui->versionsTree->setColumnCount(2);
ui->versionsTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
ui->versionsTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
ui->versionsTree->setHeaderLabels({ tr("Version"), tr("Published Date") });
ui->versionsTree->header()->setStretchLastSection(false);
ui->eplainLabel->setText(tr("Select a version to install.\n"
"\n"
"Currently installed version: %1")
.arg(m_currentVersion.toString()));
loadReleases();
connect(ui->versionsTree, &QTreeWidget::currentItemChanged, this, &SelectReleaseDialog::selectionChanged);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseDialog::reject);
}
SelectReleaseDialog::~SelectReleaseDialog()
{
delete ui;
}
void SelectReleaseDialog::loadReleases()
{
for (auto rls : m_releases) {
appendRelease(rls);
}
}
void SelectReleaseDialog::appendRelease(GitHubRelease const& release)
{
auto rls_item = new QTreeWidgetItem(ui->versionsTree);
rls_item->setText(0, release.tag_name);
rls_item->setExpanded(true);
rls_item->setText(1, release.published_at.toString());
rls_item->setData(0, Qt::UserRole, QVariant(release.id));
ui->versionsTree->addTopLevelItem(rls_item);
}
GitHubRelease SelectReleaseDialog::getRelease(QTreeWidgetItem* item)
{
int id = item->data(0, Qt::UserRole).toInt();
GitHubRelease release;
for (auto rls : m_releases) {
if (rls.id == id)
release = rls;
}
return release;
}
void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous)
{
GitHubRelease release = getRelease(current);
QString body = markdownToHTML(release.body.toUtf8());
m_selectedRelease = release;
ui->changelogTextBrowser->setHtml(body);
}
SelectReleaseAssetDialog::SelectReleaseAssetDialog(const QList<GitHubReleaseAsset>& assets, QWidget* parent)
: QDialog(parent), m_assets(assets), ui(new Ui::SelectReleaseDialog)
{
ui->setupUi(this);
ui->changelogTextBrowser->setOpenExternalLinks(true);
ui->changelogTextBrowser->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth);
ui->changelogTextBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
ui->versionsTree->setColumnCount(2);
ui->versionsTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
ui->versionsTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
ui->versionsTree->setHeaderLabels({ tr("Version"), tr("Published Date") });
ui->versionsTree->header()->setStretchLastSection(false);
ui->eplainLabel->setText(tr("Select a version to install."));
ui->changelogTextBrowser->setHidden(true);
loadAssets();
connect(ui->versionsTree, &QTreeWidget::currentItemChanged, this, &SelectReleaseAssetDialog::selectionChanged);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseAssetDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseAssetDialog::reject);
}
SelectReleaseAssetDialog::~SelectReleaseAssetDialog()
{
delete ui;
}
void SelectReleaseAssetDialog::loadAssets()
{
for (auto rls : m_assets) {
appendAsset(rls);
}
}
void SelectReleaseAssetDialog::appendAsset(GitHubReleaseAsset const& asset)
{
auto rls_item = new QTreeWidgetItem(ui->versionsTree);
rls_item->setText(0, asset.name);
rls_item->setExpanded(true);
rls_item->setText(1, asset.updated_at.toString());
rls_item->setData(0, Qt::UserRole, QVariant(asset.id));
ui->versionsTree->addTopLevelItem(rls_item);
}
GitHubReleaseAsset SelectReleaseAssetDialog::getAsset(QTreeWidgetItem* item)
{
int id = item->data(0, Qt::UserRole).toInt();
GitHubReleaseAsset selected_asset;
for (auto asset : m_assets) {
if (asset.id == id)
selected_asset = asset;
}
return selected_asset;
}
void SelectReleaseAssetDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous)
{
GitHubReleaseAsset asset = getAsset(current);
m_selectedAsset = asset;
}

View File

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#pragma once
#include <QDialog>
#include <QTreeWidgetItem>
#include "GitHubRelease.h"
#include "Version.h"
namespace Ui {
class SelectReleaseDialog;
}
class SelectReleaseDialog : public QDialog {
Q_OBJECT
public:
explicit SelectReleaseDialog(const Version& cur_version, const QList<GitHubRelease>& releases, QWidget* parent = 0);
~SelectReleaseDialog();
void loadReleases();
void appendRelease(GitHubRelease const& release);
GitHubRelease selectedRelease() { return m_selectedRelease; }
private slots:
GitHubRelease getRelease(QTreeWidgetItem* item);
void selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous);
protected:
QList<GitHubRelease> m_releases;
GitHubRelease m_selectedRelease;
Version m_currentVersion;
Ui::SelectReleaseDialog* ui;
};
class SelectReleaseAssetDialog : public QDialog {
Q_OBJECT
public:
explicit SelectReleaseAssetDialog(const QList<GitHubReleaseAsset>& assets, QWidget* parent = 0);
~SelectReleaseAssetDialog();
void loadAssets();
void appendAsset(GitHubReleaseAsset const& asset);
GitHubReleaseAsset selectedAsset() { return m_selectedAsset; }
private slots:
GitHubReleaseAsset getAsset(QTreeWidgetItem* item);
void selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous);
protected:
QList<GitHubReleaseAsset> m_assets;
GitHubReleaseAsset m_selectedAsset;
Ui::SelectReleaseDialog* ui;
};

View File

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

View File

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

View File

@ -11,15 +11,33 @@ set(SRC
org/prismlauncher/launcher/Launcher.java
org/prismlauncher/launcher/impl/AbstractLauncher.java
org/prismlauncher/launcher/impl/StandardLauncher.java
org/prismlauncher/launcher/impl/legacy/LegacyLauncher.java
org/prismlauncher/launcher/impl/legacy/LegacyFrame.java
org/prismlauncher/exception/ParameterNotFoundException.java
org/prismlauncher/exception/ParseException.java
org/prismlauncher/utils/Parameters.java
org/prismlauncher/utils/ReflectionUtils.java
org/prismlauncher/utils/logging/Level.java
org/prismlauncher/utils/logging/Log.java
net/minecraft/Launcher.java
org/prismlauncher/legacy/LegacyProxy.java
)
set(LEGACY_SRC
legacy/org/prismlauncher/legacy/LegacyFrame.java
legacy/org/prismlauncher/legacy/LegacyLauncher.java
legacy/org/prismlauncher/legacy/fix/online/Handler.java
legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java
legacy/org/prismlauncher/legacy/fix/online/SkinFix.java
legacy/org/prismlauncher/legacy/utils/Base64.java
legacy/org/prismlauncher/legacy/utils/api/MojangApi.java
legacy/org/prismlauncher/legacy/utils/api/Texture.java
legacy/org/prismlauncher/legacy/utils/json/JsonParseException.java
legacy/org/prismlauncher/legacy/utils/json/JsonParser.java
legacy/org/prismlauncher/legacy/utils/url/CustomUrlConnection.java
legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java
legacy/net/minecraft/Launcher.java
legacy/org/prismlauncher/legacy/LegacyProxy.java
)
add_jar(NewLaunch ${SRC})
add_jar(NewLaunchLegacy ${LEGACY_SRC} INCLUDE_JARS NewLaunch)
install_jar(NewLaunch "${JARS_DEST_DIR}")
install_jar(NewLaunchLegacy "${JARS_DEST_DIR}")

View File

@ -92,12 +92,11 @@ public final class Launcher extends Applet implements AppletStub {
try {
if (documentBase == null) {
if (applet.getClass().getPackage().getName().startsWith("com.mojang.")) {
if (applet.getClass().getPackage().getName().startsWith("com.mojang"))
// Special case only for Classic versions
documentBase = new URL("http://www.minecraft.net:80/game/");
} else {
else
documentBase = new URL("http://www.minecraft.net/game/");
}
}
} catch (MalformedURLException e) {
throw new AssertionError(e);

View File

@ -52,7 +52,7 @@
* limitations under the License.
*/
package org.prismlauncher.launcher.impl.legacy;
package org.prismlauncher.legacy;
import org.prismlauncher.utils.logging.Log;
@ -74,7 +74,7 @@ import javax.swing.JFrame;
import net.minecraft.Launcher;
public final class LegacyFrame extends JFrame {
final class LegacyFrame extends JFrame {
private static final long serialVersionUID = 1L;
private final Launcher launcher;
@ -130,7 +130,7 @@ public final class LegacyFrame extends JFrame {
launcher.setParameter("username", user);
launcher.setParameter("sessionid", session);
launcher.setParameter("stand-alone", true); // Show the quit button. TODO: why won't this work?
launcher.setParameter("stand-alone", true); // Show the quit button. This often doesn't seem to work.
launcher.setParameter("haspaid", true); // Some old versions need this for world saves to work.
launcher.setParameter("demo", demo);
launcher.setParameter("fullscreen", false);

View File

@ -53,23 +53,27 @@
* limitations under the License.
*/
package org.prismlauncher.launcher.impl.legacy;
package org.prismlauncher.legacy;
import org.prismlauncher.launcher.impl.AbstractLauncher;
import org.prismlauncher.utils.Parameters;
import org.prismlauncher.utils.ReflectionUtils;
import org.prismlauncher.utils.logging.Log;
import java.applet.Applet;
import java.io.File;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.List;
/**
* Used to launch old versions that support applets.
* Used to launch old versions which support applets.
*/
public final class LegacyLauncher extends AbstractLauncher {
final class LegacyLauncher extends AbstractLauncher {
private final String user, session;
private final String title;
private final String appletClass;
@ -93,11 +97,9 @@ public final class LegacyLauncher extends AbstractLauncher {
@Override
public void launch() throws Throwable {
Class<?> main = ClassLoader.getSystemClassLoader().loadClass(mainClassName);
Field gameDirField = ReflectionUtils.findMinecraftGameDirField(main);
Field gameDirField = findMinecraftGameDirField(main);
if (gameDirField == null)
Log.warning("Could not find Minecraft folder field");
else {
if (gameDirField != null) {
gameDirField.setAccessible(true);
gameDirField.set(null, new File(gameDir));
}
@ -106,7 +108,7 @@ public final class LegacyLauncher extends AbstractLauncher {
System.setProperty("minecraft.applet.TargetDirectory", gameDir);
try {
LegacyFrame window = new LegacyFrame(title, ReflectionUtils.createAppletClass(appletClass));
LegacyFrame window = new LegacyFrame(title, createAppletClass(appletClass));
window.start(user, session, width, height, maximize, serverAddress, serverPort, gameArgs.contains("--demo"));
return;
@ -115,9 +117,39 @@ public final class LegacyLauncher extends AbstractLauncher {
}
}
// find and invoke the main method, this time without size parameters
// in all versions that support applets, these are ignored
// find and invoke the main method, this time without size parameters - in all
// versions that support applets, these are ignored
MethodHandle method = ReflectionUtils.findMainMethod(main);
method.invokeExact(gameArgs.toArray(new String[0]));
}
private static Applet createAppletClass(String clazz) throws Throwable {
Class<?> appletClass = ClassLoader.getSystemClassLoader().loadClass(clazz);
MethodHandle appletConstructor = MethodHandles.lookup().findConstructor(appletClass, MethodType.methodType(void.class));
return (Applet) appletConstructor.invoke();
}
private static Field findMinecraftGameDirField(Class<?> clazz) {
// search for private static File
for (Field field : clazz.getDeclaredFields()) {
if (field.getType() != File.class)
continue;
int fieldModifiers = field.getModifiers();
if (!Modifier.isStatic(fieldModifiers))
continue;
if (!Modifier.isPrivate(fieldModifiers))
continue;
if (Modifier.isFinal(fieldModifiers))
continue;
return field;
}
return null;
}
}

View File

@ -0,0 +1,68 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.prismlauncher.legacy;
import org.prismlauncher.launcher.Launcher;
import org.prismlauncher.legacy.fix.online.OnlineFixes;
import org.prismlauncher.utils.Parameters;
// implementation of LegacyProxy
public final class LegacyProxy {
public static Launcher createLauncher(Parameters params) {
return new LegacyLauncher(params);
}
public static void applyOnlineFixes(Parameters parameters) {
OnlineFixes.apply(parameters);
}
}

View File

@ -0,0 +1,63 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.legacy.fix.online;
import org.prismlauncher.legacy.utils.url.UrlUtils;
import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
final class Handler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL address) throws IOException {
return openConnection(address, null);
}
@Override
protected URLConnection openConnection(URL address, Proxy proxy) throws IOException {
URLConnection result;
// try skin fix
result = SkinFix.openConnection(address, proxy);
if (result != null)
return result;
return UrlUtils.openConnection(address, proxy);
}
}

View File

@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.legacy.fix.online;
import org.prismlauncher.legacy.utils.Base64;
import org.prismlauncher.legacy.utils.url.UrlUtils;
import org.prismlauncher.utils.Parameters;
import org.prismlauncher.utils.logging.Log;
import java.net.URL;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
/**
* Fixes skins by redirecting to other URLs.
*
* @see {@link Handler}
* @see {@link UrlUtils}
*/
public final class OnlineFixes implements URLStreamHandlerFactory {
public static void apply(Parameters params) {
if (!"true".equals(params.getString("onlineFixes", null)))
return;
if (!UrlUtils.isSupported() || !Base64.isSupported()) {
Log.warning("Cannot access the necessary Java internals for skin fix");
Log.warning("Turning off online fixes in the settings will silence the warnings");
return;
}
try {
URL.setURLStreamHandlerFactory(new OnlineFixes());
} catch (Error e) {
Log.warning("Cannot apply skin fix: URLStreamHandlerFactory is already set");
Log.warning("Turning off online fixes in the settings will silence the warnings");
}
}
@Override
public URLStreamHandler createURLStreamHandler(String protocol) {
if ("http".equals(protocol))
return new Handler();
return null;
}
}

View File

@ -0,0 +1,179 @@
// 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.prismlauncher.legacy.fix.online;
import org.prismlauncher.legacy.utils.api.MojangApi;
import org.prismlauncher.legacy.utils.api.Texture;
import org.prismlauncher.legacy.utils.url.CustomUrlConnection;
import org.prismlauncher.legacy.utils.url.UrlUtils;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import javax.imageio.ImageIO;
final class SkinFix {
static URLConnection openConnection(URL address, Proxy proxy) throws IOException {
String skinOwner = findSkinOwner(address);
if (skinOwner != null)
// we need to correct the skin
return getSkinConnection(skinOwner, proxy);
String capeOwner = findCapeOwner(address);
if (capeOwner != null) {
// since we do not need to process the image, open a direct connection bypassing
// Handler
Texture texture = MojangApi.getTexture(MojangApi.getUuid(capeOwner), "CAPE");
if (texture == null)
return null;
return UrlUtils.openConnection(texture.getUrl(), proxy);
}
return null;
}
private static URLConnection getSkinConnection(String owner, Proxy proxy) throws IOException {
Texture texture = MojangApi.getTexture(MojangApi.getUuid(owner), "SKIN");
if (texture == null)
return null;
URLConnection connection = UrlUtils.openConnection(texture.getUrl(), proxy);
try (InputStream in = connection.getInputStream()) {
// thank you craftycodie!
// this is heavily based on
// https://github.com/craftycodie/MineOnline/blob/4f4f86f9d051e0a6fd7ff0b95b2a05f7437683d7/src/main/java/gg/codie/mineonline/gui/textures/TextureHelper.java#L17
BufferedImage image = ImageIO.read(in);
Graphics2D graphics = image.createGraphics();
graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
BufferedImage subimage;
if (image.getHeight() > 32) {
// flatten second layers
subimage = image.getSubimage(0, 32, 56, 16);
graphics.drawImage(subimage, 0, 16, null);
}
if (texture.isSlim()) {
// convert slim to classic
subimage = image.getSubimage(45, 16, 9, 16);
graphics.drawImage(subimage, 46, 16, null);
subimage = image.getSubimage(49, 16, 2, 4);
graphics.drawImage(subimage, 50, 16, null);
subimage = image.getSubimage(53, 20, 2, 12);
graphics.drawImage(subimage, 54, 20, null);
}
graphics.dispose();
// crop the image - old versions disregard all secondary layers besides the hat
ByteArrayOutputStream out = new ByteArrayOutputStream();
image = image.getSubimage(0, 0, 64, 32);
ImageIO.write(image, "png", out);
return new CustomUrlConnection(out.toByteArray());
}
}
private static String findSkinOwner(URL address) {
switch (address.getHost()) {
case "www.minecraft.net":
return stripIfPrefixed(address.getPath(), "/skin/");
case "s3.amazonaws.com":
case "skins.minecraft.net":
return stripIfPrefixed(address.getPath(), "/MinecraftSkins/");
}
return null;
}
private static String findCapeOwner(URL address) {
switch (address.getHost()) {
case "www.minecraft.net":
if (!address.getPath().equals("/cloak/get.jsp"))
return null;
return stripIfPrefixed(address.getQuery(), "user=");
case "s3.amazonaws.com":
case "skins.minecraft.net":
return stripIfPrefixed(address.getPath(), "/MinecraftCloaks/");
}
return null;
}
private static String stripIfPrefixed(String string, String prefix) {
if (string != null && string.startsWith(prefix)) {
string = string.substring(prefix.length());
if (string.endsWith(".png"))
string = string.substring(0, string.lastIndexOf('.'));
return string;
}
return null;
}
}

View File

@ -0,0 +1,92 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.legacy.utils;
import org.prismlauncher.utils.logging.Log;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.charset.StandardCharsets;
/**
* Uses Base64 with Java 8 or later, otherwise DatatypeConverter. In the latter
* case, reflection is used to allow using newer compilers.
*/
public final class Base64 {
private static boolean supported = true;
private static MethodHandle legacy;
static {
try {
Class.forName("java.util.Base64");
} catch (ClassNotFoundException e) {
try {
Class<?> datatypeConverter = Class.forName("javax.xml.bind.DatatypeConverter");
legacy = MethodHandles.lookup().findStatic(
datatypeConverter, "parseBase64Binary", MethodType.methodType(byte[].class, String.class));
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e1) {
Log.error("Base64 not supported", e1);
supported = false;
}
}
}
/**
* Determines whether base64 is supported.
*
* @return <code>true</code> if base64 can be parsed
*/
public static boolean isSupported() {
return supported;
}
public static byte[] decode(String input) {
if (!isSupported())
throw new UnsupportedOperationException();
if (legacy == null)
return java.util.Base64.getDecoder().decode(input.getBytes(StandardCharsets.UTF_8));
try {
return (byte[]) legacy.invokeExact(input);
} catch (Error | RuntimeException e) {
throw e;
} catch (Throwable e) {
throw new Error(e);
}
}
}

View File

@ -0,0 +1,98 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.legacy.utils.api;
import org.prismlauncher.legacy.utils.Base64;
import org.prismlauncher.legacy.utils.json.JsonParser;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Map;
/**
* Basic wrapper for Mojang's Minecraft API.
*/
@SuppressWarnings("unchecked")
public final class MojangApi {
public static String getUuid(String username) throws IOException {
try (InputStream in = new URL("https://api.mojang.com/users/profiles/minecraft/" + username).openStream()) {
Map<String, Object> map = (Map<String, Object>) JsonParser.parse(in);
return (String) map.get("id");
}
}
public static Texture getTexture(String player, String id) throws IOException {
Map<String, Object> map = getTextures(player);
if (map != null) {
map = (Map<String, Object>) map.get(id);
if (map == null)
return null;
URL url = new URL((String) map.get("url"));
boolean slim = false;
if (id.equals("SKIN")) {
map = (Map<String, Object>) map.get("metadata");
if (map != null && "slim".equals(map.get("model")))
slim = true;
}
return new Texture(url, slim);
}
return null;
}
public static Map<String, Object> getTextures(String player) throws IOException {
try (InputStream profileIn = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + player).openStream()) {
Map<String, Object> profile = (Map<String, Object>) JsonParser.parse(profileIn);
for (Map<String, Object> property : (Iterable<Map<String, Object>>) profile.get("properties")) {
if (property.get("name").equals("textures")) {
Map<String, Object> result =
(Map<String, Object>) JsonParser.parse(new String(Base64.decode((String) property.get("value"))));
result = (Map<String, Object>) result.get("textures");
return result;
}
}
return null;
}
}
}

View File

@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.legacy.utils.api;
import java.net.URL;
/**
* Represents a texture from the Mojang API.
*/
public final class Texture {
private final URL url;
private final boolean slim;
public Texture(URL url, boolean slim) {
this.url = url;
this.slim = slim;
}
public URL getUrl() {
return url;
}
public boolean isSlim() {
return slim;
}
}

View File

@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.legacy.utils.json;
import java.io.IOException;
public final class JsonParseException extends IOException {
private static final long serialVersionUID = 1L;
public JsonParseException(String message) {
super(message);
}
}

View File

@ -0,0 +1,408 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.legacy.utils.json;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A lightweight portable JSON parser used instead of GSON since it is not
* available in a lot of versions.
*/
public final class JsonParser {
private final Reader in;
private char[] buffer;
private int pos, length;
public static Object parse(String in) throws IOException {
return parse(new StringReader(in));
}
public static Object parse(InputStream in) throws IOException {
return parse(new InputStreamReader(in, StandardCharsets.UTF_8));
}
public static Object parse(Reader in) throws IOException {
return new JsonParser(in).readSingleValue();
}
private JsonParser(Reader in) throws IOException {
this.in = in;
pos = length = 0;
read();
}
private int character() {
if (length == -1)
return -1;
return buffer[pos];
}
private int read() throws IOException {
if (length == -1)
return -1;
if (buffer == null || pos++ == length - 1) {
pos = 0;
buffer = new char[8192];
length = in.read(buffer);
}
return character();
}
private void assertCharacter(char character) throws JsonParseException {
if (character() != character)
throw new JsonParseException(
"Expected '" + character + "' but got " + (character() != -1 ? ("'" + (char) character() + "'") : "EOF"));
}
private void assertNoEOF(String expected) throws JsonParseException {
if (character() == -1)
throw new JsonParseException("Expected " + expected + " but got EOF");
}
private void skipWhitespace() throws IOException {
while (isWhitespace()) read();
}
private boolean isWhitespace() {
return character() == ' ' || character() == '\n' || character() == '\r' || character() == '\t';
}
private Object readSingleValue() throws IOException {
skipWhitespace();
Object result = readValue();
if (!(result instanceof Double))
read();
skipWhitespace();
if (character() != -1)
throw new JsonParseException("Found trailing non-whitespace characters");
return result;
}
private Object readValue() throws IOException {
assertNoEOF("a value");
int character = character();
switch (character) {
case '{':
return readObject();
case '[':
return readArray();
case '"':
return readString();
case 't':
case 'f':
// probably boolean
Boolean bool = readBoolean();
if (bool != null)
return bool;
break;
case 'n':
// probably null
if (readNull())
return null;
break;
}
if (character == '-' || isDigit())
// probably a number
return readNumber();
throw new JsonParseException("Expected a JSON value but got '" + (char) character + "'");
}
private Map<String, Object> readObject() throws IOException {
assertCharacter('{');
Map<String, Object> obj = new HashMap<>();
boolean comma = false;
read();
skipWhitespace();
while (character() != '}') {
if (comma) {
assertCharacter(',');
read();
skipWhitespace();
}
String key = readString();
read();
skipWhitespace();
assertCharacter(':');
read();
skipWhitespace();
Object value = readValue();
obj.put(key, value);
if (!(value instanceof Double))
read();
skipWhitespace();
comma = true;
}
return obj;
}
private List<Object> readArray() throws IOException {
assertCharacter('[');
List<Object> array = new ArrayList<>();
boolean comma = false;
read();
skipWhitespace();
while (character() != ']') {
if (comma) {
assertCharacter(',');
read();
skipWhitespace();
}
Object value = readValue();
array.add(value);
if (!(value instanceof Double))
read();
skipWhitespace();
comma = true;
}
return array;
}
private String readString() throws IOException {
assertCharacter('"');
StringBuilder result = new StringBuilder();
while (read() != '"') {
int character = character();
if (character >= '\u0000' && character <= '\u001F')
throw new JsonParseException("Found unescaped control character within string");
switch (character) {
case -1:
throw new JsonParseException("Expected '\"' but got EOF");
case 0x7F:
if (read() == '"') {
return result.toString();
}
continue;
case '\\':
int seq = read();
switch (seq) {
case -1:
throw new JsonParseException("Expected an escape sequence but got EOF");
case '\\':
break;
case '/':
case '\"':
character = seq;
break;
case 'b':
character = '\b';
break;
case 'f':
character = '\f';
break;
case 'n':
character = '\n';
break;
case 'r':
character = '\r';
break;
case 't':
character = '\t';
break;
case 'u':
// char array to allow allocation in advance.
char[] digits = new char[4];
for (int index = 0; index < digits.length; index++) {
character = read();
if (index == 0 && character() == '-') {
throw new JsonParseException("Hex sequence may not be negative");
} else if (character() == -1) {
throw new JsonParseException("Expected a hex sequence but got EOF");
}
digits[index] = (char) character;
}
String digitsString = new String(digits);
try {
character = Integer.parseInt(digitsString, 16);
} catch (NumberFormatException e) {
throw new JsonParseException("Could not parse hex sequence \"" + digitsString + "\"");
}
break;
default:
throw new JsonParseException("Invalid escape sequence: \\" + (char) seq);
}
break;
}
result.append((char) character);
}
return result.toString();
}
private boolean isDigit() {
return character() >= '0' && character() <= '9';
}
private Double readNumber() throws IOException {
StringBuilder result = new StringBuilder();
if (character() == '-') {
result.append((char) character());
read();
}
if (character() == '0') {
result.append((char) character());
read();
if (isDigit())
throw new JsonParseException("Found superfluous leading zero");
} else if (!isDigit())
throw new JsonParseException("Expected digits");
while (character() != -1 && isDigit()) {
result.append((char) character());
read();
}
if (character() == '.') {
result.append('.');
read();
assertNoEOF("digits");
if (!isDigit())
throw new JsonParseException("Expected digits after decimal point");
while (character() != -1 && isDigit()) {
result.append((char) character());
read();
}
}
if (character() == 'e' || character() == 'E') {
result.append('E');
read();
assertNoEOF("digits");
if (character() == '+' || character() == '-') {
result.append((char) character());
read();
}
if (!(character() == '+' || character() == '-' || isDigit()))
throw new JsonParseException("Expected exponent digits");
while (character() != -1 && isDigit()) {
result.append((char) character());
read();
}
}
String resultStr = result.toString();
try {
return Double.parseDouble(resultStr);
} catch (NumberFormatException e) {
throw new JsonParseException("Failed to parse number '" + resultStr + "'");
}
}
private Boolean readBoolean() throws IOException {
if (character() == 't') {
if (read() == 'r' && read() == 'u' && read() == 'e') {
return true;
}
} else if (character() == 'f' && read() == 'a' && read() == 'l' && read() == 's' && read() == 'e') {
return false;
}
return null;
}
private boolean readNull() throws IOException {
return character() == 'n' && read() == 'u' && read() == 'l' && read() == 'l';
}
}

View File

@ -0,0 +1,77 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.legacy.utils.url;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
public final class CustomUrlConnection extends HttpURLConnection {
private final InputStream in;
public CustomUrlConnection(byte[] data) {
this(new ByteArrayInputStream(data));
}
public CustomUrlConnection(InputStream in) {
super(null);
this.in = in;
}
@Override
public void connect() throws IOException {
responseCode = 200;
}
@Override
public void disconnect() {
try {
in.close();
} catch (IOException e) {
}
}
@Override
public InputStream getInputStream() throws IOException {
return in;
}
@Override
public boolean usingProxy() {
return false;
}
}

View File

@ -0,0 +1,107 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.legacy.utils.url;
import org.prismlauncher.utils.logging.Log;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
/**
* A utility class for URLs which uses reflection to access constructors for
* internal classes.
*/
public final class UrlUtils {
private static URLStreamHandler http;
private static MethodHandle openConnection;
static {
try {
// we first obtain the stock URLStreamHandler for http as we overwrite it later
Method getURLStreamHandler = URL.class.getDeclaredMethod("getURLStreamHandler", String.class);
getURLStreamHandler.setAccessible(true);
http = (URLStreamHandler) getURLStreamHandler.invoke(null, "http");
// we next find the openConnection method
Method openConnectionReflect = URLStreamHandler.class.getDeclaredMethod("openConnection", URL.class, Proxy.class);
openConnectionReflect.setAccessible(true);
openConnection = MethodHandles.lookup().unreflect(openConnectionReflect);
} catch (Throwable e) {
Log.error("URL reflection failed - some features may not work", e);
}
}
/**
* Determines whether all the features of this class are available.
*
* @return <code>true</code> if all features can be used
*/
public static boolean isSupported() {
return http != null && openConnection != null;
}
public static URLConnection openConnection(URL url, Proxy proxy) throws IOException {
if (http == null)
throw new UnsupportedOperationException();
if (url.getProtocol().equals("http"))
return openConnection(http, url, proxy);
// fall back to Java's default method
// at this point, this should not cause a StackOverflowError unless we've missed
// a protocol out from the if statements
return url.openConnection();
}
public static URLConnection openConnection(URLStreamHandler handler, URL url, Proxy proxy) throws IOException {
if (openConnection == null)
throw new UnsupportedOperationException();
try {
return (URLConnection) openConnection.invokeExact(handler, url, proxy);
} catch (IOException | Error | RuntimeException e) {
throw e; // rethrow if possible
} catch (Throwable e) {
throw new AssertionError(e); // oh dear! this isn't meant to happen
}
}
}

View File

@ -57,7 +57,7 @@ package org.prismlauncher;
import org.prismlauncher.exception.ParseException;
import org.prismlauncher.launcher.Launcher;
import org.prismlauncher.launcher.impl.StandardLauncher;
import org.prismlauncher.launcher.impl.legacy.LegacyLauncher;
import org.prismlauncher.legacy.LegacyProxy;
import org.prismlauncher.utils.Parameters;
import org.prismlauncher.utils.logging.Log;
@ -106,6 +106,8 @@ public final class EntryPoint {
}
try {
LegacyProxy.applyOnlineFixes(params);
Launcher launcher;
String type = params.getString("launcher");
@ -115,7 +117,7 @@ public final class EntryPoint {
break;
case "legacy":
launcher = new LegacyLauncher(params);
launcher = LegacyProxy.createLauncher(params);
break;
default:

View File

@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 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.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.prismlauncher.legacy;
import org.prismlauncher.launcher.Launcher;
import org.prismlauncher.utils.Parameters;
// used as a fallback if NewLaunchLegacy is not on the classpath
// if it is, this class will be replaced
public final class LegacyProxy {
public static Launcher createLauncher(Parameters params) {
throw new AssertionError("NewLaunchLegacy is not loaded");
}
public static void applyOnlineFixes(Parameters params) {}
}

View File

@ -68,61 +68,6 @@ public final class ReflectionUtils {
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final ClassLoader LOADER = ClassLoader.getSystemClassLoader();
/**
* Construct a Java applet by its class name.
*
* @param clazz The class name
* @return The applet instance
* @throws Throwable
*/
public static Applet createAppletClass(String clazz) throws Throwable {
Class<?> appletClass = LOADER.loadClass(clazz);
MethodHandle appletConstructor = LOOKUP.findConstructor(appletClass, MethodType.methodType(void.class));
return (Applet) appletConstructor.invoke();
}
/**
* Best guess of the game directory field within net.minecraft.client.Minecraft.
* Designed for legacy versions - newer versions do not use a static field.
*
* @param clazz The class
* @return The first field matching criteria
*/
public static Field findMinecraftGameDirField(Class<?> clazz) {
Log.debug("Resolving minecraft game directory field");
// search for private static File
for (Field field : clazz.getDeclaredFields()) {
if (field.getType() != File.class) {
continue;
}
int fieldModifiers = field.getModifiers();
if (!Modifier.isStatic(fieldModifiers)) {
Log.debug("Rejecting field " + field.getName() + " because it is not static");
continue;
}
if (!Modifier.isPrivate(fieldModifiers)) {
Log.debug("Rejecting field " + field.getName() + " because it is not private");
continue;
}
if (Modifier.isFinal(fieldModifiers)) {
Log.debug("Rejecting field " + field.getName() + " because it is final");
continue;
}
Log.debug("Identified field " + field.getName() + " to match conditions for game directory field");
return field;
}
return null;
}
/**
* Gets the main method within a class.
*

View File

@ -350,6 +350,7 @@ Section "@Launcher_DisplayName@"
File "@Launcher_APP_BINARY_NAME@.exe"
File "@Launcher_APP_BINARY_NAME@_filelink.exe"
File "@Launcher_APP_BINARY_NAME@_updater.exe"
File "qt.conf"
File "qtlogging.ini"
File *.dll
@ -435,6 +436,7 @@ Section "Uninstall"
Delete $INSTDIR\@Launcher_APP_BINARY_NAME@.exe
Delete $INSTDIR\@Launcher_APP_BINARY_NAME@_filelink.exe
Delete $INSTDIR\@Launcher_APP_BINARY_NAME@_updater.exe
Delete $INSTDIR\qt.conf
Delete $INSTDIR\*.dll
@ -472,7 +474,6 @@ Function .onInit
${GetParameters} $R0
${GetOptions} $R0 "/NoShortcuts" $R1
${IfNot} ${Errors}
${OrIf} ${FileExists} "$InstDir\@Launcher_APP_BINARY_NAME@.exe"
!insertmacro UnselectSection ${SM_SHORTCUTS}
!insertmacro UnselectSection ${DESKTOP_SHORTCUTS}
${EndIf}