This commit is contained in:
Trial97 2023-05-31 20:12:12 +03:00
commit 29c3dc40ef
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
443 changed files with 16655 additions and 15840 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

View File

@ -15,6 +15,12 @@ on:
SPARKLE_ED25519_KEY: SPARKLE_ED25519_KEY:
description: Private key for signing Sparkle updates description: Private key for signing Sparkle updates
required: false required: false
WINDOWS_CODESIGN_CERT:
description: Certificate for signing Windows builds
required: false
WINDOWS_CODESIGN_PASSWORD:
description: Password for signing Windows builds
required: false
CACHIX_AUTH_TOKEN: CACHIX_AUTH_TOKEN:
description: Private token for authenticating against Cachix cache description: Private token for authenticating against Cachix cache
required: false required: false
@ -40,6 +46,7 @@ jobs:
- os: windows-2022 - os: windows-2022
name: "Windows-MinGW-w64" name: "Windows-MinGW-w64"
msystem: clang64 msystem: clang64
vcvars_arch: 'amd64_x86'
- os: windows-2022 - os: windows-2022
name: "Windows-MSVC-Legacy" name: "Windows-MSVC-Legacy"
@ -61,7 +68,7 @@ jobs:
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: windows
qt_arch: '' qt_arch: ''
qt_version: '6.4.0' qt_version: '6.5.1'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats'
qt_tools: '' qt_tools: ''
@ -73,7 +80,7 @@ jobs:
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: windows
qt_arch: 'win64_msvc2019_arm64' qt_arch: 'win64_msvc2019_arm64'
qt_version: '6.4.0' qt_version: '6.5.1'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats'
qt_tools: '' qt_tools: ''
@ -83,7 +90,7 @@ jobs:
qt_ver: 6 qt_ver: 6
qt_host: mac qt_host: mac
qt_arch: '' qt_arch: ''
qt_version: '6.3.0' qt_version: '6.5.1'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats'
qt_tools: '' qt_tools: ''
@ -105,6 +112,7 @@ jobs:
INSTALL_APPIMAGE_DIR: "install-appdir" INSTALL_APPIMAGE_DIR: "install-appdir"
BUILD_DIR: "build" BUILD_DIR: "build"
CCACHE_VAR: "" CCACHE_VAR: ""
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
steps: steps:
## ##
@ -135,6 +143,7 @@ jobs:
quazip-qt6:p quazip-qt6:p
ccache:p ccache:p
qt6-5compat:p qt6-5compat:p
cmark:p
- name: Force newer ccache - name: Force newer ccache
if: runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug' if: runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug'
@ -143,10 +152,19 @@ jobs:
- name: Setup ccache - name: Setup ccache
if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
uses: hendrikmuhs/ccache-action@v1.2.5 uses: hendrikmuhs/ccache-action@v1.2.9
with: with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
- name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v3.3.1
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
restore-keys: |
${{ matrix.os }}-mingw-w64-ccache
- name: Setup ccache (Windows MinGW-w64) - name: Setup ccache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
shell: msys2 {0} shell: msys2 {0}
@ -163,15 +181,6 @@ jobs:
run: | run: |
echo "CCACHE_VAR=ccache" >> $GITHUB_ENV echo "CCACHE_VAR=ccache" >> $GITHUB_ENV
- name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v3.2.1
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64
restore-keys: |
${{ matrix.os }}-mingw-w64
- name: Set short version - name: Set short version
shell: bash shell: bash
run: | run: |
@ -199,6 +208,8 @@ jobs:
if: runner.os == 'Windows' && matrix.architecture == 'arm64' if: runner.os == 'Windows' && matrix.architecture == 'arm64'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v3
with: with:
aqtversion: '==3.1.*'
py7zrversion: '>=0.20.2'
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
host: 'windows' host: 'windows'
target: 'desktop' target: 'desktop'
@ -214,6 +225,8 @@ jobs:
if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '') if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '')
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v3
with: with:
aqtversion: '==3.1.*'
py7zrversion: '>=0.20.2'
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }} host: ${{ matrix.qt_host }}
target: 'desktop' target: 'desktop'
@ -223,7 +236,7 @@ jobs:
cache: ${{ inputs.is_qt_cached }} cache: ${{ inputs.is_qt_cached }}
- name: Install MSVC (Windows MSVC) - name: Install MSVC (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == '' if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool
uses: ilammy/msvc-dev-cmd@v1 uses: ilammy/msvc-dev-cmd@v1
with: with:
vsversion: 2022 vsversion: 2022
@ -375,6 +388,23 @@ jobs:
Copy-Item D:/a/PrismLauncher/Qt/Tools/OpenSSL/Win_x86/bin/libssl-1_1.dll -Destination libssl-1_1.dll Copy-Item D:/a/PrismLauncher/Qt/Tools/OpenSSL/Win_x86/bin/libssl-1_1.dll -Destination libssl-1_1.dll
} }
- name: Fetch codesign certificate (Windows)
if: runner.os == 'Windows'
shell: bash # yes, we are not using MSYS2 or PowerShell here
run: |
echo '${{ secrets.WINDOWS_CODESIGN_CERT }}' | base64 --decode > codesign.pfx
- name: Sign executable (Windows)
if: runner.os == 'Windows'
run: |
if (Get-Content ./codesign.pfx){
cd ${{ env.INSTALL_DIR }}
# We ship the exact same executable for portable and non-portable editions, so signing just once is fine
SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_filelink.exe
} else {
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
}
- name: Package (Windows MinGW-w64, portable) - name: Package (Windows MinGW-w64, portable)
if: runner.os == 'Windows' && matrix.msystem != '' if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0} shell: msys2 {0}
@ -394,6 +424,15 @@ jobs:
cd ${{ env.INSTALL_DIR }} cd ${{ env.INSTALL_DIR }}
makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi"
- name: Sign installer (Windows)
if: runner.os == 'Windows'
run: |
if (Get-Content ./codesign.pfx){
SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe
} else {
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
}
- name: Package (Linux) - name: Package (Linux)
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: | run: |
@ -508,30 +547,12 @@ jobs:
with: with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
snap:
runs-on: ubuntu-20.04 - name: ccache stats (Windows MinGW-w64)
steps: if: runner.os == 'Windows' && matrix.msystem != ''
- name: Checkout shell: msys2 {0}
if: inputs.build_type == 'Debug'
uses: actions/checkout@v3
with:
submodules: 'true'
- name: Set short version
shell: bash
if: inputs.build_type == 'Debug'
run: | run: |
ver_short=`git rev-parse --short HEAD` ccache -s
echo "VERSION=$ver_short" >> $GITHUB_ENV
- name: Package Snap (Linux)
id: snapcraft
if: inputs.build_type == 'Debug'
uses: snapcore/action-build@v1
- name: Upload Snap (Linux)
if: inputs.build_type == 'Debug'
uses: actions/upload-artifact@v3
with:
name: prismlauncher_${{ env.VERSION }}_amd64.snap
path: ${{ steps.snapcraft.outputs.snap }}
flatpak: flatpak:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -546,11 +567,10 @@ jobs:
submodules: 'true' submodules: 'true'
- name: Build Flatpak (Linux) - name: Build Flatpak (Linux)
if: inputs.build_type == 'Debug' if: inputs.build_type == 'Debug'
uses: flatpak/flatpak-github-actions/flatpak-builder@v4 uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with: with:
bundle: "Prism Launcher.flatpak" bundle: "Prism Launcher.flatpak"
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
cache-key: flatpak-${{ github.sha }}-x86_64
nix: nix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -567,7 +587,7 @@ jobs:
submodules: 'true' submodules: 'true'
- name: Install nix - name: Install nix
if: inputs.build_type == 'Debug' if: inputs.build_type == 'Debug'
uses: cachix/install-nix-action@v18 uses: cachix/install-nix-action@v21
with: with:
install_url: https://nixos.org/nix/install install_url: https://nixos.org/nix/install
extra_nix_config: | extra_nix_config: |

View File

@ -31,4 +31,6 @@ jobs:
is_qt_cached: true is_qt_cached: true
secrets: secrets:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}

View File

@ -15,6 +15,9 @@ jobs:
is_qt_cached: false is_qt_cached: false
secrets: secrets:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
create_release: create_release:
needs: build_release needs: build_release

View File

@ -7,7 +7,7 @@ jobs:
publish: publish:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: vedantmgoyal2009/winget-releaser@v1 - uses: vedantmgoyal2009/winget-releaser@v2
with: with:
identifier: PrismLauncher.PrismLauncher identifier: PrismLauncher.PrismLauncher
version: ${{ github.event.release.tag_name }} version: ${{ github.event.release.tag_name }}

8
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

@ -16,3 +16,6 @@
[submodule "libraries/extra-cmake-modules"] [submodule "libraries/extra-cmake-modules"]
path = libraries/extra-cmake-modules path = libraries/extra-cmake-modules
url = https://github.com/KDE/extra-cmake-modules url = https://github.com/KDE/extra-cmake-modules
[submodule "libraries/cmark"]
path = libraries/cmark
url = https://github.com/commonmark/cmark.git

View File

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

View File

@ -164,17 +164,17 @@ set(Launcher_BUG_TRACKER_URL "https://github.com/PrismLauncher/PrismLauncher/iss
set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/prismlauncher/launcher/" CACHE STRING "URL for the translations platform.") set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/prismlauncher/launcher/" CACHE STRING "URL for the translations platform.")
# Matrix Space # Matrix Space
set(Launcher_MATRIX_URL "https://matrix.to/#/#prismlauncher:matrix.org" CACHE STRING "URL to the Matrix Space") set(Launcher_MATRIX_URL "https://prismlauncher.org/matrix" CACHE STRING "URL to the Matrix Space")
# Discord URL # Discord URL
set(Launcher_DISCORD_URL "https://discord.gg/prismlauncher" CACHE STRING "URL for the Discord guild.") set(Launcher_DISCORD_URL "https://prismlauncher.org/discord" CACHE STRING "URL for the Discord guild.")
# Subreddit URL # Subreddit URL
set(Launcher_SUBREDDIT_URL "https://www.reddit.com/r/PrismLauncher/" CACHE STRING "URL for the subreddit.") set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL for the subreddit.")
# Builds # Builds
set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules") set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules")
set(Launcher_QT_VERSION_MAJOR "5" CACHE STRING "Major Qt version to build against") set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against")
# API Keys # API Keys
# NOTE: These API keys are here for convenience. If you rebrand this software or intend to break the terms of service # NOTE: These API keys are here for convenience. If you rebrand this software or intend to break the terms of service
@ -208,9 +208,15 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}")
################################ 3rd Party Libs ################################ ################################ 3rd Party Libs ################################
if(NOT Launcher_FORCE_BUNDLED_LIBS) # Successive configurations of cmake without cleaning the build dir will cause zlib fallback to fail due to cached values
# Record when fallback triggered and skip this find_package
if(NOT Launcher_FORCE_BUNDLED_LIBS AND NOT FORCE_BUNDLED_ZLIB)
find_package(ZLIB QUIET) find_package(ZLIB QUIET)
endif() endif()
if(NOT ZLIB_FOUND)
set(FORCE_BUNDLED_ZLIB TRUE CACHE BOOL "")
mark_as_advanced(FORCE_BUNDLED_ZLIB)
endif()
# Find the required Qt parts # Find the required Qt parts
include(QtVersionlessBackport) include(QtVersionlessBackport)
@ -266,8 +272,13 @@ if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find ghc_filesystem # Find ghc_filesystem
find_package(ghc_filesystem QUIET) find_package(ghc_filesystem QUIET)
# Find cmark
find_package(cmark QUIET)
endif() endif()
include(ECMQtDeclareLoggingCategory)
####################################### Program Info ####################################### ####################################### Program Info #######################################
set(Launcher_APP_BINARY_NAME "prismlauncher" CACHE STRING "Name of the Launcher binary") set(Launcher_APP_BINARY_NAME "prismlauncher" CACHE STRING "Name of the Launcher binary")
@ -334,6 +345,8 @@ elseif(UNIX)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR})
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "${KDE_INSTALL_DATADIR}/${Launcher_Name}")
if(Launcher_ManPage) if(Launcher_ManPage)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6")
endif() endif()
@ -361,6 +374,8 @@ else()
message(FATAL_ERROR "Platform not supported") message(FATAL_ERROR "Platform not supported")
endif() endif()
################################ Included Libs ################################ ################################ Included Libs ################################
include(ExternalProject) include(ExternalProject)
@ -372,16 +387,26 @@ option(NBT_BUILD_TESTS "Build NBT library tests" OFF) #FIXME: fix unit tests.
add_subdirectory(libraries/libnbtplusplus) add_subdirectory(libraries/libnbtplusplus)
add_subdirectory(libraries/systeminfo) # system information library add_subdirectory(libraries/systeminfo) # system information library
add_subdirectory(libraries/hoedown) # markdown parser
add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker add_subdirectory(libraries/javacheck) # java compatibility checker
if(NOT ZLIB_FOUND) if(FORCE_BUNDLED_ZLIB)
message(STATUS "Using bundled zlib") message(STATUS "Using bundled zlib")
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) # Suppress cmake warnings and allow INTERPROCEDURAL_OPTIMIZATION for zlib set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) # Suppress cmake warnings and allow INTERPROCEDURAL_OPTIMIZATION for zlib
set(SKIP_INSTALL_ALL ON) set(SKIP_INSTALL_ALL ON)
add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL) add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL)
set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" CACHE STRING "") # On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not.
# We cannot safely undo the rename on those systems, and they generally have packages for zlib anyway.
check_include_file(unistd.h NEED_GENERATED_ZCONF)
if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" AND NOT NEED_GENERATED_ZCONF)
# zlib's cmake script renames a file, dirtying the submodule, see https://github.com/madler/zlib/issues/162
message(STATUS "Undoing Rename")
message(STATUS " ${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h")
file(RENAME "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h")
endif()
set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" CACHE STRING "" FORCE)
set_target_properties(zlibstatic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}") set_target_properties(zlibstatic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}")
add_library(ZLIB::ZLIB ALIAS zlibstatic) add_library(ZLIB::ZLIB ALIAS zlibstatic)
set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name") set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name")
@ -406,17 +431,26 @@ if(NOT tomlplusplus_FOUND)
else() else()
message(STATUS "Using system tomlplusplus") message(STATUS "Using system tomlplusplus")
endif() endif()
if(NOT cmark_FOUND)
message(STATUS "Using bundled cmark")
set(CMARK_STATIC ON CACHE BOOL "Build static libcmark library" FORCE)
set(CMARK_SHARED OFF CACHE BOOL "Build shared libcmark library" FORCE)
set(CMARK_TESTS OFF CACHE BOOL "Build cmark tests and enable testing" FORCE)
add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser
add_library(cmark::cmark ALIAS cmark_static)
else()
message(STATUS "Using system cmark")
endif()
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
add_subdirectory(libraries/gamemode) add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
if (NOT ghc_filesystem_FOUND) if (NOT ghc_filesystem_FOUND)
message(STATUS "Using bundled ghc_filesystem") message(STATUS "Using bundled ghc_filesystem")
set(GHC_FILESYSTEM_WITH_INSTALL OFF) # Workaround ghc::filesystem bug
add_subdirectory(libraries/filesystem) # Implementation of std::filesystem for old C++, for usage in old macOS add_subdirectory(libraries/filesystem) # Implementation of std::filesystem for old C++, for usage in old macOS
add_library(ghcFilesystem::ghc_filesystem ALIAS ghc_filesystem)
else() else()
message(STATUS "Using system ghc_filesystem") message(STATUS "Using system ghc_filesystem")
endif() endif()
add_subdirectory(libraries/qdcss) # css parser
############################### Built Artifacts ############################### ############################### Built Artifacts ###############################

View File

@ -1,7 +1,7 @@
## Prism Launcher ## Prism Launcher
Prism Launcher - Minecraft Launcher Prism Launcher - Minecraft Launcher
Copyright (C) 2022 Prism Launcher Contributors Copyright (C) 2022-2023 Prism Launcher Contributors
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -156,23 +156,34 @@
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA. Boston, MA 02110-1301, USA.
## Hoedown ## cmark
Copyright (c) 2008, Natacha Porté Copyright (c) 2014, John MacFarlane
Copyright (c) 2011, Vicent Martí
Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors
Permission to use, copy, modify, and distribute this software for any All rights reserved.
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES Redistribution and use in source and binary forms, with or without
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF modification, are permitted provided that the following conditions are met:
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * Redistributions of source code must retain the above copyright
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN notice, this list of conditions and the following disclaimer.
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Batch icon set ## Batch icon set

View File

@ -28,7 +28,7 @@ Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS*
For **Arch**, **Debian**, **Fedora**, **OpenSUSE (Tumbleweed)** and **Gentoo**, respectively, you can use these packages for the latest development versions: For **Arch**, **Debian**, **Fedora**, **OpenSUSE (Tumbleweed)** and **Gentoo**, respectively, you can use these packages for the latest development versions:
[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-A80030?label=MPR&logo=debian&logoColor=white)](https://mpr.makedeb.org/packages/prismlauncher-git)<br />[![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-51A2DA?label=CORP&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-3AB6A9?logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-4D4270?label=Gentoo&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-A80030?label=MPR&logo=debian&logoColor=white)](https://mpr.makedeb.org/packages/prismlauncher-git)<br />[![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-51A2DA?label=COPR&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-3AB6A9?logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-4D4270?label=Gentoo&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher)
These packages are also availiable to all the distributions based on the ones mentioned above. These packages are also availiable to all the distributions based on the ones mentioned above.
@ -38,15 +38,15 @@ Feel free to create a GitHub issue if you find a bug or want to suggest a new fe
- **Our Discord server:** - **Our Discord server:**
[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://discord.gg/prismlauncher) [![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://prismlauncher.org/discord)
- **Our Matrix space:** - **Our Matrix space:**
[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&label=Matrix%20Space&logo=matrix&color=purple)](https://matrix.to/#/#prismlauncher:matrix.org) [![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&label=Matrix%20Space&logo=matrix&color=purple)](https://prismlauncher.org/matrix)
- **Our Subreddit:** - **Our Subreddit:**
[![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://www.reddit.com/r/PrismLauncher/) [![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://prismlauncher.org/reddit)
## Translations ## Translations
@ -96,7 +96,7 @@ Be aware that if you build this software without removing the provided API keys
If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file by setting them to an empty string (`""`). If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file by setting them to an empty string (`""`).
## License ![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?label=License&logo=gnu&color=C4282D) ## License [![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?label=License&logo=gnu&color=C4282D)](LICENSE)
All launcher code is available under the GPL-3.0-only license. All launcher code is available under the GPL-3.0-only license.

View File

@ -3,11 +3,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1668681692, "lastModified": 1673956053,
"narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=", "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "009399224d5e398d03b22badca40a37ac85412a1", "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -16,6 +16,58 @@
"type": "github" "type": "github"
} }
}, },
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"libnbtplusplus": { "libnbtplusplus": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -34,11 +86,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1671417167, "lastModified": 1678693419,
"narHash": "sha256-JkHam6WQOwZN1t2C2sbp1TqMv3TVRjzrdoejqfefwrM=", "narHash": "sha256-bbSv5yqZAW6dz+3f3f3pOUZbxpPN+3OgCljgn7P+nnQ=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "bb31220cca6d044baa6dc2715b07497a2a7c4bc7", "rev": "8e3fad82be64c06fbfb9fd43993aec9ef4623936",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -48,11 +100,55 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-stable": {
"locked": {
"lastModified": 1673800717,
"narHash": "sha256-SFHraUqLSu5cC6IxTprex/nTsI81ZQAtDvlBvGDWfnA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2f9fd351ec37f5d479556cd48be4ca340da59b8f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.11",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat_2",
"flake-utils": [
"flake-utils"
],
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1678376203,
"narHash": "sha256-3tyYGyC8h7fBwncLZy5nCUjTJPrHbmNwp47LlNLOHSM=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "1a20b9708962096ec2481eeb2ddca29ed747770a",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"libnbtplusplus": "libnbtplusplus", "libnbtplusplus": "libnbtplusplus",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
} }
} }
}, },

