diff --git a/.envrc b/.envrc
new file mode 100644
index 000000000..3550a30f2
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f415741d9..691e257bf 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -15,6 +15,12 @@ on:
SPARKLE_ED25519_KEY:
description: Private key for signing Sparkle updates
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:
description: Private token for authenticating against Cachix cache
required: false
@@ -40,6 +46,7 @@ jobs:
- os: windows-2022
name: "Windows-MinGW-w64"
msystem: clang64
+ vcvars_arch: 'amd64_x86'
- os: windows-2022
name: "Windows-MSVC-Legacy"
@@ -61,7 +68,7 @@ jobs:
qt_ver: 6
qt_host: windows
qt_arch: ''
- qt_version: '6.4.0'
+ qt_version: '6.5.1'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
@@ -73,7 +80,7 @@ jobs:
qt_ver: 6
qt_host: windows
qt_arch: 'win64_msvc2019_arm64'
- qt_version: '6.4.0'
+ qt_version: '6.5.1'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
@@ -83,7 +90,7 @@ jobs:
qt_ver: 6
qt_host: mac
qt_arch: ''
- qt_version: '6.3.0'
+ qt_version: '6.5.1'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
@@ -105,6 +112,7 @@ jobs:
INSTALL_APPIMAGE_DIR: "install-appdir"
BUILD_DIR: "build"
CCACHE_VAR: ""
+ HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
steps:
##
@@ -135,6 +143,7 @@ jobs:
quazip-qt6:p
ccache:p
qt6-5compat:p
+ cmark:p
- name: Force newer ccache
if: runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug'
@@ -143,10 +152,19 @@ jobs:
- name: Setup ccache
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:
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)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
shell: msys2 {0}
@@ -163,15 +181,6 @@ jobs:
run: |
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
shell: bash
run: |
@@ -199,6 +208,8 @@ jobs:
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
uses: jurplel/install-qt-action@v3
with:
+ aqtversion: '==3.1.*'
+ py7zrversion: '>=0.20.2'
version: ${{ matrix.qt_version }}
host: 'windows'
target: 'desktop'
@@ -214,6 +225,8 @@ jobs:
if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '')
uses: jurplel/install-qt-action@v3
with:
+ aqtversion: '==3.1.*'
+ py7zrversion: '>=0.20.2'
version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }}
target: 'desktop'
@@ -223,7 +236,7 @@ jobs:
cache: ${{ inputs.is_qt_cached }}
- 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
with:
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
}
+ - 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)
if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0}
@@ -394,6 +424,15 @@ jobs:
cd ${{ env.INSTALL_DIR }}
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)
if: runner.os == 'Linux'
run: |
@@ -508,30 +547,12 @@ jobs:
with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
- snap:
- runs-on: ubuntu-20.04
- steps:
- - name: Checkout
- if: inputs.build_type == 'Debug'
- uses: actions/checkout@v3
- with:
- submodules: 'true'
- - name: Set short version
- shell: bash
- if: inputs.build_type == 'Debug'
+
+ - name: ccache stats (Windows MinGW-w64)
+ if: runner.os == 'Windows' && matrix.msystem != ''
+ shell: msys2 {0}
run: |
- ver_short=`git rev-parse --short HEAD`
- 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 }}
+ ccache -s
flatpak:
runs-on: ubuntu-latest
@@ -546,11 +567,10 @@ jobs:
submodules: 'true'
- name: Build Flatpak (Linux)
if: inputs.build_type == 'Debug'
- uses: flatpak/flatpak-github-actions/flatpak-builder@v4
+ uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: "Prism Launcher.flatpak"
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
- cache-key: flatpak-${{ github.sha }}-x86_64
nix:
runs-on: ubuntu-latest
@@ -567,7 +587,7 @@ jobs:
submodules: 'true'
- name: Install nix
if: inputs.build_type == 'Debug'
- uses: cachix/install-nix-action@v18
+ uses: cachix/install-nix-action@v21
with:
install_url: https://nixos.org/nix/install
extra_nix_config: |
diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml
index a08193a06..26ee4380b 100644
--- a/.github/workflows/trigger_builds.yml
+++ b/.github/workflows/trigger_builds.yml
@@ -31,4 +31,6 @@ jobs:
is_qt_cached: true
secrets:
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 }}
diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml
index a2f89819c..3c56a38ea 100644
--- a/.github/workflows/trigger_release.yml
+++ b/.github/workflows/trigger_release.yml
@@ -15,6 +15,9 @@ jobs:
is_qt_cached: false
secrets:
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:
needs: build_release
diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml
index ef9561cf1..eacf23099 100644
--- a/.github/workflows/winget.yml
+++ b/.github/workflows/winget.yml
@@ -7,7 +7,7 @@ jobs:
publish:
runs-on: windows-latest
steps:
- - uses: vedantmgoyal2009/winget-releaser@v1
+ - uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: PrismLauncher.PrismLauncher
version: ${{ github.event.release.tag_name }}
diff --git a/.gitignore b/.gitignore
index 010bc3235..b5523f685 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,10 +11,14 @@ html/
*.pro.user
CMakeLists.txt.user
CMakeLists.txt.user.*
+CMakeSettings.json
+/CMakeFiles
+CMakeCache.txt
/.project
/.settings
/.idea
/.vscode
+/.vs
cmake-build-*/
Debug
@@ -42,7 +46,9 @@ run/
.cache/
# Nix/NixOS
-result/
+.direnv/
+.pre-commit-config.yaml
+result
# Flatpak
.flatpak-builder
diff --git a/.gitmodules b/.gitmodules
index 95274f150..87703fee5 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -16,3 +16,6 @@
[submodule "libraries/extra-cmake-modules"]
path = libraries/extra-cmake-modules
url = https://github.com/KDE/extra-cmake-modules
+[submodule "libraries/cmark"]
+ path = libraries/cmark
+ url = https://github.com/commonmark/cmark.git
diff --git a/BUILD.md b/BUILD.md
index 2443ac567..a139039df 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -1,53 +1,3 @@
# Build Instructions
-Full build instructions will be 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
-```
-
+Full build instructions are available on [the website](https://prismlauncher.org/wiki/development/build-instructions/).
diff --git a/CMakeLists.txt b/CMakeLists.txt
index de9b6fe17..6bd12630d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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.")
# 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
-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
-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
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
# 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 ################################
-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)
endif()
+if(NOT ZLIB_FOUND)
+ set(FORCE_BUNDLED_ZLIB TRUE CACHE BOOL "")
+ mark_as_advanced(FORCE_BUNDLED_ZLIB)
+endif()
# Find the required Qt parts
include(QtVersionlessBackport)
@@ -266,8 +272,13 @@ if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find ghc_filesystem
find_package(ghc_filesystem QUIET)
+
+ # Find cmark
+ find_package(cmark QUIET)
endif()
+include(ECMQtDeclareLoggingCategory)
+
####################################### Program Info #######################################
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_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR})
+ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "${KDE_INSTALL_DATADIR}/${Launcher_Name}")
+
if(Launcher_ManPage)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6")
endif()
@@ -361,6 +374,8 @@ else()
message(FATAL_ERROR "Platform not supported")
endif()
+
+
################################ Included Libs ################################
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/systeminfo) # system information library
-add_subdirectory(libraries/hoedown) # markdown parser
add_subdirectory(libraries/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker
-if(NOT ZLIB_FOUND)
+if(FORCE_BUNDLED_ZLIB)
message(STATUS "Using bundled zlib")
+
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) # Suppress cmake warnings and allow INTERPROCEDURAL_OPTIMIZATION for zlib
set(SKIP_INSTALL_ALL ON)
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}")
add_library(ZLIB::ZLIB ALIAS zlibstatic)
set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name")
@@ -406,17 +431,26 @@ if(NOT tomlplusplus_FOUND)
else()
message(STATUS "Using system tomlplusplus")
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/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
if (NOT ghc_filesystem_FOUND)
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_library(ghcFilesystem::ghc_filesystem ALIAS ghc_filesystem)
else()
message(STATUS "Using system ghc_filesystem")
endif()
+add_subdirectory(libraries/qdcss) # css parser
############################### Built Artifacts ###############################
diff --git a/COPYING.md b/COPYING.md
index 75a5c0eb3..0221d1b08 100644
--- a/COPYING.md
+++ b/COPYING.md
@@ -1,7 +1,7 @@
## Prism 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
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,
Boston, MA 02110-1301, USA.
-## Hoedown
+## cmark
- Copyright (c) 2008, Natacha Porté
- Copyright (c) 2011, Vicent Martí
- Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors
+ Copyright (c) 2014, John MacFarlane
- Permission to use, copy, modify, and distribute this software for any
- purpose with or without fee is hereby granted, provided that the above
- copyright notice and this permission notice appear in all copies.
+ All rights reserved.
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * 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
diff --git a/README.md b/README.md
index 8765da93b..993f02f5d 100644
--- a/README.md
+++ b/README.md
@@ -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:
-[![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)
[![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)
[![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.
@@ -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:**
-[![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:**
-[![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:**
-[![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
@@ -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 (`""`).
-## 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.
diff --git a/flake.lock b/flake.lock
index 051e1664c..ad9196a9b 100644
--- a/flake.lock
+++ b/flake.lock
@@ -3,11 +3,11 @@
"flake-compat": {
"flake": false,
"locked": {
- "lastModified": 1668681692,
- "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
+ "lastModified": 1673956053,
+ "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
- "rev": "009399224d5e398d03b22badca40a37ac85412a1",
+ "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
@@ -16,6 +16,58 @@
"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": {
"flake": false,
"locked": {
@@ -34,11 +86,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1671417167,
- "narHash": "sha256-JkHam6WQOwZN1t2C2sbp1TqMv3TVRjzrdoejqfefwrM=",
+ "lastModified": 1678693419,
+ "narHash": "sha256-bbSv5yqZAW6dz+3f3f3pOUZbxpPN+3OgCljgn7P+nnQ=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "bb31220cca6d044baa6dc2715b07497a2a7c4bc7",
+ "rev": "8e3fad82be64c06fbfb9fd43993aec9ef4623936",
"type": "github"
},
"original": {
@@ -48,11 +100,55 @@
"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": {
"inputs": {
"flake-compat": "flake-compat",
+ "flake-utils": "flake-utils",
"libnbtplusplus": "libnbtplusplus",
- "nixpkgs": "nixpkgs"
+ "nixpkgs": "nixpkgs",
+ "pre-commit-hooks": "pre-commit-hooks"
}
}
},
diff --git a/flake.nix b/flake.nix
index 5615a758f..f656703ce 100644
--- a/flake.nix
+++ b/flake.nix
@@ -3,35 +3,89 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
- flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
- libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; flake = false; };
+ flake-utils.url = "github:numtide/flake-utils";
+ 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, ... }:
- let
- # User-friendly version number.
- version = builtins.substring 0 8 self.lastModifiedDate;
+ outputs = {
+ self,
+ nixpkgs,
+ 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")
- supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" ];
+ # Supported systems (qtbase is currently broken for "aarch64-darwin")
+ 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"; ... }'.
- forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
-
- # 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; };
+ packagesFn = pkgs: {
+ prismlauncher-qt5 = pkgs.libsForQt5.callPackage ./nix {
+ inherit version self libnbtplusplus;
};
- in
- {
- packages = forAllSystems (system:
- let packages = packagesFn pkgs.${system}; in
- packages // { default = packages.prismlauncher; }
- );
+ prismlauncher = pkgs.qt6Packages.callPackage ./nix {
+ inherit version self libnbtplusplus;
+ };
+ };
+ 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);
};
}
diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml
index fca306d78..0524946f0 100644
--- a/flatpak/org.prismlauncher.PrismLauncher.yml
+++ b/flatpak/org.prismlauncher.PrismLauncher.yml
@@ -32,6 +32,7 @@ modules:
config-opts:
- -DLauncher_BUILD_PLATFORM=flatpak
- -DCMAKE_BUILD_TYPE=Debug
+ - -DLauncher_QT_VERSION_MAJOR=5
build-options:
env:
JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17
@@ -39,6 +40,7 @@ modules:
sources:
- type: dir
path: ../
+ builddir: true
- name: openjdk
buildsystem: simple
build-commands:
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index f68e8792b..430a96aff 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -7,6 +7,8 @@
* Copyright (C) 2022 Sefa Eyeoglu
* Copyright (C) 2022 Lenny McLennington
* Copyright (C) 2022 Tayou
+ * Copyright (C) 2023 TheKodeToad
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -45,6 +47,7 @@
#include "net/PasteUpload.h"
#include "pathmatcher/MultiMatcher.h"
#include "pathmatcher/SimplePrefixMatcher.h"
+#include "settings/INIFile.h"
#include "ui/MainWindow.h"
#include "ui/InstanceWindow.h"
@@ -62,15 +65,11 @@
#include "ui/pages/global/APIPage.h"
#include "ui/pages/global/CustomCommandsPage.h"
-#ifdef Q_OS_WIN
-#include "ui/WinDarkmode.h"
-#include
-#endif
-
#include "ui/setupwizard/SetupWizard.h"
#include "ui/setupwizard/LanguageWizardPage.h"
#include "ui/setupwizard/JavaWizardPage.h"
#include "ui/setupwizard/PasteWizardPage.h"
+#include "ui/setupwizard/ThemeWizardPage.h"
#include "ui/dialogs/CustomMessageBox.h"
@@ -81,7 +80,9 @@
#include "ApplicationMessage.h"
#include
+#include
+#include
#include
#include
#include
@@ -105,7 +106,7 @@
#include "java/JavaUtils.h"
-#include "updater/UpdateChecker.h"
+#include "updater/ExternalUpdater.h"
#include "tools/JProfiler.h"
#include "tools/JVisualVM.h"
@@ -129,6 +130,10 @@
#include "MangoHud.h"
#endif
+#ifdef Q_OS_MAC
+#include "updater/MacSparkleUpdater.h"
+#endif
+
#if defined Q_OS_WIN32
#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. */
void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
+ static std::mutex loggerMutex;
+ const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe
+
QString out = qFormatLogMessage(type, context, msg);
out += QChar::LineFeed;
@@ -159,45 +167,6 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt
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)
@@ -259,9 +228,19 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_serverToJoin = parser.value("server");
m_profileToUse = parser.value("profile");
m_liveCheck = parser.isSet("alive");
- m_zipToImport = parser.value("import");
+
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
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"))) {
dataPath = m_rootPath;
adjustedBy = "Portable data path";
+ m_portable = true;
}
#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.
* 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";
m_peerInstance->sendMessage(activate.serialize(), timeout);
- if(!m_zipToImport.isEmpty())
+ if(!m_zipsToImport.isEmpty())
{
- ApplicationMessage import;
- import.command = "import";
- import.args.insert("path", m_zipToImport.toString());
- m_peerInstance->sendMessage(import.serialize(), timeout);
+ for (auto zip_url : m_zipsToImport) {
+ ApplicationMessage import;
+ import.command = "import";
+ import.args.insert("path", zip_url.toString());
+ m_peerInstance->sendMessage(import.serialize(), timeout);
+ }
}
}
else
@@ -435,6 +417,47 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
" " "|" " "
"%{if-category}[%{category}]: %{endif}"
"%{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.";
}
@@ -499,14 +522,10 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{
// Provide a fallback for migration from PolyMC
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
m_settings->registerSetting("IconTheme", QString("pe_colored"));
- m_settings->registerSetting("ApplicationTheme", QString("system"));
+ m_settings->registerSetting("ApplicationTheme", QString());
m_settings->registerSetting("BackgroundCat", QString("kitteh"));
// Remembered state
@@ -545,6 +564,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("InstanceDir", "instances");
m_settings->registerSetting({"CentralModsDir", "ModsDir"}, "mods");
m_settings->registerSetting("IconsDir", "icons");
+ m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
+ m_settings->registerSetting("DownloadsDirWatchRecursive", false);
// Editors
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("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?
{
@@ -687,6 +711,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->set("FlameKeyOverride", flameKey);
m_settings->reset("CFKeyOverride");
}
+ m_settings->registerSetting("ModrinthToken", "");
m_settings->registerSetting("UserAgentOverride", "");
// Init page provider
@@ -714,7 +739,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// initialize network access and proxy setup
{
- m_network = new QNetworkAccessManager();
+ m_network.reset(new QNetworkAccessManager());
QString proxyTypeStr = settings()->get("ProxyType").toString();
QString addr = settings()->get("ProxyAddr").toString();
int port = settings()->get("ProxyPort").value();
@@ -736,10 +761,10 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// initialize the updater
if(BuildConfig.UPDATER_ENABLED)
{
- auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM);
- auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json";
- qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl;
- m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL));
+ qDebug() << "Initializing updater";
+#ifdef Q_OS_MAC
+ m_updater.reset(new MacSparkleUpdater());
+#endif
qDebug() << "<> Updater started.";
}
@@ -854,12 +879,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
}
});
- {
- setIconTheme(settings()->get("IconTheme").toString());
- qDebug() << "<> Icon theme set.";
- setApplicationTheme(settings()->get("ApplicationTheme").toString(), true);
- qDebug() << "<> Application theme set.";
- }
+ applyCurrentlySelectedTheme(true);
updateCapabilities();
@@ -901,7 +921,8 @@ bool Application::createSetupWizard()
return false;
}();
bool pasteInterventionRequired = settings()->get("PastebinURL") != "";
- bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired;
+ bool themeInterventionRequired = settings()->get("ApplicationTheme") == "";
+ bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired;
if(wizardRequired)
{
@@ -920,6 +941,12 @@ bool Application::createSetupWizard()
{
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);
m_setupWizard->show();
return true;
@@ -942,7 +969,7 @@ bool Application::event(QEvent* event)
if (event->type() == QEvent::FileOpen) {
auto ev = static_cast(event);
- m_mainWindow->droppedURLs({ ev->url() });
+ m_mainWindow->processURLs({ ev->url() });
}
return QApplication::event(event);
@@ -1002,10 +1029,10 @@ void Application::performMainStartupAction()
showMainWindow(false);
qDebug() << "<> Main window shown.";
}
- if(!m_zipToImport.isEmpty())
+ if(!m_zipsToImport.isEmpty())
{
- qDebug() << "<> Importing instance from zip:" << m_zipToImport;
- m_mainWindow->droppedURLs({ m_zipToImport });
+ qDebug() << "<> Importing from zip:" << m_zipsToImport;
+ m_mainWindow->processURLs( m_zipsToImport );
}
}
@@ -1058,7 +1085,7 @@ void Application::messageReceived(const QByteArray& message)
qWarning() << "Received" << command << "message without a zip path/URL.";
return;
}
- m_mainWindow->droppedURLs({ QUrl(path) });
+ m_mainWindow->processURLs({ QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()) });
}
else if(command == "launch")
{
@@ -1127,9 +1154,14 @@ QList Application::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)
@@ -1357,16 +1389,7 @@ MainWindow* Application::showMainWindow(bool minimized)
m_mainWindow = new MainWindow();
m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").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)
{
m_mainWindow->showMinimized();
@@ -1543,7 +1566,8 @@ QString Application::getJarPath(QString jarFile)
FS::PathCombine(m_rootPath, "share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME),
#endif
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)
{
@@ -1574,6 +1598,15 @@ QString Application::getFlameAPIKey()
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 uaOverride = m_settings->get("UserAgentOverride").toString();
@@ -1694,3 +1727,14 @@ bool Application::handleDataMigration(const QString& currentData,
}
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.";
+ }
+}
diff --git a/launcher/Application.h b/launcher/Application.h
index 7884227a1..ced0af17d 100644
--- a/launcher/Application.h
+++ b/launcher/Application.h
@@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu
* Copyright (C) 2022 Tayou
+ * Copyright (C) 2023 TheKodeToad
*
* 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
@@ -43,7 +44,6 @@
#include
#include
#include
-#include
#include
@@ -63,7 +63,7 @@ class AccountList;
class IconList;
class QNetworkAccessManager;
class JavaInstallList;
-class UpdateChecker;
+class ExternalUpdater;
class BaseProfilerFactory;
class BaseDetachedToolFactory;
class TranslationsModel;
@@ -120,14 +120,18 @@ public:
void setIconTheme(const QString& name);
+ void applyCurrentlySelectedTheme(bool initial = false);
+
QList getValidApplicationThemes();
- void setApplicationTheme(const QString& name, bool initial);
+ void setApplicationTheme(const QString& name);
- shared_qobject_ptr updateChecker() {
- return m_updateChecker;
+ shared_qobject_ptr updater() {
+ return m_updater;
}
+ void triggerUpdateCheck();
+
std::shared_ptr translations();
std::shared_ptr javalist();
@@ -174,6 +178,7 @@ public:
QString getMSAClientID();
QString getFlameAPIKey();
+ QString getModrinthAPIToken();
QString getUserAgent();
QString getUserAgentUncached();
@@ -182,6 +187,10 @@ public:
return m_rootPath;
}
+ bool isPortable() {
+ return m_portable;
+ }
+
const Capabilities capabilities() {
return m_capabilities;
}
@@ -206,6 +215,7 @@ signals:
void updateAllowedChanged(bool status);
void globalSettingsAboutToOpen();
void globalSettingsClosed();
+ int currentCatChanged(int index);
#ifdef Q_OS_MACOS
void clickedOnDock();
@@ -248,7 +258,7 @@ private:
shared_qobject_ptr m_network;
- shared_qobject_ptr m_updateChecker;
+ shared_qobject_ptr m_updater;
shared_qobject_ptr m_accounts;
shared_qobject_ptr m_metacache;
@@ -269,6 +279,7 @@ private:
QString m_rootPath;
Status m_status = Application::StartingUp;
Capabilities m_capabilities;
+ bool m_portable = false;
#ifdef Q_OS_MACOS
Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive;
@@ -303,8 +314,7 @@ public:
QString m_serverToJoin;
QString m_profileToUse;
bool m_liveCheck = false;
- QUrl m_zipToImport;
+ QList m_zipsToImport;
QString m_instanceIdToShowWindowOf;
std::unique_ptr logFile;
};
-
diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp
index 8680361c6..a8fce879c 100644
--- a/launcher/BaseInstance.cpp
+++ b/launcher/BaseInstance.cpp
@@ -40,6 +40,8 @@
#include
#include
#include
+#include
+#include
#include "settings/INISettingsObject.h"
#include "settings/Setting.h"
@@ -64,6 +66,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s
m_settings->registerSetting("totalTimePlayed", 0);
m_settings->registerSetting("lastTimePlayed", 0);
+ m_settings->registerSetting("linkedInstances", "[]");
+
// Game time override
auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false);
m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride);
@@ -182,6 +186,38 @@ bool BaseInstance::shouldStopOnConsoleOverflow() const
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)
{
if(iconKey() == key)
diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h
index a2a4f8246..83a8064fa 100644
--- a/launcher/BaseInstance.h
+++ b/launcher/BaseInstance.h
@@ -282,6 +282,12 @@ public:
int getConsoleMaxLines() 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:
void changeStatus(Status newStatus);
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index a0d92b6ee..273b5449a 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -26,6 +26,7 @@ set(CORE_SOURCES
MMCZip.cpp
StringUtils.h
StringUtils.cpp
+ QVariantUtils.h
RuntimeContext.h
# Basic instance manipulation tasks (derived from InstanceTask)
@@ -38,9 +39,9 @@ set(CORE_SOURCES
InstanceImportTask.h
InstanceImportTask.cpp
- # Mod downloading task
- ModDownloadTask.h
- ModDownloadTask.cpp
+ # Resource downloading task
+ ResourceDownloadTask.h
+ ResourceDownloadTask.cpp
# Use tracking separate from memory management
Usable.h
@@ -123,6 +124,8 @@ set(NET_SOURCES
net/HttpMetaCache.h
net/MetaCacheSink.cpp
net/MetaCacheSink.h
+ net/Logging.h
+ net/Logging.cpp
net/NetAction.h
net/NetJob.cpp
net/NetJob.h
@@ -161,12 +164,6 @@ set(LAUNCH_SOURCES
# Old update system
set(UPDATE_SOURCES
- updater/GoUpdate.h
- updater/GoUpdate.cpp
- updater/UpdateChecker.h
- updater/UpdateChecker.cpp
- updater/DownloadTask.h
- updater/DownloadTask.cpp
updater/ExternalUpdater.h
)
@@ -331,12 +328,18 @@ set(MINECRAFT_SOURCES
minecraft/mod/Resource.cpp
minecraft/mod/ResourceFolderModel.h
minecraft/mod/ResourceFolderModel.cpp
+ minecraft/mod/DataPack.h
+ minecraft/mod/DataPack.cpp
minecraft/mod/ResourcePack.h
minecraft/mod/ResourcePack.cpp
minecraft/mod/ResourcePackFolderModel.h
minecraft/mod/ResourcePackFolderModel.cpp
minecraft/mod/TexturePack.h
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.cpp
minecraft/mod/ShaderPackFolderModel.h
@@ -347,10 +350,18 @@ set(MINECRAFT_SOURCES
minecraft/mod/tasks/LocalModParseTask.cpp
minecraft/mod/tasks/LocalModUpdateTask.h
minecraft/mod/tasks/LocalModUpdateTask.cpp
+ minecraft/mod/tasks/LocalDataPackParseTask.h
+ minecraft/mod/tasks/LocalDataPackParseTask.cpp
minecraft/mod/tasks/LocalResourcePackParseTask.h
minecraft/mod/tasks/LocalResourcePackParseTask.cpp
minecraft/mod/tasks/LocalTexturePackParseTask.h
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
minecraft/AssetsUtils.h
@@ -459,7 +470,7 @@ set(API_SOURCES
modplatform/ModIndex.h
modplatform/ModIndex.cpp
- modplatform/ModAPI.h
+ modplatform/ResourceAPI.h
modplatform/EnsureMetadataTask.h
modplatform/EnsureMetadataTask.cpp
@@ -470,8 +481,8 @@ set(API_SOURCES
modplatform/flame/FlameAPI.cpp
modplatform/modrinth/ModrinthAPI.h
modplatform/modrinth/ModrinthAPI.cpp
- modplatform/helpers/NetworkModAPI.h
- modplatform/helpers/NetworkModAPI.cpp
+ modplatform/helpers/NetworkResourceAPI.h
+ modplatform/helpers/NetworkResourceAPI.cpp
modplatform/helpers/HashUtils.h
modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h
@@ -516,13 +527,6 @@ set(MODRINTH_SOURCES
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
modplatform/packwiz/Packwiz.h
modplatform/packwiz/Packwiz.cpp
@@ -551,6 +555,85 @@ set(ATLAUNCHER_SOURCES
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 ################################
set(LOGIC_SOURCES
@@ -573,7 +656,6 @@ set(LOGIC_SOURCES
${FTB_SOURCES}
${FLAME_SOURCES}
${MODRINTH_SOURCES}
- ${MODPACKSCH_SOURCES}
${PACKWIZ_SOURCES}
${TECHNIC_SOURCES}
${ATLAUNCHER_SOURCES}
@@ -589,8 +671,6 @@ SET(LAUNCHER_SOURCES
Application.cpp
DataMigrationTask.h
DataMigrationTask.cpp
- UpdateController.cpp
- UpdateController.h
ApplicationMessage.h
ApplicationMessage.cpp
@@ -599,7 +679,7 @@ SET(LAUNCHER_SOURCES
DesktopServices.cpp
VersionProxyModel.h
VersionProxyModel.cpp
- HoeDown.h
+ Markdown.h
# Super secret!
KonamiCode.h
@@ -651,6 +731,8 @@ SET(LAUNCHER_SOURCES
ui/setupwizard/LanguageWizardPage.h
ui/setupwizard/PasteWizardPage.cpp
ui/setupwizard/PasteWizardPage.h
+ ui/setupwizard/ThemeWizardPage.cpp
+ ui/setupwizard/ThemeWizardPage.h
# GUI - themes
ui/themes/FusionTheme.cpp
@@ -694,8 +776,11 @@ SET(LAUNCHER_SOURCES
ui/pages/instance/ManagedPackPage.cpp
ui/pages/instance/ManagedPackPage.h
ui/pages/instance/TexturePackPage.h
+ ui/pages/instance/TexturePackPage.cpp
ui/pages/instance/ResourcePackPage.h
+ ui/pages/instance/ResourcePackPage.cpp
ui/pages/instance/ShaderPackPage.h
+ ui/pages/instance/ShaderPackPage.cpp
ui/pages/instance/ModFolderPage.cpp
ui/pages/instance/ModFolderPage.h
ui/pages/instance/NotesPage.cpp
@@ -737,11 +822,26 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/VanillaPage.cpp
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.h
ui/pages/modplatform/ModModel.cpp
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.h
ui/pages/modplatform/atlauncher/AtlListModel.cpp
@@ -753,13 +853,6 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
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.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/FlamePage.cpp
ui/pages/modplatform/flame/FlamePage.h
- ui/pages/modplatform/flame/FlameModModel.cpp
- ui/pages/modplatform/flame/FlameModModel.h
- ui/pages/modplatform/flame/FlameModPage.cpp
- ui/pages/modplatform/flame/FlameModPage.h
+ ui/pages/modplatform/flame/FlameResourceModels.cpp
+ ui/pages/modplatform/flame/FlameResourceModels.h
+ ui/pages/modplatform/flame/FlameResourcePages.cpp
+ ui/pages/modplatform/flame/FlameResourcePages.h
ui/pages/modplatform/modrinth/ModrinthPage.cpp
ui/pages/modplatform/modrinth/ModrinthPage.h
@@ -787,10 +880,10 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/ImportPage.cpp
ui/pages/modplatform/ImportPage.h
- ui/pages/modplatform/modrinth/ModrinthModModel.cpp
- ui/pages/modplatform/modrinth/ModrinthModModel.h
- ui/pages/modplatform/modrinth/ModrinthModPage.cpp
- ui/pages/modplatform/modrinth/ModrinthModPage.h
+ ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
+ ui/pages/modplatform/modrinth/ModrinthResourceModels.h
+ ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
+ ui/pages/modplatform/modrinth/ModrinthResourcePages.h
# GUI - dialogs
ui/dialogs/AboutDialog.cpp
@@ -809,8 +902,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ExportInstanceDialog.h
ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h
- ui/dialogs/ImportResourcePackDialog.cpp
- ui/dialogs/ImportResourcePackDialog.h
+ ui/dialogs/ImportResourceDialog.cpp
+ ui/dialogs/ImportResourceDialog.h
ui/dialogs/LoginDialog.cpp
ui/dialogs/LoginDialog.h
ui/dialogs/MSALoginDialog.cpp
@@ -829,14 +922,12 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ProgressDialog.h
ui/dialogs/ReviewMessageBox.cpp
ui/dialogs/ReviewMessageBox.h
- ui/dialogs/UpdateDialog.cpp
- ui/dialogs/UpdateDialog.h
ui/dialogs/VersionSelectDialog.cpp
ui/dialogs/VersionSelectDialog.h
ui/dialogs/SkinUploadDialog.cpp
ui/dialogs/SkinUploadDialog.h
- ui/dialogs/ModDownloadDialog.cpp
- ui/dialogs/ModDownloadDialog.h
+ ui/dialogs/ResourceDownloadDialog.cpp
+ ui/dialogs/ResourceDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp
ui/dialogs/ScrollMessageBox.h
ui/dialogs/BlockedModsDialog.cpp
@@ -882,6 +973,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/VariableSizedImageObject.cpp
ui/widgets/ProjectItem.h
ui/widgets/ProjectItem.cpp
+ ui/widgets/SubTaskProgressBar.h
+ ui/widgets/SubTaskProgressBar.cpp
ui/widgets/VersionListView.cpp
ui/widgets/VersionListView.h
ui/widgets/VersionSelectWidget.cpp
@@ -890,6 +983,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/ProgressWidget.cpp
ui/widgets/WideBar.h
ui/widgets/WideBar.cpp
+ ui/widgets/ThemeCustomizationWidget.h
+ ui/widgets/ThemeCustomizationWidget.cpp
# GUI - instance group view
ui/instanceview/InstanceProxyModel.cpp
@@ -905,18 +1000,10 @@ SET(LAUNCHER_SOURCES
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
+ ui/MainWindow.ui
ui/setupwizard/PasteWizardPage.ui
+ ui/setupwizard/ThemeWizardPage.ui
ui/pages/global/AccountListPage.ui
ui/pages/global/JavaPage.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/AtlPage.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/legacy_ftb/Page.ui
ui/pages/modplatform/ImportPage.ui
- ui/pages/modplatform/ftb/FtbPage.ui
ui/pages/modplatform/modrinth/ModrinthPage.ui
ui/pages/modplatform/technic/TechnicPage.ui
ui/widgets/InstanceCardWidget.ui
ui/widgets/CustomCommands.ui
ui/widgets/InfoFrame.ui
ui/widgets/ModFilterWidget.ui
+ ui/widgets/SubTaskProgressBar.ui
+ ui/widgets/ThemeCustomizationWidget.ui
ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/ProfileSetupDialog.ui
ui/dialogs/ProgressDialog.ui
ui/dialogs/NewInstanceDialog.ui
- ui/dialogs/UpdateDialog.ui
ui/dialogs/NewComponentDialog.ui
ui/dialogs/NewsDialog.ui
ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/IconPickerDialog.ui
- ui/dialogs/ImportResourcePackDialog.ui
+ ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui
@@ -1002,6 +1089,7 @@ target_link_libraries(Launcher_logic
nbt++
${ZLIB_LIBRARIES}
tomlplusplus::tomlplusplus
+ qdcss
BuildConfig
Katabasis
Qt${QT_VERSION_MAJOR}::Widgets
@@ -1025,7 +1113,7 @@ target_link_libraries(Launcher_logic
)
target_link_libraries(Launcher_logic
QuaZip::QuaZip
- hoedown
+ cmark::cmark
LocalPeer
Launcher_rainbow
)
@@ -1070,6 +1158,41 @@ install(TARGETS ${Launcher_Name}
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)
# Add Sparkle updater
# 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\" \" \")"
COMPONENT Runtime
)
+ # add qtlogging.ini as a config file
+ install(
+ FILES "qtlogging.ini"
+ DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}
+ COMPONENT Runtime
+ )
# Bundle plugins
# Image formats
install(
diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp
index 302eaf967..2984a1b4f 100644
--- a/launcher/DesktopServices.cpp
+++ b/launcher/DesktopServices.cpp
@@ -37,7 +37,6 @@
#include
#include
#include
-#include "Application.h"
/**
* This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing.
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index 3e8e10a51..d98526dfd 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu
+ * Copyright (C) 2022 TheKodeToad
+ * 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
@@ -35,6 +37,8 @@
#include "FileSystem.h"
+#include "BuildConfig.h"
+
#include
#include
#include
@@ -42,13 +46,17 @@
#include
#include
#include
+#include
#include
#include
+#include
+#include
#include "DesktopServices.h"
#include "StringUtils.h"
#if defined Q_OS_WIN32
+#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include
#include
@@ -56,9 +64,14 @@
#include
#include
#include
+#include
#include
#include
#include
+// for ShellExecute
+#include
+#include
+#include
#else
#include
#endif
@@ -66,22 +79,96 @@
// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
#ifdef __APPLE__
-#include // for deployment target to support pre-catalina targets without std::fs
-#endif // __APPLE__
+#include // 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() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
#define GHC_USE_STD_FS
#include
namespace fs = std::filesystem;
-#endif // MacOS min version check
-#endif // Other OSes version check
+#endif // MacOS min version check
+#endif // Other OSes version check
#ifndef GHC_USE_STD_FS
#include
namespace fs = ghc::filesystem;
#endif
+// clone
+#if defined(Q_OS_LINUX)
+#include
+#include /* Definition of FICLONE* constants */
+#include
+#include
+#include
+#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+#include
+#include
+#elif defined(Q_OS_WIN)
+// winbtrfs clone vs rundll32 shellbtrfs.dll,ReflinkCopy
+#include
+#include
+#include
+#include
+// refs
+#include
+#if defined(__MINGW32__)
+#include
+#endif
+#endif
+
+#if defined(Q_OS_WIN)
+
+#if defined(__MINGW32__)
+
+typedef struct _DUPLICATE_EXTENTS_DATA {
+ HANDLE FileHandle;
+ LARGE_INTEGER SourceFileOffset;
+ LARGE_INTEGER TargetFileOffset;
+ LARGE_INTEGER ByteCount;
+} DUPLICATE_EXTENTS_DATA, *PDUPLICATE_EXTENTS_DATA;
+
+typedef struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER {
+ WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32
+ WORD Reserved; // Must be 0
+ DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx
+ DWORD ChecksumChunkSizeInBytes;
+ DWORD ClusterSizeInBytes;
+} FSCTL_GET_INTEGRITY_INFORMATION_BUFFER, *PFSCTL_GET_INTEGRITY_INFORMATION_BUFFER;
+
+typedef struct _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER {
+ WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32
+ WORD Reserved; // Must be 0
+ DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx
+} FSCTL_SET_INTEGRITY_INFORMATION_BUFFER, *PFSCTL_SET_INTEGRITY_INFORMATION_BUFFER;
+
+#endif
+
+#ifndef FSCTL_DUPLICATE_EXTENTS_TO_FILE
+#define FSCTL_DUPLICATE_EXTENTS_TO_FILE CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 209, METHOD_BUFFERED, FILE_WRITE_DATA)
+#endif
+
+#ifndef FSCTL_GET_INTEGRITY_INFORMATION
+#define FSCTL_GET_INTEGRITY_INFORMATION \
+ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 159, METHOD_BUFFERED, FILE_ANY_ACCESS) // FSCTL_GET_INTEGRITY_INFORMATION_BUFFER
+#endif
+
+#ifndef FSCTL_SET_INTEGRITY_INFORMATION
+#define FSCTL_SET_INTEGRITY_INFORMATION \
+ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 160, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA) // FSCTL_SET_INTEGRITY_INFORMATION_BUFFER
+#endif
+
+#ifndef ERROR_NOT_CAPABLE
+#define ERROR_NOT_CAPABLE 775L
+#endif
+
+#ifndef ERROR_BLOCK_TOO_MANY_REFERENCES
+#define ERROR_BLOCK_TOO_MANY_REFERENCES 347L
+#endif
+
+#endif
+
namespace FS {
void ensureExists(const QDir& dir)
@@ -150,9 +237,11 @@ bool ensureFolderPathExists(QString foldernamepath)
return success;
}
-/// @brief Copies a directory and it's contents from src to dest
-/// @param offset subdirectory form src to copy to dest
-/// @return if there was an error during the filecopy
+/**
+ * @brief Copies a directory and it's contents from src to dest
+ * @param offset subdirectory form src to copy to dest
+ * @return if there was an error during the filecopy
+ */
bool copy::operator()(const QString& offset, bool dryRun)
{
using copy_opts = fs::copy_options;
@@ -213,6 +302,287 @@ bool copy::operator()(const QString& offset, bool dryRun)
return err.value() == 0;
}
+/// qDebug print support for the LinkPair struct
+QDebug operator<<(QDebug debug, const LinkPair& lp)
+{
+ QDebugStateSaver saver(debug);
+
+ debug.nospace() << "LinkPair{ src: " << lp.src << " , dst: " << lp.dst << " }";
+ return debug;
+}
+
+bool create_link::operator()(const QString& offset, bool dryRun)
+{
+ m_linked = 0; // reset counter
+ m_path_results.clear();
+ m_links_to_make.clear();
+
+ m_path_results.clear();
+
+ make_link_list(offset);
+
+ if (!dryRun)
+ return make_links();
+
+ return true;
+}
+
+/**
+ * @brief Make a list of all the links to make
+ * @param offset subdirectory of src to link to dest
+ */
+void create_link::make_link_list(const QString& offset)
+{
+ for (auto pair : m_path_pairs) {
+ const QString& srcPath = pair.src;
+ const QString& dstPath = pair.dst;
+
+ auto src = PathCombine(QDir(srcPath).absolutePath(), offset);
+ auto dst = PathCombine(QDir(dstPath).absolutePath(), offset);
+
+ // you can't hard link a directory so make sure if we deal with a directory we do so recursively
+ if (m_useHardLinks)
+ m_recursive = true;
+
+ // Function that'll do the actual linking
+ auto link_file = [&](QString src_path, QString relative_dst_path) {
+ if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) {
+ qDebug() << "path" << relative_dst_path << "in black list or not in whitelist";
+ return;
+ }
+
+ auto dst_path = PathCombine(dst, relative_dst_path);
+ LinkPair link = { src_path, dst_path };
+ m_links_to_make.append(link);
+ };
+
+ if ((!m_recursive) || !fs::is_directory(StringUtils::toStdString(src))) {
+ if (m_debug)
+ qDebug() << "linking single file or dir:" << src << "to" << dst;
+ link_file(src, "");
+ } else {
+ if (m_debug)
+ qDebug() << "linking recursively:" << src << "to" << dst << ", max_depth:" << m_max_depth;
+ QDir src_dir(src);
+ QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories);
+
+ QStringList linkedPaths;
+
+ while (source_it.hasNext()) {
+ auto src_path = source_it.next();
+ auto relative_path = src_dir.relativeFilePath(src_path);
+
+ if (m_max_depth >= 0 && pathDepth(relative_path) > m_max_depth){
+ relative_path = pathTruncate(relative_path, m_max_depth);
+ src_path = src_dir.filePath(relative_path);
+ if (linkedPaths.contains(src_path)) {
+ continue;
+ }
+ }
+
+ linkedPaths.append(src_path);
+
+ link_file(src_path, relative_path);
+ }
+ }
+ }
+}
+
+bool create_link::make_links()
+{
+ for (auto link : m_links_to_make) {
+ QString src_path = link.src;
+ QString dst_path = link.dst;
+ auto src_path_std = StringUtils::toStdString(link.src);
+ auto dst_path_std = StringUtils::toStdString(link.dst);
+
+ ensureFilePathExists(dst_path);
+ if (m_useHardLinks) {
+ if (m_debug)
+ qDebug() << "making hard link:" << src_path << "to" << dst_path;
+ fs::create_hard_link(src_path_std, dst_path_std, m_os_err);
+ } else if (fs::is_directory(src_path_std)) {
+ if (m_debug)
+ qDebug() << "making directory_symlink:" << src_path << "to" << dst_path;
+ fs::create_directory_symlink(src_path_std, dst_path_std, m_os_err);
+ } else {
+ if (m_debug)
+ qDebug() << "making symlink:" << src_path << "to" << dst_path;
+ fs::create_symlink(src_path_std, dst_path_std, m_os_err);
+ }
+
+ if (m_os_err) {
+ qWarning() << "Failed to link files:" << QString::fromStdString(m_os_err.message());
+ qDebug() << "Source file:" << src_path;
+ qDebug() << "Destination file:" << dst_path;
+ qDebug() << "Error category:" << m_os_err.category().name();
+ qDebug() << "Error code:" << m_os_err.value();
+ emit linkFailed(src_path, dst_path, QString::fromStdString(m_os_err.message()), m_os_err.value());
+ } else {
+ m_linked++;
+ emit fileLinked(src_path, dst_path);
+ }
+ if (m_os_err)
+ return false;
+ }
+ return true;
+}
+
+void create_link::runPrivileged(const QString& offset)
+{
+ m_linked = 0; // reset counter
+ m_path_results.clear();
+ m_links_to_make.clear();
+
+ bool gotResults = false;
+
+ make_link_list(offset);
+
+ QString serverName = BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink_server" + StringUtils::getRandomAlphaNumeric();
+
+ connect(&m_linkServer, &QLocalServer::newConnection, this, [&]() {
+ qDebug() << "Client connected, sending out pairs";
+ // construct block of data to send
+ QByteArray block;
+ QDataStream out(&block, QIODevice::WriteOnly);
+
+ qint32 blocksize = quint32(sizeof(quint32));
+ for (auto link : m_links_to_make) {
+ blocksize += quint32(link.src.size());
+ blocksize += quint32(link.dst.size());
+ }
+ qDebug() << "About to write block of size:" << blocksize;
+ out << blocksize;
+
+ out << quint32(m_links_to_make.length());
+ for (auto link : m_links_to_make) {
+ out << link.src;
+ out << link.dst;
+ }
+
+ QLocalSocket* clientConnection = m_linkServer.nextPendingConnection();
+ connect(clientConnection, &QLocalSocket::disconnected, clientConnection, &QLocalSocket::deleteLater);
+
+ connect(clientConnection, &QLocalSocket::readyRead, this, [&, clientConnection]() {
+ QDataStream in;
+ quint32 blockSize = 0;
+ in.setDevice(clientConnection);
+
+ qDebug() << "Reading path results from client";
+ qDebug() << "bytes available" << clientConnection->bytesAvailable();
+
+ // Relies on the fact that QDataStream serializes a quint32 into
+ // sizeof(quint32) bytes
+ if (clientConnection->bytesAvailable() < (int)sizeof(quint32))
+ return;
+ qDebug() << "reading block size";
+ in >> blockSize;
+
+ qDebug() << "blocksize is" << blockSize;
+ qDebug() << "bytes available" << clientConnection->bytesAvailable();
+ if (clientConnection->bytesAvailable() < blockSize || in.atEnd())
+ return;
+
+ quint32 numResults;
+ in >> numResults;
+ qDebug() << "numResults" << numResults;
+
+ for (quint32 i = 0; i < numResults; i++) {
+ FS::LinkResult result;
+ in >> result.src;
+ in >> result.dst;
+ in >> result.err_msg;
+ qint32 err_value;
+ in >> err_value;
+ result.err_value = err_value;
+ if (result.err_value) {
+ qDebug() << "privileged link fail" << result.src << "to" << result.dst << "code" << result.err_value << result.err_msg;
+ emit linkFailed(result.src, result.dst, result.err_msg, result.err_value);
+ } else {
+ qDebug() << "privileged link success" << result.src << "to" << result.dst;
+ m_linked++;
+ emit fileLinked(result.src, result.dst);
+ }
+ m_path_results.append(result);
+ }
+ gotResults = true;
+ qDebug() << "results received, closing connection";
+ clientConnection->close();
+ });
+
+ qint64 byteswritten = clientConnection->write(block);
+ bool bytesflushed = clientConnection->flush();
+ qDebug() << "block flushed" << byteswritten << bytesflushed;
+ });
+
+ qDebug() << "Listening on pipe" << serverName;
+ if (!m_linkServer.listen(serverName)) {
+ qDebug() << "Unable to start local pipe server on" << serverName << ":" << m_linkServer.errorString();
+ return;
+ }
+
+ ExternalLinkFileProcess* linkFileProcess = new ExternalLinkFileProcess(serverName, m_useHardLinks, this);
+ connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [&]() { emit finishedPrivileged(gotResults); });
+ connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater);
+
+ linkFileProcess->start();
+}
+
+void ExternalLinkFileProcess::runLinkFile()
+{
+ QString fileLinkExe =
+ PathCombine(QCoreApplication::instance()->applicationDirPath(), BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink");
+ QString params = "-s " + m_server;
+
+ params += " -H " + QVariant(m_useHardLinks).toString();
+
+#if defined Q_OS_WIN32
+ SHELLEXECUTEINFO ShExecInfo;
+
+ fileLinkExe = fileLinkExe + ".exe";
+
+ qDebug() << "Running: runas" << fileLinkExe << params;
+
+ LPCWSTR programNameWin = (const wchar_t*)fileLinkExe.utf16();
+ LPCWSTR paramsWin = (const wchar_t*)params.utf16();
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfoa
+ ShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO);
+ ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
+ ShExecInfo.hwnd = NULL; // Optional. A handle to the owner window, used to display and position any UI that the system might produce
+ // while executing this function.
+ ShExecInfo.lpVerb = L"runas"; // elevate to admin, show UAC
+ ShExecInfo.lpFile = programNameWin;
+ ShExecInfo.lpParameters = paramsWin;
+ ShExecInfo.lpDirectory = NULL;
+ ShExecInfo.nShow = SW_HIDE;
+ ShExecInfo.hInstApp = NULL;
+
+ ShellExecuteEx(&ShExecInfo);
+
+ WaitForSingleObject(ShExecInfo.hProcess, INFINITE);
+ CloseHandle(ShExecInfo.hProcess);
+#endif
+
+ qDebug() << "Process exited";
+}
+
+bool move(const QString& source, const QString& dest)
+{
+ std::error_code err;
+
+ ensureFilePathExists(dest);
+ fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err);
+
+ if (err) {
+ qWarning() << "Failed to move file:" << QString::fromStdString(err.message());
+ qDebug() << "Source file:" << source;
+ qDebug() << "Destination file:" << dest;
+ }
+
+ return err.value() == 0;
+}
+
bool deletePath(QString path)
{
std::error_code err;
@@ -226,7 +596,7 @@ bool deletePath(QString path)
return err.value() == 0;
}
-bool trash(QString path, QString *pathInTrash = nullptr)
+bool trash(QString path, QString* pathInTrash)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
return false;
@@ -234,6 +604,10 @@ bool trash(QString path, QString *pathInTrash = nullptr)
// FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal
if (DesktopServices::isFlatpak())
return false;
+#if defined Q_OS_WIN32
+ if (IsWindowsServer())
+ return false;
+#endif
return QFile::moveToTrash(path, pathInTrash);
#endif
}
@@ -257,11 +631,60 @@ QString PathCombine(const QString& path1, const QString& path2, const QString& p
return PathCombine(PathCombine(path1, path2, path3), path4);
}
-QString AbsolutePath(QString path)
+QString AbsolutePath(const QString& path)
{
return QFileInfo(path).absolutePath();
}
+int pathDepth(const QString& path)
+{
+ if (path.isEmpty())
+ return 0;
+
+ QFileInfo info(path);
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+ auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), QString::SkipEmptyParts);
+#else
+ auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts);
+#endif
+
+ int numParts = parts.length();
+ numParts -= parts.count(".");
+ numParts -= parts.count("..") * 2;
+
+ return numParts;
+}
+
+QString pathTruncate(const QString& path, int depth)
+{
+ if (path.isEmpty() || (depth < 0))
+ return "";
+
+ QString trunc = QFileInfo(path).path();
+
+ if (pathDepth(trunc) > depth ) {
+ return pathTruncate(trunc, depth);
+ }
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+ auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), QString::SkipEmptyParts);
+#else
+ auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts);
+#endif
+
+ if (parts.startsWith(".") && !path.startsWith(".")) {
+ parts.removeFirst();
+ }
+ if (QDir::toNativeSeparators(path).startsWith(QDir::separator())) {
+ parts.prepend("");
+ }
+
+ trunc = parts.join(QDir::separator());
+
+ return trunc;
+}
+
QString ResolveExecutable(QString path)
{
if (path.isEmpty()) {
@@ -359,11 +782,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
stream << "#!/bin/bash"
<< "\n";
- stream << "\""
- << target
- << "\" "
- << argstring
- << "\n";
+ stream << "\"" << target << "\" " << argstring << "\n";
stream.flush();
f.close();
@@ -386,8 +805,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
<< "\n";
stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n";
stream << "Name=" << name.toLocal8Bit() << "\n";
- if (!icon.isEmpty())
- {
+ if (!icon.isEmpty()) {
stream << "Icon=" << icon.toLocal8Bit() << "\n";
}
@@ -400,55 +818,45 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
#elif defined(Q_OS_WIN)
QFileInfo targetInfo(target);
- if (!targetInfo.exists())
- {
+ if (!targetInfo.exists()) {
qWarning() << "Target file does not exist!";
return false;
}
target = targetInfo.absoluteFilePath();
- if (target.length() >= MAX_PATH)
- {
+ if (target.length() >= MAX_PATH) {
qWarning() << "Target file path is too long!";
return false;
}
- if (!icon.isEmpty() && icon.length() >= MAX_PATH)
- {
+ if (!icon.isEmpty() && icon.length() >= MAX_PATH) {
qWarning() << "Icon path is too long!";
return false;
}
destination += ".lnk";
- if (destination.length() >= MAX_PATH)
- {
+ if (destination.length() >= MAX_PATH) {
qWarning() << "Destination path is too long!";
return false;
}
QString argStr;
int argCount = args.count();
- for (int i = 0; i < argCount; i++)
- {
- if (args[i].contains(' '))
- {
+ for (int i = 0; i < argCount; i++) {
+ if (args[i].contains(' ')) {
argStr.append('"').append(args[i]).append('"');
- }
- else
- {
+ } else {
argStr.append(args[i]);
}
- if (i < argCount - 1)
- {
+ if (i < argCount - 1) {
argStr.append(" ");
}
}
- if (argStr.length() >= MAX_PATH)
- {
+ if (argStr.length() >= MAX_PATH) {
qWarning() << "Arguments string is too long!";
return false;
}
@@ -457,8 +865,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
// ...yes, you need to initialize the entire COM stack just to make a shortcut
hres = CoInitialize(nullptr);
- if (FAILED(hres))
- {
+ if (FAILED(hres)) {
qWarning() << "Failed to initialize COM!";
return false;
}
@@ -469,8 +876,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
// create an IShellLink instance - this stores the shortcut's attributes
hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);
- if (SUCCEEDED(hres))
- {
+ if (SUCCEEDED(hres)) {
wmemset(wsz, 0, MAX_PATH);
target.toWCharArray(wsz);
psl->SetPath(wsz);
@@ -481,10 +887,9 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
wmemset(wsz, 0, MAX_PATH);
targetInfo.absolutePath().toWCharArray(wsz);
- psl->SetWorkingDirectory(wsz); // "Starts in" attribute
+ psl->SetWorkingDirectory(wsz); // "Starts in" attribute
- if (!icon.isEmpty())
- {
+ if (!icon.isEmpty()) {
wmemset(wsz, 0, MAX_PATH);
icon.toWCharArray(wsz);
psl->SetIconLocation(wsz, 0);
@@ -494,27 +899,21 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
// this is the interface that will actually let us save the shortcut to disk!
IPersistFile* ppf;
hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf);
- if (SUCCEEDED(hres))
- {
+ if (SUCCEEDED(hres)) {
wmemset(wsz, 0, MAX_PATH);
destination.toWCharArray(wsz);
hres = ppf->Save(wsz, TRUE);
- if (FAILED(hres))
- {
+ if (FAILED(hres)) {
qWarning() << "IPresistFile->Save() failed";
qWarning() << "hres = " << hres;
}
ppf->Release();
- }
- else
- {
+ } else {
qWarning() << "Failed to query IPersistFile interface from IShellLink instance";
qWarning() << "hres = " << hres;
}
psl->Release();
- }
- else
- {
+ } else {
qWarning() << "Failed to create IShellLink instance";
qWarning() << "hres = " << hres;
}
@@ -550,4 +949,490 @@ bool overrideFolder(QString overwritten_path, QString override_path)
return err.value() == 0;
}
+QString getFilesystemTypeName(FilesystemType type)
+{
+ auto iter = s_filesystem_type_names.constFind(type);
+ if (iter != s_filesystem_type_names.constEnd()) {
+ return iter.value().constFirst();
+ }
+ return getFilesystemTypeName(FilesystemType::UNKNOWN);
}
+
+FilesystemType getFilesystemTypeFuzzy(const QString& name)
+{
+ for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) {
+ auto fs_names = iter.value();
+ for (auto fs_name : fs_names) {
+ if (name.toUpper().contains(fs_name.toUpper()))
+ return iter.key();
+ }
+ }
+ return FilesystemType::UNKNOWN;
+}
+
+FilesystemType getFilesystemType(const QString& name)
+{
+ for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) {
+ auto fs_names = iter.value();
+ if(fs_names.contains(name.toUpper()))
+ return iter.key();
+ }
+ return FilesystemType::UNKNOWN;
+}
+
+/**
+ * @brief path to the near ancestor that exists
+ *
+ */
+QString nearestExistentAncestor(const QString& path)
+{
+ if (QFileInfo::exists(path))
+ return path;
+
+ QDir dir(path);
+ if (!dir.makeAbsolute())
+ return {};
+ do {
+ dir.setPath(QDir::cleanPath(dir.filePath(QStringLiteral(".."))));
+ } while (!dir.exists() && !dir.isRoot());
+
+ return dir.exists() ? dir.path() : QString();
+}
+
+/**
+ * @brief colect information about the filesystem under a file
+ *
+ */
+FilesystemInfo statFS(const QString& path)
+{
+ FilesystemInfo info;
+
+ QStorageInfo storage_info(nearestExistentAncestor(path));
+
+ info.fsTypeName = storage_info.fileSystemType();
+
+ info.fsType = getFilesystemTypeFuzzy(info.fsTypeName);
+
+ info.blockSize = storage_info.blockSize();
+ info.bytesAvailable = storage_info.bytesAvailable();
+ info.bytesFree = storage_info.bytesFree();
+ info.bytesTotal = storage_info.bytesTotal();
+
+ info.name = storage_info.name();
+ info.rootPath = storage_info.rootPath();
+
+ return info;
+}
+
+/**
+ * @brief if the Filesystem is reflink/clone capable
+ *
+ */
+bool canCloneOnFS(const QString& path)
+{
+ FilesystemInfo info = statFS(path);
+ return canCloneOnFS(info);
+}
+bool canCloneOnFS(const FilesystemInfo& info)
+{
+ return canCloneOnFS(info.fsType);
+}
+bool canCloneOnFS(FilesystemType type)
+{
+ return s_clone_filesystems.contains(type);
+}
+
+/**
+ * @brief if the Filesystem is reflink/clone capable and both paths are on the same device
+ *
+ */
+bool canClone(const QString& src, const QString& dst)
+{
+ auto srcVInfo = statFS(src);
+ auto dstVInfo = statFS(dst);
+
+ bool sameDevice = srcVInfo.rootPath == dstVInfo.rootPath;
+
+ return sameDevice && canCloneOnFS(srcVInfo) && canCloneOnFS(dstVInfo);
+}
+
+/**
+ * @brief reflink/clones a directory and it's contents from src to dest
+ * @param offset subdirectory form src to copy to dest
+ * @return if there was an error during the filecopy
+ */
+bool clone::operator()(const QString& offset, bool dryRun)
+{
+ if (!canClone(m_src.absolutePath(), m_dst.absolutePath())) {
+ qWarning() << "Can not clone: not same device or not clone/reflink filesystem";
+ qDebug() << "Source path:" << m_src.absolutePath();
+ qDebug() << "Destination path:" << m_dst.absolutePath();
+ emit cloneFailed(m_src.absolutePath(), m_dst.absolutePath());
+ return false;
+ }
+
+ m_cloned = 0; // reset counter
+
+ auto src = PathCombine(m_src.absolutePath(), offset);
+ auto dst = PathCombine(m_dst.absolutePath(), offset);
+
+ std::error_code err;
+
+ // Function that'll do the actual cloneing
+ auto cloneFile = [&](QString src_path, QString relative_dst_path) {
+ if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist))
+ return;
+
+ auto dst_path = PathCombine(dst, relative_dst_path);
+ if (!dryRun) {
+ ensureFilePathExists(dst_path);
+ clone_file(src_path, dst_path, err);
+ }
+ if (err) {
+ qDebug() << "Failed to clone files: error" << err.value() << "message" << QString::fromStdString(err.message());
+ qDebug() << "Source file:" << src_path;
+ qDebug() << "Destination file:" << dst_path;
+ }
+ m_cloned++;
+ emit fileCloned(src_path, dst_path);
+ };
+
+ // We can't use copy_opts::recursive because we need to take into account the
+ // blacklisted paths, so we iterate over the source directory, and if there's no blacklist
+ // match, we copy the file.
+ QDir src_dir(src);
+ QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories);
+
+ while (source_it.hasNext()) {
+ auto src_path = source_it.next();
+ auto relative_path = src_dir.relativeFilePath(src_path);
+
+ cloneFile(src_path, relative_path);
+ }
+
+ // If the root src is not a directory, the previous iterator won't run.
+ if (!fs::is_directory(StringUtils::toStdString(src)))
+ cloneFile(src, "");
+
+ return err.value() == 0;
+}
+
+/**
+ * @brief clone/reflink file from src to dst
+ *
+ */
+bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
+{
+ auto src_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(src).absoluteFilePath()));
+ auto dst_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(dst).absoluteFilePath()));
+
+ FilesystemInfo srcinfo = statFS(src);
+ FilesystemInfo dstinfo = statFS(dst);
+
+ if ((srcinfo.rootPath != dstinfo.rootPath) || (srcinfo.fsType != dstinfo.fsType)) {
+ ec = std::make_error_code(std::errc::not_supported);
+ qWarning() << "reflink/clone must be to the same device and filesystem! src and dst root filesystems do not match.";
+ return false;
+ }
+
+#if defined(Q_OS_WIN)
+
+ if (!win_ioctl_clone(src_path, dst_path, ec)) {
+ qDebug() << "failed win_ioctl_clone";
+ qWarning() << "clone/reflink not supported on windows outside of btrfs or ReFS!";
+ qWarning() << "check out https://github.com/maharmstone/btrfs for btrfs support!";
+ return false;
+ }
+
+#elif defined(Q_OS_LINUX)
+
+ if (!linux_ficlone(src_path, dst_path, ec)) {
+ qDebug() << "failed linux_ficlone:";
+ return false;
+ }
+
+#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+
+ if (!macos_bsd_clonefile(src_path, dst_path, ec)) {
+ qDebug() << "failed macos_bsd_clonefile:";
+ return false;
+ }
+
+#else
+
+ qWarning() << "clone/reflink not supported! unknown OS";
+ ec = std::make_error_code(std::errc::not_supported);
+ return false;
+
+#endif
+
+ return true;
+}
+
+#if defined(Q_OS_WIN)
+
+static long RoundUpToPowerOf2(long originalValue, long roundingMultiplePowerOf2)
+{
+ long mask = roundingMultiplePowerOf2 - 1;
+ return (originalValue + mask) & ~mask;
+}
+
+bool win_ioctl_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec)
+{
+ /**
+ * This algorithm inspired from https://github.com/0xbadfca11/reflink
+ * LICENSE MIT
+ *
+ * Additional references
+ * https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file
+ * https://github.com/microsoft/CopyOnWrite/blob/main/lib/Windows/WindowsCopyOnWriteFilesystem.cs#L94
+ */
+
+ HANDLE hSourceFile = CreateFileW(src_path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
+ if (hSourceFile == INVALID_HANDLE_VALUE) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to open source file" << src_path.c_str();
+ return false;
+ }
+
+ ULONG fs_flags;
+ if (!GetVolumeInformationByHandleW(hSourceFile, nullptr, 0, nullptr, nullptr, &fs_flags, nullptr, 0)) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to get Filesystem information for " << src_path.c_str();
+ CloseHandle(hSourceFile);
+ return false;
+ }
+ if (!(fs_flags & FILE_SUPPORTS_BLOCK_REFCOUNTING)) {
+ SetLastError(ERROR_NOT_CAPABLE);
+ ec = std::error_code(GetLastError(), std::system_category());
+ qWarning() << "Filesystem at " << src_path.c_str() << " does not support reflink";
+ CloseHandle(hSourceFile);
+ return false;
+ }
+
+ FILE_END_OF_FILE_INFO sourceFileLength;
+ if (!GetFileSizeEx(hSourceFile, &sourceFileLength.EndOfFile)) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to size of source file" << src_path.c_str();
+ CloseHandle(hSourceFile);
+ return false;
+ }
+ FILE_BASIC_INFO sourceFileBasicInfo;
+ if (!GetFileInformationByHandleEx(hSourceFile, FileBasicInfo, &sourceFileBasicInfo, sizeof(sourceFileBasicInfo))) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to source file info" << src_path.c_str();
+ CloseHandle(hSourceFile);
+ return false;
+ }
+ ULONG junk;
+ FSCTL_GET_INTEGRITY_INFORMATION_BUFFER sourceFileIntegrity;
+ if (!DeviceIoControl(hSourceFile, FSCTL_GET_INTEGRITY_INFORMATION, nullptr, 0, &sourceFileIntegrity, sizeof(sourceFileIntegrity), &junk,
+ nullptr)) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to source file integrity info" << src_path.c_str();
+ CloseHandle(hSourceFile);
+ return false;
+ }
+
+ HANDLE hDestFile = CreateFileW(dst_path.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE, 0, nullptr, CREATE_NEW, 0, hSourceFile);
+
+ if (hDestFile == INVALID_HANDLE_VALUE) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to open dest file" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ return false;
+ }
+ FILE_DISPOSITION_INFO destFileDispose = { TRUE };
+ if (!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose))) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to set dest file info" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+
+ if (!DeviceIoControl(hDestFile, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &junk, nullptr)) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to set dest sparseness" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ FSCTL_SET_INTEGRITY_INFORMATION_BUFFER setDestFileintegrity = { sourceFileIntegrity.ChecksumAlgorithm, sourceFileIntegrity.Reserved,
+ sourceFileIntegrity.Flags };
+ if (!DeviceIoControl(hDestFile, FSCTL_SET_INTEGRITY_INFORMATION, &setDestFileintegrity, sizeof(setDestFileintegrity), nullptr, 0,
+ nullptr, nullptr)) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to set dest file integrity info" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ if (!SetFileInformationByHandle(hDestFile, FileEndOfFileInfo, &sourceFileLength, sizeof(sourceFileLength))) {
+ ec = std::error_code(GetLastError(), std::system_category());
+ qDebug() << "Failed to set dest file size" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+
+ const LONG64 splitThreshold = (1LL << 32) - sourceFileIntegrity.ClusterSizeInBytes;
+
+ DUPLICATE_EXTENTS_DATA dupExtent;
+ dupExtent.FileHandle = hSourceFile;
+ for (LONG64 offset = 0, remain = RoundUpToPowerOf2(sourceFileLength.EndOfFile.QuadPart, sourceFileIntegrity.ClusterSizeInBytes);
+ remain > 0; offset += splitThreshold, remain -= splitThreshold) {
+ dupExtent.SourceFileOffset.QuadPart = dupExtent.TargetFileOffset.QuadPart = offset;
+ dupExtent.ByteCount.QuadPart = std::min(splitThreshold, remain);
+
+ if (!DeviceIoControl(hDestFile, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &dupExtent, sizeof(dupExtent), nullptr, 0, &junk, nullptr)) {
+ DWORD err = GetLastError();
+ QString additionalMessage;
+ if (err == ERROR_BLOCK_TOO_MANY_REFERENCES) {
+ static const int MaxClonesPerFile = 8175;
+ additionalMessage =
+ QString(
+ " This is ERROR_BLOCK_TOO_MANY_REFERENCES and may mean you have surpassed the maximum "
+ "allowed %1 references for a single file. "
+ "See "
+ "https://docs.microsoft.com/en-us/windows-server/storage/refs/block-cloning#functionality-restrictions-and-remarks")
+ .arg(MaxClonesPerFile);
+ }
+ ec = std::error_code(err, std::system_category());
+ qDebug() << "Failed copy-on-write cloning of" << src_path.c_str() << "to" << dst_path.c_str() << "with error" << err
+ << additionalMessage;
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ }
+
+ if (!(sourceFileBasicInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE)) {
+ FILE_SET_SPARSE_BUFFER setDestSparse = { FALSE };
+ if (!DeviceIoControl(hDestFile, FSCTL_SET_SPARSE, &setDestSparse, sizeof(setDestSparse), nullptr, 0, &junk, nullptr)) {
+ qDebug() << "Failed to set dest file sparseness" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ }
+
+ sourceFileBasicInfo.CreationTime.QuadPart = 0;
+ if (!SetFileInformationByHandle(hDestFile, FileBasicInfo, &sourceFileBasicInfo, sizeof(sourceFileBasicInfo))) {
+ qDebug() << "Failed to set dest file creation time" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ if (!FlushFileBuffers(hDestFile)) {
+ qDebug() << "Failed to flush dest file buffer" << dst_path.c_str();
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+ return false;
+ }
+ destFileDispose = { FALSE };
+ bool result = !!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose));
+
+ CloseHandle(hSourceFile);
+ CloseHandle(hDestFile);
+
+ return result;
+}
+
+#elif defined(Q_OS_LINUX)
+
+bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
+{
+ // https://man7.org/linux/man-pages/man2/ioctl_ficlone.2.html
+
+ int src_fd = open(src_path.c_str(), O_RDONLY);
+ if (src_fd == -1) {
+ qDebug() << "Failed to open file:" << src_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast(errno));
+ return false;
+ }
+ int dst_fd = open(dst_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
+ if (dst_fd == -1) {
+ qDebug() << "Failed to open file:" << dst_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast(errno));
+ close(src_fd);
+ return false;
+ }
+ // attempt to clone
+ if (ioctl(dst_fd, FICLONE, src_fd) == -1) {
+ qDebug() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast(errno));
+ close(src_fd);
+ close(dst_fd);
+ return false;
+ }
+ if (close(src_fd)) {
+ qDebug() << "Failed to close file:" << src_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ }
+ if (close(dst_fd)) {
+ qDebug() << "Failed to close file:" << dst_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ }
+ return true;
+}
+
+#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)
+{
+ // clonefile(const char * src, const char * dst, int flags);
+ // https://www.manpagez.com/man/2/clonefile/
+
+ qDebug() << "attempting file clone via clonefile" << src_path.c_str() << "to" << dst_path.c_str();
+ if (clonefile(src_path.c_str(), dst_path.c_str(), 0) == -1) {
+ qDebug() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast(errno));
+ return false;
+ }
+ return true;
+}
+#endif
+
+/**
+ * @brief if the Filesystem is symlink capable
+ *
+ */
+bool canLinkOnFS(const QString& path)
+{
+ FilesystemInfo info = statFS(path);
+ return canLinkOnFS(info);
+}
+bool canLinkOnFS(const FilesystemInfo& info)
+{
+ return canLinkOnFS(info.fsType);
+}
+bool canLinkOnFS(FilesystemType type)
+{
+ return !s_non_link_filesystems.contains(type);
+}
+/**
+ * @brief if the Filesystem is symlink capable on both ends
+ *
+ */
+bool canLink(const QString& src, const QString& dst)
+{
+ return canLinkOnFS(src) && canLinkOnFS(dst);
+}
+
+uintmax_t hardLinkCount(const QString& path)
+{
+ std::error_code err;
+ int count = fs::hard_link_count(StringUtils::toStdString(path), err);
+ if (err) {
+ qWarning() << "Failed to count hard links for" << path << ":" << QString::fromStdString(err.message());
+ count = 0;
+ }
+ return count;
+}
+
+} // namespace FS
diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h
index ac8937258..cb581d0c5 100644
--- a/launcher/FileSystem.h
+++ b/launcher/FileSystem.h
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu
+ * Copyright (C) 2022 TheKodeToad
+ * 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
@@ -38,9 +40,13 @@
#include "Exception.h"
#include "pathmatcher/IPathMatcher.h"
+#include
+
#include
#include
+#include
#include
+#include
namespace FS {
@@ -76,7 +82,9 @@ bool ensureFilePathExists(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 {
Q_OBJECT
public:
@@ -121,6 +129,135 @@ class copy : public QObject {
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 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 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 m_path_pairs;
+ QList m_path_results;
+ QList 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
*/
@@ -129,13 +266,30 @@ bool deletePath(QString path);
/**
* 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, const QString& path3);
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
@@ -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.
*/
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 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 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 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
diff --git a/launcher/HoeDown.h b/launcher/HoeDown.h
deleted file mode 100644
index cb62de6cf..000000000
--- a/launcher/HoeDown.h
+++ /dev/null
@@ -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
-#include
-#include
-#include
-
-/**
- * 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(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;
-};
diff --git a/launcher/InstanceCopyPrefs.cpp b/launcher/InstanceCopyPrefs.cpp
index 7b93a5164..0650002b8 100644
--- a/launcher/InstanceCopyPrefs.cpp
+++ b/launcher/InstanceCopyPrefs.cpp
@@ -16,8 +16,13 @@ bool InstanceCopyPrefs::allTrue() const
copyScreenshots;
}
+
// Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat")
QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const
+{
+ return getSelectedFiltersAsRegex({});
+}
+QString InstanceCopyPrefs::getSelectedFiltersAsRegex(const QStringList& additionalFilters) const
{
QStringList filters;
@@ -42,6 +47,10 @@ QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const
if(!copyScreenshots)
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 (!filters.isEmpty()) {
const QString MC_ROOT = "[.]?minecraft/";
@@ -93,6 +102,31 @@ bool InstanceCopyPrefs::isCopyScreenshotsEnabled() const
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 =======
void InstanceCopyPrefs::enableCopySaves(bool b)
{
@@ -133,3 +167,28 @@ void InstanceCopyPrefs::enableCopyScreenshots(bool 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;
+}
\ No newline at end of file
diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h
index 6988b2df3..c7bde0682 100644
--- a/launcher/InstanceCopyPrefs.h
+++ b/launcher/InstanceCopyPrefs.h
@@ -10,6 +10,7 @@ struct InstanceCopyPrefs {
public:
[[nodiscard]] bool allTrue() const;
[[nodiscard]] QString getSelectedFiltersAsRegex() const;
+ [[nodiscard]] QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const;
// Getters
[[nodiscard]] bool isCopySavesEnabled() const;
[[nodiscard]] bool isKeepPlaytimeEnabled() const;
@@ -19,6 +20,11 @@ struct InstanceCopyPrefs {
[[nodiscard]] bool isCopyServersEnabled() const;
[[nodiscard]] bool isCopyModsEnabled() 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
void enableCopySaves(bool b);
void enableKeepPlaytime(bool b);
@@ -28,6 +34,11 @@ struct InstanceCopyPrefs {
void enableCopyServers(bool b);
void enableCopyMods(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
bool copySaves = true;
@@ -38,4 +49,9 @@ struct InstanceCopyPrefs {
bool copyServers = true;
bool copyMods = true;
bool copyScreenshots = true;
+ bool useSymLinks = false;
+ bool linkRecursively = false;
+ bool useHardLinks = false;
+ bool dontLinkSaves = false;
+ bool useClone = false;
};
diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp
index 188d163b6..4ac3b51ad 100644
--- a/launcher/InstanceCopyTask.cpp
+++ b/launcher/InstanceCopyTask.cpp
@@ -1,18 +1,31 @@
#include "InstanceCopyTask.h"
-#include "settings/INISettingsObject.h"
+#include
+#include
#include "FileSystem.h"
#include "NullInstance.h"
#include "pathmatcher/RegexpMatcher.h"
-#include
+#include "settings/INISettingsObject.h"
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs)
{
m_origInstance = origInstance;
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();
- if (!filters.isEmpty())
- {
+ if (m_useLinks || m_useHardLinks) {
+ if (!filters.isEmpty())
+ filters += "|";
+ filters += "instance.cfg";
+ }
+
+ qDebug() << "CopyFilters:" << filters;
+
+ if (!filters.isEmpty()) {
// Set regex filter:
// FIXME: get this from the original instance type...
auto matcherReal = new RegexpMatcher(filters);
@@ -25,11 +38,78 @@ void InstanceCopyTask::executeTask()
{
setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
- m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]{
- FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
- folderCopy.followSymlinks(false).matcher(m_matcher.get());
+ auto copySaves = [&]() {
+ FS::copy savesCopy(FS::PathCombine(m_origInstance->instanceRoot(), "saves"), FS::PathCombine(m_stagingPath, "saves"));
+ 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::finished, this, &InstanceCopyTask::copyFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &InstanceCopyTask::copyAborted);
@@ -39,8 +119,7 @@ void InstanceCopyTask::executeTask()
void InstanceCopyTask::copyFinished()
{
auto successful = m_copyFuture.result();
- if(!successful)
- {
+ if (!successful) {
emitFailed(tr("Instance folder copy failed."));
return;
}
@@ -50,9 +129,11 @@ void InstanceCopyTask::copyFinished()
InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath));
inst->setName(name());
inst->setIconKey(m_instIcon);
- if(!m_keepPlaytime) {
+ if (!m_keepPlaytime) {
inst->resetTimePlayed();
}
+ if (m_useLinks)
+ inst->addLinkedInstanceId(m_origInstance->id());
emitSucceeded();
}
diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h
index 1f29b8545..aea9d99a1 100644
--- a/launcher/InstanceCopyTask.h
+++ b/launcher/InstanceCopyTask.h
@@ -30,4 +30,9 @@ private:
QFutureWatcher m_copyFutureWatcher;
std::unique_ptr m_matcher;
bool m_keepPlaytime;
+ bool m_useLinks = false;
+ bool m_useHardLinks = false;
+ bool m_copySaves = false;
+ bool m_linkRecursively = false;
+ bool m_useClone = false;
};
diff --git a/launcher/InstanceCreationTask.h b/launcher/InstanceCreationTask.h
index 03ee1a7aa..380fdf8a4 100644
--- a/launcher/InstanceCreationTask.h
+++ b/launcher/InstanceCreationTask.h
@@ -34,7 +34,7 @@ class InstanceCreationTask : public InstanceTask {
QString getError() const { return m_error_message; }
protected:
- void setError(QString message) { m_error_message = message; };
+ void setError(const QString& message) { m_error_message = message; };
protected:
bool m_abort = false;
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 6b3fd296f..352848f02 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -41,6 +41,7 @@
#include "MMCZip.h"
#include "NullInstance.h"
+#include "QObjectPtr.h"
#include "icons/IconList.h"
#include "icons/IconUtils.h"
@@ -66,7 +67,12 @@ bool InstanceImportTask::abort()
if (m_filesNetJob)
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();
}
@@ -88,11 +94,12 @@ void InstanceImportTask::executeTask()
entry->setStale(true);
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));
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
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::aborted, this, &InstanceImportTask::downloadAborted);
@@ -185,18 +192,20 @@ void InstanceImportTask::processZipPack()
// make sure we extract just the pack
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath());
connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &InstanceImportTask::extractFinished);
- connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &InstanceImportTask::extractAborted);
m_extractFutureWatcher.setFuture(m_extractFuture);
}
void InstanceImportTask::extractFinished()
{
m_packZip.reset();
- if (!m_extractFuture.result())
- {
+
+ if (m_extractFuture.isCanceled())
+ return;
+ if (!m_extractFuture.result().has_value()) {
emitFailed(tr("Failed to extract modpack"));
return;
}
+
QDir extractDir(m_stagingPath);
qDebug() << "Fixing permissions for extracted pack files...";
@@ -250,14 +259,9 @@ void InstanceImportTask::extractFinished()
}
}
-void InstanceImportTask::extractAborted()
-{
- emitAborted();
-}
-
void InstanceImportTask::processFlame()
{
- FlameCreationTask* inst_creation_task = nullptr;
+ shared_qobject_ptr inst_creation_task = nullptr;
if (!m_extra_info.isEmpty()) {
auto pack_id_it = m_extra_info.constFind("pack_id");
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())
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(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
} else {
// 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(m_stagingPath, m_globalSettings, m_parent, QString(), QString());
}
inst_creation_task->setName(*this);
@@ -283,25 +287,26 @@ void InstanceImportTask::processFlame()
inst_creation_task->setGroup(m_instGroup);
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());
emitSucceeded();
});
- connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
- connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
- connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
- connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
+ connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed);
+ connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress);
+ connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propogateStepProgress);
+ 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(inst_creation_task, &Task::aborted, this, &Task::abort);
- connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable);
+ connect(this, &Task::aborted, inst_creation_task.get(), &InstanceCreationTask::abort);
+ connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort);
+ connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable);
inst_creation_task->start();
}
void InstanceImportTask::processTechnic()
{
- shared_qobject_ptr packProcessor = new Technic::TechnicPackProcessor();
+ shared_qobject_ptr packProcessor{ new Technic::TechnicPackProcessor };
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed);
packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath);
@@ -361,7 +366,7 @@ void InstanceImportTask::processModrinth()
} else {
QString pack_id;
if (!m_sourceUrl.isEmpty()) {
- QRegularExpression regex(R"(data\/(.*)\/versions)");
+ QRegularExpression regex(R"(data\/([^\/]*)\/versions)");
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::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::details, this, &InstanceImportTask::setDetails);
connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort);
diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h
index 6b8ac966c..7fda439fc 100644
--- a/launcher/InstanceImportTask.h
+++ b/launcher/InstanceImportTask.h
@@ -81,7 +81,6 @@ private slots:
void downloadProgressChanged(qint64 current, qint64 total);
void downloadAborted();
void extractFinished();
- void extractAborted();
private: /* data */
NetJob::Ptr m_filesNetJob;
diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp
index 68e3e92cb..b4c520cd9 100644
--- a/launcher/InstanceList.cpp
+++ b/launcher/InstanceList.cpp
@@ -129,6 +129,16 @@ QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const
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
{
Q_UNUSED(parent);
@@ -787,7 +797,9 @@ class InstanceStaging : public Task {
connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable);
connect(child, &Task::status, this, &InstanceStaging::setStatus);
+ connect(child, &Task::details, this, &InstanceStaging::setDetails);
connect(child, &Task::progress, this, &InstanceStaging::setProgress);
+ connect(child, &Task::stepProgress, this, &InstanceStaging::propogateStepProgress);
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
}
@@ -865,7 +877,7 @@ Task* InstanceList::wrapInstanceTask(InstanceTask* task)
QString InstanceList::getStagedInstancePath()
{
- QString key = QUuid::createUuid().toString();
+ QString key = QUuid::createUuid().toString(QUuid::WithoutBraces);
QString tempDir = ".LAUNCHER_TEMP/";
QString relPath = FS::PathCombine(tempDir, key);
QDir rootPath(m_instDir);
diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h
index edacba3cd..48bede07d 100644
--- a/launcher/InstanceList.h
+++ b/launcher/InstanceList.h
@@ -154,6 +154,8 @@ public:
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
+ QStringList getLinkedInstancesById(const QString &id) const;
+
signals:
void dataIsInvalid();
void instancesChanged();
diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h
index 5d8beca93..b4b6e739d 100644
--- a/launcher/InstancePageProvider.h
+++ b/launcher/InstancePageProvider.h
@@ -36,9 +36,10 @@ public:
values.append(new VersionPage(onesix.get()));
values.append(ManagedPackPage::createPage(onesix.get()));
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(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 TexturePackPage(onesix.get(), onesix->texturePackList()));
values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList()));
diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp
index 52cc868a7..e29e22709 100644
--- a/launcher/JavaCommon.cpp
+++ b/launcher/JavaCommon.cpp
@@ -122,8 +122,7 @@ void JavaCommon::TestCheck::run()
return;
}
checker.reset(new JavaChecker());
- connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this,
- SLOT(checkFinished(JavaCheckResult)));
+ connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinished);
checker->m_path = m_path;
checker->performCheck();
}
@@ -137,8 +136,7 @@ void JavaCommon::TestCheck::checkFinished(JavaCheckResult result)
return;
}
checker.reset(new JavaChecker());
- connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this,
- SLOT(checkFinishedWithArgs(JavaCheckResult)));
+ connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinishedWithArgs);
checker->m_path = m_path;
checker->m_args = m_args;
checker->m_minMem = m_minMem;
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
index 11e3de152..070ee283c 100644
--- a/launcher/LaunchController.cpp
+++ b/launcher/LaunchController.cpp
@@ -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 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";
}
- m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
+ m_launcher->prependStep(makeShared(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
} else {
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(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher));
// Prepend Version
- m_launcher->prependStep(new TextPrint(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher));
+ m_launcher->prependStep(makeShared(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher));
m_launcher->start();
}
diff --git a/launcher/Launcher.in b/launcher/Launcher.in
index 68fac26a0..1a23f2555 100755
--- a/launcher/Launcher.in
+++ b/launcher/Launcher.in
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# Basic start script for running the launcher with the libs packaged with it.
function printerror {
diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp
index 6447f5c6f..d70f6d005 100644
--- a/launcher/LoggedProcess.cpp
+++ b/launcher/LoggedProcess.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022,2023 Sefa Eyeoglu
+ * Copyright (c) 2023 flowln
*
* 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
@@ -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.
connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut);
connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr);
- connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(on_exit(int,QProcess::ExitStatus)));
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- 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, QOverload::of(&QProcess::finished), this, &LoggedProcess::on_exit);
+ connect(this, &QProcess::errorOccurred, this, &LoggedProcess::on_error);
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);
+
+ if (!m_leftover_line.isEmpty()) {
+ str.prepend(m_leftover_line);
+ m_leftover_line = "";
+ }
+
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, QString::SkipEmptyParts);
#else
auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, Qt::SkipEmptyParts);
#endif
+
+ if (!str.endsWith(QChar::LineFeed))
+ m_leftover_line = lines.takeLast();
return lines;
}
diff --git a/launcher/LoggedProcess.h b/launcher/LoggedProcess.h
index 2360d1ea1..af3ed79f4 100644
--- a/launcher/LoggedProcess.h
+++ b/launcher/LoggedProcess.h
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022,2023 Sefa Eyeoglu
*
* 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
@@ -88,9 +88,12 @@ private slots:
private:
void changeState(LoggedProcess::State state);
+ QStringList reprocess(const QByteArray& data, QTextDecoder& decoder);
+
private:
QTextDecoder m_err_decoder = QTextDecoder(QTextCodec::codecForLocale());
QTextDecoder m_out_decoder = QTextDecoder(QTextCodec::codecForLocale());
+ QString m_leftover_line;
bool m_killed = false;
State m_state = NotRunning;
int m_exit_code = 0;
diff --git a/launcher/MMCTime.cpp b/launcher/MMCTime.cpp
index 70bc4135f..1702ec066 100644
--- a/launcher/MMCTime.cpp
+++ b/launcher/MMCTime.cpp
@@ -18,6 +18,8 @@
#include
#include
+#include
+#include
QString Time::prettifyDuration(int64_t duration) {
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);
}
+
+QString Time::humanReadableDuration(double duration, int precision) {
+
+ using days = std::chrono::duration>;
+
+ QString outStr;
+ QTextStream os(&outStr);
+
+ bool neg = false;
+ if (duration < 0) {
+ neg = true; // flag
+ duration *= -1; // invert
+ }
+
+ auto std_duration = std::chrono::duration(duration);
+ auto d = std::chrono::duration_cast(std_duration);
+ std_duration -= d;
+ auto h = std::chrono::duration_cast(std_duration);
+ std_duration -= h;
+ auto m = std::chrono::duration_cast(std_duration);
+ std_duration -= m;
+ auto s = std::chrono::duration_cast(std_duration);
+ std_duration -= s;
+ auto ms = std::chrono::duration_cast(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;
+}
\ No newline at end of file
diff --git a/launcher/MMCTime.h b/launcher/MMCTime.h
index 10ff2ffe6..6a5780b44 100644
--- a/launcher/MMCTime.h
+++ b/launcher/MMCTime.h
@@ -22,4 +22,13 @@ namespace Time {
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);
}
diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp
index f66003432..1a336375b 100644
--- a/launcher/MMCZip.cpp
+++ b/launcher/MMCZip.cpp
@@ -94,20 +94,28 @@ bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet &containe
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);
if (!directory.exists()) return false;
for (auto e : files) {
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;
}
-bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files)
+bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
{
QuaZip zip(fileCompressed);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
@@ -116,7 +124,7 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList
return false;
}
- auto result = compressDirFiles(&zip, dir, files);
+ auto result = compressDirFiles(&zip, dir, files, followSymlinks);
zip.close();
if(zip.getZipError()!=0) {
@@ -275,7 +283,8 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re
// ours
std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
{
- QDir directory(target);
+ auto target_top_dir = QUrl::fromLocalFile(target);
+
QStringList extracted;
qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target;
@@ -294,48 +303,53 @@ std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & su
return std::nullopt;
}
- do
- {
- QString name = zip->getCurrentFileName();
- if(!name.startsWith(subdir))
- {
+ do {
+ QString file_name = zip->getCurrentFileName();
+ if (!file_name.startsWith(subdir))
continue;
- }
- name.remove(0, subdir.size());
- auto original_name = name;
+ auto relative_file_name = QDir::fromNativeSeparators(file_name.remove(0, subdir.size()));
+ 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
- QString path;
- if(name.contains('/') && !name.endsWith('/')){
- path = name.section('/', 0, -2) + "/";
- FS::ensureFolderPathExists(FS::PathCombine(target, path));
+ QString sub_path;
+ if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) {
+ sub_path = relative_file_name.section('/', 0, -2) + '/';
+ FS::ensureFolderPathExists(FS::PathCombine(target, sub_path));
- name = name.split('/').last();
+ relative_file_name = relative_file_name.split('/').last();
}
- QString absFilePath;
- if(name.isEmpty())
- {
- absFilePath = directory.absoluteFilePath(name) + "/";
- }
- else
- {
- absFilePath = directory.absoluteFilePath(path + name);
+ QString target_file_path;
+ if (relative_file_name.isEmpty()) {
+ target_file_path = target + '/';
+ } else {
+ target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name);
+ if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/'))
+ target_file_path += '/';
}
- if (!JlCompress::extractFile(zip, "", absFilePath))
- {
- qWarning() << "Failed to extract file" << original_name << "to" << 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;
+ return std::nullopt;
+ }
+
+ if (!JlCompress::extractFile(zip, "", target_file_path)) {
+ qWarning() << "Failed to extract file" << original_name << "to" << target_file_path;
JlCompress::removeFile(extracted);
return std::nullopt;
}
- extracted.append(absFilePath);
- QFile::setPermissions(absFilePath, QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser);
+ extracted.append(target_file_path);
+ 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());
+
return extracted;
}
diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h
index 81f9cb908..2a78f830f 100644
--- a/launcher/MMCZip.h
+++ b/launcher/MMCZip.h
@@ -59,18 +59,20 @@ namespace MMCZip
* \param zip target archive
* \param dir directory that will be compressed (to compress with relative paths)
* \param files list of files to compress
+ * \param followSymlinks should follow symlinks when compressing file data
* \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
* \param fileCompressed target archive file
* \param dir directory that will be compressed (to compress with relative paths)
* \param files list of files to compress
+ * \param followSymlinks should follow symlinks when compressing file data
* \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
diff --git a/launcher/MangoHud.cpp b/launcher/MangoHud.cpp
index d635518e3..90e48e298 100644
--- a/launcher/MangoHud.cpp
+++ b/launcher/MangoHud.cpp
@@ -19,6 +19,7 @@
#include
#include
#include
+#include
#include
#include "MangoHud.h"
@@ -75,9 +76,27 @@ QString getLibraryString()
}
for (QString vkLayer : vkLayerList) {
- QString filePath = FS::PathCombine(vkLayer, "MangoHud.json");
- if (!QFile::exists(filePath))
+ // prefer to use architecture specific vulkan layers
+ 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;
+ }
auto conf = Json::requireDocument(filePath, vkLayer);
auto confObject = Json::requireObject(conf, vkLayer);
diff --git a/launcher/Markdown.h b/launcher/Markdown.h
new file mode 100644
index 000000000..f115dd570
--- /dev/null
+++ b/launcher/Markdown.h
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 Joshua Goins
+ *
+ * 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 .
+ */
+
+#pragma once
+
+#include
+#include
+
+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;
+}
\ No newline at end of file
diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp
deleted file mode 100644
index 2b0343f44..000000000
--- a/launcher/ModDownloadTask.cpp
+++ /dev/null
@@ -1,72 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
-* PolyMC - Minecraft Launcher
-* Copyright (c) 2022 flowln
-* Copyright (C) 2022 Sefa Eyeoglu
-*
-* 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 .
-*/
-
-#include "ModDownloadTask.h"
-
-#include "Application.h"
-#include "minecraft/mod/ModFolderModel.h"
-
-ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr 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};
-}
diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h
index b1ef1c8dd..a1c64b433 100644
--- a/launcher/QObjectPtr.h
+++ b/launcher/QObjectPtr.h
@@ -20,18 +20,34 @@ using unique_qobject_ptr = QScopedPointer;
template
class shared_qobject_ptr : public QSharedPointer {
public:
- constexpr shared_qobject_ptr() : QSharedPointer() {}
- constexpr shared_qobject_ptr(T* ptr) : QSharedPointer(ptr, &QObject::deleteLater) {}
+ constexpr explicit shared_qobject_ptr() : QSharedPointer() {}
+ constexpr explicit shared_qobject_ptr(T* ptr) : QSharedPointer(ptr, &QObject::deleteLater) {}
constexpr shared_qobject_ptr(std::nullptr_t null_ptr) : QSharedPointer(null_ptr, &QObject::deleteLater) {}
template
constexpr shared_qobject_ptr(const shared_qobject_ptr& other) : QSharedPointer(other)
{}
+ template
+ constexpr shared_qobject_ptr(const QSharedPointer& other) : QSharedPointer(other)
+ {}
+
void reset() { QSharedPointer::reset(); }
+ void reset(T*&& other)
+ {
+ shared_qobject_ptr t(other);
+ this->swap(t);
+ }
void reset(const shared_qobject_ptr& other)
{
shared_qobject_ptr t(other);
this->swap(t);
}
};
+
+template
+shared_qobject_ptr makeShared(Args... args)
+{
+ auto obj = new T(args...);
+ return shared_qobject_ptr(obj);
+}
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/QVariantUtils.h
similarity index 55%
rename from launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
rename to launcher/QVariantUtils.h
index 40d82e6fd..7e422c3e8 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
+++ b/launcher/QVariantUtils.h
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu
+ * Copyright (C) 2023 flowln
*
* 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
@@ -35,32 +36,35 @@
#pragma once
-#include "modplatform/ModAPI.h"
-#include "ui/pages/modplatform/ModPage.h"
-#include "modplatform/modrinth/ModrinthAPI.h"
+#include
+#include
-class ModrinthModPage : public ModPage {
- Q_OBJECT
+namespace QVariantUtils {
- public:
- static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
+template
+inline QList toList(QVariant src) {
+ QVariantList variantList = src.toList();
+
+ QList list_t;
+ list_t.reserve(variantList.size());
+ for (const QVariant& v : variantList)
{
- return ModPage::create(dialog, instance);
+ list_t.append(v.value());
+ }
+ return list_t;
+}
+
+template
+inline QVariant fromList(QList val) {
+ QVariantList variantList;
+ variantList.reserve(val.size());
+ for (const T& v : val)
+ {
+ variantList.append(v);
}
- ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance);
- ~ModrinthModPage() override = default;
+ return variantList;
+}
- 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;
-};
+}
\ No newline at end of file
diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp
new file mode 100644
index 000000000..61b918aaf
--- /dev/null
+++ b/launcher/ResourceDownloadTask.cpp
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+* Prism Launcher - Minecraft Launcher
+* Copyright (c) 2022-2023 flowln
+* Copyright (C) 2022 Sefa Eyeoglu
+*
+* 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 .
+*/
+
+#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 packs,
+ bool is_indexed)
+ : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs)
+{
+ if (auto model = dynamic_cast(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(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 };
+}
diff --git a/launcher/ModDownloadTask.h b/launcher/ResourceDownloadTask.h
similarity index 59%
rename from launcher/ModDownloadTask.h
rename to launcher/ResourceDownloadTask.h
index 950204703..73ad2d070 100644
--- a/launcher/ModDownloadTask.h
+++ b/launcher/ResourceDownloadTask.h
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
-* PolyMC - Minecraft Launcher
-* Copyright (c) 2022 flowln
+* Prism Launcher - Minecraft Launcher
+* Copyright (c) 2022-2023 flowln
* Copyright (C) 2022 Sefa Eyeoglu
*
* This program is free software: you can redistribute it and/or modify
@@ -25,32 +25,32 @@
#include "modplatform/ModIndex.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
-class ModFolderModel;
+class ResourceFolderModel;
-class ModDownloadTask : public SequentialTask {
+class ResourceDownloadTask : public SequentialTask {
Q_OBJECT
public:
- explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods, bool is_indexed = true);
- const QString& getFilename() const { return m_mod_version.fileName; }
+ explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr packs, bool is_indexed = true);
+ 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:
- ModPlatform::IndexedPack m_mod;
- ModPlatform::IndexedVersion m_mod_version;
- const std::shared_ptr mods;
+ ModPlatform::IndexedPack m_pack;
+ ModPlatform::IndexedVersion m_pack_version;
+ const std::shared_ptr m_pack_model;
NetJob::Ptr m_filesNetJob;
LocalModUpdateTask::Ptr m_update_task;
void downloadProgressChanged(qint64 current, qint64 total);
-
void downloadFailed(QString reason);
-
void downloadSucceeded();
std::tuple to_delete {"", ""};
private slots:
- void hasOldMod(QString name, QString filename);
+ void hasOldResource(QString name, QString filename);
};
diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp
index 0f3c3669f..e08e6fdce 100644
--- a/launcher/StringUtils.cpp
+++ b/launcher/StringUtils.cpp
@@ -1,5 +1,45 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu
+ * Copyright (C) 2023 flowln
+ *
+ * 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 .
+ *
+ * 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
+#include
+#include
+
/// 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
@@ -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
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);
+}
diff --git a/launcher/StringUtils.h b/launcher/StringUtils.h
index 1799605b3..f90a6ac75 100644
--- a/launcher/StringUtils.h
+++ b/launcher/StringUtils.h
@@ -1,6 +1,43 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu
+ * Copyright (C) 2023 flowln
+ *
+ * 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 .
+ *
+ * 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
#include
+#include
namespace StringUtils {
@@ -29,4 +66,17 @@ inline QString fromStdString(string s)
#endif
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
diff --git a/launcher/UpdateController.cpp b/launcher/UpdateController.cpp
deleted file mode 100644
index 9ff448549..000000000
--- a/launcher/UpdateController.cpp
+++ /dev/null
@@ -1,443 +0,0 @@
-#include
-#include
-#include
-#include
-#include "UpdateController.h"
-#include
-#include
-#include
-#include
-
-#include "BuildConfig.h"
-
-
-// from
-#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;
-}
diff --git a/launcher/UpdateController.h b/launcher/UpdateController.h
deleted file mode 100644
index 715554e53..000000000
--- a/launcher/UpdateController.h
+++ /dev/null
@@ -1,44 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-
-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 m_replace_backups;
- QList m_delete_backups;
- enum Failure
- {
- Replace,
- Delete,
- Start,
- Nothing
- } m_failedOperationType = Nothing;
- QString m_failedFile;
-};
diff --git a/launcher/Version.cpp b/launcher/Version.cpp
index b9090e299..e4311f314 100644
--- a/launcher/Version.cpp
+++ b/launcher/Version.cpp
@@ -1,85 +1,128 @@
#include "Version.h"
-#include
-#include
+#include
#include
#include
+#include
-Version::Version(const QString &str) : m_string(str)
+Version::Version(QString str) : m_string(std::move(str))
{
parse();
}
-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;
- }
+#define VERSION_OPERATOR(return_on_different) \
+ bool exclude_our_sections = false; \
+ bool exclude_their_sections = false; \
+ \
+ const auto size = qMax(m_sections.size(), other.m_sections.size()); \
+ for (int i = 0; i < size; ++i) { \
+ Section sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); \
+ Section sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); \
+ \
+ { /* Don't include appendixes in the comparison */ \
+ 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;
}
-bool Version::operator<=(const Version &other) const
+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 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;
- }
- }
+ VERSION_OPERATOR(false)
return true;
}
-bool Version::operator!=(const Version &other) const
+bool Version::operator!=(const Version& other) const
{
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()
{
m_sections.clear();
+ QString currentSection;
- // FIXME: this is bad. versions can contain a lot more separators...
- QStringList parts = m_string.split('.');
+ if (m_string.isEmpty())
+ return;
- for (const auto& part : parts)
- {
- m_sections.append(Section(part));
+ auto classChange = [&](QChar lastChar, QChar currentChar) {
+ if (lastChar.isNull())
+ return false;
+ if (lastChar.isDigit() != currentChar.isDigit())
+ return true;
+
+ const QList 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;
}
diff --git a/launcher/Version.h b/launcher/Version.h
index aceb7a073..659f8e54e 100644
--- a/launcher/Version.h
+++ b/launcher/Version.h
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
+ * Copyright (C) 2023 flowln
* Copyright (C) 2022 Sefa Eyeoglu
*
* This program is free software: you can redistribute it and/or modify
@@ -35,17 +36,17 @@
#pragma once
+#include
+#include
#include
#include
-#include
class QUrl;
-class Version
-{
-public:
- Version(const QString &str);
- Version() {}
+class Version {
+ public:
+ Version(QString str);
+ Version() = default;
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;
- QString toString() const
- {
- return m_string;
- }
+ QString toString() const { return m_string; }
-private:
- QString m_string;
- struct Section
- {
- explicit Section(const QString &fullString)
+ friend QDebug operator<<(QDebug debug, const Version& v);
+
+ private:
+ struct Section {
+ explicit Section(QString fullString) : m_fullString(std::move(fullString))
{
- m_fullString = fullString;
int cutoff = m_fullString.size();
- for(int i = 0; i < m_fullString.size(); i++)
- {
- if(!m_fullString[i].isDigit())
- {
+ for (int i = 0; i < m_fullString.size(); i++) {
+ if (!m_fullString[i].isDigit()) {
cutoff = i;
break;
}
}
+
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto numPart = QStringView{m_fullString}.left(cutoff);
#else
auto numPart = m_fullString.leftRef(cutoff);
#endif
- if(numPart.size())
- {
- numValid = true;
+
+ if (!numPart.isEmpty()) {
+ m_isNull = false;
m_numPart = numPart.toInt();
}
+
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto stringPart = QStringView{m_fullString}.mid(cutoff);
#else
auto stringPart = m_fullString.midRef(cutoff);
#endif
- if(stringPart.size())
- {
+
+ if (!stringPart.isEmpty()) {
+ m_isNull = false;
m_stringPart = stringPart.toString();
}
}
- explicit Section() {}
- bool numValid = false;
+
+ explicit Section() = default;
+
+ bool m_isNull = true;
+
int m_numPart = 0;
QString m_stringPart;
+
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)
- {
- return m_numPart != other.m_numPart || m_stringPart != other.m_stringPart;
- }
- else
- {
- return m_fullString != other.m_fullString;
+ if (m_isNull && !other.m_isNull)
+ return false;
+ if (!m_isNull && other.m_isNull)
+ return false;
+
+ if (!m_isNull && !other.m_isNull) {
+ return (m_numPart == other.m_numPart) && (m_stringPart == other.m_stringPart);
}
+
+ return true;
}
- inline bool operator<(const Section &other) const
- {
- if(numValid && other.numValid)
- {
- if(m_numPart < other.m_numPart)
+
+ inline bool operator<(const Section& other) const
+ {
+ static auto unequal_is_less = [](Section const& non_null) -> bool {
+ 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;
- if(m_numPart == other.m_numPart && m_stringPart < other.m_stringPart)
+ if (m_numPart == other.m_numPart && m_stringPart < other.m_stringPart)
return true;
+
+ if (!m_stringPart.isEmpty() && other.m_stringPart.isEmpty())
+ return false;
+ if (m_stringPart.isEmpty() && !other.m_stringPart.isEmpty())
+ return true;
+
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
{
- if(numValid && other.numValid)
- {
- 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;
- }
+ return !(*this < other || *this == other);
}
};
+
+ private:
+ QString m_string;
QList m_sections;
void parse();
};
+
+
diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp
new file mode 100644
index 000000000..c9599b820
--- /dev/null
+++ b/launcher/filelink/FileLink.cpp
@@ -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 .
+ *
+ */
+
+#include "FileLink.h"
+#include "BuildConfig.h"
+
+#include "StringUtils.h"
+
+#include
+
+#include
+#include
+
+#include
+
+#include
+
+#include
+
+#if defined Q_OS_WIN32
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include
+#include
+#endif
+
+// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
+
+#ifdef __APPLE__
+#include // 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() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
+#define GHC_USE_STD_FS
+#include
+namespace fs = std::filesystem;
+#endif // MacOS min version check
+#endif // Other OSes version check
+
+#ifndef GHC_USE_STD_FS
+#include