102
flake.nix
View File

@ -3,35 +3,89 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; flake-utils.url = "github:numtide/flake-utils";
libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; flake = false; }; pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
};
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
};
libnbtplusplus = {
url = "github:PrismLauncher/libnbtplusplus";
flake = false;
};
}; };
outputs = { self, nixpkgs, libnbtplusplus, ... }: outputs = {
let self,
# User-friendly version number. nixpkgs,
version = builtins.substring 0 8 self.lastModifiedDate; flake-utils,
pre-commit-hooks,
libnbtplusplus,
...
}: let
# User-friendly version number.
version = builtins.substring 0 8 self.lastModifiedDate;
# Supported systems (qtbase is currently broken for "aarch64-darwin") # Supported systems (qtbase is currently broken for "aarch64-darwin")
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" ]; supportedSystems = with flake-utils.lib.system; [
x86_64-linux
x86_64-darwin
aarch64-linux
];
# Helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'. packagesFn = pkgs: {
forAllSystems = nixpkgs.lib.genAttrs supportedSystems; prismlauncher-qt5 = pkgs.libsForQt5.callPackage ./nix {
inherit version self libnbtplusplus;
# Nixpkgs instantiated for supported systems.
pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system});
packagesFn = pkgs: rec {
prismlauncher-qt5 = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; };
prismlauncher = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus; };
}; };
in prismlauncher = pkgs.qt6Packages.callPackage ./nix {
{ inherit version self libnbtplusplus;
packages = forAllSystems (system: };
let packages = packagesFn pkgs.${system}; in };
packages // { default = packages.prismlauncher; } in
); flake-utils.lib.eachSystem supportedSystems (system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
checks = {
pre-commit-check = pre-commit-hooks.lib.${system}.run {
src = ./.;
hooks = {
markdownlint.enable = true;
overlay = final: packagesFn; alejandra.enable = true;
deadnix.enable = true;
clang-format = {
enable =
false; # As most of the codebase is **not** formatted, we don't want clang-format yet
types_or = ["c" "c++"];
};
};
};
};
packages = let
packages = packagesFn pkgs;
in
packages // {default = packages.prismlauncher;};
devShells.default = pkgs.mkShell {
inherit (self.checks.${system}.pre-commit-check) shellHook;
packages = with pkgs; [
nodePackages.markdownlint-cli
alejandra
deadnix
clang-tools
];
inputsFrom = [self.packages.${system}.default];
buildInputs = with pkgs; [ccache ninja];
};
})
// {
overlays.default = final: _: (packagesFn final);
}; };
} }

View File

@ -32,6 +32,7 @@ modules:
config-opts: config-opts:
- -DLauncher_BUILD_PLATFORM=flatpak - -DLauncher_BUILD_PLATFORM=flatpak
- -DCMAKE_BUILD_TYPE=Debug - -DCMAKE_BUILD_TYPE=Debug
- -DLauncher_QT_VERSION_MAJOR=5
build-options: build-options:
env: env:
JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17 JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17
@ -39,6 +40,7 @@ modules:
sources: sources:
- type: dir - type: dir
path: ../ path: ../
builddir: true
- name: openjdk - name: openjdk
buildsystem: simple buildsystem: simple
build-commands: build-commands:

View File

@ -7,6 +7,8 @@
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Lenny McLennington <lenny@sneed.church> * Copyright (C) 2022 Lenny McLennington <lenny@sneed.church>
* Copyright (C) 2022 Tayou <tayou@gmx.net> * Copyright (C) 2022 Tayou <tayou@gmx.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -45,6 +47,7 @@
#include "net/PasteUpload.h" #include "net/PasteUpload.h"
#include "pathmatcher/MultiMatcher.h" #include "pathmatcher/MultiMatcher.h"
#include "pathmatcher/SimplePrefixMatcher.h" #include "pathmatcher/SimplePrefixMatcher.h"
#include "settings/INIFile.h"
#include "ui/MainWindow.h" #include "ui/MainWindow.h"
#include "ui/InstanceWindow.h" #include "ui/InstanceWindow.h"
@ -62,15 +65,11 @@
#include "ui/pages/global/APIPage.h" #include "ui/pages/global/APIPage.h"
#include "ui/pages/global/CustomCommandsPage.h" #include "ui/pages/global/CustomCommandsPage.h"
#ifdef Q_OS_WIN
#include "ui/WinDarkmode.h"
#include <versionhelpers.h>
#endif
#include "ui/setupwizard/SetupWizard.h" #include "ui/setupwizard/SetupWizard.h"
#include "ui/setupwizard/LanguageWizardPage.h" #include "ui/setupwizard/LanguageWizardPage.h"
#include "ui/setupwizard/JavaWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h"
#include "ui/setupwizard/PasteWizardPage.h" #include "ui/setupwizard/PasteWizardPage.h"
#include "ui/setupwizard/ThemeWizardPage.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
@ -81,7 +80,9 @@
#include "ApplicationMessage.h" #include "ApplicationMessage.h"
#include <iostream> #include <iostream>
#include <mutex>
#include <QFileOpenEvent>
#include <QAccessible> #include <QAccessible>
#include <QCommandLineParser> #include <QCommandLineParser>
#include <QDir> #include <QDir>
@ -105,7 +106,7 @@
#include "java/JavaUtils.h" #include "java/JavaUtils.h"
#include "updater/UpdateChecker.h" #include "updater/ExternalUpdater.h"
#include "tools/JProfiler.h" #include "tools/JProfiler.h"
#include "tools/JVisualVM.h" #include "tools/JVisualVM.h"
@ -129,6 +130,10 @@
#include "MangoHud.h" #include "MangoHud.h"
#endif #endif
#ifdef Q_OS_MAC
#include "updater/MacSparkleUpdater.h"
#endif
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
@ -150,6 +155,9 @@ namespace {
/** This is used so that we can output to the log file in addition to the CLI. */ /** This is used so that we can output to the log file in addition to the CLI. */
void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{ {
static std::mutex loggerMutex;
const std::lock_guard<std::mutex> lock(loggerMutex); // synchronized, QFile logFile is not thread-safe
QString out = qFormatLogMessage(type, context, msg); QString out = qFormatLogMessage(type, context, msg);
out += QChar::LineFeed; out += QChar::LineFeed;
@ -159,45 +167,6 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt
fflush(stderr); fflush(stderr);
} }
QString getIdealPlatform(QString currentPlatform) {
auto info = Sys::getKernelInfo();
switch(info.kernelType) {
case Sys::KernelType::Darwin: {
if(info.kernelMajor >= 17) {
// macOS 10.13 or newer
return "osx64-5.15.2";
}
else {
// macOS 10.12 or older
return "osx64";
}
}
case Sys::KernelType::Windows: {
// FIXME: 5.15.2 is not stable on Windows, due to a large number of completely unpredictable and hard to reproduce issues
break;
/*
if(info.kernelMajor == 6 && info.kernelMinor >= 1) {
// Windows 7
return "win32-5.15.2";
}
else if (info.kernelMajor > 6) {
// Above Windows 7
return "win32-5.15.2";
}
else {
// Below Windows 7
return "win32";
}
*/
}
case Sys::KernelType::Undetermined:
case Sys::KernelType::Linux: {
break;
}
}
return currentPlatform;
}
} }
Application::Application(int &argc, char **argv) : QApplication(argc, argv) Application::Application(int &argc, char **argv) : QApplication(argc, argv)
@ -259,9 +228,19 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_serverToJoin = parser.value("server"); m_serverToJoin = parser.value("server");
m_profileToUse = parser.value("profile"); m_profileToUse = parser.value("profile");
m_liveCheck = parser.isSet("alive"); m_liveCheck = parser.isSet("alive");
m_zipToImport = parser.value("import");
m_instanceIdToShowWindowOf = parser.value("show"); m_instanceIdToShowWindowOf = parser.value("show");
for (auto zip_path : parser.values("import")){
m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath()));
}
// treat unspecified positional arguments as import urls
for (auto zip_path : parser.positionalArguments()) {
m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath()));
}
// error if --launch is missing with --server or --profile // error if --launch is missing with --server or --profile
if((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) if((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty())
{ {
@ -309,6 +288,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
dataPath = m_rootPath; dataPath = m_rootPath;
adjustedBy = "Portable data path"; adjustedBy = "Portable data path";
m_portable = true;
} }
#endif #endif
} }
@ -345,7 +325,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
} }
/* /*
* Establish the mechanism for communication with an already running PolyMC that uses the same data path. * Establish the mechanism for communication with an already running PrismLauncher that uses the same data path.
* If there is one, tell it what the user actually wanted to do and exit. * If there is one, tell it what the user actually wanted to do and exit.
* We want to initialize this before logging to avoid messing with the log of a potential already running copy. * We want to initialize this before logging to avoid messing with the log of a potential already running copy.
*/ */
@ -363,12 +343,14 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
activate.command = "activate"; activate.command = "activate";
m_peerInstance->sendMessage(activate.serialize(), timeout); m_peerInstance->sendMessage(activate.serialize(), timeout);
if(!m_zipToImport.isEmpty()) if(!m_zipsToImport.isEmpty())
{ {
ApplicationMessage import; for (auto zip_url : m_zipsToImport) {
import.command = "import"; ApplicationMessage import;
import.args.insert("path", m_zipToImport.toString()); import.command = "import";
m_peerInstance->sendMessage(import.serialize(), timeout); import.args.insert("path", zip_url.toString());
m_peerInstance->sendMessage(import.serialize(), timeout);
}
} }
} }
else else
@ -435,6 +417,47 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
" " "|" " " " " "|" " "
"%{if-category}[%{category}]: %{endif}" "%{if-category}[%{category}]: %{endif}"
"%{message}"); "%{message}");
bool foundLoggingRules = false;
auto logRulesFile = QStringLiteral("qtlogging.ini");
auto logRulesPath = FS::PathCombine(dataPath, logRulesFile);
qDebug() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath);
// search the dataPath()
// seach app data standard path
if(!foundLoggingRules && !isPortable() && dirParam.isEmpty()) {
logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile));
if(!logRulesPath.isEmpty()) {
qDebug() << "Found" << logRulesPath << "...";
foundLoggingRules = true;
}
}
// seach root path
if(!foundLoggingRules) {
logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
qDebug() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath);
}
if(foundLoggingRules) {
// load and set logging rules
qDebug() << "Loading logging rules from:" << logRulesPath;
QSettings loggingRules(logRulesPath, QSettings::IniFormat);
loggingRules.beginGroup("Rules");
QStringList rule_names = loggingRules.childKeys();
QStringList rules;
qDebug() << "Setting log rules:";
for (auto rule_name : rule_names) {
auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString());
rules.append(rule);
qDebug() << " " << rule;
}
auto rules_str = rules.join("\n");
QLoggingCategory::setFilterRules(rules_str);
}
qDebug() << "<> Log initialized."; qDebug() << "<> Log initialized.";
} }
@ -499,14 +522,10 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{ {
// Provide a fallback for migration from PolyMC // Provide a fallback for migration from PolyMC
m_settings.reset(new INISettingsObject({ BuildConfig.LAUNCHER_CONFIGFILE, "polymc.cfg", "multimc.cfg" }, this)); m_settings.reset(new INISettingsObject({ BuildConfig.LAUNCHER_CONFIGFILE, "polymc.cfg", "multimc.cfg" }, this));
// Updates
// Multiple channels are separated by spaces
m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL);
m_settings->registerSetting("AutoUpdate", true);
// Theming // Theming
m_settings->registerSetting("IconTheme", QString("pe_colored")); m_settings->registerSetting("IconTheme", QString("pe_colored"));
m_settings->registerSetting("ApplicationTheme", QString("system")); m_settings->registerSetting("ApplicationTheme", QString());
m_settings->registerSetting("BackgroundCat", QString("kitteh")); m_settings->registerSetting("BackgroundCat", QString("kitteh"));
// Remembered state // Remembered state
@ -545,6 +564,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("InstanceDir", "instances"); m_settings->registerSetting("InstanceDir", "instances");
m_settings->registerSetting({"CentralModsDir", "ModsDir"}, "mods"); m_settings->registerSetting({"CentralModsDir", "ModsDir"}, "mods");
m_settings->registerSetting("IconsDir", "icons"); m_settings->registerSetting("IconsDir", "icons");
m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
m_settings->registerSetting("DownloadsDirWatchRecursive", false);
// Editors // Editors
m_settings->registerSetting("JsonEditor", QString()); m_settings->registerSetting("JsonEditor", QString());
@ -641,6 +662,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("UpdateDialogGeometry", ""); m_settings->registerSetting("UpdateDialogGeometry", "");
m_settings->registerSetting("ModDownloadGeometry", ""); m_settings->registerSetting("ModDownloadGeometry", "");
m_settings->registerSetting("RPDownloadGeometry", "");
m_settings->registerSetting("TPDownloadGeometry", "");
m_settings->registerSetting("ShaderDownloadGeometry", "");
// HACK: This code feels so stupid is there a less stupid way of doing this? // HACK: This code feels so stupid is there a less stupid way of doing this?
{ {
@ -687,6 +711,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->set("FlameKeyOverride", flameKey); m_settings->set("FlameKeyOverride", flameKey);
m_settings->reset("CFKeyOverride"); m_settings->reset("CFKeyOverride");
} }
m_settings->registerSetting("ModrinthToken", "");
m_settings->registerSetting("UserAgentOverride", ""); m_settings->registerSetting("UserAgentOverride", "");
// Init page provider // Init page provider
@ -714,7 +739,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// initialize network access and proxy setup // initialize network access and proxy setup
{ {
m_network = new QNetworkAccessManager(); m_network.reset(new QNetworkAccessManager());
QString proxyTypeStr = settings()->get("ProxyType").toString(); QString proxyTypeStr = settings()->get("ProxyType").toString();
QString addr = settings()->get("ProxyAddr").toString(); QString addr = settings()->get("ProxyAddr").toString();
int port = settings()->get("ProxyPort").value<qint16>(); int port = settings()->get("ProxyPort").value<qint16>();
@ -736,10 +761,10 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// initialize the updater // initialize the updater
if(BuildConfig.UPDATER_ENABLED) if(BuildConfig.UPDATER_ENABLED)
{ {
auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM); qDebug() << "Initializing updater";
auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json"; #ifdef Q_OS_MAC
qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl; m_updater.reset(new MacSparkleUpdater());
m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL)); #endif
qDebug() << "<> Updater started."; qDebug() << "<> Updater started.";
} }
@ -854,12 +879,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
} }
}); });
{ applyCurrentlySelectedTheme(true);
setIconTheme(settings()->get("IconTheme").toString());
qDebug() << "<> Icon theme set.";
setApplicationTheme(settings()->get("ApplicationTheme").toString(), true);
qDebug() << "<> Application theme set.";
}
updateCapabilities(); updateCapabilities();
@ -901,7 +921,8 @@ bool Application::createSetupWizard()
return false; return false;
}(); }();
bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; bool pasteInterventionRequired = settings()->get("PastebinURL") != "";
bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired; bool themeInterventionRequired = settings()->get("ApplicationTheme") == "";
bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired;
if(wizardRequired) if(wizardRequired)
{ {
@ -920,6 +941,12 @@ bool Application::createSetupWizard()
{ {
m_setupWizard->addPage(new PasteWizardPage(m_setupWizard)); m_setupWizard->addPage(new PasteWizardPage(m_setupWizard));
} }
if (themeInterventionRequired)
{
settings()->set("ApplicationTheme", QString("system")); // set default theme after going into theme wizard
m_setupWizard->addPage(new ThemeWizardPage(m_setupWizard));
}
connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished);
m_setupWizard->show(); m_setupWizard->show();
return true; return true;
@ -942,7 +969,7 @@ bool Application::event(QEvent* event)
if (event->type() == QEvent::FileOpen) { if (event->type() == QEvent::FileOpen) {
auto ev = static_cast<QFileOpenEvent*>(event); auto ev = static_cast<QFileOpenEvent*>(event);
m_mainWindow->droppedURLs({ ev->url() }); m_mainWindow->processURLs({ ev->url() });
} }
return QApplication::event(event); return QApplication::event(event);
@ -1002,10 +1029,10 @@ void Application::performMainStartupAction()
showMainWindow(false); showMainWindow(false);
qDebug() << "<> Main window shown."; qDebug() << "<> Main window shown.";
} }
if(!m_zipToImport.isEmpty()) if(!m_zipsToImport.isEmpty())
{ {
qDebug() << "<> Importing instance from zip:" << m_zipToImport; qDebug() << "<> Importing from zip:" << m_zipsToImport;
m_mainWindow->droppedURLs({ m_zipToImport }); m_mainWindow->processURLs( m_zipsToImport );
} }
} }
@ -1058,7 +1085,7 @@ void Application::messageReceived(const QByteArray& message)
qWarning() << "Received" << command << "message without a zip path/URL."; qWarning() << "Received" << command << "message without a zip path/URL.";
return; return;
} }
m_mainWindow->droppedURLs({ QUrl(path) }); m_mainWindow->processURLs({ QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()) });
} }
else if(command == "launch") else if(command == "launch")
{ {
@ -1127,9 +1154,14 @@ QList<ITheme*> Application::getValidApplicationThemes()
return m_themeManager->getValidApplicationThemes(); return m_themeManager->getValidApplicationThemes();
} }
void Application::setApplicationTheme(const QString& name, bool initial) void Application::applyCurrentlySelectedTheme(bool initial)
{ {
m_themeManager->setApplicationTheme(name, initial); m_themeManager->applyCurrentlySelectedTheme(initial);
}
void Application::setApplicationTheme(const QString& name)
{
m_themeManager->setApplicationTheme(name);
} }
void Application::setIconTheme(const QString& name) void Application::setIconTheme(const QString& name)
@ -1357,16 +1389,7 @@ MainWindow* Application::showMainWindow(bool minimized)
m_mainWindow = new MainWindow(); m_mainWindow = new MainWindow();
m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray())); m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray()));
m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray())); m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray()));
#ifdef Q_OS_WIN
if (IsWindows10OrGreater())
{
if (QString::compare(settings()->get("ApplicationTheme").toString(), "dark") == 0) {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true);
} else {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false);
}
}
#endif
if(minimized) if(minimized)
{ {
m_mainWindow->showMinimized(); m_mainWindow->showMinimized();
@ -1543,7 +1566,8 @@ QString Application::getJarPath(QString jarFile)
FS::PathCombine(m_rootPath, "share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME), FS::PathCombine(m_rootPath, "share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME),
#endif #endif
FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(m_rootPath, "jars"),
FS::PathCombine(applicationDirPath(), "jars") FS::PathCombine(applicationDirPath(), "jars"),
FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging
}; };
for(QString p : potentialPaths) for(QString p : potentialPaths)
{ {
@ -1574,6 +1598,15 @@ QString Application::getFlameAPIKey()
return BuildConfig.FLAME_API_KEY; return BuildConfig.FLAME_API_KEY;
} }
QString Application::getModrinthAPIToken()
{
QString tokenOverride = m_settings->get("ModrinthToken").toString();
if (!tokenOverride.isEmpty())
return tokenOverride;
return QString();
}
QString Application::getUserAgent() QString Application::getUserAgent()
{ {
QString uaOverride = m_settings->get("UserAgentOverride").toString(); QString uaOverride = m_settings->get("UserAgentOverride").toString();
@ -1694,3 +1727,14 @@ bool Application::handleDataMigration(const QString& currentData,
} }
return true; return true;
} }
void Application::triggerUpdateCheck()
{
if (m_updater) {
qDebug() << "Checking for updates.";
m_updater->setBetaAllowed(false); // There are no other channels than stable
m_updater->checkForUpdates();
} else {
qDebug() << "Updater not available.";
}
}

View File

@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Tayou <tayou@gmx.net> * Copyright (C) 2022 Tayou <tayou@gmx.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -43,7 +44,6 @@
#include <QIcon> #include <QIcon>
#include <QDateTime> #include <QDateTime>
#include <QUrl> #include <QUrl>
#include <updater/GoUpdate.h>
#include <BaseInstance.h> #include <BaseInstance.h>
@ -63,7 +63,7 @@ class AccountList;
class IconList; class IconList;
class QNetworkAccessManager; class QNetworkAccessManager;
class JavaInstallList; class JavaInstallList;
class UpdateChecker; class ExternalUpdater;
class BaseProfilerFactory; class BaseProfilerFactory;
class BaseDetachedToolFactory; class BaseDetachedToolFactory;
class TranslationsModel; class TranslationsModel;
@ -120,14 +120,18 @@ public:
void setIconTheme(const QString& name); void setIconTheme(const QString& name);
void applyCurrentlySelectedTheme(bool initial = false);
QList<ITheme*> getValidApplicationThemes(); QList<ITheme*> getValidApplicationThemes();
void setApplicationTheme(const QString& name, bool initial); void setApplicationTheme(const QString& name);
shared_qobject_ptr<UpdateChecker> updateChecker() { shared_qobject_ptr<ExternalUpdater> updater() {
return m_updateChecker; return m_updater;
} }
void triggerUpdateCheck();
std::shared_ptr<TranslationsModel> translations(); std::shared_ptr<TranslationsModel> translations();
std::shared_ptr<JavaInstallList> javalist(); std::shared_ptr<JavaInstallList> javalist();
@ -174,6 +178,7 @@ public:
QString getMSAClientID(); QString getMSAClientID();
QString getFlameAPIKey(); QString getFlameAPIKey();
QString getModrinthAPIToken();
QString getUserAgent(); QString getUserAgent();
QString getUserAgentUncached(); QString getUserAgentUncached();
@ -182,6 +187,10 @@ public:
return m_rootPath; return m_rootPath;
} }
bool isPortable() {
return m_portable;
}
const Capabilities capabilities() { const Capabilities capabilities() {
return m_capabilities; return m_capabilities;
} }
@ -206,6 +215,7 @@ signals:
void updateAllowedChanged(bool status); void updateAllowedChanged(bool status);
void globalSettingsAboutToOpen(); void globalSettingsAboutToOpen();
void globalSettingsClosed(); void globalSettingsClosed();
int currentCatChanged(int index);
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
void clickedOnDock(); void clickedOnDock();
@ -248,7 +258,7 @@ private:
shared_qobject_ptr<QNetworkAccessManager> m_network; shared_qobject_ptr<QNetworkAccessManager> m_network;
shared_qobject_ptr<UpdateChecker> m_updateChecker; shared_qobject_ptr<ExternalUpdater> m_updater;
shared_qobject_ptr<AccountList> m_accounts; shared_qobject_ptr<AccountList> m_accounts;
shared_qobject_ptr<HttpMetaCache> m_metacache; shared_qobject_ptr<HttpMetaCache> m_metacache;
@ -269,6 +279,7 @@ private:
QString m_rootPath; QString m_rootPath;
Status m_status = Application::StartingUp; Status m_status = Application::StartingUp;
Capabilities m_capabilities; Capabilities m_capabilities;
bool m_portable = false;
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive; Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive;
@ -303,8 +314,7 @@ public:
QString m_serverToJoin; QString m_serverToJoin;
QString m_profileToUse; QString m_profileToUse;
bool m_liveCheck = false; bool m_liveCheck = false;
QUrl m_zipToImport; QList<QUrl> m_zipsToImport;
QString m_instanceIdToShowWindowOf; QString m_instanceIdToShowWindowOf;
std::unique_ptr<QFile> logFile; std::unique_ptr<QFile> logFile;
}; };

View File

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

View File

@ -282,6 +282,12 @@ public:
int getConsoleMaxLines() const; int getConsoleMaxLines() const;
bool shouldStopOnConsoleOverflow() const; bool shouldStopOnConsoleOverflow() const;
QStringList getLinkedInstances() const;
void setLinkedInstances(const QStringList& list);
void addLinkedInstanceId(const QString& id);
bool removeLinkedInstanceId(const QString& id);
bool isLinkedToInstanceId(const QString& id) const;
protected: protected:
void changeStatus(Status newStatus); void changeStatus(Status newStatus);

View File

@ -26,6 +26,7 @@ set(CORE_SOURCES
MMCZip.cpp MMCZip.cpp
StringUtils.h StringUtils.h
StringUtils.cpp StringUtils.cpp
QVariantUtils.h
RuntimeContext.h RuntimeContext.h
# Basic instance manipulation tasks (derived from InstanceTask) # Basic instance manipulation tasks (derived from InstanceTask)
@ -38,9 +39,9 @@ set(CORE_SOURCES
InstanceImportTask.h InstanceImportTask.h
InstanceImportTask.cpp InstanceImportTask.cpp
# Mod downloading task # Resource downloading task
ModDownloadTask.h ResourceDownloadTask.h
ModDownloadTask.cpp ResourceDownloadTask.cpp
# Use tracking separate from memory management # Use tracking separate from memory management
Usable.h Usable.h
@ -123,6 +124,8 @@ set(NET_SOURCES
net/HttpMetaCache.h net/HttpMetaCache.h
net/MetaCacheSink.cpp net/MetaCacheSink.cpp
net/MetaCacheSink.h net/MetaCacheSink.h
net/Logging.h
net/Logging.cpp
net/NetAction.h net/NetAction.h
net/NetJob.cpp net/NetJob.cpp
net/NetJob.h net/NetJob.h
@ -161,12 +164,6 @@ set(LAUNCH_SOURCES
# Old update system # Old update system
set(UPDATE_SOURCES set(UPDATE_SOURCES
updater/GoUpdate.h
updater/GoUpdate.cpp
updater/UpdateChecker.h
updater/UpdateChecker.cpp
updater/DownloadTask.h
updater/DownloadTask.cpp
updater/ExternalUpdater.h updater/ExternalUpdater.h
) )
@ -331,12 +328,18 @@ set(MINECRAFT_SOURCES
minecraft/mod/Resource.cpp minecraft/mod/Resource.cpp
minecraft/mod/ResourceFolderModel.h minecraft/mod/ResourceFolderModel.h
minecraft/mod/ResourceFolderModel.cpp minecraft/mod/ResourceFolderModel.cpp
minecraft/mod/DataPack.h
minecraft/mod/DataPack.cpp
minecraft/mod/ResourcePack.h minecraft/mod/ResourcePack.h
minecraft/mod/ResourcePack.cpp minecraft/mod/ResourcePack.cpp
minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.h
minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/ResourcePackFolderModel.cpp
minecraft/mod/TexturePack.h minecraft/mod/TexturePack.h
minecraft/mod/TexturePack.cpp minecraft/mod/TexturePack.cpp
minecraft/mod/ShaderPack.h
minecraft/mod/ShaderPack.cpp
minecraft/mod/WorldSave.h
minecraft/mod/WorldSave.cpp
minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.h
minecraft/mod/TexturePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.cpp
minecraft/mod/ShaderPackFolderModel.h minecraft/mod/ShaderPackFolderModel.h
@ -347,10 +350,18 @@ set(MINECRAFT_SOURCES
minecraft/mod/tasks/LocalModParseTask.cpp minecraft/mod/tasks/LocalModParseTask.cpp
minecraft/mod/tasks/LocalModUpdateTask.h minecraft/mod/tasks/LocalModUpdateTask.h
minecraft/mod/tasks/LocalModUpdateTask.cpp minecraft/mod/tasks/LocalModUpdateTask.cpp
minecraft/mod/tasks/LocalDataPackParseTask.h
minecraft/mod/tasks/LocalDataPackParseTask.cpp
minecraft/mod/tasks/LocalResourcePackParseTask.h minecraft/mod/tasks/LocalResourcePackParseTask.h
minecraft/mod/tasks/LocalResourcePackParseTask.cpp minecraft/mod/tasks/LocalResourcePackParseTask.cpp
minecraft/mod/tasks/LocalTexturePackParseTask.h minecraft/mod/tasks/LocalTexturePackParseTask.h
minecraft/mod/tasks/LocalTexturePackParseTask.cpp minecraft/mod/tasks/LocalTexturePackParseTask.cpp
minecraft/mod/tasks/LocalShaderPackParseTask.h
minecraft/mod/tasks/LocalShaderPackParseTask.cpp
minecraft/mod/tasks/LocalWorldSaveParseTask.h
minecraft/mod/tasks/LocalWorldSaveParseTask.cpp
minecraft/mod/tasks/LocalResourceParse.h
minecraft/mod/tasks/LocalResourceParse.cpp
# Assets # Assets
minecraft/AssetsUtils.h minecraft/AssetsUtils.h
@ -459,7 +470,7 @@ set(API_SOURCES
modplatform/ModIndex.h modplatform/ModIndex.h
modplatform/ModIndex.cpp modplatform/ModIndex.cpp
modplatform/ModAPI.h modplatform/ResourceAPI.h
modplatform/EnsureMetadataTask.h modplatform/EnsureMetadataTask.h
modplatform/EnsureMetadataTask.cpp modplatform/EnsureMetadataTask.cpp
@ -470,8 +481,8 @@ set(API_SOURCES
modplatform/flame/FlameAPI.cpp modplatform/flame/FlameAPI.cpp
modplatform/modrinth/ModrinthAPI.h modplatform/modrinth/ModrinthAPI.h
modplatform/modrinth/ModrinthAPI.cpp modplatform/modrinth/ModrinthAPI.cpp
modplatform/helpers/NetworkModAPI.h modplatform/helpers/NetworkResourceAPI.h
modplatform/helpers/NetworkModAPI.cpp modplatform/helpers/NetworkResourceAPI.cpp
modplatform/helpers/HashUtils.h modplatform/helpers/HashUtils.h
modplatform/helpers/HashUtils.cpp modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h modplatform/helpers/OverrideUtils.h
@ -516,13 +527,6 @@ set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthInstanceCreationTask.h modplatform/modrinth/ModrinthInstanceCreationTask.h
) )
set(MODPACKSCH_SOURCES
modplatform/modpacksch/FTBPackInstallTask.h
modplatform/modpacksch/FTBPackInstallTask.cpp
modplatform/modpacksch/FTBPackManifest.h
modplatform/modpacksch/FTBPackManifest.cpp
)
set(PACKWIZ_SOURCES set(PACKWIZ_SOURCES
modplatform/packwiz/Packwiz.h modplatform/packwiz/Packwiz.h
modplatform/packwiz/Packwiz.cpp modplatform/packwiz/Packwiz.cpp
@ -551,6 +555,85 @@ set(ATLAUNCHER_SOURCES
modplatform/atlauncher/ATLShareCode.h modplatform/atlauncher/ATLShareCode.h
) )
set(LINKEXE_SOURCES
filelink/FileLink.h
filelink/FileLink.cpp
FileSystem.h
FileSystem.cpp
Exception.h
StringUtils.h
StringUtils.cpp
DesktopServices.h
DesktopServices.cpp
)
######## Logging categories ########
ecm_qt_declare_logging_category(CORE_SOURCES
HEADER Logging.h
IDENTIFIER authCredentials
CATEGORY_NAME "launcher.auth.credentials"
DEFAULT_SEVERITY Warning
DESCRIPTION "Secrets and credentials for debugging purposes"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskLogC
CATEGORY_NAME "launcher.task"
DEFAULT_SEVERITY Debug
DESCRIPTION "Task actions"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskNetLogC
CATEGORY_NAME "launcher.task.net"
DEFAULT_SEVERITY Debug
DESCRIPTION "task network action"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskDownloadLogC
CATEGORY_NAME "launcher.task.net.download"
DEFAULT_SEVERITY Debug
DESCRIPTION "task network download actions"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskUploadLogC
CATEGORY_NAME "launcher.task.net.upload"
DEFAULT_SEVERITY Debug
DESCRIPTION "task network upload actions"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskMetaCacheLogC
CATEGORY_NAME "launcher.task.net.metacache"
DEFAULT_SEVERITY Debug
DESCRIPTION "task network meta-cache actions"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskHttpMetaCacheLogC
CATEGORY_NAME "launcher.task.net.metacache.http"
DEFAULT_SEVERITY Debug
DESCRIPTION "task network http meta-cache actions"
EXPORT "${Launcher_Name}"
)
if(KDE_INSTALL_LOGGINGCATEGORIESDIR) # only install if there is a standard path for this
ecm_qt_install_logging_categories(
EXPORT "${Launcher_Name}"
DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}"
)
endif()
################################ COMPILE ################################ ################################ COMPILE ################################
set(LOGIC_SOURCES set(LOGIC_SOURCES
@ -573,7 +656,6 @@ set(LOGIC_SOURCES
${FTB_SOURCES} ${FTB_SOURCES}
${FLAME_SOURCES} ${FLAME_SOURCES}
${MODRINTH_SOURCES} ${MODRINTH_SOURCES}
${MODPACKSCH_SOURCES}
${PACKWIZ_SOURCES} ${PACKWIZ_SOURCES}
${TECHNIC_SOURCES} ${TECHNIC_SOURCES}
${ATLAUNCHER_SOURCES} ${ATLAUNCHER_SOURCES}
@ -589,8 +671,6 @@ SET(LAUNCHER_SOURCES
Application.cpp Application.cpp
DataMigrationTask.h DataMigrationTask.h
DataMigrationTask.cpp DataMigrationTask.cpp
UpdateController.cpp
UpdateController.h
ApplicationMessage.h ApplicationMessage.h
ApplicationMessage.cpp ApplicationMessage.cpp
@ -599,7 +679,7 @@ SET(LAUNCHER_SOURCES
DesktopServices.cpp DesktopServices.cpp
VersionProxyModel.h VersionProxyModel.h
VersionProxyModel.cpp VersionProxyModel.cpp
HoeDown.h Markdown.h
# Super secret! # Super secret!
KonamiCode.h KonamiCode.h
@ -651,6 +731,8 @@ SET(LAUNCHER_SOURCES
ui/setupwizard/LanguageWizardPage.h ui/setupwizard/LanguageWizardPage.h
ui/setupwizard/PasteWizardPage.cpp ui/setupwizard/PasteWizardPage.cpp
ui/setupwizard/PasteWizardPage.h ui/setupwizard/PasteWizardPage.h
ui/setupwizard/ThemeWizardPage.cpp
ui/setupwizard/ThemeWizardPage.h
# GUI - themes # GUI - themes
ui/themes/FusionTheme.cpp ui/themes/FusionTheme.cpp
@ -694,8 +776,11 @@ SET(LAUNCHER_SOURCES
ui/pages/instance/ManagedPackPage.cpp ui/pages/instance/ManagedPackPage.cpp
ui/pages/instance/ManagedPackPage.h ui/pages/instance/ManagedPackPage.h
ui/pages/instance/TexturePackPage.h ui/pages/instance/TexturePackPage.h
ui/pages/instance/TexturePackPage.cpp
ui/pages/instance/ResourcePackPage.h ui/pages/instance/ResourcePackPage.h
ui/pages/instance/ResourcePackPage.cpp
ui/pages/instance/ShaderPackPage.h ui/pages/instance/ShaderPackPage.h
ui/pages/instance/ShaderPackPage.cpp
ui/pages/instance/ModFolderPage.cpp ui/pages/instance/ModFolderPage.cpp
ui/pages/instance/ModFolderPage.h ui/pages/instance/ModFolderPage.h
ui/pages/instance/NotesPage.cpp ui/pages/instance/NotesPage.cpp
@ -737,11 +822,26 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/VanillaPage.cpp ui/pages/modplatform/VanillaPage.cpp
ui/pages/modplatform/VanillaPage.h ui/pages/modplatform/VanillaPage.h
ui/pages/modplatform/ResourcePage.cpp
ui/pages/modplatform/ResourcePage.h
ui/pages/modplatform/ResourceModel.cpp
ui/pages/modplatform/ResourceModel.h
ui/pages/modplatform/ModPage.cpp ui/pages/modplatform/ModPage.cpp
ui/pages/modplatform/ModPage.h ui/pages/modplatform/ModPage.h
ui/pages/modplatform/ModModel.cpp ui/pages/modplatform/ModModel.cpp
ui/pages/modplatform/ModModel.h ui/pages/modplatform/ModModel.h
ui/pages/modplatform/ResourcePackPage.cpp
ui/pages/modplatform/ResourcePackModel.cpp
# Needed for MOC to find them without a corresponding .cpp
ui/pages/modplatform/TexturePackPage.h
ui/pages/modplatform/TexturePackModel.cpp
ui/pages/modplatform/ShaderPackPage.cpp
ui/pages/modplatform/ShaderPackModel.cpp
ui/pages/modplatform/atlauncher/AtlFilterModel.cpp ui/pages/modplatform/atlauncher/AtlFilterModel.cpp
ui/pages/modplatform/atlauncher/AtlFilterModel.h ui/pages/modplatform/atlauncher/AtlFilterModel.h
ui/pages/modplatform/atlauncher/AtlListModel.cpp ui/pages/modplatform/atlauncher/AtlListModel.cpp
@ -753,13 +853,6 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
ui/pages/modplatform/ftb/FtbFilterModel.cpp
ui/pages/modplatform/ftb/FtbFilterModel.h
ui/pages/modplatform/ftb/FtbListModel.cpp
ui/pages/modplatform/ftb/FtbListModel.h
ui/pages/modplatform/ftb/FtbPage.cpp
ui/pages/modplatform/ftb/FtbPage.h
ui/pages/modplatform/legacy_ftb/Page.cpp ui/pages/modplatform/legacy_ftb/Page.cpp
ui/pages/modplatform/legacy_ftb/Page.h ui/pages/modplatform/legacy_ftb/Page.h
ui/pages/modplatform/legacy_ftb/ListModel.h ui/pages/modplatform/legacy_ftb/ListModel.h
@ -769,10 +862,10 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/flame/FlameModel.h ui/pages/modplatform/flame/FlameModel.h
ui/pages/modplatform/flame/FlamePage.cpp ui/pages/modplatform/flame/FlamePage.cpp
ui/pages/modplatform/flame/FlamePage.h ui/pages/modplatform/flame/FlamePage.h
ui/pages/modplatform/flame/FlameModModel.cpp ui/pages/modplatform/flame/FlameResourceModels.cpp
ui/pages/modplatform/flame/FlameModModel.h ui/pages/modplatform/flame/FlameResourceModels.h
ui/pages/modplatform/flame/FlameModPage.cpp ui/pages/modplatform/flame/FlameResourcePages.cpp
ui/pages/modplatform/flame/FlameModPage.h ui/pages/modplatform/flame/FlameResourcePages.h
ui/pages/modplatform/modrinth/ModrinthPage.cpp ui/pages/modplatform/modrinth/ModrinthPage.cpp
ui/pages/modplatform/modrinth/ModrinthPage.h ui/pages/modplatform/modrinth/ModrinthPage.h
@ -787,10 +880,10 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/ImportPage.cpp ui/pages/modplatform/ImportPage.cpp
ui/pages/modplatform/ImportPage.h ui/pages/modplatform/ImportPage.h
ui/pages/modplatform/modrinth/ModrinthModModel.cpp ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
ui/pages/modplatform/modrinth/ModrinthModModel.h ui/pages/modplatform/modrinth/ModrinthResourceModels.h
ui/pages/modplatform/modrinth/ModrinthModPage.cpp ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
ui/pages/modplatform/modrinth/ModrinthModPage.h ui/pages/modplatform/modrinth/ModrinthResourcePages.h
# GUI - dialogs # GUI - dialogs
ui/dialogs/AboutDialog.cpp ui/dialogs/AboutDialog.cpp
@ -809,8 +902,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ExportInstanceDialog.h ui/dialogs/ExportInstanceDialog.h
ui/dialogs/IconPickerDialog.cpp ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourcePackDialog.cpp ui/dialogs/ImportResourceDialog.cpp
ui/dialogs/ImportResourcePackDialog.h ui/dialogs/ImportResourceDialog.h
ui/dialogs/LoginDialog.cpp ui/dialogs/LoginDialog.cpp
ui/dialogs/LoginDialog.h ui/dialogs/LoginDialog.h
ui/dialogs/MSALoginDialog.cpp ui/dialogs/MSALoginDialog.cpp
@ -829,14 +922,12 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ProgressDialog.h ui/dialogs/ProgressDialog.h
ui/dialogs/ReviewMessageBox.cpp ui/dialogs/ReviewMessageBox.cpp
ui/dialogs/ReviewMessageBox.h ui/dialogs/ReviewMessageBox.h
ui/dialogs/UpdateDialog.cpp
ui/dialogs/UpdateDialog.h
ui/dialogs/VersionSelectDialog.cpp ui/dialogs/VersionSelectDialog.cpp
ui/dialogs/VersionSelectDialog.h ui/dialogs/VersionSelectDialog.h
ui/dialogs/SkinUploadDialog.cpp ui/dialogs/SkinUploadDialog.cpp
ui/dialogs/SkinUploadDialog.h ui/dialogs/SkinUploadDialog.h
ui/dialogs/ModDownloadDialog.cpp ui/dialogs/ResourceDownloadDialog.cpp
ui/dialogs/ModDownloadDialog.h ui/dialogs/ResourceDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp ui/dialogs/ScrollMessageBox.cpp
ui/dialogs/ScrollMessageBox.h ui/dialogs/ScrollMessageBox.h
ui/dialogs/BlockedModsDialog.cpp ui/dialogs/BlockedModsDialog.cpp
@ -882,6 +973,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/VariableSizedImageObject.cpp ui/widgets/VariableSizedImageObject.cpp
ui/widgets/ProjectItem.h ui/widgets/ProjectItem.h
ui/widgets/ProjectItem.cpp ui/widgets/ProjectItem.cpp
ui/widgets/SubTaskProgressBar.h
ui/widgets/SubTaskProgressBar.cpp
ui/widgets/VersionListView.cpp ui/widgets/VersionListView.cpp
ui/widgets/VersionListView.h ui/widgets/VersionListView.h
ui/widgets/VersionSelectWidget.cpp ui/widgets/VersionSelectWidget.cpp
@ -890,6 +983,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/ProgressWidget.cpp ui/widgets/ProgressWidget.cpp
ui/widgets/WideBar.h ui/widgets/WideBar.h
ui/widgets/WideBar.cpp ui/widgets/WideBar.cpp
ui/widgets/ThemeCustomizationWidget.h
ui/widgets/ThemeCustomizationWidget.cpp
# GUI - instance group view # GUI - instance group view
ui/instanceview/InstanceProxyModel.cpp ui/instanceview/InstanceProxyModel.cpp
@ -905,18 +1000,10 @@ SET(LAUNCHER_SOURCES
ui/instanceview/VisualGroup.h ui/instanceview/VisualGroup.h
) )
if(WIN32)
set(LAUNCHER_SOURCES
${LAUNCHER_SOURCES}
# GUI - dark titlebar for Windows 10/11
ui/WinDarkmode.h
ui/WinDarkmode.cpp
)
endif()
qt_wrap_ui(LAUNCHER_UI qt_wrap_ui(LAUNCHER_UI
ui/MainWindow.ui
ui/setupwizard/PasteWizardPage.ui ui/setupwizard/PasteWizardPage.ui
ui/setupwizard/ThemeWizardPage.ui
ui/pages/global/AccountListPage.ui ui/pages/global/AccountListPage.ui
ui/pages/global/JavaPage.ui ui/pages/global/JavaPage.ui
ui/pages/global/LauncherPage.ui ui/pages/global/LauncherPage.ui
@ -938,29 +1025,29 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
ui/pages/modplatform/atlauncher/AtlPage.ui ui/pages/modplatform/atlauncher/AtlPage.ui
ui/pages/modplatform/VanillaPage.ui ui/pages/modplatform/VanillaPage.ui
ui/pages/modplatform/ModPage.ui ui/pages/modplatform/ResourcePage.ui
ui/pages/modplatform/flame/FlamePage.ui ui/pages/modplatform/flame/FlamePage.ui
ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/legacy_ftb/Page.ui
ui/pages/modplatform/ImportPage.ui ui/pages/modplatform/ImportPage.ui
ui/pages/modplatform/ftb/FtbPage.ui
ui/pages/modplatform/modrinth/ModrinthPage.ui ui/pages/modplatform/modrinth/ModrinthPage.ui
ui/pages/modplatform/technic/TechnicPage.ui ui/pages/modplatform/technic/TechnicPage.ui
ui/widgets/InstanceCardWidget.ui ui/widgets/InstanceCardWidget.ui
ui/widgets/CustomCommands.ui ui/widgets/CustomCommands.ui
ui/widgets/InfoFrame.ui ui/widgets/InfoFrame.ui
ui/widgets/ModFilterWidget.ui ui/widgets/ModFilterWidget.ui
ui/widgets/SubTaskProgressBar.ui
ui/widgets/ThemeCustomizationWidget.ui
ui/dialogs/CopyInstanceDialog.ui ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProfileSetupDialog.ui
ui/dialogs/ProgressDialog.ui ui/dialogs/ProgressDialog.ui
ui/dialogs/NewInstanceDialog.ui ui/dialogs/NewInstanceDialog.ui
ui/dialogs/UpdateDialog.ui
ui/dialogs/NewComponentDialog.ui ui/dialogs/NewComponentDialog.ui
ui/dialogs/NewsDialog.ui ui/dialogs/NewsDialog.ui
ui/dialogs/ProfileSelectDialog.ui ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/IconPickerDialog.ui ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourcePackDialog.ui ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui ui/dialogs/AboutDialog.ui
@ -1002,6 +1089,7 @@ target_link_libraries(Launcher_logic
nbt++ nbt++
${ZLIB_LIBRARIES} ${ZLIB_LIBRARIES}
tomlplusplus::tomlplusplus tomlplusplus::tomlplusplus
qdcss
BuildConfig BuildConfig
Katabasis Katabasis
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
@ -1025,7 +1113,7 @@ target_link_libraries(Launcher_logic
) )
target_link_libraries(Launcher_logic target_link_libraries(Launcher_logic
QuaZip::QuaZip QuaZip::QuaZip
hoedown cmark::cmark
LocalPeer LocalPeer
Launcher_rainbow Launcher_rainbow
) )
@ -1070,6 +1158,41 @@ install(TARGETS ${Launcher_Name}
FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
) )
if(WIN32)
add_library(filelink_logic STATIC ${LINKEXE_SOURCES})
target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(filelink_logic
systeminfo
BuildConfig
ghcFilesystem::ghc_filesystem
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
# Qt${QT_VERSION_MAJOR}::Concurrent
${Launcher_QT_LIBS}
)
add_executable("${Launcher_Name}_filelink" WIN32 filelink/main.cpp)
target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest)
target_link_libraries("${Launcher_Name}_filelink" filelink_logic)
if(DEFINED Launcher_APP_BINARY_NAME)
set_target_properties("${Launcher_Name}_filelink" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_filelink")
endif()
if(DEFINED Launcher_BINARY_RPATH)
SET_TARGET_PROPERTIES("${Launcher_Name}_filelink" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}")
endif()
install(TARGETS "${Launcher_Name}_filelink"
BUNDLE DESTINATION "." COMPONENT Runtime
LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime
RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime
FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
)
endif()
if (UNIX AND APPLE) if (UNIX AND APPLE)
# Add Sparkle updater # Add Sparkle updater
# It has to be copied here instead of just allowing fixup_bundle to install it, otherwise essential parts of # It has to be copied here instead of just allowing fixup_bundle to install it, otherwise essential parts of
@ -1086,6 +1209,12 @@ if(INSTALL_BUNDLE STREQUAL "full")
CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")"
COMPONENT Runtime COMPONENT Runtime
) )
# add qtlogging.ini as a config file
install(
FILES "qtlogging.ini"
DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}
COMPONENT Runtime
)
# Bundle plugins # Bundle plugins
# Image formats # Image formats
install( install(

View File

@ -37,7 +37,6 @@
#include <QDesktopServices> #include <QDesktopServices>
#include <QProcess> #include <QProcess>
#include <QDebug> #include <QDebug>
#include "Application.h"
/** /**
* This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing. * This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing.

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,76 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <hoedown/html.h>
#include <hoedown/document.h>
#include <QString>
#include <QByteArray>
/**
* hoedown wrapper, because dealing with resource lifetime in C is stupid
*/
class HoeDown
{
public:
class buffer
{
public:
buffer(size_t unit = 4096)
{
buf = hoedown_buffer_new(unit);
}
~buffer()
{
hoedown_buffer_free(buf);
}
const char * cstr()
{
return hoedown_buffer_cstr(buf);
}
void put(QByteArray input)
{
hoedown_buffer_put(buf, reinterpret_cast<uint8_t *>(input.data()), input.size());
}
const uint8_t * data() const
{
return buf->data;
}
size_t size() const
{
return buf->size;
}
hoedown_buffer * buf;
} ib, ob;
HoeDown()
{
renderer = hoedown_html_renderer_new((hoedown_html_flags) 0,0);
document = hoedown_document_new(renderer, (hoedown_extensions) 0, 8);
}
~HoeDown()
{
hoedown_document_free(document);
hoedown_html_renderer_free(renderer);
}
QString process(QByteArray input)
{
ib.put(input);
hoedown_document_render(document, ob.buf, ib.data(), ib.size());
return ob.cstr();
}
private:
hoedown_document * document;
hoedown_renderer * renderer;
};

View File

@ -16,8 +16,13 @@ bool InstanceCopyPrefs::allTrue() const
copyScreenshots; copyScreenshots;
} }
// Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat") // Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat")
QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const
{
return getSelectedFiltersAsRegex({});
}
QString InstanceCopyPrefs::getSelectedFiltersAsRegex(const QStringList& additionalFilters) const
{ {
QStringList filters; QStringList filters;
@ -42,6 +47,10 @@ QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const
if(!copyScreenshots) if(!copyScreenshots)
filters << "screenshots"; filters << "screenshots";
for (auto filter : additionalFilters) {
filters << filter;
}
// If we have any filters to add, join them as a single regex string to return: // If we have any filters to add, join them as a single regex string to return:
if (!filters.isEmpty()) { if (!filters.isEmpty()) {
const QString MC_ROOT = "[.]?minecraft/"; const QString MC_ROOT = "[.]?minecraft/";
@ -93,6 +102,31 @@ bool InstanceCopyPrefs::isCopyScreenshotsEnabled() const
return copyScreenshots; return copyScreenshots;
} }
bool InstanceCopyPrefs::isUseSymLinksEnabled() const
{
return useSymLinks;
}
bool InstanceCopyPrefs::isUseHardLinksEnabled() const
{
return useHardLinks;
}
bool InstanceCopyPrefs::isLinkRecursivelyEnabled() const
{
return linkRecursively;
}
bool InstanceCopyPrefs::isDontLinkSavesEnabled() const
{
return dontLinkSaves;
}
bool InstanceCopyPrefs::isUseCloneEnabled() const
{
return useClone;
}
// ======= Setters ======= // ======= Setters =======
void InstanceCopyPrefs::enableCopySaves(bool b) void InstanceCopyPrefs::enableCopySaves(bool b)
{ {
@ -133,3 +167,28 @@ void InstanceCopyPrefs::enableCopyScreenshots(bool b)
{ {
copyScreenshots = b; copyScreenshots = b;
} }
void InstanceCopyPrefs::enableUseSymLinks(bool b)
{
useSymLinks = b;
}
void InstanceCopyPrefs::enableLinkRecursively(bool b)
{
linkRecursively = b;
}
void InstanceCopyPrefs::enableUseHardLinks(bool b)
{
useHardLinks = b;
}
void InstanceCopyPrefs::enableDontLinkSaves(bool b)
{
dontLinkSaves = b;
}
void InstanceCopyPrefs::enableUseClone(bool b)
{
useClone = b;
}

View File

@ -10,6 +10,7 @@ struct InstanceCopyPrefs {
public: public:
[[nodiscard]] bool allTrue() const; [[nodiscard]] bool allTrue() const;
[[nodiscard]] QString getSelectedFiltersAsRegex() const; [[nodiscard]] QString getSelectedFiltersAsRegex() const;
[[nodiscard]] QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const;
// Getters // Getters
[[nodiscard]] bool isCopySavesEnabled() const; [[nodiscard]] bool isCopySavesEnabled() const;
[[nodiscard]] bool isKeepPlaytimeEnabled() const; [[nodiscard]] bool isKeepPlaytimeEnabled() const;
@ -19,6 +20,11 @@ struct InstanceCopyPrefs {
[[nodiscard]] bool isCopyServersEnabled() const; [[nodiscard]] bool isCopyServersEnabled() const;
[[nodiscard]] bool isCopyModsEnabled() const; [[nodiscard]] bool isCopyModsEnabled() const;
[[nodiscard]] bool isCopyScreenshotsEnabled() const; [[nodiscard]] bool isCopyScreenshotsEnabled() const;
[[nodiscard]] bool isUseSymLinksEnabled() const;
[[nodiscard]] bool isLinkRecursivelyEnabled() const;
[[nodiscard]] bool isUseHardLinksEnabled() const;
[[nodiscard]] bool isDontLinkSavesEnabled() const;
[[nodiscard]] bool isUseCloneEnabled() const;
// Setters // Setters
void enableCopySaves(bool b); void enableCopySaves(bool b);
void enableKeepPlaytime(bool b); void enableKeepPlaytime(bool b);
@ -28,6 +34,11 @@ struct InstanceCopyPrefs {
void enableCopyServers(bool b); void enableCopyServers(bool b);
void enableCopyMods(bool b); void enableCopyMods(bool b);
void enableCopyScreenshots(bool b); void enableCopyScreenshots(bool b);
void enableUseSymLinks(bool b);
void enableLinkRecursively(bool b);
void enableUseHardLinks(bool b);
void enableDontLinkSaves(bool b);
void enableUseClone(bool b);
protected: // data protected: // data
bool copySaves = true; bool copySaves = true;
@ -38,4 +49,9 @@ struct InstanceCopyPrefs {
bool copyServers = true; bool copyServers = true;
bool copyMods = true; bool copyMods = true;
bool copyScreenshots = true; bool copyScreenshots = true;
bool useSymLinks = false;
bool linkRecursively = false;
bool useHardLinks = false;
bool dontLinkSaves = false;
bool useClone = false;
}; };

View File

@ -1,18 +1,31 @@
#include "InstanceCopyTask.h" #include "InstanceCopyTask.h"
#include "settings/INISettingsObject.h" #include <QDebug>
#include <QtConcurrentRun>
#include "FileSystem.h" #include "FileSystem.h"
#include "NullInstance.h" #include "NullInstance.h"
#include "pathmatcher/RegexpMatcher.h" #include "pathmatcher/RegexpMatcher.h"
#include <QtConcurrentRun> #include "settings/INISettingsObject.h"
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs)
{ {
m_origInstance = origInstance; m_origInstance = origInstance;
m_keepPlaytime = prefs.isKeepPlaytimeEnabled(); m_keepPlaytime = prefs.isKeepPlaytimeEnabled();
m_useLinks = prefs.isUseSymLinksEnabled();
m_linkRecursively = prefs.isLinkRecursivelyEnabled();
m_useHardLinks = prefs.isLinkRecursivelyEnabled() && prefs.isUseHardLinksEnabled();
m_copySaves = prefs.isLinkRecursivelyEnabled() && prefs.isDontLinkSavesEnabled() && prefs.isCopySavesEnabled();
m_useClone = prefs.isUseCloneEnabled();
QString filters = prefs.getSelectedFiltersAsRegex(); QString filters = prefs.getSelectedFiltersAsRegex();
if (!filters.isEmpty()) if (m_useLinks || m_useHardLinks) {
{ if (!filters.isEmpty())
filters += "|";
filters += "instance.cfg";
}
qDebug() << "CopyFilters:" << filters;
if (!filters.isEmpty()) {
// Set regex filter: // Set regex filter:
// FIXME: get this from the original instance type... // FIXME: get this from the original instance type...
auto matcherReal = new RegexpMatcher(filters); auto matcherReal = new RegexpMatcher(filters);
@ -25,11 +38,78 @@ void InstanceCopyTask::executeTask()
{ {
setStatus(tr("Copying instance %1").arg(m_origInstance->name())); setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]{ auto copySaves = [&]() {
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); FS::copy savesCopy(FS::PathCombine(m_origInstance->instanceRoot(), "saves"), FS::PathCombine(m_stagingPath, "saves"));
folderCopy.followSymlinks(false).matcher(m_matcher.get()); savesCopy.followSymlinks(true);
return folderCopy(); return savesCopy();
};
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this, copySaves] {
if (m_useClone) {
FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath);
folderClone.matcher(m_matcher.get());
return folderClone();
} else if (m_useLinks || m_useHardLinks) {
FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath);
int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder
folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get());
bool there_were_errors = false;
if (!folderLink()) {
#if defined Q_OS_WIN32
if (!m_useHardLinks) {
qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks";
qDebug() << "attempting to run with privelage";
QEventLoop loop;
bool got_priv_results = false;
connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&](bool gotResults) {
if (!gotResults) {
qDebug() << "Privileged run exited without results!";
}
got_priv_results = gotResults;
loop.quit();
});
folderLink.runPrivileged();
loop.exec(); // wait for the finished signal
for (auto result : folderLink.getResults()) {
if (result.err_value != 0) {
there_were_errors = true;
}
}
if (m_copySaves) {
there_were_errors |= !copySaves();
}
return got_priv_results && !there_were_errors;
} else {
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
}
#else
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
#endif
return false;
}
if (m_copySaves) {
there_were_errors |= !copySaves();
}
return !there_were_errors;
} else {
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
folderCopy.followSymlinks(false).matcher(m_matcher.get());
return folderCopy();
}
}); });
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished); connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted); connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted);
@ -39,8 +119,7 @@ void InstanceCopyTask::executeTask()
void InstanceCopyTask::copyFinished() void InstanceCopyTask::copyFinished()
{ {
auto successful = m_copyFuture.result(); auto successful = m_copyFuture.result();
if(!successful) if (!successful) {
{
emitFailed(tr("Instance folder copy failed.")); emitFailed(tr("Instance folder copy failed."));
return; return;
} }
@ -50,9 +129,11 @@ void InstanceCopyTask::copyFinished()
InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath));
inst->setName(name()); inst->setName(name());
inst->setIconKey(m_instIcon); inst->setIconKey(m_instIcon);
if(!m_keepPlaytime) { if (!m_keepPlaytime) {
inst->resetTimePlayed(); inst->resetTimePlayed();
} }
if (m_useLinks)
inst->addLinkedInstanceId(m_origInstance->id());
emitSucceeded(); emitSucceeded();
} }

View File

@ -30,4 +30,9 @@ private:
QFutureWatcher<bool> m_copyFutureWatcher; QFutureWatcher<bool> m_copyFutureWatcher;
std::unique_ptr<IPathMatcher> m_matcher; std::unique_ptr<IPathMatcher> m_matcher;
bool m_keepPlaytime; bool m_keepPlaytime;
bool m_useLinks = false;
bool m_useHardLinks = false;
bool m_copySaves = false;
bool m_linkRecursively = false;
bool m_useClone = false;
}; };

View File

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

View File

@ -41,6 +41,7 @@
#include "MMCZip.h" #include "MMCZip.h"
#include "NullInstance.h" #include "NullInstance.h"
#include "QObjectPtr.h"
#include "icons/IconList.h" #include "icons/IconList.h"
#include "icons/IconUtils.h" #include "icons/IconUtils.h"
@ -66,7 +67,12 @@ bool InstanceImportTask::abort()
if (m_filesNetJob) if (m_filesNetJob)
m_filesNetJob->abort(); m_filesNetJob->abort();
m_extractFuture.cancel(); if (m_extractFuture.isRunning()) {
// NOTE: The tasks created by QtConcurrent::run() can't actually get cancelled,
// but we can use this call to check the state when the extraction finishes.
m_extractFuture.cancel();
m_extractFuture.waitForFinished();
}
return Task::abort(); return Task::abort();
} }
@ -88,11 +94,12 @@ void InstanceImportTask::executeTask()
entry->setStale(true); entry->setStale(true);
m_archivePath = entry->getFullPath(); m_archivePath = entry->getFullPath();
m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network()); m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network()));
m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propogateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed); connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted); connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);
@ -185,18 +192,20 @@ void InstanceImportTask::processZipPack()
// make sure we extract just the pack // make sure we extract just the pack
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath()); m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath());
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &InstanceImportTask::extractFinished); connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &InstanceImportTask::extractFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &InstanceImportTask::extractAborted);
m_extractFutureWatcher.setFuture(m_extractFuture); m_extractFutureWatcher.setFuture(m_extractFuture);
} }
void InstanceImportTask::extractFinished() void InstanceImportTask::extractFinished()
{ {
m_packZip.reset(); m_packZip.reset();
if (!m_extractFuture.result())
{ if (m_extractFuture.isCanceled())
return;
if (!m_extractFuture.result().has_value()) {
emitFailed(tr("Failed to extract modpack")); emitFailed(tr("Failed to extract modpack"));
return; return;
} }
QDir extractDir(m_stagingPath); QDir extractDir(m_stagingPath);
qDebug() << "Fixing permissions for extracted pack files..."; qDebug() << "Fixing permissions for extracted pack files...";
@ -250,14 +259,9 @@ void InstanceImportTask::extractFinished()
} }
} }
void InstanceImportTask::extractAborted()
{
emitAborted();
}
void InstanceImportTask::processFlame() void InstanceImportTask::processFlame()
{ {
FlameCreationTask* inst_creation_task = nullptr; shared_qobject_ptr<FlameCreationTask> inst_creation_task = nullptr;
if (!m_extra_info.isEmpty()) { if (!m_extra_info.isEmpty()) {
auto pack_id_it = m_extra_info.constFind("pack_id"); auto pack_id_it = m_extra_info.constFind("pack_id");
Q_ASSERT(pack_id_it != m_extra_info.constEnd()); Q_ASSERT(pack_id_it != m_extra_info.constEnd());
@ -272,10 +276,10 @@ void InstanceImportTask::processFlame()
if (original_instance_id_it != m_extra_info.constEnd()) if (original_instance_id_it != m_extra_info.constEnd())
original_instance_id = original_instance_id_it.value(); original_instance_id = original_instance_id_it.value();
inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); inst_creation_task = makeShared<FlameCreationTask>(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
} else { } else {
// FIXME: Find a way to get IDs in directly imported ZIPs // FIXME: Find a way to get IDs in directly imported ZIPs
inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, {}, {}); inst_creation_task = makeShared<FlameCreationTask>(m_stagingPath, m_globalSettings, m_parent, QString(), QString());
} }
inst_creation_task->setName(*this); inst_creation_task->setName(*this);
@ -283,25 +287,26 @@ void InstanceImportTask::processFlame()
inst_creation_task->setGroup(m_instGroup); inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] { connect(inst_creation_task.get(), &Task::succeeded, this, [this, inst_creation_task] {
setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID()); setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID());
emitSucceeded(); emitSucceeded();
}); });
connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed);
connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress); connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress);
connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propogateStepProgress);
connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater); connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort); connect(this, &Task::aborted, inst_creation_task.get(), &InstanceCreationTask::abort);
connect(inst_creation_task, &Task::aborted, this, &Task::abort); connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort);
connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable);
inst_creation_task->start(); inst_creation_task->start();
} }
void InstanceImportTask::processTechnic() void InstanceImportTask::processTechnic()
{ {
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor(); shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor{ new Technic::TechnicPackProcessor };
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed);
packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath);
@ -361,7 +366,7 @@ void InstanceImportTask::processModrinth()
} else { } else {
QString pack_id; QString pack_id;
if (!m_sourceUrl.isEmpty()) { if (!m_sourceUrl.isEmpty()) {
QRegularExpression regex(R"(data\/(.*)\/versions)"); QRegularExpression regex(R"(data\/([^\/]*)\/versions)");
pack_id = regex.match(m_sourceUrl.toString()).captured(1); pack_id = regex.match(m_sourceUrl.toString()).captured(1);
} }
@ -380,7 +385,9 @@ void InstanceImportTask::processModrinth()
}); });
connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress); connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
connect(inst_creation_task, &Task::stepProgress, this, &InstanceImportTask::propogateStepProgress);
connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task, &Task::details, this, &InstanceImportTask::setDetails);
connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater); connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort); connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort);

View File

@ -81,7 +81,6 @@ private slots:
void downloadProgressChanged(qint64 current, qint64 total); void downloadProgressChanged(qint64 current, qint64 total);
void downloadAborted(); void downloadAborted();
void extractFinished(); void extractFinished();
void extractAborted();
private: /* data */ private: /* data */
NetJob::Ptr m_filesNetJob; NetJob::Ptr m_filesNetJob;

View File

@ -129,6 +129,16 @@ QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const
return mimeData; return mimeData;
} }
QStringList InstanceList::getLinkedInstancesById(const QString &id) const
{
QStringList linkedInstances;
for (auto inst : m_instances) {
if (inst->isLinkedToInstanceId(id))
linkedInstances.append(inst->id());
}
return linkedInstances;
}
int InstanceList::rowCount(const QModelIndex& parent) const int InstanceList::rowCount(const QModelIndex& parent) const
{ {
Q_UNUSED(parent); Q_UNUSED(parent);
@ -787,7 +797,9 @@ class InstanceStaging : public Task {
connect(child, &Task::aborted, this, &InstanceStaging::childAborted); connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable); connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable);
connect(child, &Task::status, this, &InstanceStaging::setStatus); connect(child, &Task::status, this, &InstanceStaging::setStatus);
connect(child, &Task::details, this, &InstanceStaging::setDetails);
connect(child, &Task::progress, this, &InstanceStaging::setProgress); connect(child, &Task::progress, this, &InstanceStaging::setProgress);
connect(child, &Task::stepProgress, this, &InstanceStaging::propogateStepProgress);
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded); connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
} }
@ -865,7 +877,7 @@ Task* InstanceList::wrapInstanceTask(InstanceTask* task)
QString InstanceList::getStagedInstancePath() QString InstanceList::getStagedInstancePath()
{ {
QString key = QUuid::createUuid().toString(); QString key = QUuid::createUuid().toString(QUuid::WithoutBraces);
QString tempDir = ".LAUNCHER_TEMP/"; QString tempDir = ".LAUNCHER_TEMP/";
QString relPath = FS::PathCombine(tempDir, key); QString relPath = FS::PathCombine(tempDir, key);
QDir rootPath(m_instDir); QDir rootPath(m_instDir);

View File

@ -154,6 +154,8 @@ public:
QStringList mimeTypes() const override; QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override; QMimeData *mimeData(const QModelIndexList &indexes) const override;
QStringList getLinkedInstancesById(const QString &id) const;
signals: signals:
void dataIsInvalid(); void dataIsInvalid();
void instancesChanged(); void instancesChanged();

View File

@ -36,9 +36,10 @@ public:
values.append(new VersionPage(onesix.get())); values.append(new VersionPage(onesix.get()));
values.append(ManagedPackPage::createPage(onesix.get())); values.append(ManagedPackPage::createPage(onesix.get()));
auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList()); auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList());
modsPage->setFilter("%1 (*.zip *.jar *.litemod)"); modsPage->setFilter("%1 (*.zip *.jar *.litemod *.nilmod)");
values.append(modsPage); values.append(modsPage);
values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList())); values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList()));
values.append(new NilModFolderPage(onesix.get(), onesix->nilModList()));
values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList())); values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList()));
values.append(new TexturePackPage(onesix.get(), onesix->texturePackList())); values.append(new TexturePackPage(onesix.get(), onesix->texturePackList()));
values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList())); values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList()));

View File

@ -122,8 +122,7 @@ void JavaCommon::TestCheck::run()
return; return;
} }
checker.reset(new JavaChecker()); checker.reset(new JavaChecker());
connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this, connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinished);
SLOT(checkFinished(JavaCheckResult)));
checker->m_path = m_path; checker->m_path = m_path;
checker->performCheck(); checker->performCheck();
} }
@ -137,8 +136,7 @@ void JavaCommon::TestCheck::checkFinished(JavaCheckResult result)
return; return;
} }
checker.reset(new JavaChecker()); checker.reset(new JavaChecker());
connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this, connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinishedWithArgs);
SLOT(checkFinishedWithArgs(JavaCheckResult)));
checker->m_path = m_path; checker->m_path = m_path;
checker->m_args = m_args; checker->m_args = m_args;
checker->m_minMem = m_minMem; checker->m_minMem = m_minMem;

View File

@ -112,7 +112,15 @@ void LaunchController::decideAccount()
} }
} }
m_accountToUse = accounts->defaultAccount(); // Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used
auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString();
auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId);
if (instanceAccountIndex == -1) {
m_accountToUse = accounts->defaultAccount();
} else {
m_accountToUse = accounts->at(instanceAccountIndex);
}
if (!m_accountToUse) if (!m_accountToUse)
{ {
// If no default account is set, ask the user which one to use. // If no default account is set, ask the user which one to use.
@ -374,15 +382,15 @@ void LaunchController::launchInstance()
} }
resolved_servers = resolved_servers + "]\n\n"; resolved_servers = resolved_servers + "]\n\n";
} }
m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::Launcher)); m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
} else { } else {
online_mode = m_demo ? "demo" : "offline"; online_mode = m_demo ? "demo" : "offline";
} }
m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher));
// Prepend Version // Prepend Version
m_launcher->prependStep(new TextPrint(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher)); m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher));
m_launcher->start(); m_launcher->start();
} }

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022,2023 Sefa Eyeoglu <contact@scrumplex.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -88,9 +88,12 @@ private slots:
private: private:
void changeState(LoggedProcess::State state); void changeState(LoggedProcess::State state);
QStringList reprocess(const QByteArray& data, QTextDecoder& decoder);
private: private:
QTextDecoder m_err_decoder = QTextDecoder(QTextCodec::codecForLocale()); QTextDecoder m_err_decoder = QTextDecoder(QTextCodec::codecForLocale());
QTextDecoder m_out_decoder = QTextDecoder(QTextCodec::codecForLocale()); QTextDecoder m_out_decoder = QTextDecoder(QTextCodec::codecForLocale());
QString m_leftover_line;
bool m_killed = false; bool m_killed = false;
State m_state = NotRunning; State m_state = NotRunning;
int m_exit_code = 0; int m_exit_code = 0;

View File

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

View File

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

View File

@ -94,20 +94,28 @@ bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &containe
return true; return true;
} }
bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files) bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks)
{ {
QDir directory(dir); QDir directory(dir);
if (!directory.exists()) return false; if (!directory.exists()) return false;
for (auto e : files) { for (auto e : files) {
auto filePath = directory.relativeFilePath(e.absoluteFilePath()); auto filePath = directory.relativeFilePath(e.absoluteFilePath());
if( !JlCompress::compressFile(zip, e.absoluteFilePath(), filePath)) return false; auto srcPath = e.absoluteFilePath();
if (followSymlinks) {
if (e.isSymLink()) {
srcPath = e.symLinkTarget();
} else {
srcPath = e.canonicalFilePath();
}
}
if( !JlCompress::compressFile(zip, srcPath, filePath)) return false;
} }
return true; return true;
} }
bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files) bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
{ {
QuaZip zip(fileCompressed); QuaZip zip(fileCompressed);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
@ -116,7 +124,7 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList
return false; return false;
} }
auto result = compressDirFiles(&zip, dir, files); auto result = compressDirFiles(&zip, dir, files, followSymlinks);
zip.close(); zip.close();
if(zip.getZipError()!=0) { if(zip.getZipError()!=0) {
@ -275,7 +283,8 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re
// ours // ours
std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
{ {
QDir directory(target); auto target_top_dir = QUrl::fromLocalFile(target);
QStringList extracted; QStringList extracted;
qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target;
@ -294,48 +303,53 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
return std::nullopt; return std::nullopt;
} }
do do {
{ QString file_name = zip->getCurrentFileName();
QString name = zip->getCurrentFileName(); if (!file_name.startsWith(subdir))
if(!name.startsWith(subdir))
{
continue; continue;
}
name.remove(0, subdir.size()); auto relative_file_name = QDir::fromNativeSeparators(file_name.remove(0, subdir.size()));
auto original_name = name; auto original_name = relative_file_name;
// Fix subdirs/files ending with a / getting transformed into absolute paths
if (relative_file_name.startsWith('/'))
relative_file_name = relative_file_name.mid(1);
// Fix weird "folders with a single file get squashed" thing // Fix weird "folders with a single file get squashed" thing
QString path; QString sub_path;
if(name.contains('/') && !name.endsWith('/')){ if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) {
path = name.section('/', 0, -2) + "/"; sub_path = relative_file_name.section('/', 0, -2) + '/';
FS::ensureFolderPathExists(FS::PathCombine(target, path)); FS::ensureFolderPathExists(FS::PathCombine(target, sub_path));
name = name.split('/').last(); relative_file_name = relative_file_name.split('/').last();
} }
QString absFilePath; QString target_file_path;
if(name.isEmpty()) if (relative_file_name.isEmpty()) {
{ target_file_path = target + '/';
absFilePath = directory.absoluteFilePath(name) + "/"; } else {
} target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name);
else if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/'))
{ target_file_path += '/';
absFilePath = directory.absoluteFilePath(path + name);
} }
if (!JlCompress::extractFile(zip, "", absFilePath)) if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
{ qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" << target;
qWarning() << "Failed to extract file" << original_name << "to" << absFilePath; return std::nullopt;
}
if (!JlCompress::extractFile(zip, "", target_file_path)) {
qWarning() << "Failed to extract file" << original_name << "to" << target_file_path;
JlCompress::removeFile(extracted); JlCompress::removeFile(extracted);
return std::nullopt; return std::nullopt;
} }
extracted.append(absFilePath); extracted.append(target_file_path);
QFile::setPermissions(absFilePath, QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser); QFile::setPermissions(target_file_path, QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser);
qDebug() << "Extracted file" << name << "to" << absFilePath; qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (zip->goToNextFile()); } while (zip->goToNextFile());
return extracted; return extracted;
} }

View File

@ -59,18 +59,20 @@ namespace MMCZip
* \param zip target archive * \param zip target archive
* \param dir directory that will be compressed (to compress with relative paths) * \param dir directory that will be compressed (to compress with relative paths)
* \param files list of files to compress * \param files list of files to compress
* \param followSymlinks should follow symlinks when compressing file data
* \return true for success or false for failure * \return true for success or false for failure
*/ */
bool compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files); bool compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks = false);
/** /**
* Compress directory, by providing a list of files to compress * Compress directory, by providing a list of files to compress
* \param fileCompressed target archive file * \param fileCompressed target archive file
* \param dir directory that will be compressed (to compress with relative paths) * \param dir directory that will be compressed (to compress with relative paths)
* \param files list of files to compress * \param files list of files to compress
* \param followSymlinks should follow symlinks when compressing file data
* \return true for success or false for failure * \return true for success or false for failure
*/ */
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files); bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false);
/** /**
* take a source jar, add mods to it, resulting in target jar * take a source jar, add mods to it, resulting in target jar

View File

@ -19,6 +19,7 @@
#include <QStringList> #include <QStringList>
#include <QDir> #include <QDir>
#include <QString> #include <QString>
#include <QSysInfo>
#include <QtGlobal> #include <QtGlobal>
#include "MangoHud.h" #include "MangoHud.h"
@ -75,9 +76,27 @@ QString getLibraryString()
} }
for (QString vkLayer : vkLayerList) { for (QString vkLayer : vkLayerList) {
QString filePath = FS::PathCombine(vkLayer, "MangoHud.json"); // prefer to use architecture specific vulkan layers
if (!QFile::exists(filePath)) QString currentArch = QSysInfo::currentCpuArchitecture();
if (currentArch == "arm64") {
currentArch = "aarch64";
}
QStringList manifestNames = { QString("MangoHud.%1.json").arg(currentArch), "MangoHud.json" };
QString filePath = "";
for (QString manifestName : manifestNames) {
QString tryPath = FS::PathCombine(vkLayer, manifestName);
if (QFile::exists(tryPath)) {
filePath = tryPath;
break;
}
}
if (filePath.isEmpty()) {
continue; continue;
}
auto conf = Json::requireDocument(filePath, vkLayer); auto conf = Json::requireDocument(filePath, vkLayer);
auto confObject = Json::requireObject(conf, vkLayer); auto confObject = Json::requireObject(conf, vkLayer);

34
launcher/Markdown.h Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -25,32 +25,32 @@
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h" #include "minecraft/mod/tasks/LocalModUpdateTask.h"
class ModFolderModel; class ResourceFolderModel;
class ModDownloadTask : public SequentialTask { class ResourceDownloadTask : public SequentialTask {
Q_OBJECT Q_OBJECT
public: public:
explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed = true); explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr<ResourceFolderModel> packs, bool is_indexed = true);
const QString& getFilename() const { return m_mod_version.fileName; } const QString& getFilename() const { return m_pack_version.fileName; }
const QString& getCustomPath() const { return m_pack_version.custom_target_folder; }
const QVariant& getVersionID() const { return m_pack_version.fileId; }
private: private:
ModPlatform::IndexedPack m_mod; ModPlatform::IndexedPack m_pack;
ModPlatform::IndexedVersion m_mod_version; ModPlatform::IndexedVersion m_pack_version;
const std::shared_ptr<ModFolderModel> mods; const std::shared_ptr<ResourceFolderModel> m_pack_model;
NetJob::Ptr m_filesNetJob; NetJob::Ptr m_filesNetJob;
LocalModUpdateTask::Ptr m_update_task; LocalModUpdateTask::Ptr m_update_task;
void downloadProgressChanged(qint64 current, qint64 total); void downloadProgressChanged(qint64 current, qint64 total);
void downloadFailed(QString reason); void downloadFailed(QString reason);
void downloadSucceeded(); void downloadSucceeded();
std::tuple<QString, QString> to_delete {"", ""}; std::tuple<QString, QString> to_delete {"", ""};
private slots: private slots:
void hasOldMod(QString name, QString filename); void hasOldResource(QString name, QString filename);
}; };

View File

@ -1,5 +1,45 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "StringUtils.h" #include "StringUtils.h"
#include <QRegularExpression>
#include <QUuid>
#include <cmath>
/// If you're wondering where these came from exactly, then know you're not the only one =D /// If you're wondering where these came from exactly, then know you're not the only one =D
/// TAKEN FROM Qt, because it doesn't expose it intelligently /// TAKEN FROM Qt, because it doesn't expose it intelligently
@ -74,3 +114,71 @@ int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSe
// The two strings are the same (02 == 2) so fall back to the normal sort // The two strings are the same (02 == 2) so fall back to the normal sort
return QString::compare(s1, s2, cs); return QString::compare(s1, s2, cs);
} }
QString StringUtils::truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit)
{
auto display_options = QUrl::RemoveUserInfo | QUrl::RemoveFragment | QUrl::NormalizePathSegments;
auto str_url = url.toDisplayString(display_options);
if (str_url.length() <= max_len)
return str_url;
auto url_path_parts = url.path().split('/');
QString last_path_segment = url_path_parts.takeLast();
if (url_path_parts.size() >= 1 && url_path_parts.first().isEmpty())
url_path_parts.removeFirst(); // drop empty first segment (from leading / )
if (url_path_parts.size() >= 1)
url_path_parts.removeLast(); // drop the next to last path segment
auto url_template = QStringLiteral("%1://%2/%3%4");
auto url_compact = url_path_parts.isEmpty()
? url_template.arg(url.scheme(), url.host(), QStringList({ "...", last_path_segment }).join('/'), url.query())
: url_template.arg(url.scheme(), url.host(),
QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), url.query());
// remove url parts one by one if it's still too long
while (url_compact.length() > max_len && url_path_parts.size() >= 1) {
url_path_parts.removeLast(); // drop the next to last path segment
url_compact = url_path_parts.isEmpty()
? url_template.arg(url.scheme(), url.host(), QStringList({ "...", last_path_segment }).join('/'), url.query())
: url_template.arg(url.scheme(), url.host(),
QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), url.query());
}
if ((url_compact.length() >= max_len) && hard_limit) {
// still too long, truncate normaly
url_compact = QString(str_url);
auto to_remove = url_compact.length() - max_len + 3;
url_compact.remove(url_compact.length() - to_remove - 1, to_remove);
url_compact.append("...");
}
return url_compact;
}
static const QStringList s_units_si{ "KB", "MB", "GB", "TB" };
static const QStringList s_units_kibi{ "KiB", "MiB", "GiB", "TiB" };
QString StringUtils::humanReadableFileSize(double bytes, bool use_si, int decimal_points)
{
const QStringList units = use_si ? s_units_si : s_units_kibi;
const int scale = use_si ? 1000 : 1024;
int u = -1;
double r = pow(10, decimal_points);
do {
bytes /= scale;
u++;
} while (round(abs(bytes) * r) / r >= scale && u < units.length() - 1);
return QString::number(bytes, 'f', 2) + " " + units[u];
}
QString StringUtils::getRandomAlphaNumeric()
{
return QUuid::createUuid().toString(QUuid::Id128);
}

View File

@ -1,6 +1,43 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once #pragma once
#include <QString> #include <QString>
#include <QUrl>
namespace StringUtils { namespace StringUtils {
@ -29,4 +66,17 @@ inline QString fromStdString(string s)
#endif #endif
int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs); 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.
*/
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();
} // namespace StringUtils } // namespace StringUtils

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -66,9 +66,8 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren
m_watcher.reset(new QFileSystemWatcher()); m_watcher.reset(new QFileSystemWatcher());
is_watching = false; is_watching = false;
connect(m_watcher.get(), SIGNAL(directoryChanged(QString)), connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged);
SLOT(directoryChanged(QString))); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged);
connect(m_watcher.get(), SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
directoryChanged(path); directoryChanged(path);

View File

@ -87,15 +87,11 @@ void JavaChecker::performCheck()
process->setProcessEnvironment(CleanEnviroment()); process->setProcessEnvironment(CleanEnviroment());
qDebug() << "Running java checker: " + m_path + args.join(" ");; qDebug() << "Running java checker: " + m_path + args.join(" ");;
connect(process.get(), SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished(int, QProcess::ExitStatus))); connect(process.get(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &JavaChecker::finished);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error);
connect(process.get(), SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError))); connect(process.get(), &QProcess::readyReadStandardOutput, this, &JavaChecker::stdoutReady);
#else connect(process.get(), &QProcess::readyReadStandardError, this, &JavaChecker::stderrReady);
connect(process.get(), SIGNAL(error(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError))); connect(&killTimer, &QTimer::timeout, this, &JavaChecker::timeout);
#endif
connect(process.get(), SIGNAL(readyReadStandardOutput()), this, SLOT(stdoutReady()));
connect(process.get(), SIGNAL(readyReadStandardError()), this, SLOT(stderrReady()));
connect(&killTimer, SIGNAL(timeout()), SLOT(timeout()));
killTimer.setSingleShot(true); killTimer.setSingleShot(true);
killTimer.start(15000); killTimer.start(15000);
process->start(); process->start();

View File

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

View File

@ -67,7 +67,7 @@ void JavaInstallList::load()
if(m_status != Status::InProgress) if(m_status != Status::InProgress)
{ {
m_status = Status::InProgress; m_status = Status::InProgress;
m_loadTask = new JavaListLoadTask(this); m_loadTask.reset(new JavaListLoadTask(this));
m_loadTask->start(); m_loadTask->start();
} }
} }
@ -167,7 +167,7 @@ void JavaListLoadTask::executeTask()
JavaUtils ju; JavaUtils ju;
QList<QString> candidate_paths = ju.FindJavaPaths(); QList<QString> candidate_paths = ju.FindJavaPaths();
m_job = new JavaCheckerJob("Java detection"); m_job.reset(new JavaCheckerJob("Java detection"));
connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished); connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished);
connect(m_job.get(), &Task::progress, this, &Task::setProgress); connect(m_job.get(), &Task::progress, this, &Task::setProgress);

View File

@ -412,8 +412,6 @@ QList<QString> JavaUtils::FindJavaPaths()
#elif defined(Q_OS_LINUX) #elif defined(Q_OS_LINUX)
QList<QString> JavaUtils::FindJavaPaths() QList<QString> JavaUtils::FindJavaPaths()
{ {
qDebug() << "Linux Java detection incomplete - defaulting to \"java\"";
QList<QString> javas; QList<QString> javas;
javas.append(this->GetDefaultJava()->path); javas.append(this->GetDefaultJava()->path);
auto scanJavaDir = [&](const QString & dirPath) auto scanJavaDir = [&](const QString & dirPath)
@ -421,20 +419,11 @@ QList<QString> JavaUtils::FindJavaPaths()
QDir dir(dirPath); QDir dir(dirPath);
if(!dir.exists()) if(!dir.exists())
return; return;
auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for(auto & entry: entries) for(auto & entry: entries)
{ {
QString prefix; QString prefix;
if(entry.isAbsolute()) prefix = entry.canonicalFilePath();
{
prefix = entry.absoluteFilePath();
}
else
{
prefix = entry.filePath();
}
javas.append(FS::PathCombine(prefix, "jre/bin/java")); javas.append(FS::PathCombine(prefix, "jre/bin/java"));
javas.append(FS::PathCombine(prefix, "bin/java")); javas.append(FS::PathCombine(prefix, "bin/java"));
} }

View File

@ -93,7 +93,7 @@ void CheckJava::executeTask()
|| storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0 || storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0
|| storedVendor.size() == 0) || storedVendor.size() == 0)
{ {
m_JavaChecker = new JavaChecker(); m_JavaChecker.reset(new JavaChecker);
emit logLine(QString("Checking Java version..."), MessageLevel::Launcher); emit logLine(QString("Checking Java version..."), MessageLevel::Launcher);
connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished); connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished);
m_JavaChecker->m_path = realJavaPath; m_JavaChecker->m_path = realJavaPath;

View File

@ -26,9 +26,11 @@ void Update::executeTask()
m_updateTask.reset(m_parent->instance()->createUpdateTask(m_mode)); m_updateTask.reset(m_parent->instance()->createUpdateTask(m_mode));
if(m_updateTask) if(m_updateTask)
{ {
connect(m_updateTask.get(), SIGNAL(finished()), this, SLOT(updateFinished())); connect(m_updateTask.get(), &Task::finished, this, &Update::updateFinished);
connect(m_updateTask.get(), &Task::progress, this, &Task::setProgress); connect(m_updateTask.get(), &Task::progress, this, &Update::setProgress);
connect(m_updateTask.get(), &Task::status, this, &Task::setStatus); connect(m_updateTask.get(), &Task::stepProgress, this, &Update::propogateStepProgress);
connect(m_updateTask.get(), &Task::status, this, &Update::setStatus);
connect(m_updateTask.get(), &Task::details, this, &Update::setDetails);
emit progressReportingRequest(); emit progressReportingRequest();
return; return;
} }

View File

@ -126,7 +126,7 @@ void Meta::BaseEntity::load(Net::Mode loadType)
{ {
return; return;
} }
m_updateTask = new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()), APPLICATION->network()); m_updateTask.reset(new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()), APPLICATION->network()));
auto url = this->url(); auto url = this->url();
auto entry = APPLICATION->metacache()->resolveEntry("meta", localFilename()); auto entry = APPLICATION->metacache()->resolveEntry("meta", localFilename());
entry->setStale(true); entry->setStale(true);

View File

@ -99,6 +99,11 @@ QString Meta::Version::localFilename() const
return m_uid + '/' + m_version + ".json"; return m_uid + '/' + m_version + ".json";
} }
::Version Meta::Version::toComparableVersion() const
{
return { const_cast<Meta::Version*>(this)->descriptor() };
}
void Meta::Version::setType(const QString &type) void Meta::Version::setType(const QString &type)
{ {
m_type = type; m_type = type;

View File

@ -16,6 +16,7 @@
#pragma once #pragma once
#include "BaseVersion.h" #include "BaseVersion.h"
#include "../Version.h"
#include <QJsonObject> #include <QJsonObject>
#include <QStringList> #include <QStringList>
@ -85,6 +86,8 @@ public:
QString localFilename() const override; QString localFilename() const override;
[[nodiscard]] ::Version toComparableVersion() const;
public: // for usage by format parsers only public: // for usage by format parsers only
void setType(const QString &type); void setType(const QString &type);
void setTime(const qint64 time); void setTime(const qint64 time);

View File

@ -340,7 +340,7 @@ QString AssetObject::getRelPath()
NetJob::Ptr AssetsIndex::getDownloadJob() NetJob::Ptr AssetsIndex::getDownloadJob()
{ {
auto job = new NetJob(QObject::tr("Assets for %1").arg(id), APPLICATION->network()); auto job = makeShared<NetJob>(QObject::tr("Assets for %1").arg(id), APPLICATION->network());
for (auto &object : objects.values()) for (auto &object : objects.values())
{ {
auto dl = object.getDownloadAction(); auto dl = object.getDownloadAction();

View File

@ -572,7 +572,7 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly)
// add stuff... // add stuff...
for(auto &add: toAdd) for(auto &add: toAdd)
{ {
ComponentPtr component = new Component(d->m_list, add.uid); auto component = makeShared<Component>(d->m_list, add.uid);
if(!add.equalsVersion.isEmpty()) if(!add.equalsVersion.isEmpty())
{ {
// exact version // exact version

View File

@ -192,6 +192,10 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunch", false);
m_settings->registerSetting("JoinServerOnLaunchAddress", ""); m_settings->registerSetting("JoinServerOnLaunchAddress", "");
// Use account for instance, this does not have a global override
m_settings->registerSetting("UseAccountForInstance", false);
m_settings->registerSetting("InstanceAccountId", "");
qDebug() << "Instance-type specific settings were loaded!"; qDebug() << "Instance-type specific settings were loaded!";
setSpecificSettingsLoaded(true); setSpecificSettingsLoaded(true);
@ -286,6 +290,11 @@ QString MinecraftInstance::coreModsDir() const
return FS::PathCombine(gameRoot(), "coremods"); return FS::PathCombine(gameRoot(), "coremods");
} }
QString MinecraftInstance::nilModsDir() const
{
return FS::PathCombine(gameRoot(), "nilmods");
}
QString MinecraftInstance::resourcePacksDir() const QString MinecraftInstance::resourcePacksDir() const
{ {
return FS::PathCombine(gameRoot(), "resourcepacks"); return FS::PathCombine(gameRoot(), "resourcepacks");
@ -457,8 +466,8 @@ QMap<QString, QString> MinecraftInstance::getVariables()
QMap<QString, QString> out; QMap<QString, QString> out;
out.insert("INST_NAME", name()); out.insert("INST_NAME", name());
out.insert("INST_ID", id()); out.insert("INST_ID", id());
out.insert("INST_DIR", QDir(instanceRoot()).absolutePath()); out.insert("INST_DIR", QDir::toNativeSeparators(QDir(instanceRoot()).absolutePath()));
out.insert("INST_MC_DIR", QDir(gameRoot()).absolutePath()); out.insert("INST_MC_DIR", QDir::toNativeSeparators(QDir(gameRoot()).absolutePath()));
out.insert("INST_JAVA", settings()->get("JavaPath").toString()); out.insert("INST_JAVA", settings()->get("JavaPath").toString());
out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); out.insert("INST_JAVA_ARGS", javaArguments().join(' '));
return out; return out;
@ -916,7 +925,10 @@ QString MinecraftInstance::getStatusbarDescription()
if(m_settings->get("ShowGameTime").toBool()) if(m_settings->get("ShowGameTime").toBool())
{ {
if (lastTimePlayed() > 0) { if (lastTimePlayed() > 0) {
description.append(tr(", last played for %1").arg(Time::prettifyDuration(lastTimePlayed()))); QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch());
description.append(tr(", last played on %1 for %2")
.arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat))
.arg(Time::prettifyDuration(lastTimePlayed())));
} }
if (totalTimePlayed() > 0) { if (totalTimePlayed() > 0) {
@ -958,12 +970,12 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
// print a header // print a header
{ {
process->appendStep(new TextPrint(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); process->appendStep(makeShared<TextPrint>(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher));
} }
// check java // check java
{ {
process->appendStep(new CheckJava(pptr)); process->appendStep(makeShared<CheckJava>(pptr));
} }
// check launch method // check launch method
@ -971,13 +983,13 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
QString method = launchMethod(); QString method = launchMethod();
if(!validMethods.contains(method)) if(!validMethods.contains(method))
{ {
process->appendStep(new TextPrint(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal)); process->appendStep(makeShared<TextPrint>(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal));
return process; return process;
} }
// create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732)
{ {
process->appendStep(new CreateGameFolders(pptr)); process->appendStep(makeShared<CreateGameFolders>(pptr));
} }
if (!serverToJoin && settings()->get("JoinServerOnLaunch").toBool()) if (!serverToJoin && settings()->get("JoinServerOnLaunch").toBool())
@ -989,7 +1001,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
if(serverToJoin && serverToJoin->port == 25565) if(serverToJoin && serverToJoin->port == 25565)
{ {
// Resolve server address to join on launch // Resolve server address to join on launch
auto *step = new LookupServerAddress(pptr); auto step = makeShared<LookupServerAddress>(pptr);
step->setLookupAddress(serverToJoin->address); step->setLookupAddress(serverToJoin->address);
step->setOutputAddressPtr(serverToJoin); step->setOutputAddressPtr(serverToJoin);
process->appendStep(step); process->appendStep(step);
@ -998,7 +1010,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
// run pre-launch command if that's needed // run pre-launch command if that's needed
if(getPreLaunchCommand().size()) if(getPreLaunchCommand().size())
{ {
auto step = new PreLaunchCommand(pptr); auto step = makeShared<PreLaunchCommand>(pptr);
step->setWorkingDirectory(gameRoot()); step->setWorkingDirectory(gameRoot());
process->appendStep(step); process->appendStep(step);
} }
@ -1007,43 +1019,43 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
if(session->status != AuthSession::PlayableOffline) if(session->status != AuthSession::PlayableOffline)
{ {
if(!session->demo) { if(!session->demo) {
process->appendStep(new ClaimAccount(pptr, session)); process->appendStep(makeShared<ClaimAccount>(pptr, session));
} }
process->appendStep(new Update(pptr, Net::Mode::Online)); process->appendStep(makeShared<Update>(pptr, Net::Mode::Online));
} }
else else
{ {
process->appendStep(new Update(pptr, Net::Mode::Offline)); process->appendStep(makeShared<Update>(pptr, Net::Mode::Offline));
} }
// if there are any jar mods // if there are any jar mods
{ {
process->appendStep(new ModMinecraftJar(pptr)); process->appendStep(makeShared<ModMinecraftJar>(pptr));
} }
// Scan mods folders for mods // Scan mods folders for mods
{ {
process->appendStep(new ScanModFolders(pptr)); process->appendStep(makeShared<ScanModFolders>(pptr));
} }
// print some instance info here... // print some instance info here...
{ {
process->appendStep(new PrintInstanceInfo(pptr, session, serverToJoin)); process->appendStep(makeShared<PrintInstanceInfo>(pptr, session, serverToJoin));
} }
// extract native jars if needed // extract native jars if needed
{ {
process->appendStep(new ExtractNatives(pptr)); process->appendStep(makeShared<ExtractNatives>(pptr));
} }
// reconstruct assets if needed // reconstruct assets if needed
{ {
process->appendStep(new ReconstructAssets(pptr)); process->appendStep(makeShared<ReconstructAssets>(pptr));
} }
// verify that minimum Java requirements are met // verify that minimum Java requirements are met
{ {
process->appendStep(new VerifyJavaInstall(pptr)); process->appendStep(makeShared<VerifyJavaInstall>(pptr));
} }
{ {
@ -1051,7 +1063,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
auto method = launchMethod(); auto method = launchMethod();
if(method == "LauncherPart") if(method == "LauncherPart")
{ {
auto step = new LauncherPartLaunch(pptr); auto step = makeShared<LauncherPartLaunch>(pptr);
step->setWorkingDirectory(gameRoot()); step->setWorkingDirectory(gameRoot());
step->setAuthSession(session); step->setAuthSession(session);
step->setServerToJoin(serverToJoin); step->setServerToJoin(serverToJoin);
@ -1059,7 +1071,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
} }
else if (method == "DirectJava") else if (method == "DirectJava")
{ {
auto step = new DirectJavaLaunch(pptr); auto step = makeShared<DirectJavaLaunch>(pptr);
step->setWorkingDirectory(gameRoot()); step->setWorkingDirectory(gameRoot());
step->setAuthSession(session); step->setAuthSession(session);
step->setServerToJoin(serverToJoin); step->setServerToJoin(serverToJoin);
@ -1070,7 +1082,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
// run post-exit command if that's needed // run post-exit command if that's needed
if(getPostExitCommand().size()) if(getPostExitCommand().size())
{ {
auto step = new PostLaunchCommand(pptr); auto step = makeShared<PostLaunchCommand>(pptr);
step->setWorkingDirectory(gameRoot()); step->setWorkingDirectory(gameRoot());
process->appendStep(step); process->appendStep(step);
} }
@ -1080,8 +1092,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
} }
if(m_settings->get("QuitAfterGameStop").toBool()) if(m_settings->get("QuitAfterGameStop").toBool())
{ {
auto step = new QuitAfterGameStop(pptr); process->appendStep(makeShared<QuitAfterGameStop>(pptr));
process->appendStep(step);
} }
m_launchProcess = process; m_launchProcess = process;
emit launchTaskChanged(m_launchProcess); emit launchTaskChanged(m_launchProcess);
@ -1098,67 +1109,79 @@ JavaVersion MinecraftInstance::getJavaVersion()
return JavaVersion(settings()->get("JavaVersion").toString()); return JavaVersion(settings()->get("JavaVersion").toString());
} }
std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList()
{ {
if (!m_loader_mod_list) if (!m_loader_mod_list)
{ {
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_loader_mod_list.reset(new ModFolderModel(modsRoot(), is_indexed)); m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed));
m_loader_mod_list->disableInteraction(isRunning()); m_loader_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction);
} }
return m_loader_mod_list; return m_loader_mod_list;
} }
std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList()
{ {
if (!m_core_mod_list) if (!m_core_mod_list)
{ {
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_core_mod_list.reset(new ModFolderModel(coreModsDir(), is_indexed)); m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed));
m_core_mod_list->disableInteraction(isRunning()); m_core_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction);
} }
return m_core_mod_list; return m_core_mod_list;
} }
std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() const std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList()
{
if (!m_nil_mod_list)
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false));
m_nil_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_nil_mod_list.get(), &ModFolderModel::disableInteraction);
}
return m_nil_mod_list;
}
std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList()
{ {
if (!m_resource_pack_list) if (!m_resource_pack_list)
{ {
m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir())); m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this));
} }
return m_resource_pack_list; return m_resource_pack_list;
} }
std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() const std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList()
{ {
if (!m_texture_pack_list) if (!m_texture_pack_list)
{ {
m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir())); m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this));
} }
return m_texture_pack_list; return m_texture_pack_list;
} }
std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList()
{ {
if (!m_shader_pack_list) if (!m_shader_pack_list)
{ {
m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir())); m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this));
} }
return m_shader_pack_list; return m_shader_pack_list;
} }
std::shared_ptr<WorldList> MinecraftInstance::worldList() const std::shared_ptr<WorldList> MinecraftInstance::worldList()
{ {
if (!m_world_list) if (!m_world_list)
{ {
m_world_list.reset(new WorldList(worldDir())); m_world_list.reset(new WorldList(worldDir(), this));
} }
return m_world_list; return m_world_list;
} }
std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel() const std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel()
{ {
if (!m_game_options) if (!m_game_options)
{ {

View File

@ -84,6 +84,7 @@ public:
QString shaderPacksDir() const; QString shaderPacksDir() const;
QString modsRoot() const override; QString modsRoot() const override;
QString coreModsDir() const; QString coreModsDir() const;
QString nilModsDir() const;
QString modsCacheLocation() const; QString modsCacheLocation() const;
QString libDir() const; QString libDir() const;
QString worldDir() const; QString worldDir() const;
@ -114,13 +115,14 @@ public:
std::shared_ptr<PackProfile> getPackProfile() const; std::shared_ptr<PackProfile> getPackProfile() const;
////// Mod Lists ////// ////// Mod Lists //////
std::shared_ptr<ModFolderModel> loaderModList() const; std::shared_ptr<ModFolderModel> loaderModList();
std::shared_ptr<ModFolderModel> coreModList() const; std::shared_ptr<ModFolderModel> coreModList();
std::shared_ptr<ResourcePackFolderModel> resourcePackList() const; std::shared_ptr<ModFolderModel> nilModList();
std::shared_ptr<TexturePackFolderModel> texturePackList() const; std::shared_ptr<ResourcePackFolderModel> resourcePackList();
std::shared_ptr<ShaderPackFolderModel> shaderPackList() const; std::shared_ptr<TexturePackFolderModel> texturePackList();
std::shared_ptr<WorldList> worldList() const; std::shared_ptr<ShaderPackFolderModel> shaderPackList();
std::shared_ptr<GameOptions> gameOptionsModel() const; std::shared_ptr<WorldList> worldList();
std::shared_ptr<GameOptions> gameOptionsModel();
////// Launch stuff ////// ////// Launch stuff //////
Task::Ptr createUpdateTask(Net::Mode mode) override; Task::Ptr createUpdateTask(Net::Mode mode) override;
@ -170,6 +172,7 @@ protected: // data
std::shared_ptr<PackProfile> m_components; std::shared_ptr<PackProfile> m_components;
mutable std::shared_ptr<ModFolderModel> m_loader_mod_list; mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
mutable std::shared_ptr<ModFolderModel> m_core_mod_list; mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
mutable std::shared_ptr<ModFolderModel> m_nil_mod_list;
mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list; mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list;
mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list; mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list;
mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list; mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list;

View File

@ -22,6 +22,7 @@ void MinecraftLoadAndCheck::executeTask()
connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::subtaskFailed); connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::subtaskFailed);
connect(m_task.get(), &Task::aborted, this, [this]{ subtaskFailed(tr("Aborted")); }); connect(m_task.get(), &Task::aborted, this, [this]{ subtaskFailed(tr("Aborted")); });
connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::progress); connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::progress);
connect(m_task.get(), &Task::stepProgress, this, &MinecraftLoadAndCheck::propogateStepProgress);
connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus);
} }

View File

@ -43,7 +43,7 @@ void MinecraftUpdate::executeTask()
m_tasks.clear(); m_tasks.clear();
// create folders // create folders
{ {
m_tasks.append(new FoldersTask(m_inst)); m_tasks.append(makeShared<FoldersTask>(m_inst));
} }
// add metadata update task if necessary // add metadata update task if necessary
@ -59,17 +59,17 @@ void MinecraftUpdate::executeTask()
// libraries download // libraries download
{ {
m_tasks.append(new LibrariesTask(m_inst)); m_tasks.append(makeShared<LibrariesTask>(m_inst));
} }
// FML libraries download and copy into the instance // FML libraries download and copy into the instance
{ {
m_tasks.append(new FMLLibrariesTask(m_inst)); m_tasks.append(makeShared<FMLLibrariesTask>(m_inst));
} }
// assets update // assets update
{ {
m_tasks.append(new AssetUpdateTask(m_inst)); m_tasks.append(makeShared<AssetUpdateTask>(m_inst));
} }
if(!m_preFailure.isEmpty()) if(!m_preFailure.isEmpty())
@ -100,7 +100,9 @@ void MinecraftUpdate::next()
disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
disconnect(task.get(), &Task::aborted, this, &Task::abort); disconnect(task.get(), &Task::aborted, this, &Task::abort);
disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
disconnect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propogateStepProgress);
disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
disconnect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails);
} }
if(m_currentTask == m_tasks.size()) if(m_currentTask == m_tasks.size())
{ {
@ -118,7 +120,9 @@ void MinecraftUpdate::next()
connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
connect(task.get(), &Task::aborted, this, &Task::abort); connect(task.get(), &Task::aborted, this, &Task::abort);
connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
connect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propogateStepProgress);
connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
connect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails);
// if the task is already running, do not start it again // if the task is already running, do not start it again
if(!task->isRunning()) if(!task->isRunning())
{ {

View File

@ -39,6 +39,8 @@
#include "minecraft/ParseUtils.h" #include "minecraft/ParseUtils.h"
#include <minecraft/MojangVersionFormat.h> #include <minecraft/MojangVersionFormat.h>
#include <QRegularExpression>
using namespace Json; using namespace Json;
static void readString(const QJsonObject &root, const QString &key, QString &variable) static void readString(const QJsonObject &root, const QString &key, QString &variable)
@ -121,6 +123,15 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
out->uid = root.value("fileId").toString(); out->uid = root.value("fileId").toString();
} }
const QRegularExpression valid_uid_regex{ QRegularExpression::anchoredPattern(QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) };
if (!valid_uid_regex.match(out->uid).hasMatch()) {
qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid;
out->addProblem(
ProblemSeverity::Error,
QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.")
);
}
out->version = root.value("version").toString(); out->version = root.value("version").toString();
MojangVersionFormat::readVersionProperties(root, out.get()); MojangVersionFormat::readVersionProperties(root, out.get());

View File

@ -1,7 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu <contact@scrumplex.net>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022-2023 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -49,18 +52,20 @@
#include "minecraft/OneSixVersionFormat.h" #include "minecraft/OneSixVersionFormat.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/ProfileUtils.h"
#include "Json.h" #include "Json.h"
#include "PackProfile.h" #include "PackProfile.h"
#include "PackProfile_p.h" #include "PackProfile_p.h"
#include "ComponentUpdateTask.h" #include "ComponentUpdateTask.h"
#include "modplatform/ModAPI.h" #include "Application.h"
#include "modplatform/ResourceAPI.h"
static const QMap<QString, ModAPI::ModLoaderType> modloaderMapping{ static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{
{"net.minecraftforge", ModAPI::Forge}, {"net.minecraftforge", ResourceAPI::Forge},
{"net.fabricmc.fabric-loader", ModAPI::Fabric}, {"net.fabricmc.fabric-loader", ResourceAPI::Fabric},
{"org.quiltmc.quilt-loader", ModAPI::Quilt} {"org.quiltmc.quilt-loader", ResourceAPI::Quilt}
}; };
PackProfile::PackProfile(MinecraftInstance * instance) PackProfile::PackProfile(MinecraftInstance * instance)
@ -129,7 +134,7 @@ static ComponentPtr componentFromJsonV1(PackProfile * parent, const QString & co
// critical // critical
auto uid = Json::requireString(obj.value("uid")); auto uid = Json::requireString(obj.value("uid"));
auto filePath = componentJsonPattern.arg(uid); auto filePath = componentJsonPattern.arg(uid);
auto component = new Component(parent, uid); auto component = makeShared<Component>(parent, uid);
component->m_version = Json::ensureString(obj.value("version")); component->m_version = Json::ensureString(obj.value("version"));
component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false); component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false);
component->m_important = Json::ensureBoolean(obj.value("important"), false); component->m_important = Json::ensureBoolean(obj.value("important"), false);
@ -517,23 +522,23 @@ bool PackProfile::revertToBase(int index)
return true; return true;
} }
Component * PackProfile::getComponent(const QString &id) ComponentPtr PackProfile::getComponent(const QString &id)
{ {
auto iter = d->componentIndex.find(id); auto iter = d->componentIndex.find(id);
if (iter == d->componentIndex.end()) if (iter == d->componentIndex.end())
{ {
return nullptr; return nullptr;
} }
return (*iter).get(); return (*iter);
} }
Component * PackProfile::getComponent(int index) ComponentPtr PackProfile::getComponent(int index)
{ {
if(index < 0 || index >= d->components.size()) if(index < 0 || index >= d->components.size())
{ {
return nullptr; return nullptr;
} }
return d->components[index].get(); return d->components[index];
} }
QVariant PackProfile::data(const QModelIndex &index, int role) const QVariant PackProfile::data(const QModelIndex &index, int role) const
@ -729,16 +734,47 @@ void PackProfile::invalidateLaunchProfile()
void PackProfile::installJarMods(QStringList selectedFiles) void PackProfile::installJarMods(QStringList selectedFiles)
{ {
// FIXME: get rid of _internal
installJarMods_internal(selectedFiles); installJarMods_internal(selectedFiles);
} }
void PackProfile::installCustomJar(QString selectedFile) void PackProfile::installCustomJar(QString selectedFile)
{ {
// FIXME: get rid of _internal
installCustomJar_internal(selectedFile); installCustomJar_internal(selectedFile);
} }
bool PackProfile::installComponents(QStringList selectedFiles)
{
const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
if (!FS::ensureFolderPathExists(patchDir))
return false;
bool result = true;
for (const QString& source : selectedFiles) {
const QFileInfo sourceInfo(source);
auto versionFile = ProfileUtils::parseJsonFile(sourceInfo, false);
const QString target = FS::PathCombine(patchDir, versionFile->uid + ".json");
if (!QFile::copy(source, target)) {
qWarning() << "Component" << source << "could not be copied to target" << target;
result = false;
continue;
}
appendComponent(makeShared<Component>(this, versionFile->uid, versionFile));
}
scheduleSave();
invalidateLaunchProfile();
return result;
}
void PackProfile::installAgents(QStringList selectedFiles) void PackProfile::installAgents(QStringList selectedFiles)
{ {
// FIXME: get rid of _internal
installAgents_internal(selectedFiles); installAgents_internal(selectedFiles);
} }
@ -764,7 +800,7 @@ bool PackProfile::installEmpty(const QString& uid, const QString& name)
file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
file.close(); file.close();
appendComponent(new Component(this, f->uid, f)); appendComponent(makeShared<Component>(this, f->uid, f));
scheduleSave(); scheduleSave();
invalidateLaunchProfile(); invalidateLaunchProfile();
return true; return true;
@ -871,7 +907,7 @@ bool PackProfile::installJarMods_internal(QStringList filepaths)
file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
file.close(); file.close();
appendComponent(new Component(this, f->uid, f)); appendComponent(makeShared<Component>(this, f->uid, f));
} }
scheduleSave(); scheduleSave();
invalidateLaunchProfile(); invalidateLaunchProfile();
@ -932,7 +968,7 @@ bool PackProfile::installCustomJar_internal(QString filepath)
file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
file.close(); file.close();
appendComponent(new Component(this, f->uid, f)); appendComponent(makeShared<Component>(this, f->uid, f));
scheduleSave(); scheduleSave();
invalidateLaunchProfile(); invalidateLaunchProfile();
@ -988,7 +1024,7 @@ bool PackProfile::installAgents_internal(QStringList filepaths)
patchFile.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson()); patchFile.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson());
patchFile.close(); patchFile.close();
appendComponent(new Component(this, versionFile->uid, versionFile)); appendComponent(makeShared<Component>(this, versionFile->uid, versionFile));
} }
scheduleSave(); scheduleSave();
@ -1037,7 +1073,7 @@ bool PackProfile::setComponentVersion(const QString& uid, const QString& version
else else
{ {
// add new // add new
auto component = new Component(this, uid); auto component = makeShared<Component>(this, uid);
component->m_version = version; component->m_version = version;
component->m_important = important; component->m_important = important;
appendComponent(component); appendComponent(component);
@ -1066,19 +1102,22 @@ void PackProfile::disableInteraction(bool disable)
} }
} }
ModAPI::ModLoaderTypes PackProfile::getModLoaders() std::optional<ResourceAPI::ModLoaderTypes> PackProfile::getModLoaders()
{ {
ModAPI::ModLoaderTypes result = ModAPI::Unspecified; ResourceAPI::ModLoaderTypes result;
bool has_any_loader = false;
QMapIterator<QString, ModAPI::ModLoaderType> i(modloaderMapping); QMapIterator<QString, ResourceAPI::ModLoaderType> i(modloaderMapping);
while (i.hasNext()) while (i.hasNext()) {
{
i.next(); i.next();
Component* c = getComponent(i.key()); if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) {
if (c != nullptr && c->isEnabled()) {
result |= i.value(); result |= i.value();
has_any_loader = true;
} }
} }
if (!has_any_loader)
return {};
return result; return result;
} }

View File

@ -1,7 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu <contact@scrumplex.net>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022-2023 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -49,7 +52,7 @@
#include "BaseVersion.h" #include "BaseVersion.h"
#include "MojangDownloadInfo.h" #include "MojangDownloadInfo.h"
#include "net/Mode.h" #include "net/Mode.h"
#include "modplatform/ModAPI.h" #include "modplatform/ResourceAPI.h"
class MinecraftInstance; class MinecraftInstance;
struct PackProfileData; struct PackProfileData;
@ -86,6 +89,9 @@ public:
/// install a jar/zip as a replacement for the main jar /// install a jar/zip as a replacement for the main jar
void installCustomJar(QString selectedFile); void installCustomJar(QString selectedFile);
/// install MMC/Prism component files
bool installComponents(QStringList selectedFiles);
/// install Java agent files /// install Java agent files
void installAgents(QStringList selectedFiles); void installAgents(QStringList selectedFiles);
@ -136,16 +142,16 @@ signals:
public: public:
/// get the profile component by id /// get the profile component by id
Component * getComponent(const QString &id); ComponentPtr getComponent(const QString &id);
/// get the profile component by index /// get the profile component by index
Component * getComponent(int index); ComponentPtr getComponent(int index);
/// Add the component to the internal list of patches /// Add the component to the internal list of patches
// todo(merged): is this the best approach // todo(merged): is this the best approach
void appendComponent(ComponentPtr component); void appendComponent(ComponentPtr component);
ModAPI::ModLoaderTypes getModLoaders(); std::optional<ResourceAPI::ModLoaderTypes> getModLoaders();
private: private:
void scheduleSave(); void scheduleSave();

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -55,6 +56,8 @@
#include <optional> #include <optional>
#include "FileSystem.h"
using std::optional; using std::optional;
using std::nullopt; using std::nullopt;
@ -545,6 +548,10 @@ bool World::replace(World &with)
bool World::destroy() bool World::destroy()
{ {
if(!is_valid) return false; if(!is_valid) return false;
if (FS::trash(m_containerFile.filePath()))
return true;
if (m_containerFile.isDir()) if (m_containerFile.isDir())
{ {
QDir d(m_containerFile.filePath()); QDir d(m_containerFile.filePath());
@ -562,3 +569,25 @@ bool World::operator==(const World &other) const
{ {
return is_valid == other.is_valid && folderName() == other.folderName(); return is_valid == other.is_valid && folderName() == other.folderName();
} }
bool World::isSymLinkUnder(const QString& instPath) const
{
if (isSymLink())
return true;
auto instDir = QDir(instPath);
auto relAbsPath = instDir.relativeFilePath(m_containerFile.absoluteFilePath());
auto relCanonPath = instDir.relativeFilePath(m_containerFile.canonicalFilePath());
return relAbsPath != relCanonPath;
}
bool World::isMoreThanOneHardLink() const
{
if (m_containerFile.isDir())
{
return FS::hardLinkCount(QDir(m_containerFile.absoluteFilePath()).filePath("level.dat")) > 1;
}
return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1;
}

View File

@ -95,6 +95,21 @@ public:
// WEAK compare operator - used for replacing worlds // WEAK compare operator - used for replacing worlds
bool operator==(const World &other) const; bool operator==(const World &other) const;
[[nodiscard]] auto isSymLink() const -> bool{ return m_containerFile.isSymLink(); }
/**
* @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance
*
* @param instPath path to an instance directory
* @return true
* @return false
*/
[[nodiscard]] bool isSymLinkUnder(const QString& instPath) const;
[[nodiscard]] bool isMoreThanOneHardLink() const;
QString canonicalFilePath() const { return m_containerFile.canonicalFilePath(); }
private: private:
void readFromZip(const QFileInfo &file); void readFromZip(const QFileInfo &file);
void readFromFS(const QFileInfo &file); void readFromFS(const QFileInfo &file);

View File

@ -45,16 +45,15 @@
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QDebug> #include <QDebug>
WorldList::WorldList(const QString &dir) WorldList::WorldList(const QString &dir, BaseInstance* instance)
: QAbstractListModel(), m_dir(dir) : QAbstractListModel(), m_instance(instance), m_dir(dir)
{ {
FS::ensureFolderPathExists(m_dir.absolutePath()); FS::ensureFolderPathExists(m_dir.absolutePath());
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
m_watcher = new QFileSystemWatcher(this); m_watcher = new QFileSystemWatcher(this);
is_watching = false; is_watching = false;
connect(m_watcher, SIGNAL(directoryChanged(QString)), this, connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged);
SLOT(directoryChanged(QString)));
} }
void WorldList::startWatching() void WorldList::startWatching()
@ -128,6 +127,10 @@ bool WorldList::isValid()
return m_dir.exists() && m_dir.isReadable(); return m_dir.exists() && m_dir.isReadable();
} }
QString WorldList::instDirPath() const {
return QFileInfo(m_instance->instanceRoot()).absoluteFilePath();
}
bool WorldList::deleteWorld(int index) bool WorldList::deleteWorld(int index)
{ {
if (index >= worlds.size() || index < 0) if (index >= worlds.size() || index < 0)
@ -173,7 +176,7 @@ bool WorldList::resetIcon(int row)
int WorldList::columnCount(const QModelIndex &parent) const int WorldList::columnCount(const QModelIndex &parent) const
{ {
return parent.isValid()? 0 : 4; return parent.isValid()? 0 : 5;
} }
QVariant WorldList::data(const QModelIndex &index, int role) const QVariant WorldList::data(const QModelIndex &index, int role) const
@ -207,6 +210,14 @@ QVariant WorldList::data(const QModelIndex &index, int role) const
case SizeColumn: case SizeColumn:
return locale.formattedDataSize(world.bytes()); return locale.formattedDataSize(world.bytes());
case InfoColumn:
if (world.isSymLinkUnder(instDirPath())) {
return tr("This world is symbolically linked from elsewhere.");
}
if (world.isMoreThanOneHardLink()) {
return tr("\nThis world is hard linked elsewhere.");
}
return "";
default: default:
return QVariant(); return QVariant();
} }
@ -222,7 +233,16 @@ QVariant WorldList::data(const QModelIndex &index, int role) const
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
{ {
if (column == InfoColumn) {
if (world.isSymLinkUnder(instDirPath())) {
return tr("Warning: This world is symbolically linked from elsewhere. Editing it will also change the original."
"\nCanonical Path: %1").arg(world.canonicalFilePath());
}
if (world.isMoreThanOneHardLink()) {
return tr("Warning: This world is hard linked elsewhere. Editing it will also change the original.");
}
}
return world.folderName(); return world.folderName();
} }
case ObjectRole: case ObjectRole:
@ -274,6 +294,9 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol
case SizeColumn: case SizeColumn:
//: World size on disk //: World size on disk
return tr("Size"); return tr("Size");
case InfoColumn:
//: special warnings?
return tr("Info");
default: default:
return QVariant(); return QVariant();
} }
@ -289,6 +312,8 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol
return tr("Date and time the world was last played."); return tr("Date and time the world was last played.");
case SizeColumn: case SizeColumn:
return tr("Size of the world on disk."); return tr("Size of the world on disk.");
case InfoColumn:
return tr("Information and warnings about the world.");
default: default:
return QVariant(); return QVariant();
} }

View File

@ -21,6 +21,7 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QMimeData> #include <QMimeData>
#include "minecraft/World.h" #include "minecraft/World.h"
#include "BaseInstance.h"
class QFileSystemWatcher; class QFileSystemWatcher;
@ -33,7 +34,8 @@ public:
NameColumn, NameColumn,
GameModeColumn, GameModeColumn,
LastPlayedColumn, LastPlayedColumn,
SizeColumn SizeColumn,
InfoColumn
}; };
enum Roles enum Roles
@ -48,7 +50,7 @@ public:
IconFileRole IconFileRole
}; };
WorldList(const QString &dir); WorldList(const QString &dir, BaseInstance* instance);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
@ -112,6 +114,8 @@ public:
return m_dir; return m_dir;
} }
QString instDirPath() const;
const QList<World> &allWorlds() const const QList<World> &allWorlds() const
{ {
return worlds; return worlds;
@ -124,6 +128,7 @@ signals:
void changed(); void changed();
protected: protected:
BaseInstance* m_instance;
QFileSystemWatcher *m_watcher; QFileSystemWatcher *m_watcher;
bool is_watching; bool is_watching;
QDir m_dir; QDir m_dir;

View File

@ -55,12 +55,12 @@ void AuthRequest::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) {
reply_ = APPLICATION->network()->get(request_); reply_ = APPLICATION->network()->get(request_);
status_ = Requesting; status_ = Requesting;
timedReplies_.add(new Katabasis::Reply(reply_, timeout)); timedReplies_.add(new Katabasis::Reply(reply_, timeout));
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
connect(reply_, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError);
#else #else // &QNetworkReply::error SIGNAL depricated
connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError);
#endif #endif
connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished);
connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
} }
@ -70,14 +70,14 @@ void AuthRequest::post(const QNetworkRequest &req, const QByteArray &data, int t
status_ = Requesting; status_ = Requesting;
reply_ = APPLICATION->network()->post(request_, data_); reply_ = APPLICATION->network()->post(request_, data_);
timedReplies_.add(new Katabasis::Reply(reply_, timeout)); timedReplies_.add(new Katabasis::Reply(reply_, timeout));
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
connect(reply_, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError);
#else #else // &QNetworkReply::error SIGNAL depricated
connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError);
#endif #endif
connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished);
connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); connect(reply_, &QNetworkReply::uploadProgress, this, &AuthRequest::onUploadProgress);
} }
void AuthRequest::onRequestFinished() { void AuthRequest::onRequestFinished() {

View File

@ -75,7 +75,7 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) {
MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username) MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username)
{ {
MinecraftAccountPtr account = new MinecraftAccount(); auto account = makeShared<MinecraftAccount>();
account->data.type = AccountType::Mojang; account->data.type = AccountType::Mojang;
account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
@ -91,7 +91,7 @@ MinecraftAccountPtr MinecraftAccount::createBlankMSA()
MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username) MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username)
{ {
MinecraftAccountPtr account = new MinecraftAccount(); auto account = makeShared<MinecraftAccount>();
account->data.type = AccountType::Offline; account->data.type = AccountType::Offline;
account->data.yggdrasilToken.token = "offline"; account->data.yggdrasilToken.token = "offline";
account->data.yggdrasilToken.validity = Katabasis::Validity::Certain; account->data.yggdrasilToken.validity = Katabasis::Validity::Certain;
@ -133,8 +133,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password) {
Q_ASSERT(m_currentTask.get() == nullptr); Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new MojangLogin(&data, password)); m_currentTask.reset(new MojangLogin(&data, password));
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
emit activityChanged(true); emit activityChanged(true);
return m_currentTask; return m_currentTask;
@ -144,8 +144,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() {
Q_ASSERT(m_currentTask.get() == nullptr); Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new MSAInteractive(&data)); m_currentTask.reset(new MSAInteractive(&data));
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
emit activityChanged(true); emit activityChanged(true);
return m_currentTask; return m_currentTask;
@ -155,8 +155,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginOffline() {
Q_ASSERT(m_currentTask.get() == nullptr); Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new OfflineLogin(&data)); m_currentTask.reset(new OfflineLogin(&data));
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
emit activityChanged(true); emit activityChanged(true);
return m_currentTask; return m_currentTask;
@ -177,8 +177,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() {
m_currentTask.reset(new MojangRefresh(&data)); m_currentTask.reset(new MojangRefresh(&data));
} }
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
emit activityChanged(true); emit activityChanged(true);
return m_currentTask; return m_currentTask;

View File

@ -1,5 +1,6 @@
#include "Parsers.h" #include "Parsers.h"
#include "Json.h" #include "Json.h"
#include "Logging.h"
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonArray> #include <QJsonArray>
@ -75,9 +76,7 @@ bool getBool(QJsonValue value, bool & out) {
bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString name) { bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString name) {
qDebug() << "Parsing" << name <<":"; qDebug() << "Parsing" << name <<":";
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
QJsonParseError jsonError; QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) { if(jsonError.error) {
@ -137,9 +136,7 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString na
bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
qDebug() << "Parsing Minecraft profile..."; qDebug() << "Parsing Minecraft profile...";
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
QJsonParseError jsonError; QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
@ -275,9 +272,7 @@ decoded base64 "value":
bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) {
qDebug() << "Parsing Minecraft profile..."; qDebug() << "Parsing Minecraft profile...";
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
QJsonParseError jsonError; QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
@ -389,9 +384,7 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) {
bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) { bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) {
qDebug() << "Parsing Minecraft entitlements..."; qDebug() << "Parsing Minecraft entitlements...";
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
QJsonParseError jsonError; QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
@ -424,9 +417,7 @@ bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output)
bool parseRolloutResponse(QByteArray & data, bool& result) { bool parseRolloutResponse(QByteArray & data, bool& result) {
qDebug() << "Parsing Rollout response..."; qDebug() << "Parsing Rollout response...";
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
QJsonParseError jsonError; QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
@ -455,9 +446,7 @@ bool parseRolloutResponse(QByteArray & data, bool& result) {
bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) {
QJsonParseError jsonError; QJsonParseError jsonError;
qDebug() << "Parsing Mojang response..."; qDebug() << "Parsing Mojang response...";
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) { if(jsonError.error) {
qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " << jsonError.errorString(); qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " << jsonError.errorString();

View File

@ -10,28 +10,28 @@
#include "minecraft/auth/steps/GetSkinStep.h" #include "minecraft/auth/steps/GetSkinStep.h"
MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent) { MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent) {
m_steps.append(new MSAStep(m_data, MSAStep::Action::Refresh)); m_steps.append(makeShared<MSAStep>(m_data, MSAStep::Action::Refresh));
m_steps.append(new XboxUserStep(m_data)); m_steps.append(makeShared<XboxUserStep>(m_data));
m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
m_steps.append(new LauncherLoginStep(m_data)); m_steps.append(makeShared<LauncherLoginStep>(m_data));
m_steps.append(new XboxProfileStep(m_data)); m_steps.append(makeShared<XboxProfileStep>(m_data));
m_steps.append(new EntitlementsStep(m_data)); m_steps.append(makeShared<EntitlementsStep>(m_data));
m_steps.append(new MinecraftProfileStep(m_data)); m_steps.append(makeShared<MinecraftProfileStep>(m_data));
m_steps.append(new GetSkinStep(m_data)); m_steps.append(makeShared<GetSkinStep>(m_data));
} }
MSAInteractive::MSAInteractive( MSAInteractive::MSAInteractive(
AccountData* data, AccountData* data,
QObject* parent QObject* parent
) : AuthFlow(data, parent) { ) : AuthFlow(data, parent) {
m_steps.append(new MSAStep(m_data, MSAStep::Action::Login)); m_steps.append(makeShared<MSAStep>(m_data, MSAStep::Action::Login));
m_steps.append(new XboxUserStep(m_data)); m_steps.append(makeShared<XboxUserStep>(m_data));
m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
m_steps.append(new LauncherLoginStep(m_data)); m_steps.append(makeShared<LauncherLoginStep>(m_data));
m_steps.append(new XboxProfileStep(m_data)); m_steps.append(makeShared<XboxProfileStep>(m_data));
m_steps.append(new EntitlementsStep(m_data)); m_steps.append(makeShared<EntitlementsStep>(m_data));
m_steps.append(new MinecraftProfileStep(m_data)); m_steps.append(makeShared<MinecraftProfileStep>(m_data));
m_steps.append(new GetSkinStep(m_data)); m_steps.append(makeShared<GetSkinStep>(m_data));
} }

View File

@ -9,10 +9,10 @@ MojangRefresh::MojangRefresh(
AccountData *data, AccountData *data,
QObject *parent QObject *parent
) : AuthFlow(data, parent) { ) : AuthFlow(data, parent) {
m_steps.append(new YggdrasilStep(m_data, QString())); m_steps.append(makeShared<YggdrasilStep>(m_data, QString()));
m_steps.append(new MinecraftProfileStepMojang(m_data)); m_steps.append(makeShared<MinecraftProfileStepMojang>(m_data));
m_steps.append(new MigrationEligibilityStep(m_data)); m_steps.append(makeShared<MigrationEligibilityStep>(m_data));
m_steps.append(new GetSkinStep(m_data)); m_steps.append(makeShared<GetSkinStep>(m_data));
} }
MojangLogin::MojangLogin( MojangLogin::MojangLogin(
@ -20,8 +20,8 @@ MojangLogin::MojangLogin(
QString password, QString password,
QObject *parent QObject *parent
): AuthFlow(data, parent), m_password(password) { ): AuthFlow(data, parent), m_password(password) {
m_steps.append(new YggdrasilStep(m_data, m_password)); m_steps.append(makeShared<YggdrasilStep>(m_data, m_password));
m_steps.append(new MinecraftProfileStepMojang(m_data)); m_steps.append(makeShared<MinecraftProfileStepMojang>(m_data));
m_steps.append(new MigrationEligibilityStep(m_data)); m_steps.append(makeShared<MigrationEligibilityStep>(m_data));
m_steps.append(new GetSkinStep(m_data)); m_steps.append(makeShared<GetSkinStep>(m_data));
} }

View File

@ -6,12 +6,12 @@ OfflineRefresh::OfflineRefresh(
AccountData *data, AccountData *data,
QObject *parent QObject *parent
) : AuthFlow(data, parent) { ) : AuthFlow(data, parent) {
m_steps.append(new OfflineStep(m_data)); m_steps.append(makeShared<OfflineStep>(m_data));
} }
OfflineLogin::OfflineLogin( OfflineLogin::OfflineLogin(
AccountData *data, AccountData *data,
QObject *parent QObject *parent
) : AuthFlow(data, parent) { ) : AuthFlow(data, parent) {
m_steps.append(new OfflineStep(m_data)); m_steps.append(makeShared<OfflineStep>(m_data));
} }

View File

@ -3,6 +3,7 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QUuid> #include <QUuid>
#include "Logging.h"
#include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
@ -41,9 +42,7 @@ void EntitlementsStep::onRequestDone(
auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater(); requestor->deleteLater();
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
// TODO: check presence of same entitlementsRequestId? // TODO: check presence of same entitlementsRequestId?
// TODO: validate JWTs? // TODO: validate JWTs?

View File

@ -2,9 +2,10 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include "Logging.h"
#include "minecraft/auth/AccountTask.h"
#include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "minecraft/auth/AccountTask.h"
#include "net/NetUtils.h" #include "net/NetUtils.h"
LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) { LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {
@ -51,14 +52,10 @@ void LauncherLoginStep::onRequestDone(
auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater(); requestor->deleteLater();
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
if (error != QNetworkReply::NoError) { if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error; qWarning() << "Reply error:" << error;
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
if (Net::isApplicationError(error)) { if (Net::isApplicationError(error)) {
emit finished( emit finished(
AccountTaskState::STATE_FAILED_SOFT, AccountTaskState::STATE_FAILED_SOFT,
@ -76,9 +73,7 @@ void LauncherLoginStep::onRequestDone(
if(!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) { if(!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) {
qWarning() << "Could not parse login_with_xbox response..."; qWarning() << "Could not parse login_with_xbox response...";
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
emit finished( emit finished(
AccountTaskState::STATE_FAILED_SOFT, AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to parse the Minecraft access token response.") tr("Failed to parse the Minecraft access token response.")

View File

@ -42,6 +42,7 @@
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "Application.h" #include "Application.h"
#include "Logging.h"
using OAuth2 = Katabasis::DeviceFlow; using OAuth2 = Katabasis::DeviceFlow;
using Activity = Katabasis::Activity; using Activity = Katabasis::Activity;
@ -117,14 +118,12 @@ void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity) {
// Succeeded or did not invalidate tokens // Succeeded or did not invalidate tokens
emit hideVerificationUriAndCode(); emit hideVerificationUriAndCode();
QVariantMap extraTokens = m_oauth2->extraTokens(); QVariantMap extraTokens = m_oauth2->extraTokens();
#ifndef NDEBUG
if (!extraTokens.isEmpty()) { if (!extraTokens.isEmpty()) {
qDebug() << "Extra tokens in response:"; qCDebug(authCredentials()) << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) { foreach (QString key, extraTokens.keys()) {
qDebug() << "\t" << key << ":" << extraTokens.value(key); qCDebug(authCredentials()) << "\t" << key << ":" << extraTokens.value(key);
} }
} }
#endif
emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
return; return;
} }

View File

@ -2,6 +2,7 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include "Logging.h"
#include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h" #include "net/NetUtils.h"
@ -40,9 +41,7 @@ void MinecraftProfileStep::onRequestDone(
auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater(); requestor->deleteLater();
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
if (error == QNetworkReply::ContentNotFoundError) { if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state. // NOTE: Succeed even if we do not have a profile. This is a valid account state.
if(m_data->type == AccountType::Mojang) { if(m_data->type == AccountType::Mojang) {

View File

@ -2,6 +2,7 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include "Logging.h"
#include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h" #include "net/NetUtils.h"
@ -43,9 +44,7 @@ void MinecraftProfileStepMojang::onRequestDone(
auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater(); requestor->deleteLater();
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
if (error == QNetworkReply::ContentNotFoundError) { if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state. // NOTE: Succeed even if we do not have a profile. This is a valid account state.
if(m_data->type == AccountType::Mojang) { if(m_data->type == AccountType::Mojang) {

View File

@ -4,6 +4,7 @@
#include <QJsonParseError> #include <QJsonParseError>
#include <QJsonDocument> #include <QJsonDocument>
#include "Logging.h"
#include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h" #include "net/NetUtils.h"
@ -58,9 +59,7 @@ void XboxAuthorizationStep::onRequestDone(
auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater(); requestor->deleteLater();
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
if (error != QNetworkReply::NoError) { if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error; qWarning() << "Reply error:" << error;
if (Net::isApplicationError(error)) { if (Net::isApplicationError(error)) {

View File

@ -3,7 +3,7 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QUrlQuery> #include <QUrlQuery>
#include "Logging.h"
#include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h" #include "net/NetUtils.h"
@ -56,9 +56,7 @@ void XboxProfileStep::onRequestDone(
if (error != QNetworkReply::NoError) { if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error; qWarning() << "Reply error:" << error;
#ifndef NDEBUG qCDebug(authCredentials()) << data;
qDebug() << data;
#endif
if (Net::isApplicationError(error)) { if (Net::isApplicationError(error)) {
emit finished( emit finished(
AccountTaskState::STATE_FAILED_SOFT, AccountTaskState::STATE_FAILED_SOFT,
@ -74,9 +72,7 @@ void XboxProfileStep::onRequestDone(
return; return;
} }
#ifndef NDEBUG qCDebug(authCredentials()) << "XBox profile: " << data;
qDebug() << "XBox profile: " << data;
#endif
emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile")); emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile"));
} }

View File

@ -38,6 +38,10 @@ void XboxUserStep::perform() {
QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json"); request.setRawHeader("Accept", "application/json");
// set contract-verison header (prevent err 400 bad-request?)
// https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders
request.setRawHeader("x-xbl-contract-version", "1");
auto *requestor = new AuthRequest(this); auto *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone); connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone);
requestor->post(request, xbox_auth_data.toUtf8()); requestor->post(request, xbox_auth_data.toUtf8());

View File

@ -36,6 +36,7 @@
#include "LauncherPartLaunch.h" #include "LauncherPartLaunch.h"
#include <QStandardPaths> #include <QStandardPaths>
#include <QRegularExpression>
#include "launch/LaunchTask.h" #include "launch/LaunchTask.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"

View File

@ -55,6 +55,12 @@ void ScanModFolders::executeTask()
if(!cores->update()) { if(!cores->update()) {
m_coreModsDone = true; m_coreModsDone = true;
} }
auto nils = m_inst->nilModList();
connect(nils.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone);
if(!nils->update()) {
m_nilModsDone = true;
}
checkDone(); checkDone();
} }
@ -70,9 +76,15 @@ void ScanModFolders::coreModsDone()
checkDone(); checkDone();
} }
void ScanModFolders::nilModsDone()
{
m_nilModsDone = true;
checkDone();
}
void ScanModFolders::checkDone() void ScanModFolders::checkDone()
{ {
if(m_modsDone && m_coreModsDone) { if(m_modsDone && m_coreModsDone && m_nilModsDone) {
emitSucceeded(); emitSucceeded();
} }
} }

View File

@ -33,10 +33,12 @@ public:
private slots: private slots:
void coreModsDone(); void coreModsDone();
void modsDone(); void modsDone();
void nilModsDone();
private: private:
void checkDone(); void checkDone();
private: // DATA private: // DATA
bool m_modsDone = false; bool m_modsDone = false;
bool m_nilModsDone = false;
bool m_coreModsDone = false; bool m_coreModsDone = false;
}; };

View File

@ -0,0 +1,108 @@
// 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 "DataPack.h"
#include <QDebug>
#include <QMap>
#include <QRegularExpression>
#include "Version.h"
// Values taken from:
// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_data_pack#%22pack_format%22
static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } },
{ 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } },
{ 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } },
{ 10, { Version("1.19"), Version("1.19.3") } },
};
void DataPack::setPackFormat(int new_format_id)
{
QMutexLocker locker(&m_data_lock);
if (!s_pack_format_versions.contains(new_format_id)) {
qWarning() << "Pack format '" << new_format_id << "' is not a recognized data pack id!";
}
m_pack_format = new_format_id;
}
void DataPack::setDescription(QString new_description)
{
QMutexLocker locker(&m_data_lock);
m_description = new_description;
}
std::pair<Version, Version> DataPack::compatibleVersions() const
{
if (!s_pack_format_versions.contains(m_pack_format)) {
return { {}, {} };
}
return s_pack_format_versions.constFind(m_pack_format).value();
}
std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) const
{
auto const& cast_other = static_cast<DataPack const&>(other);
switch (type) {
default: {
auto res = Resource::compare(other, type);
if (res.first != 0)
return res;
}
case SortType::PACK_FORMAT: {
auto this_ver = packFormat();
auto other_ver = cast_other.packFormat();
if (this_ver > other_ver)
return { 1, type == SortType::PACK_FORMAT };
if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT };
}
}
return { 0, false };
}
bool DataPack::applyFilter(QRegularExpression filter) const
{
if (filter.match(description()).hasMatch())
return true;
if (filter.match(QString::number(packFormat())).hasMatch())
return true;
if (filter.match(compatibleVersions().first.toString()).hasMatch())
return true;
if (filter.match(compatibleVersions().second.toString()).hasMatch())
return true;
return Resource::applyFilter(filter);
}
bool DataPack::valid() const
{
return m_pack_format != 0;
}

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