Merge branch 'develop' into feat/launcher-updater

Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
Rachel Powers 2023-07-30 14:19:31 -07:00 committed by GitHub
commit 7c1a465ec9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
164 changed files with 4287 additions and 2043 deletions

8
.editorconfig Normal file
View File

@ -0,0 +1,8 @@
# EditorConfig specs and documentation: https://EditorConfig.org
# top-most EditorConfig file
root = true
# C++ Code Style settings
[*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}]
cpp_generate_documentation_comments = doxygen_slash_star

32
.github/workflows/backport.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Backport
on:
pull_request_target:
types: [closed, labeled]
# WARNING:
# When extending this action, be aware that $GITHUB_TOKEN allows write access to
# the GitHub repository. This means that it should not evaluate user input in a
# way that allows code injection.
permissions:
contents: read
jobs:
backport:
permissions:
contents: write # for korthout/backport-action to create branch
pull-requests: write # for korthout/backport-action to create PR to backport
name: Backport Pull Request
if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs
uses: korthout/backport-action@v1.3.1
with:
# Config README: https://github.com/korthout/backport-action#backport-action
pull_description: |-
Bot-based backport to `${target_branch}`, triggered by a label in #${pull_number}.

View File

@ -272,23 +272,23 @@ jobs:
- name: Configure CMake (macOS)
if: runner.os == 'macOS' && matrix.qt_ver == 6
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja
- name: Configure CMake (macOS-Legacy)
if: runner.os == 'macOS' && matrix.qt_ver == 5
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -G Ninja
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -G Ninja
- name: Configure CMake (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0}
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja
- name: Configure CMake (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == ''
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }}
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }}
# https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix)
if ("${{ env.CCACHE_VAR }}")
{
@ -303,7 +303,7 @@ jobs:
- name: Configure CMake (Linux)
if: runner.os == 'Linux'
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -G Ninja
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -G Ninja
##
# BUILD
@ -630,33 +630,3 @@ jobs:
with:
bundle: "Prism Launcher.flatpak"
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
nix:
runs-on: ubuntu-latest
strategy:
matrix:
package:
- prismlauncher
- prismlauncher-qt5
steps:
- name: Clone repository
if: inputs.build_type == 'Debug'
uses: actions/checkout@v3
with:
submodules: 'true'
- name: Install nix
if: inputs.build_type == 'Debug'
uses: cachix/install-nix-action@v22
with:
install_url: https://nixos.org/nix/install
extra_nix_config: |
auto-optimise-store = true
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v12
if: inputs.build_type == 'Debug'
with:
name: prismlauncher
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build
if: inputs.build_type == 'Debug'
run: nix build .#${{ matrix.package }} --print-build-logs

28
.github/workflows/update-flake.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Update Flake Lockfile
on:
schedule:
# run weekly on sunday
- cron: "0 0 * * 0"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-flake:
if: github.repository == 'PrismLauncher/PrismLauncher'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v22
- uses: DeterminateSystems/update-flake-lock@v19
with:
commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile"
pr-labels: |
Linux
simple change

3
.gitmodules vendored
View File

@ -19,3 +19,6 @@
[submodule "libraries/cmark"]
path = libraries/cmark
url = https://github.com/commonmark/cmark.git
[submodule "flatpak/shared-modules"]
path = flatpak/shared-modules
url = https://github.com/flathub/shared-modules.git

View File

@ -85,6 +85,38 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0")
# set CXXFLAGS for build targets
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}")
option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" on)
# If this is a Debug build turn on address sanitiser
if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND DEBUG_ADDRESS_SANITIZER)
message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off")
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
# using clang with clang-cl front end
message(STATUS "Address Sanitizer available on Clang MSVC frontend")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-")
else()
# AppleClang and Clang
message(STATUS "Address Sanitizer available on Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
endif()
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
# GCC
message(STATUS "Address Sanitizer available on GCC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
link_libraries("asan")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
message(STATUS "Address Sanitizer available on MSVC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-")
else()
message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}")
endif()
endif()
option(ENABLE_LTO "Enable Link Time Optimization" off)
if(ENABLE_LTO)
@ -146,7 +178,7 @@ set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.
set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0")
# Build platform.
set(Launcher_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.")
set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.")
# Github repo URL with releases for updater
set(Launcher_UPDATER_GITHUB_REPO "https://github.com/PrismLauncher/PrismLauncher" CACHE STRING "Base github URL for the updater.")
@ -340,7 +372,7 @@ elseif(UNIX)
set(BINARY_DEST_DIR "bin")
set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}")
set(JARS_DEST_DIR "share/${Launcher_APP_BINARY_NAME}")
set(JARS_DEST_DIR "share/${Launcher_Name}")
# install as bundle with no dependencies included
set(INSTALL_BUNDLE "nodeps")
@ -353,7 +385,7 @@ 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}")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}")
if(Launcher_ManPage)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6")

View File

@ -19,7 +19,7 @@ In an effort to ensure that the code you contribute is actually compatible with
This can be done by appending `-s` to your `git commit` call, or by manually appending the following text to your commit message:
```
```text
<commit message>
Signed-off-by: Author name <Author email>
@ -27,7 +27,7 @@ Signed-off-by: Author name <Author email>
By signing off your work, you agree to the terms below:
```
```text
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
@ -61,3 +61,9 @@ As a bonus, you can also [cryptographically sign your commits][gh-signing-commit
[gh-signing-commits]: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits
[gh-vigilant-mode]: https://docs.github.com/en/authentication/managing-commit-signature-verification/displaying-verification-statuses-for-all-of-your-commits
## Backporting to Release Branches
We use [automated backports](https://github.com/PrismLauncher/PrismLauncher/blob/develop/.github/workflows/backport.yml) to merge specific contributions from develop into `release` branches.
This is done when pull requests are merged and have labels such as `backport release-7.x` - which should be added along with the milestone for the release.

View File

@ -42,7 +42,7 @@ Feel free to create a GitHub issue if you find a bug or want to suggest a new fe
- **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://prismlauncher.org/matrix)
[![Prism Launcher 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:**
@ -50,7 +50,7 @@ Feel free to create a GitHub issue if you find a bug or want to suggest a new fe
## Translations
The translation effort for PrismLauncher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at <https://github.com/PrismLauncher/Translations>
The translation effort for Prism Launcher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at <https://github.com/PrismLauncher/Translations>
## Building
@ -82,14 +82,16 @@ Thanks to the awesome people over at [MacStadium](https://www.macstadium.com/),
## Forking/Redistributing/Custom builds policy
We don't care what you do with your fork/custom build as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy:
You are free to fork, redistribute and provide custom builds as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy:
- Make it clear that your fork is not PrismLauncher and is not endorsed by or affiliated with the PrismLauncher project (<https://prismlauncher.org>).
- Go through [CMakeLists.txt](CMakeLists.txt) and change PrismLauncher's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled).
- Make it clear that your fork is not Prism Launcher and is not endorsed by or affiliated with the Prism Launcher project (<https://prismlauncher.org>).
- Go through [CMakeLists.txt](CMakeLists.txt) and change Prism Launcher's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled).
If you have any questions or want any clarification on the above conditions please make an issue and ask us.
Be aware that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions:
If you are just building Prism Launcher for your distribution, please make sure to set the `Launcher_BUILD_PLATFORM` to a slug representing your distribution. Examples are `archlinux`, `fedora` and `nixpkgs`.
Note that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions:
- [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use)
- [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions)

View File

@ -74,7 +74,7 @@ Config::Config()
MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@";
MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@";
if (BUILD_PLATFORM == "macOS" && !MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty())
if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty())
{
UPDATER_ENABLED = true;
} else if(!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) {

View File

@ -68,7 +68,7 @@ class Config {
bool UPDATER_ENABLED = false;
/// A short string identifying this build's platform. For example, "lin64" or "win32".
/// A short string identifying this build's platform or distribution.
QString BUILD_PLATFORM;
/// A short string identifying this build's valid artifacts int he updater. For example, "lin64" or "win32".

54
flake.lock generated
View File

@ -21,11 +21,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1683560683,
"narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
"lastModified": 1688466019,
"narHash": "sha256-VeM2akYrBYMsb4W/MmBo1zmaMfgbL4cH3Pu8PGyIwJ0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"rev": "8e8d955c22df93dbe24f19ea04f47a74adbdc5ec",
"type": "github"
},
"original": {
@ -35,12 +35,15 @@
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"lastModified": 1685518550,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github"
},
"original": {
@ -73,11 +76,11 @@
"libnbtplusplus": {
"flake": false,
"locked": {
"lastModified": 1650031308,
"narHash": "sha256-TvVOjkUobYJD9itQYueELJX3wmecvEdCbJ0FinW2mL4=",
"lastModified": 1690036783,
"narHash": "sha256-A5kTgICnx+Qdq3Fir/bKTfdTt/T1NQP2SC+nhN1ENug=",
"owner": "PrismLauncher",
"repo": "libnbtplusplus",
"rev": "2203af7eeb48c45398139b583615134efd8d407f",
"rev": "a5e8fd52b8bf4ab5d5bcc042b2a247867589985f",
"type": "github"
},
"original": {
@ -88,11 +91,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1685012353,
"narHash": "sha256-U3oOge4cHnav8OLGdRVhL45xoRj4Ppd+It6nPC9nNIU=",
"lastModified": 1690630721,
"narHash": "sha256-Y04onHyBQT4Erfr2fc82dbJTfXGYrf4V0ysLUYnPOP8=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "aeb75dba965e790de427b73315d5addf91a54955",
"rev": "d2b52322f35597c62abf56de91b0236746b2a03d",
"type": "github"
},
"original": {
@ -105,11 +108,11 @@
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1682879489,
"narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
"lastModified": 1688049487,
"narHash": "sha256-100g4iaKC9MalDjUW9iN6Jl/OocTDtXdeAj7pEGIRh4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
"rev": "4bc72cae107788bf3f24f30db2e2f685c9298dc9",
"type": "github"
},
"original": {
@ -135,11 +138,11 @@
]
},
"locked": {
"lastModified": 1684842236,
"narHash": "sha256-rYWsIXHvNhVQ15RQlBUv67W3YnM+Pd+DuXGMvCBq2IE=",
"lastModified": 1690628027,
"narHash": "sha256-OTSbA2hM6VmxyZ/4siYPANffMBzIsKu04GLjXcv8ST0=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "61e567d6497bc9556f391faebe5e410e6623217f",
"rev": "1e2443dd3f669eb65433b2fc26a3065e05a7dc9c",
"type": "github"
},
"original": {
@ -156,6 +159,21 @@
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

22
flatpak/libdecor.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "libdecor",
"buildsystem": "meson",
"config-opts": [
"-Ddemo=false"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.freedesktop.org/libdecor/libdecor.git",
"commit": "73260393a97291c887e1074ab7f318e031be0ac6"
},
{
"type": "patch",
"path": "patches/weird_libdecor.patch"
}
],
"cleanup": [
"/include",
"/lib/pkgconfig"
]
}

View File

@ -5,13 +5,6 @@ sdk: org.kde.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk17
- org.freedesktop.Sdk.Extension.openjdk8
add-extensions:
com.valvesoftware.Steam.Utility.gamescope:
version: stable
add-ld-path: lib
no-autodownload: true
autodelete: false
directory: utils/gamescope
command: prismlauncher
finish-args:
@ -26,21 +19,31 @@ finish-args:
# Mod drag&drop
- --filesystem=xdg-download:ro
cleanup:
- /lib/libGLU*
modules:
# Might be needed by some Controller mods (see https://github.com/isXander/Controlify/issues/31)
- shared-modules/libusb/libusb.json
# Needed for proper Wayland support
- libdecor.json
- name: prismlauncher
buildsystem: cmake-ninja
builddir: true
config-opts:
- -DLauncher_BUILD_PLATFORM=flatpak
- -DCMAKE_BUILD_TYPE=Debug
- -DCMAKE_BUILD_TYPE=RelWithDebInfo
- -DLauncher_QT_VERSION_MAJOR=5
build-options:
env:
JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17
JAVA_COMPILER: /usr/lib/sdk/openjdk17/jvm/openjdk-17/bin/javac
sources:
- type: dir
path: ../
builddir: true
- type: dir
path: ../
- name: openjdk
buildsystem: simple
build-commands:
@ -49,14 +52,45 @@ modules:
- mv /app/jre /app/jdk/17
- /usr/lib/sdk/openjdk8/install.sh
- mv /app/jre /app/jdk/8
cleanup: [/jre]
cleanup:
- /jre
- name: glfw
buildsystem: cmake-ninja
config-opts:
- -DCMAKE_BUILD_TYPE=RelWithDebInfo
- -DBUILD_SHARED_LIBS:BOOL=ON
- -DGLFW_USE_WAYLAND=ON
sources:
- type: git
url: https://github.com/glfw/glfw.git
commit: 3fa2360720eeba1964df3c0ecf4b5df8648a8e52
- type: patch
path: patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch
- type: patch
path: patches/0005-Add-warning-about-being-an-unofficial-patch.patch
- type: patch
path: patches/0007-Platform-Prefer-Wayland-over-X11.patch
cleanup:
- /include
- /lib/cmake
- /lib/pkgconfig
- name: xrandr
buildsystem: autotools
sources:
- type: archive
url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.1.tar.xz
sha256: 7bc76daf9d72f8aff885efad04ce06b90488a1a169d118dea8a2b661832e8762
cleanup: [/share/man, /bin/xkeystone]
url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.2.tar.xz
sha256: c8bee4790d9058bacc4b6246456c58021db58a87ddda1a9d0139bf5f18f1f240
x-checker-data:
type: anitya
project-id: 14957
stable-only: true
url-template: https://xorg.freedesktop.org/archive/individual/app/xrandr-$version.tar.xz
cleanup:
- /share/man
- /bin/xkeystone
- name: gamemode
buildsystem: meson
config-opts:
@ -67,19 +101,56 @@ modules:
# post-install is running inside the build dir, we need it from the source though
- install -Dm755 ../data/gamemoderun -t /app/bin
sources:
- type: git
url: https://github.com/FeralInteractive/gamemode
tag: "1.7"
commit: 4dc99dff76218718763a6b07fc1900fa6d1dafd9
- type: archive
archive-type: tar-gzip
url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.7
sha256: 57ce73ba605d1cf12f8d13725006a895182308d93eba0f69f285648449641803
x-checker-data:
type: json
url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest
version-query: .tag_name
url-query: .tarball_url
timestamp-query: .published_at
cleanup:
- /include
- /lib/pkgconfig
- /lib/libgamemodeauto.a
- name: glxinfo
buildsystem: meson
config-opts:
- --bindir=/app/mesa-demos
- -Degl=disabled
- -Dglut=disabled
- -Dosmesa=disabled
- -Dvulkan=disabled
- -Dwayland=disabled
post-install:
- mv -v /app/mesa-demos/glxinfo /app/bin
sources:
- type: archive
url: https://archive.mesa3d.org/demos/mesa-demos-9.0.0.tar.xz
sha256: 3046a3d26a7b051af7ebdd257a5f23bfeb160cad6ed952329cdff1e9f1ed496b
x-checker-data:
type: anitya
project-id: 16781
stable-only: true
url-template: https://archive.mesa3d.org/demos/mesa-demos-$version.tar.xz
cleanup:
- /include
- /mesa-demos
- /share
modules:
- shared-modules/glu/glu-9.json
- name: enhance
buildsystem: simple
build-commands:
- mkdir -p /app/utils/gamescope
- install -Dm755 prime-run /app/bin/prime-run
- mv /app/bin/prismlauncher /app/bin/prismrun
- install -Dm755 prismlauncher /app/bin/prismlauncher
sources:
- type: file
path: ../flatpak/prime-run
path: prime-run
- type: file
path: ../flatpak/prismlauncher
path: prismlauncher

View File

@ -0,0 +1,24 @@
diff --git a/src/wl_window.c b/src/wl_window.c
index 52d3b9eb..4ac4eb5d 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -2117,8 +2117,7 @@ void _glfwSetWindowTitleWayland(_GLFWwindow* window, const char* title)
void _glfwSetWindowIconWayland(_GLFWwindow* window,
int count, const GLFWimage* images)
{
- _glfwInputError(GLFW_FEATURE_UNAVAILABLE,
- "Wayland: The platform does not support setting the window icon");
+ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the window icon\n");
}
void _glfwGetWindowPosWayland(_GLFWwindow* window, int* xpos, int* ypos)
@@ -2361,8 +2360,7 @@ void _glfwRequestWindowAttentionWayland(_GLFWwindow* window)
void _glfwFocusWindowWayland(_GLFWwindow* window)
{
- _glfwInputError(GLFW_FEATURE_UNAVAILABLE,
- "Wayland: The platform does not support setting the input focus");
+ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the input focus\n");
}
void _glfwSetWindowMonitorWayland(_GLFWwindow* window,

View File

@ -0,0 +1,17 @@
diff --git a/src/init.c b/src/init.c
index 06dbb3f2..a7c6da86 100644
--- a/src/init.c
+++ b/src/init.c
@@ -449,6 +449,12 @@ GLFWAPI int glfwInit(void)
_glfw.initialized = GLFW_TRUE;
glfwDefaultWindowHints();
+
+ fprintf(stderr, "!!! Patched GLFW from https://github.com/Admicos/minecraft-wayland\n"
+ "!!! If any issues with the window, or some issues with rendering, occur, "
+ "first try with the built-in GLFW, and if that solves the issue, report there first.\n"
+ "!!! Use outside Minecraft is untested, and things might break.\n");
+
return GLFW_TRUE;
}

View File

@ -0,0 +1,20 @@
diff --git a/src/platform.c b/src/platform.c
index c5966ae7..3e7442f9 100644
--- a/src/platform.c
+++ b/src/platform.c
@@ -49,12 +49,12 @@ static const struct
#if defined(_GLFW_COCOA)
{ GLFW_PLATFORM_COCOA, _glfwConnectCocoa },
#endif
-#if defined(_GLFW_X11)
- { GLFW_PLATFORM_X11, _glfwConnectX11 },
-#endif
#if defined(_GLFW_WAYLAND)
{ GLFW_PLATFORM_WAYLAND, _glfwConnectWayland },
#endif
+#if defined(_GLFW_X11)
+ { GLFW_PLATFORM_X11, _glfwConnectX11 },
+#endif
};
GLFWbool _glfwSelectPlatform(int desiredID, _GLFWplatform* platform)

View File

@ -0,0 +1,40 @@
diff --git a/src/libdecor.c b/src/libdecor.c
index a9c1106..1aa38b3 100644
--- a/src/libdecor.c
+++ b/src/libdecor.c
@@ -1391,22 +1391,32 @@ calculate_priority(const struct libdecor_plugin_description *plugin_description)
static bool
check_symbol_conflicts(const struct libdecor_plugin_description *plugin_description)
{
+ bool ret = true;
char * const *symbol;
+ void* main_prog = dlopen(NULL, RTLD_LAZY);
+ if (!main_prog) {
+ fprintf(stderr, "Plugin \"%s\" couldn't check conflicting symbols: \"%s\".\n",
+ plugin_description->description, dlerror());
+ return false;
+ }
+
symbol = plugin_description->conflicting_symbols;
while (*symbol) {
dlerror();
- dlsym (RTLD_DEFAULT, *symbol);
+ dlsym (main_prog, *symbol);
if (!dlerror()) {
fprintf(stderr, "Plugin \"%s\" uses conflicting symbol \"%s\".\n",
plugin_description->description, *symbol);
- return false;
+ ret = false;
+ break;
}
symbol++;
}
- return true;
+ dlclose(main_prog);
+ return ret;
}
static struct plugin_loader *

View File

@ -5,7 +5,7 @@ for i in {0..9}; do
test -S "$XDG_RUNTIME_DIR"/discord-ipc-"$i" || ln -sf {app/com.discordapp.Discord,"$XDG_RUNTIME_DIR"}/discord-ipc-"$i";
done
export PATH="${PATH}${PATH:+:}/app/utils/gamescope/bin:/usr/lib/extensions/vulkan/MangoHud/bin"
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}${LD_LIBRARY_PATH:+:}/usr/lib/extensions/vulkan/MangoHud/\$LIB/"
export PATH="${PATH}${PATH:+:}/usr/lib/extensions/vulkan/gamescope/bin:/usr/lib/extensions/vulkan/MangoHud/bin"
export VK_LAYER_PATH="/usr/lib/extensions/vulkan/share/vulkan/implicit_layer.d/"
exec /app/bin/prismrun "$@"

@ -0,0 +1 @@
Subproject commit 45094ca570be383d06df729b6972830ec63bd3df

6
garnix.yaml Normal file
View File

@ -0,0 +1,6 @@
builds:
exclude: []
include:
- "checks.x86_64-linux.*"
- "devShells.*.*"
- "packages.*.*"

View File

@ -6,9 +6,10 @@
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Lenny McLennington <lenny@sneed.church>
* Copyright (C) 2022 Tayou <tayou@gmx.net>
* Copyright (C) 2022 Tayou <git@tayou.org>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
* Copyright (C) 2023 seth <getchoo at tuta dot io>
*
* 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
@ -537,8 +538,12 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
}
}
// seach root path
if (!foundLoggingRules) {
logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
if(!foundLoggingRules) {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
logRulesPath = FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME, logRulesFile);
#else
logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
#endif
qDebug() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath);
}
@ -580,6 +585,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME) << ", (c) 2022-2023 "
<< qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
qDebug() << "Version : " << BuildConfig.printableVersionString();
qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM;
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
qDebug() << "Compiled for : " << BuildConfig.systemID();
@ -673,6 +679,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// Language
m_settings->registerSetting("Language", QString());
m_settings->registerSetting("UseSystemLocale", false);
// Console
m_settings->registerSetting("ShowConsole", false);
@ -709,6 +716,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("IgnoreJavaCompatibility", false);
m_settings->registerSetting("IgnoreJavaWizard", false);
// Mod loader settings
m_settings->registerSetting("DisableQuiltBeacon", false);
// Native library workarounds
m_settings->registerSetting("UseNativeOpenAL", false);
m_settings->registerSetting("UseNativeGLFW", false);
@ -790,8 +800,16 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->reset("PastebinCustomAPIBase");
}
}
// meta URL
m_settings->registerSetting("MetaURLOverride", "");
{
// Meta URL
m_settings->registerSetting("MetaURLOverride", "");
QUrl metaUrl(m_settings->get("MetaURLOverride").toString());
// get rid of invalid meta urls
if (!metaUrl.isValid() || metaUrl.scheme() != "http" || metaUrl.scheme() != "https")
m_settings->reset("MetaURLOverride");
}
m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false);
@ -1087,11 +1105,7 @@ bool Application::createSetupWizard()
}
return false;
}();
bool languageRequired = [&]() {
if (settings()->get("Language").toString().isEmpty())
return true;
return false;
}();
bool languageRequired = settings()->get("Language").toString().isEmpty();
bool pasteInterventionRequired = settings()->get("PastebinURL") != "";
bool themeInterventionRequired = settings()->get("ApplicationTheme") == "";
bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired;
@ -1351,6 +1365,16 @@ QIcon Application::getThemedIcon(const QString& name)
return QIcon::fromTheme(name);
}
QList<CatPack*> Application::getValidCatPacks()
{
return m_themeManager->getValidCatPacks();
}
QString Application::getCatPack(QString catName)
{
return m_themeManager->getCatPack(catName);
}
bool Application::openJsonEditor(const QString& filename)
{
const QString file = QDir::current().absoluteFilePath(filename);
@ -1680,7 +1704,7 @@ QString Application::getJarPath(QString jarFile)
{
QStringList potentialPaths = {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
FS::PathCombine(m_rootPath, "share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME),
FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME),
#endif
FS::PathCombine(m_rootPath, "jars"),
FS::PathCombine(applicationDirPath(), "jars"),

View File

@ -2,7 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Tayou <tayou@gmx.net>
* Copyright (C) 2022 Tayou <git@tayou.org>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
@ -48,6 +48,7 @@
#include <BaseInstance.h>
#include "minecraft/launch/MinecraftServerTarget.h"
#include "ui/themes/CatPack.h"
class LaunchController;
class LocalPeer;
@ -126,9 +127,11 @@ public:
void setApplicationTheme(const QString& name);
shared_qobject_ptr<ExternalUpdater> updater() {
return m_updater;
}
QList<CatPack*> getValidCatPacks();
QString getCatPack(QString catName = "");
shared_qobject_ptr<ExternalUpdater> updater() { return m_updater; }
void triggerUpdateCheck();

View File

@ -15,16 +15,15 @@
#pragma once
#include <memory>
#include <QString>
#include <QMetaType>
#include <QString>
#include <memory>
/*!
* An abstract base class for versions.
*/
class BaseVersion
{
public:
class BaseVersion {
public:
using Ptr = std::shared_ptr<BaseVersion>;
virtual ~BaseVersion() {}
/*!
@ -45,14 +44,8 @@ public:
*/
virtual QString typeString() const = 0;
virtual bool operator<(BaseVersion &a)
{
return name() < a.name();
};
virtual bool operator>(BaseVersion &a)
{
return name() > a.name();
};
virtual bool operator<(BaseVersion& a) { return name() < a.name(); };
virtual bool operator>(BaseVersion& a) { return name() > a.name(); };
};
Q_DECLARE_METATYPE(BaseVersion::Ptr)

View File

@ -501,6 +501,9 @@ set(API_SOURCES
modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h
modplatform/helpers/OverrideUtils.cpp
modplatform/helpers/ExportToModList.h
modplatform/helpers/ExportToModList.cpp
)
set(FTB_SOURCES
@ -528,6 +531,8 @@ set(FLAME_SOURCES
modplatform/flame/FlameCheckUpdate.h
modplatform/flame/FlameInstanceCreationTask.h
modplatform/flame/FlameInstanceCreationTask.cpp
modplatform/flame/FlamePackExportTask.h
modplatform/flame/FlamePackExportTask.cpp
)
set(MODRINTH_SOURCES
@ -829,6 +834,8 @@ SET(LAUNCHER_SOURCES
ui/themes/SystemTheme.h
ui/themes/ThemeManager.cpp
ui/themes/ThemeManager.h
ui/themes/CatPack.cpp
ui/themes/CatPack.h
# Processes
LaunchController.h
@ -980,8 +987,10 @@ SET(LAUNCHER_SOURCES
ui/dialogs/EditAccountDialog.h
ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h
ui/dialogs/ExportMrPackDialog.cpp
ui/dialogs/ExportMrPackDialog.h
ui/dialogs/ExportPackDialog.cpp
ui/dialogs/ExportPackDialog.h
ui/dialogs/ExportToModListDialog.cpp
ui/dialogs/ExportToModListDialog.h
ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourceDialog.cpp
@ -1137,7 +1146,8 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/ExportMrPackDialog.ui
ui/dialogs/ExportPackDialog.ui
ui/dialogs/ExportToModListDialog.ui
ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui

View File

@ -40,6 +40,7 @@
#include <QFileSystemModel>
#include <QSortFilterProxyModel>
#include <QStack>
#include <algorithm>
#include "FileSystem.h"
#include "SeparatorPrefixTree.h"
#include "StringUtils.h"
@ -254,3 +255,25 @@ bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex&
return true;
}
bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
auto fileInfo = fsm->fileInfo(index);
return !ignoreFile(fileInfo);
}
bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const
{
auto fileName = fileInfo.fileName();
auto path = relPath(fileInfo.absoluteFilePath());
return std::any_of(m_ignoreFiles.cbegin(), m_ignoreFiles.cend(), [fileName](auto iFileName) { return fileName == iFileName; }) ||
m_ignoreFilePaths.covers(path);
}
bool FileIgnoreProxy::filterFile(const QString& fileName) const
{
return blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(root), fileName));
}

View File

@ -36,6 +36,7 @@
#pragma once
#include <QFileInfo>
#include <QSortFilterProxyModel>
#include "SeparatorPrefixTree.h"
@ -63,10 +64,22 @@ class FileIgnoreProxy : public QSortFilterProxyModel {
inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; }
inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; }
// list of file names that need to be removed completely from model
inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; }
// list of relative paths that need to be removed completely from model
inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; }
bool filterFile(const QString& fileName) const;
protected:
bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const;
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const;
bool ignoreFile(QFileInfo file) const;
private:
const QString root;
SeparatorPrefixTree<'/'> blocked;
QStringList m_ignoreFiles;
SeparatorPrefixTree<'/'> m_ignoreFilePaths;
};

View File

@ -36,6 +36,7 @@
*/
#include "FileSystem.h"
#include <QPair>
#include "BuildConfig.h"
@ -102,7 +103,7 @@ namespace fs = ghc::filesystem;
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <unistd.h>
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
#elif defined(Q_OS_MACOS)
#include <sys/attr.h>
#include <sys/clonefile.h>
#elif defined(Q_OS_WIN)
@ -280,6 +281,7 @@ bool copy::operator()(const QString& offset, bool dryRun)
{
using copy_opts = fs::copy_options;
m_copied = 0; // reset counter
m_failedPaths.clear();
// NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32
@ -314,6 +316,9 @@ bool copy::operator()(const QString& offset, bool dryRun)
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path;
m_failedPaths.append(dst_path);
emit copyFailed(relative_dst_path);
return;
}
m_copied++;
emit fileCopied(relative_dst_path);
@ -810,9 +815,43 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name));
}
#if defined(Q_OS_MACOS)
destination += ".command";
// Create the Application
QDir applicationDirectory = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/";
QFile f(destination);
if (!applicationDirectory.mkpath(".")) {
qWarning() << "Couldn't create application directory";
return false;
}
QDir application = applicationDirectory.path() + "/" + name + ".app/";
if (application.exists()) {
qWarning() << "Application already exists!";
return false;
}
if (!application.mkpath(".")) {
qWarning() << "Couldn't create application";
return false;
}
QDir content = application.path() + "/Contents/";
QDir resources = content.path() + "/Resources/";
QDir binaryDir = content.path() + "/MacOS/";
QFile info = content.path() + "/Info.plist";
if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) {
qWarning() << "Couldn't create directories within application";
return false;
}
info.open(QIODevice::WriteOnly | QIODevice::Text);
QFile(icon).rename(resources.path() + "/Icon.icns");
// Create the Command file
QString exec = binaryDir.path() + "/Run.command";
QFile f(exec);
f.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream stream(&f);
@ -829,6 +868,28 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther);
// Generate the Info.plist
QTextStream infoStream(&info);
infoStream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \n"
"<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" "
"\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
"<plist version=\"1.0\">\n"
"<dict>\n"
" <key>CFBundleExecutable</key>\n"
" <string>Run.command</string>\n" // The path to the executable
" <key>CFBundleIconFile</key>\n"
" <string>Icon.icns</string>\n"
" <key>CFBundleName</key>\n"
" <string>" << name << "</string>\n" // Name of the application
" <key>CFBundlePackageType</key>\n"
" <string>APPL</string>\n"
" <key>CFBundleShortVersionString</key>\n"
" <string>1.0</string>\n"
" <key>CFBundleVersion</key>\n"
" <string>1.0</string>\n"
"</dict>\n"
"</plist>";
return true;
#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated
@ -1114,6 +1175,7 @@ bool clone::operator()(const QString& offset, bool dryRun)
}
m_cloned = 0; // reset counter
m_failedClones.clear();
auto src = PathCombine(m_src.absolutePath(), offset);
auto dst = PathCombine(m_dst.absolutePath(), offset);
@ -1134,6 +1196,9 @@ bool clone::operator()(const QString& offset, bool dryRun)
qDebug() << "Failed to clone files: error" << err.value() << "message" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path;
m_failedClones.append(qMakePair(src_path, dst_path));
emit cloneFailed(src_path, dst_path);
return;
}
m_cloned++;
emit fileCloned(src_path, dst_path);
@ -1193,7 +1258,7 @@ bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
return false;
}
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
#elif defined(Q_OS_MACOS)
if (!macos_bsd_clonefile(src_path, dst_path, ec)) {
qDebug() << "failed macos_bsd_clonefile:";
@ -1422,7 +1487,7 @@ bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std
return true;
}
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
#elif defined(Q_OS_MACOS)
bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
{

View File

@ -43,6 +43,7 @@
#include <system_error>
#include <QDir>
#include <QPair>
#include <QFlags>
#include <QLocalServer>
#include <QObject>
@ -129,9 +130,12 @@ class copy : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCopied() { return m_copied; }
int totalFailed() { return m_failedPaths.length(); }
QStringList failed() { return m_failedPaths; }
signals:
void fileCopied(const QString& relativeName);
void copyFailed(const QString& relativeName);
// TODO: maybe add a "shouldCopy" signal in the future?
private:
@ -145,6 +149,7 @@ class copy : public QObject {
QDir m_src;
QDir m_dst;
int m_copied;
QStringList m_failedPaths;
};
struct LinkPair {
@ -489,6 +494,9 @@ class clone : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCloned() { return m_cloned; }
int totalFailed() { return m_failedClones.length(); }
QList<QPair<QString, QString>> failed() { return m_failedClones; }
signals:
void fileCloned(const QString& src, const QString& dst);
@ -503,6 +511,7 @@ class clone : public QObject {
QDir m_src;
QDir m_dst;
int m_cloned;
QList<QPair<QString, QString>> m_failedClones;
};
/**

View File

@ -101,7 +101,7 @@ void InstanceImportTask::executeTask()
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::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);
@ -295,7 +295,7 @@ void InstanceImportTask::processFlame()
});
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::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
@ -387,7 +387,7 @@ 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::stepProgress, this, &InstanceImportTask::propagateStepProgress);
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);

View File

@ -799,7 +799,7 @@ class InstanceStaging : public Task {
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(child, &Task::stepProgress, this, &InstanceStaging::propagateStepProgress);
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
}

View File

@ -187,8 +187,8 @@ void LaunchController::login() {
switch(m_accountToUse->accountState()) {
case AccountState::Offline: {
m_session->wants_online = false;
// NOTE: fallthrough is intentional
}
/* fallthrough */
case AccountState::Online: {
if(!m_session->wants_online) {
// we ask the user for a player name
@ -267,8 +267,8 @@ void LaunchController::login() {
// This means some sort of soft error that we can fix with a refresh ... so let's refresh.
case AccountState::Unchecked: {
m_accountToUse->refresh();
// NOTE: fallthrough intentional
}
/* fallthrough */
case AccountState::Working: {
// refresh is in progress, we need to wait for it to finish to proceed.
ProgressDialog progDialog(m_parentWidget);
@ -390,7 +390,10 @@ void LaunchController::launchInstance()
m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher));
// Prepend Version
m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher));
{
auto versionString = QString("%1 version: %2 (%3)").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM);
m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), versionString + "\n\n", MessageLevel::Launcher));
}
m_launcher->start();
}

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -33,56 +34,50 @@
* limitations under the License.
*/
#include "MMCZip.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include "MMCZip.h"
#include "FileSystem.h"
#include <QCoreApplication>
#include <QDebug>
#include <QtConcurrentRun>
namespace MMCZip {
// ours
bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, const FilterFunction filter)
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction filter)
{
QuaZip modZip(from.filePath());
modZip.open(QuaZip::mdUnzip);
QuaZipFile fileInsideMod(&modZip);
QuaZipFile zipOutFile(into);
for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile())
{
for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) {
QString filename = modZip.getCurrentFileName();
if (filter && !filter(filename))
{
qDebug() << "Skipping file " << filename << " from "
<< from.fileName() << " - filtered";
if (filter && !filter(filename)) {
qDebug() << "Skipping file " << filename << " from " << from.fileName() << " - filtered";
continue;
}
if (contained.contains(filename))
{
qDebug() << "Skipping already contained file " << filename << " from "
<< from.fileName();
if (contained.contains(filename)) {
qDebug() << "Skipping already contained file " << filename << " from " << from.fileName();
continue;
}
contained.insert(filename);
if (!fileInsideMod.open(QIODevice::ReadOnly))
{
if (!fileInsideMod.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open " << filename << " from " << from.fileName();
return false;
}
QuaZipNewInfo info_out(fileInsideMod.getActualFileName());
if (!zipOutFile.open(QIODevice::WriteOnly, info_out))
{
if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) {
qCritical() << "Failed to open " << filename << " in the jar";
fileInsideMod.close();
return false;
}
if (!JlCompress::copyData(fileInsideMod, zipOutFile))
{
if (!JlCompress::copyData(fileInsideMod, zipOutFile)) {
zipOutFile.close();
fileInsideMod.close();
qCritical() << "Failed to copy data of " << filename << " into the jar";
@ -94,10 +89,11 @@ bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &containe
return true;
}
bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks)
bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks)
{
QDir directory(dir);
if (!directory.exists()) return false;
if (!directory.exists())
return false;
for (auto e : files) {
auto filePath = directory.relativeFilePath(e.absoluteFilePath());
@ -109,17 +105,18 @@ bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, boo
srcPath = e.canonicalFilePath();
}
}
if( !JlCompress::compressFile(zip, srcPath, filePath)) return false;
if (!JlCompress::compressFile(zip, srcPath, filePath))
return false;
}
return true;
}
bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
{
QuaZip zip(fileCompressed);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
if(!zip.open(QuaZip::mdCreate)) {
if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(fileCompressed);
return false;
}
@ -127,7 +124,7 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList
auto result = compressDirFiles(&zip, dir, files, followSymlinks);
zip.close();
if(zip.getZipError()!=0) {
if (zip.getZipError() != 0) {
QFile::remove(fileCompressed);
return false;
}
@ -137,11 +134,10 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList
#if defined(LAUNCHER_APPLICATION)
// ours
bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods)
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods)
{
QuaZip zipOut(targetJarPath);
if (!zipOut.open(QuaZip::mdCreate))
{
if (!zipOut.open(QuaZip::mdCreate)) {
QFile::remove(targetJarPath);
qCritical() << "Failed to open the minecraft.jar for modding";
return false;
@ -152,37 +148,29 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
// Modify the jar
// This needs to be done in reverse-order to ensure we respect the loading order of components
for (auto i = mods.crbegin(); i != mods.crend(); i++)
{
for (auto i = mods.crbegin(); i != mods.crend(); i++) {
const auto* mod = *i;
// do not merge disabled mods.
if (!mod->enabled())
continue;
if (mod->type() == ResourceType::ZIPFILE)
{
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles))
{
if (mod->type() == ResourceType::ZIPFILE) {
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) {
zipOut.close();
QFile::remove(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false;
}
}
else if (mod->type() == ResourceType::SINGLEFILE)
{
} else if (mod->type() == ResourceType::SINGLEFILE) {
// FIXME: buggy - does not work with addedFiles
auto filename = mod->fileinfo();
if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName()))
{
if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) {
zipOut.close();
QFile::remove(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false;
}
addedFiles.insert(filename.fileName());
}
else if (mod->type() == ResourceType::FOLDER)
{
} else if (mod->type() == ResourceType::FOLDER) {
// untested, but seems to be unused / not possible to reach
// FIXME: buggy - does not work with addedFiles
auto filename = mod->fileinfo();
@ -191,25 +179,21 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
dir.cdUp();
QString parent_dir = dir.absolutePath();
auto files = QFileInfoList();
MMCZip::collectFileListRecursively(what_to_zip, nullptr, &files, nullptr);
collectFileListRecursively(what_to_zip, nullptr, &files, nullptr);
for (auto e : files) {
if (addedFiles.contains(e.filePath()))
files.removeAll(e);
}
if (!MMCZip::compressDirFiles(&zipOut, parent_dir, files))
{
if (!compressDirFiles(&zipOut, parent_dir, files)) {
zipOut.close();
QFile::remove(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false;
}
qDebug() << "Adding folder " << filename.fileName() << " from "
<< filename.absoluteFilePath();
}
else
{
qDebug() << "Adding folder " << filename.fileName() << " from " << filename.absoluteFilePath();
} else {
// Make sure we do not continue launching when something is missing or undefined...
zipOut.close();
QFile::remove(targetJarPath);
@ -218,8 +202,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
}
}
if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key){return !key.contains("META-INF");}))
{
if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) {
zipOut.close();
QFile::remove(targetJarPath);
qCritical() << "Failed to insert minecraft.jar contents.";
@ -228,8 +211,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
// Recompress the jar
zipOut.close();
if (zipOut.getZipError() != 0)
{
if (zipOut.getZipError() != 0) {
QFile::remove(targetJarPath);
qCritical() << "Failed to finalize minecraft.jar!";
return false;
@ -239,7 +221,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
#endif
// ours
QString MMCZip::findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root)
QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root)
{
QuaZipDir rootDir(zip, root);
for (auto&& fileName : rootDir.entryList(QDir::Files)) {
@ -263,27 +245,23 @@ QString MMCZip::findFolderOfFileInZip(QuaZip* zip, const QString& what, const QS
}
// ours
bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root)
bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root)
{
QuaZipDir rootDir(zip, root);
for(auto fileName: rootDir.entryList(QDir::Files))
{
if(fileName == what)
{
for (auto fileName : rootDir.entryList(QDir::Files)) {
if (fileName == what) {
result.append(root);
return true;
}
}
for(auto fileName: rootDir.entryList(QDir::Dirs))
{
for (auto fileName : rootDir.entryList(QDir::Dirs)) {
findFilesInZip(zip, what, result, root + fileName);
}
return !result.isEmpty();
}
// ours
std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, const QString& target)
{
auto target_top_dir = QUrl::fromLocalFile(target);
@ -291,16 +269,13 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target;
auto numEntries = zip->getEntriesCount();
if(numEntries < 0) {
if (numEntries < 0) {
qWarning() << "Failed to enumerate files in archive";
return std::nullopt;
}
else if(numEntries == 0) {
} else if (numEntries == 0) {
qDebug() << "Extracting empty archives seems odd...";
return extracted;
}
else if (!zip->goToFirstFile())
{
} else if (!zip->goToFirstFile()) {
qWarning() << "Failed to seek to first file in zip";
return std::nullopt;
}
@ -336,7 +311,8 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
}
if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" << target;
qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path"
<< target;
return std::nullopt;
}
@ -347,7 +323,8 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
}
extracted.append(target_file_path);
QFile::setPermissions(target_file_path, QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser);
QFile::setPermissions(target_file_path,
QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser);
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (zip->goToNextFile());
@ -356,66 +333,66 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
}
// ours
bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &target)
bool extractRelFile(QuaZip* zip, const QString& file, const QString& target)
{
return JlCompress::extractFile(zip, file, target);
}
// ours
std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir)
std::optional<QStringList> extractDir(QString fileCompressed, QString dir)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip))
{
if (!zip.open(QuaZip::mdUnzip)) {
// check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed);
if(fileInfo.size() == 22) {
if (fileInfo.size() == 22) {
return QStringList();
}
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
;
return std::nullopt;
}
return MMCZip::extractSubDir(&zip, "", dir);
return extractSubDir(&zip, "", dir);
}
// ours
std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir)
std::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip))
{
if (!zip.open(QuaZip::mdUnzip)) {
// check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed);
if(fileInfo.size() == 22) {
if (fileInfo.size() == 22) {
return QStringList();
}
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
;
return std::nullopt;
}
return MMCZip::extractSubDir(&zip, subdir, dir);
return extractSubDir(&zip, subdir, dir);
}
// ours
bool MMCZip::extractFile(QString fileCompressed, QString file, QString target)
bool extractFile(QString fileCompressed, QString file, QString target)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip))
{
if (!zip.open(QuaZip::mdUnzip)) {
// check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed);
if(fileInfo.size() == 22) {
if (fileInfo.size() == 22) {
return true;
}
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
return false;
}
return MMCZip::extractRelFile(&zip, file, target);
return extractRelFile(&zip, file, target);
}
bool MMCZip::collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList *files,
MMCZip::FilterFunction excludeFilter) {
bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter)
{
QDir rootDirectory(rootDir);
if (!rootDirectory.exists()) return false;
if (!rootDirectory.exists())
return false;
QDir directory;
if (subDir == nullptr)
@ -423,25 +400,107 @@ bool MMCZip::collectFileListRecursively(const QString& rootDir, const QString& s
else
directory = QDir(subDir);
if (!directory.exists()) return false; // shouldn't ever happen
if (!directory.exists())
return false; // shouldn't ever happen
// recurse directories
QFileInfoList entries = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden);
for (const auto& e: entries) {
for (const auto& e : entries) {
if (!collectFileListRecursively(rootDir, e.filePath(), files, excludeFilter))
return false;
}
// collect files
entries = directory.entryInfoList(QDir::Files);
for (const auto& e: entries) {
for (const auto& e : entries) {
QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath());
if (excludeFilter && excludeFilter(relativeFilePath)) {
qDebug() << "Skipping file " << relativeFilePath;
continue;
}
files->append(e); // we want the original paths for MMCZip::compressDirFiles
files->append(e); // we want the original paths for compressDirFiles
}
return true;
}
void ExportToZipTask::executeTask()
{
setStatus("Adding files...");
setProgress(0, m_files.length());
m_build_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); });
connect(&m_build_zip_watcher, &QFutureWatcher<ZipResult>::finished, this, &ExportToZipTask::finish);
m_build_zip_watcher.setFuture(m_build_zip_future);
}
auto ExportToZipTask::exportZip() -> ZipResult
{
if (!m_dir.exists()) {
return ZipResult(tr("Folder doesn't exist"));
}
if (!m_output.isOpen() && !m_output.open(QuaZip::mdCreate)) {
return ZipResult(tr("Could not create file"));
}
for (auto fileName : m_extra_files.keys()) {
if (m_build_zip_future.isCanceled())
return ZipResult();
QuaZipFile indexFile(&m_output);
if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileName))) {
return ZipResult(tr("Could not create:") + fileName);
}
indexFile.write(m_extra_files[fileName]);
}
for (const QFileInfo& file : m_files) {
if (m_build_zip_future.isCanceled())
return ZipResult();
auto absolute = file.absoluteFilePath();
auto relative = m_dir.relativeFilePath(absolute);
setStatus("Compresing: " + relative);
setProgress(m_progress + 1, m_progressTotal);
if (m_follow_symlinks) {
if (file.isSymLink())
absolute = file.symLinkTarget();
else
absolute = file.canonicalFilePath();
}
if (!m_exclude_files.contains(relative) && !JlCompress::compressFile(&m_output, absolute, m_destination_prefix + relative)) {
return ZipResult(tr("Could not read and compress %1").arg(relative));
}
}
m_output.close();
if (m_output.getZipError() != 0) {
return ZipResult(tr("A zip error occurred"));
}
return ZipResult();
}
void ExportToZipTask::finish()
{
if (m_build_zip_future.isCanceled()) {
QFile::remove(m_output_path);
emitAborted();
} else if (auto result = m_build_zip_future.result(); result.has_value()) {
QFile::remove(m_output_path);
emitFailed(result.value());
} else {
emitSucceeded();
}
}
bool ExportToZipTask::abort()
{
if (m_build_zip_future.isRunning()) {
m_build_zip_future.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
} // namespace MMCZip

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -35,115 +36,161 @@
#pragma once
#include <QString>
#include <quazip.h>
#include <quazip/JlCompress.h>
#include <QDir>
#include <QFileInfo>
#include <QFuture>
#include <QFutureWatcher>
#include <QHash>
#include <QSet>
#include <QUrl>
#include <QString>
#include <functional>
#include <memory>
#include <optional>
#if defined(LAUNCHER_APPLICATION)
#include "minecraft/mod/Mod.h"
#endif
#include <functional>
#include "tasks/Task.h"
#include <quazip/JlCompress.h>
#include <optional>
namespace MMCZip {
using FilterFunction = std::function<bool(const QString&)>;
namespace MMCZip
{
using FilterFunction = std::function<bool(const QString &)>;
/**
* Merge two zip files, using a filter function
*/
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction filter = nullptr);
/**
* Merge two zip files, using a filter function
*/
bool mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained,
const FilterFunction filter = nullptr);
/**
* Compress directory, by providing a list of files to compress
* \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 followSymlinks = false);
/**
* Compress directory, by providing a list of files to compress
* \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 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 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 followSymlinks = false);
#if defined(LAUNCHER_APPLICATION)
/**
* take a source jar, add mods to it, resulting in target jar
*/
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods);
/**
* take a source jar, add mods to it, resulting in target jar
*/
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods);
#endif
/**
* Find a single file in archive by file name (not path)
*
* \param ignore_paths paths to skip when recursing the search
*
* \return the path prefix where the file is
*/
QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths = {}, const QString& root = QString(""));
/**
* Find a single file in archive by file name (not path)
*
* \param ignore_paths paths to skip when recursing the search
*
* \return the path prefix where the file is
*/
QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QStringList& ignore_paths = {}, const QString &root = QString(""));
/**
* Find a multiple files of the same name in archive by file name
* If a file is found in a path, no deeper paths are searched
*
* \return true if anything was found
*/
bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root = QString());
/**
* Find a multiple files of the same name in archive by file name
* If a file is found in a path, no deeper paths are searched
*
* \return true if anything was found
*/
bool findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root = QString());
/**
* Extract a subdirectory from an archive
*/
std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, const QString& target);
/**
* Extract a subdirectory from an archive
*/
std::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
bool extractRelFile(QuaZip* zip, const QString& file, const QString& target);
bool extractRelFile(QuaZip *zip, const QString & file, const QString &target);
/**
* Extract a whole archive.
*
* \param fileCompressed The name of the archive.
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
std::optional<QStringList> extractDir(QString fileCompressed, QString dir);
/**
* Extract a whole archive.
*
* \param fileCompressed The name of the archive.
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
std::optional<QStringList> extractDir(QString fileCompressed, QString dir);
/**
* Extract a subdirectory from an archive
*
* \param fileCompressed The name of the archive.
* \param subdir The directory within the archive to extract
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
std::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
/**
* Extract a subdirectory from an archive
*
* \param fileCompressed The name of the archive.
* \param subdir The directory within the archive to extract
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
std::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
/**
* Extract a single file from an archive into a directory
*
* \param fileCompressed The name of the archive.
* \param file The file within the archive to extract
* \param dir The directory to extract to, the current directory if left empty.
* \return true for success or false for failure
*/
bool extractFile(QString fileCompressed, QString file, QString dir);
/**
* Extract a single file from an archive into a directory
*
* \param fileCompressed The name of the archive.
* \param file The file within the archive to extract
* \param dir The directory to extract to, the current directory if left empty.
* \return true for success or false for failure
*/
bool extractFile(QString fileCompressed, QString file, QString dir);
/**
* Populate a QFileInfoList with a directory tree recursively, while allowing to excludeFilter what shouldn't be included.
* \param rootDir directory to start off
* \param subDir subdirectory, should be nullptr for first invocation
* \param files resulting list of QFileInfo
* \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude)
* \return true for success or false for failure
*/
bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter);
/**
* Populate a QFileInfoList with a directory tree recursively, while allowing to excludeFilter what shouldn't be included.
* \param rootDir directory to start off
* \param subDir subdirectory, should be nullptr for first invocation
* \param files resulting list of QFileInfo
* \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude)
* \return true for success or false for failure
*/
bool collectFileListRecursively(const QString &rootDir, const QString &subDir, QFileInfoList *files, FilterFunction excludeFilter);
}
class ExportToZipTask : public Task {
public:
ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
: m_output_path(outputPath)
, m_output(outputPath)
, m_dir(dir)
, m_files(files)
, m_destination_prefix(destinationPrefix)
, m_follow_symlinks(followSymlinks)
{
setAbortable(true);
};
ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){};
virtual ~ExportToZipTask() = default;
void setExcludeFiles(QStringList excludeFiles) { m_exclude_files = excludeFiles; }
void addExtraFile(QString fileName, QByteArray data) { m_extra_files.insert(fileName, data); }
typedef std::optional<QString> ZipResult;
protected:
virtual void executeTask() override;
bool abort() override;
ZipResult exportZip();
void finish();
private:
QString m_output_path;
QuaZip m_output;
QDir m_dir;
QFileInfoList m_files;
QString m_destination_prefix;
bool m_follow_symlinks;
QStringList m_exclude_files;
QHash<QString, QByteArray> m_extra_files;
QFuture<ZipResult> m_build_zip_future;
QFutureWatcher<ZipResult> m_build_zip_watcher;
};
} // namespace MMCZip

View File

@ -56,7 +56,7 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
m_filesNetJob->addNetAction(Net::ApiDownload::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::stepProgress, this, &ResourceDownloadTask::propagateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed);
addTask(m_filesNetJob);

View File

@ -193,31 +193,21 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
}
case Qt::ToolTipRole:
{
switch(column)
if(column == Name && hasRecommended)
{
case Name:
auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
if(value.toBool())
{
if(hasRecommended)
return tr("Recommended");
} else if(hasLatest) {
auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
if(value.toBool())
{
auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
if(value.toBool())
{
return tr("Recommended");
}
else if(hasLatest)
{
auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
if(value.toBool())
{
return tr("Latest");
}
}
return tr("Latest");
}
}
default:
{
return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole);
}
} else {
return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole);
}
}
case Qt::DecorationRole:

View File

@ -1,29 +1,64 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JavaInstall.h"
#include "BaseVersion.h"
#include "StringUtils.h"
bool JavaInstall::operator<(const JavaInstall &rhs)
bool JavaInstall::operator<(const JavaInstall& rhs)
{
auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive);
if(archCompare != 0)
if (archCompare != 0)
return archCompare < 0;
if(id < rhs.id)
{
if (id < rhs.id) {
return true;
}
if(id > rhs.id)
{
if (id > rhs.id) {
return false;
}
return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0;
}
bool JavaInstall::operator==(const JavaInstall &rhs)
bool JavaInstall::operator==(const JavaInstall& rhs)
{
return arch == rhs.arch && id == rhs.id && path == rhs.path;
}
bool JavaInstall::operator>(const JavaInstall &rhs)
bool JavaInstall::operator>(const JavaInstall& rhs)
{
return (!operator<(rhs)) && (!operator==(rhs));
}
bool JavaInstall::operator<(BaseVersion& a)
{
try {
return operator<(dynamic_cast<JavaInstall&>(a));
} catch (const std::bad_cast& e) {
return BaseVersion::operator<(a);
}
}
bool JavaInstall::operator>(BaseVersion& a)
{
try {
return operator>(dynamic_cast<JavaInstall&>(a));
} catch (const std::bad_cast& e) {
return BaseVersion::operator>(a);
}
}

View File

@ -1,33 +1,40 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "BaseVersion.h"
#include "JavaVersion.h"
struct JavaInstall : public BaseVersion
{
JavaInstall(){}
JavaInstall(QString id, QString arch, QString path)
: id(id), arch(arch), path(path)
{
}
virtual QString descriptor()
{
return id.toString();
}
struct JavaInstall : public BaseVersion {
JavaInstall() {}
JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {}
virtual QString descriptor() { return id.toString(); }
virtual QString name()
{
return id.toString();
}
virtual QString name() { return id.toString(); }
virtual QString typeString() const
{
return arch;
}
virtual QString typeString() const { return arch; }
bool operator<(const JavaInstall & rhs);
bool operator==(const JavaInstall & rhs);
bool operator>(const JavaInstall & rhs);
virtual bool operator<(BaseVersion& a) override;
virtual bool operator>(BaseVersion& a) override;
bool operator<(const JavaInstall& rhs);
bool operator==(const JavaInstall& rhs);
bool operator>(const JavaInstall& rhs);
JavaVersion id;
QString arch;

View File

@ -28,7 +28,7 @@ void Update::executeTask()
{
connect(m_updateTask.get(), &Task::finished, this, &Update::updateFinished);
connect(m_updateTask.get(), &Task::progress, this, &Update::setProgress);
connect(m_updateTask.get(), &Task::stepProgress, this, &Update::propogateStepProgress);
connect(m_updateTask.get(), &Task::stepProgress, this, &Update::propagateStepProgress);
connect(m_updateTask.get(), &Task::status, this, &Update::setStatus);
connect(m_updateTask.get(), &Task::details, this, &Update::setDetails);
emit progressReportingRequest();

View File

@ -45,10 +45,10 @@ QVariant Index::data(const QModelIndex &index, int role) const
switch (role)
{
case Qt::DisplayRole:
switch (index.column())
{
case 0: return list->humanReadable();
default: break;
if (index.column() == 0) {
return list->humanReadable();
} else {
break;
}
case UidRole: return list->uid();
case NameRole: return list->name();

View File

@ -4,6 +4,7 @@
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 seth <getchoo at tuta dot io>
*
* 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
@ -186,6 +187,10 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride);
m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride);
// Mod loader specific options
auto modLoaderSettings = m_settings->registerSetting("OverrideModLoaderSettings", false);
m_settings->registerOverride(global_settings->getSetting("DisableQuiltBeacon"), modLoaderSettings);
m_settings->set("InstanceType", "OneSix");
}
@ -391,6 +396,12 @@ QStringList MinecraftInstance::extraArguments()
agent->library()->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath());
list.append("-javaagent:"+jar[0]+(agent->argument().isEmpty() ? "" : "="+agent->argument()));
}
{
const auto loaders = version->getModLoaders();
if (loaders.has_value() && loaders.value() & ResourceAPI::Quilt && settings()->get("DisableQuiltBeacon").toBool())
list.append("-Dloader.disable_beacon=true");
}
return list;
}
@ -832,7 +843,7 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess
{
addToFilter(sessionRef.session, tr("<SESSION ID>"));
}
if (sessionRef.access_token != "offline") {
if (sessionRef.access_token != "0") {
addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>"));
}
if(sessionRef.client_token.size()) {
@ -1112,36 +1123,27 @@ JavaVersion MinecraftInstance::getJavaVersion()
std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList()
{
if (!m_loader_mod_list)
{
if (!m_loader_mod_list) {
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed));
m_loader_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction);
}
return m_loader_mod_list;
}
std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList()
{
if (!m_core_mod_list)
{
if (!m_core_mod_list) {
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed));
m_core_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction);
}
return m_core_mod_list;
}
std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList()
{
if (!m_nil_mod_list)
{
if (!m_nil_mod_list) {
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false));
m_nil_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_nil_mod_list.get(), &ModFolderModel::disableInteraction);
}
return m_nil_mod_list;
}

View File

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

View File

@ -100,7 +100,7 @@ void MinecraftUpdate::next()
disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
disconnect(task.get(), &Task::aborted, this, &Task::abort);
disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
disconnect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propogateStepProgress);
disconnect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propagateStepProgress);
disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
disconnect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails);
}
@ -120,7 +120,7 @@ void MinecraftUpdate::next()
connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
connect(task.get(), &Task::aborted, this, &Task::abort);
connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
connect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propogateStepProgress);
connect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propagateStepProgress);
connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
connect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails);
// if the task is already running, do not start it again

View File

@ -65,7 +65,8 @@
static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{
{"net.minecraftforge", ResourceAPI::Forge},
{"net.fabricmc.fabric-loader", ResourceAPI::Fabric},
{"org.quiltmc.quilt-loader", ResourceAPI::Quilt}
{"org.quiltmc.quilt-loader", ResourceAPI::Quilt},
{"com.mumfrey.liteloader", ResourceAPI::LiteLoader}
};
PackProfile::PackProfile(MinecraftInstance * instance)

View File

@ -374,6 +374,10 @@ bool AccountData::resumeStateFromV3(QJsonObject data) {
}
yggdrasilToken = tokenFromJSONV3(data, "ygg");
// versions before 7.2 used "offline" as the offline token
if (yggdrasilToken.token == "offline")
yggdrasilToken.token = "0";
minecraftProfile = profileFromJSONV3(data, "profile");
if(!entitlementFromJSONV3(data, minecraftEntitlement)) {
if(minecraftProfile.validity != Katabasis::Validity::None) {

View File

@ -328,6 +328,9 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
case AccountState::Gone: {
return tr("Gone", "Account status");
}
default: {
return tr("Unknown", "Account status");
}
}
}
@ -354,11 +357,12 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
return QVariant::fromValue(account);
case Qt::CheckStateRole:
switch (index.column())
{
case ProfileNameColumn:
return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
if (index.column() == ProfileNameColumn) {
return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
} else {
return QVariant();
}
default:
return QVariant();

View File

@ -26,6 +26,7 @@ bool AuthSession::MakeOffline(QString offline_playername)
return false;
}
session = "-";
access_token = "0";
player_name = offline_playername;
status = PlayableOffline;
return true;

View File

@ -37,6 +37,7 @@
#include "MinecraftAccount.h"
#include <QCryptographicHash>
#include <QUuid>
#include <QJsonObject>
#include <QJsonArray>
@ -93,14 +94,14 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username)
{
auto account = makeShared<MinecraftAccount>();
account->data.type = AccountType::Offline;
account->data.yggdrasilToken.token = "offline";
account->data.yggdrasilToken.token = "0";
account->data.yggdrasilToken.validity = Katabasis::Validity::Certain;
account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftEntitlement.ownsMinecraft = true;
account->data.minecraftEntitlement.canPlayMinecraft = true;
account->data.minecraftProfile.id = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftProfile.name = username;
account->data.minecraftProfile.validity = Katabasis::Validity::Certain;
return account;
@ -334,3 +335,32 @@ void MinecraftAccount::incrementUses()
qWarning() << "Profile" << data.profileId() << "is now in use.";
}
}
QUuid MinecraftAccount::uuidFromUsername(QString username) {
auto input = QString("OfflinePlayer:%1").arg(username).toUtf8();
// basically a reimplementation of Java's UUID#nameUUIDFromBytes
QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
auto bOr = [](QByteArray& array, int index, char value) {
array[index] = array.at(index) | value;
};
auto bAnd = [](QByteArray& array, int index, char value) {
array[index] = array.at(index) & value;
};
#else
auto bOr = [](QByteArray& array, qsizetype index, char value) {
array[index] |= value;
};
auto bAnd = [](QByteArray& array, qsizetype index, char value) {
array[index] &= value;
};
#endif
bAnd(digest, 6, (char) 0x0f); // clear version
bOr(digest, 6, (char) 0x30); // set to version 3
bAnd(digest, 8, (char) 0x3f); // clear variant
bOr(digest, 8, (char) 0x80); // set to IETF variant
return QUuid::fromRfc4122(digest);
}

View File

@ -98,6 +98,8 @@ public: /* construction */
static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json);
static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json);
static QUuid uuidFromUsername(QString username);
//! Saves a MinecraftAccount to a JSON object and returns it.
QJsonObject saveToJson() const;

View File

@ -273,6 +273,7 @@ void Yggdrasil::processReply() {
AccountTaskState::STATE_FAILED_GONE,
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")
);
return;
}
default:
changeState(

View File

@ -74,6 +74,7 @@ std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) con
auto res = Resource::compare(other, type);
if (res.first != 0)
return res;
break;
}
case SortType::PACK_FORMAT: {
auto this_ver = packFormat();
@ -83,6 +84,7 @@ std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) con
return { 1, type == SortType::PACK_FORMAT };
if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT };
break;
}
}
return { 0, false };

View File

@ -91,6 +91,7 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
auto res = Resource::compare(other, type);
if (res.first != 0)
return res;
break;
}
case SortType::VERSION: {
auto this_ver = Version(version());
@ -99,11 +100,13 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
return { 1, type == SortType::VERSION };
if (this_ver < other_ver)
return { -1, type == SortType::VERSION };
break;
}
case SortType::PROVIDER: {
auto compare_result = QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive);
if (compare_result != 0)
return { compare_result, type == SortType::PROVIDER };
break;
}
}
return { 0, false };
@ -123,7 +126,7 @@ bool Mod::applyFilter(QRegularExpression filter) const
return Resource::applyFilter(filter);
}
auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool
{
if (!preserve_metadata) {
qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name());
@ -136,7 +139,7 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
}
}
return Resource::destroy();
return Resource::destroy(attempt_trash);
}
auto Mod::details() const -> const ModDetails&
@ -166,6 +169,13 @@ auto Mod::homeurl() const -> QString
return details().homeurl;
}
auto Mod::metaurl() const -> QString
{
if (metadata() == nullptr)
return homeurl();
return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id);
}
auto Mod::description() const -> QString
{
return details().description;

View File

@ -70,6 +70,7 @@ public:
auto provider() const -> std::optional<QString>;
auto licenses() const -> const QList<ModLicense>&;
auto issueTracker() const -> QString;
auto metaurl() const -> QString;
/** Get the intneral path to the mod's icon file*/
QString iconPath() const { return m_local_details.icon_file; };
@ -92,7 +93,7 @@ public:
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
// Delete all the files of this mod
auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool;
auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool;
void finishResolvingWithDetails(ModDetails&& details);

View File

@ -199,10 +199,10 @@ Task* ModFolderModel::createParseTask(Resource& resource)
bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
{
for(auto mod : allMods()){
if(mod->fileinfo().fileName() == filename){
for(auto mod : allMods()) {
if(mod->fileinfo().fileName() == filename) {
auto index_dir = indexDir();
mod->destroy(index_dir, preserve_metadata);
mod->destroy(index_dir, preserve_metadata, false);
update();
@ -215,16 +215,11 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{
if(!m_can_interact) {
return false;
}
if(indexes.isEmpty())
if (indexes.isEmpty())
return true;
for (auto i: indexes)
{
if(i.column() != 0) {
for (auto i : indexes) {
if (i.column() != 0) {
continue;
}
auto m = at(i.row());

View File

@ -71,6 +71,7 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
return { 1, type == SortType::ENABLED };
if (!enabled() && other.enabled())
return { -1, type == SortType::ENABLED };
break;
case SortType::NAME: {
QString this_name{ name() };
QString other_name{ other.name() };
@ -81,12 +82,14 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
if (compare_result != 0)
return { compare_result, type == SortType::NAME };
break;
}
case SortType::DATE:
if (dateTimeChanged() > other.dateTimeChanged())
return { 1, type == SortType::DATE };
if (dateTimeChanged() < other.dateTimeChanged())
return { -1, type == SortType::DATE };
break;
}
return { 0, false };
@ -145,14 +148,10 @@ bool Resource::enable(EnableAction action)
return true;
}
bool Resource::destroy()
bool Resource::destroy(bool attemptTrash)
{
m_type = ResourceType::UNKNOWN;
if (FS::trash(m_file_info.filePath()))
return true;
return FS::deletePath(m_file_info.filePath());
return (attemptTrash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath());
}
bool Resource::isSymLinkUnder(const QString& instPath) const

View File

@ -92,7 +92,7 @@ class Resource : public QObject {
}
// Delete all files of this resource.
bool destroy();
bool destroy(bool attemptTrash = true);
[[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); }

View File

@ -1,14 +1,15 @@
#include "ResourceFolderModel.h"
#include <QMessageBox>
#include <QCoreApplication>
#include <QDebug>
#include <QFileInfo>
#include <QIcon>
#include <QMenu>
#include <QMimeData>
#include <QStyle>
#include <QThreadPool>
#include <QUrl>
#include <QMenu>
#include "Application.h"
#include "FileSystem.h"
@ -18,6 +19,7 @@
#include "settings/Setting.h"
#include "tasks/Task.h"
#include "ui/dialogs/CustomMessageBox.h"
ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir)
: QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this)
@ -77,10 +79,6 @@ bool ResourceFolderModel::stopWatching(const QStringList paths)
bool ResourceFolderModel::installResource(QString original_path)
{
if (!m_can_interact) {
return false;
}
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
original_path = FS::NormalizePath(original_path);
QFileInfo file_info(original_path);
@ -159,7 +157,7 @@ bool ResourceFolderModel::uninstallResource(QString file_name)
{
for (auto& resource : m_resources) {
if (resource->fileinfo().fileName() == file_name) {
auto res = resource->destroy();
auto res = resource->destroy(false);
update();
@ -171,9 +169,6 @@ bool ResourceFolderModel::uninstallResource(QString file_name)
bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
{
if (!m_can_interact)
return false;
if (indexes.isEmpty())
return true;
@ -192,11 +187,8 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
return true;
}
bool ResourceFolderModel::setResourceEnabled(const QModelIndexList &indexes, EnableAction action)
bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action)
{
if (!m_can_interact)
return false;
if (indexes.isEmpty())
return true;
@ -249,15 +241,18 @@ bool ResourceFolderModel::update()
connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
Qt::ConnectionType::QueuedConnection);
connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
connect(m_current_update_task.get(), &Task::finished, this, [=] {
m_current_update_task.reset();
if (m_scheduled_update) {
m_scheduled_update = false;
update();
} else {
emit updateFinished();
}
}, Qt::ConnectionType::QueuedConnection);
connect(
m_current_update_task.get(), &Task::finished, this,
[=] {
m_current_update_task.reset();
if (m_scheduled_update) {
m_scheduled_update = false;
update();
} else {
emit updateFinished();
}
},
Qt::ConnectionType::QueuedConnection);
QThreadPool::globalInstance()->start(m_current_update_task.get());
@ -347,15 +342,9 @@ Qt::DropActions ResourceFolderModel::supportedDropActions() const
Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const
{
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
auto flags = defaultFlags;
if (!m_can_interact) {
flags &= ~Qt::ItemIsDropEnabled;
} else {
flags |= Qt::ItemIsDropEnabled;
if (index.isValid()) {
flags |= Qt::ItemIsUserCheckable;
}
}
auto flags = defaultFlags | Qt::ItemIsDropEnabled;
if (index.isValid())
flags |= Qt::ItemIsUserCheckable;
return flags;
}
@ -428,16 +417,17 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
if (column == NAME_COLUMN) {
if (at(row).isSymLinkUnder(instDirPath())) {
return m_resources[row]->internal_id() +
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
"\nCanonical Path: %1")
.arg(at(row).fileinfo().canonicalFilePath());;
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
"\nCanonical Path: %1")
.arg(at(row).fileinfo().canonicalFilePath());
;
}
if (at(row).isMoreThanOneHardLink()) {
return m_resources[row]->internal_id() +
tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original.");
tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original.");
}
}
return m_resources[row]->internal_id();
case Qt::DecorationRole: {
if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()))
@ -463,8 +453,20 @@ bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& valu
if (row < 0 || row >= rowCount(index.parent()) || !index.isValid())
return false;
if (role == Qt::CheckStateRole)
if (role == Qt::CheckStateRole) {
if (m_instance != nullptr && m_instance->isRunning()) {
auto response =
CustomMessageBox::selectable(nullptr, "Confirm toggle",
"If you enable/disable this resource while the game is running it may crash your game.\n"
"Are you sure you want to do this?",
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return false;
}
return setResourceEnabled({ index }, EnableAction::TOGGLE);
}
return false;
}
@ -583,16 +585,6 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const
return m_column_sort_keys.at(column);
}
void ResourceFolderModel::enableInteraction(bool enabled)
{
if (m_can_interact == enabled)
return;
m_can_interact = enabled;
if (size())
emit dataChanged(index(0), index(size() - 1));
}
/* Standard Proxy Model for createFilterProxyModel */
[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{
@ -628,6 +620,7 @@ void ResourceFolderModel::enableInteraction(bool enabled)
return (compare_result.first > 0);
}
QString ResourceFolderModel::instDirPath() const {
QString ResourceFolderModel::instDirPath() const
{
return QFileInfo(m_instance->instanceRoot()).absoluteFilePath();
}

View File

@ -14,8 +14,8 @@
#include "BaseInstance.h"
#include "tasks/Task.h"
#include "tasks/ConcurrentTask.h"
#include "tasks/Task.h"
class QSortFilterProxyModel;
@ -141,10 +141,6 @@ class ResourceFolderModel : public QAbstractListModel {
QString instDirPath() const;
public slots:
void enableInteraction(bool enabled);
void disableInteraction(bool disabled) { enableInteraction(!disabled); }
signals:
void updateFinished();
@ -193,7 +189,11 @@ class ResourceFolderModel : public QAbstractListModel {
* if the resource is complex and has more stuff to parse.
*/
virtual void onParseSucceeded(int ticket, QString resource_id);
virtual void onParseFailed(int ticket, QString resource_id) { Q_UNUSED(ticket); Q_UNUSED(resource_id); }
virtual void onParseFailed(int ticket, QString resource_id)
{
Q_UNUSED(ticket);
Q_UNUSED(resource_id);
}
protected:
// Represents the relationship between a column's index (represented by the list index), and it's sorting key.
@ -203,8 +203,6 @@ class ResourceFolderModel : public QAbstractListModel {
QStringList m_column_names_translated = {tr("Enable"), tr("Name"), tr("Last Modified")};
QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Stretch, QHeaderView::ResizeToContents };
bool m_can_interact = true;
QDir m_dir;
BaseInstance* m_instance;
QFileSystemWatcher m_watcher;

View File

@ -102,6 +102,7 @@ std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type)
auto res = Resource::compare(other, type);
if (res.first != 0)
return res;
break;
}
case SortType::PACK_FORMAT: {
auto this_ver = packFormat();
@ -111,6 +112,7 @@ std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type)
return { 1, type == SortType::PACK_FORMAT };
if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT };
break;
}
}
return { 0, false };

View File

@ -1,9 +1,9 @@
#include "LocalModParseTask.h"
#include <qdcss.h>
#include <quazip/quazip.h>
#include <quazip/quazipfile.h>
#include <toml++/toml.h>
#include <qdcss.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
@ -370,12 +370,11 @@ ModDetails ReadQuiltModInfo(QByteArray contents)
details.icon_file = icon.toString();
}
}
}
return details;
}
ModDetails ReadForgeInfo(QString fileName)
ModDetails ReadForgeInfo(QByteArray contents)
{
ModDetails details;
// Read the data
@ -383,7 +382,7 @@ ModDetails ReadForgeInfo(QString fileName)
details.mod_id = "Forge";
details.homeurl = "http://www.minecraftforge.net/forum/";
INIFile ini;
if (!ini.loadFile(fileName))
if (!ini.loadFile(contents))
return details;
QString major = ini.get("forge.major.number", "0").toString();
@ -555,7 +554,7 @@ bool processZIP(Mod& mod, ProcessingLevel level)
return false;
}
details = ReadForgeInfo(file.getFileName());
details = ReadForgeInfo(file.readAll());
file.close();
zip.close();

View File

@ -44,7 +44,11 @@ static const QMap<PackedResourceType, QString> s_packed_type_names = {
namespace ResourceUtils {
PackedResourceType identify(QFileInfo file){
if (file.exists() && file.isFile()) {
if (ResourcePackUtils::validate(file)) {
if (ModUtils::validate(file)) {
// mods can contain resource and data packs so they must be tested first
qDebug() << file.fileName() << "is a mod";
return PackedResourceType::Mod;
} else if (ResourcePackUtils::validate(file)) {
qDebug() << file.fileName() << "is a resource pack";
return PackedResourceType::ResourcePack;
} else if (TexturePackUtils::validate(file)) {
@ -53,9 +57,6 @@ PackedResourceType identify(QFileInfo file){
} else if (DataPackUtils::validate(file)) {
qDebug() << file.fileName() << "is a data pack";
return PackedResourceType::DataPack;
} else if (ModUtils::validate(file)) {
qDebug() << file.fileName() << "is a mod";
return PackedResourceType::Mod;
} else if (WorldSaveUtils::validate(file)) {
qDebug() << file.fileName() << "is a world save";
return PackedResourceType::WorldSave;

View File

@ -103,7 +103,7 @@ void ModFolderLoadTask::executeTask()
while (iter.hasNext()) {
auto mod = iter.next().value();
if (mod->status() == ModStatus::NotInstalled) {
mod->destroy(m_index_dir, false);
mod->destroy(m_index_dir, false, false);
iter.remove();
}
}

View File

@ -47,7 +47,7 @@ void AssetUpdateTask::executeTask()
connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed);
connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress);
connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propogateStepProgress);
connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress);
qDebug() << m_inst->name() << ": Starting asset index download";
downloadJob->start();
@ -86,7 +86,7 @@ void AssetUpdateTask::assetIndexFinished()
connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed);
connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress);
connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propogateStepProgress);
connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress);
downloadJob->start();
return;
}

View File

@ -77,7 +77,7 @@ void FMLLibrariesTask::executeTask()
connect(dljob.get(), &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed);
connect(dljob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
connect(dljob.get(), &NetJob::progress, this, &FMLLibrariesTask::progress);
connect(dljob.get(), &NetJob::stepProgress, this, &FMLLibrariesTask::propogateStepProgress);
connect(dljob.get(), &NetJob::stepProgress, this, &FMLLibrariesTask::propagateStepProgress);
downloadJob.reset(dljob);
downloadJob->start();
}

View File

@ -70,7 +70,7 @@ void LibrariesTask::executeTask()
connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed);
connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); });
connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress);
connect(downloadJob.get(), &NetJob::stepProgress, this, &LibrariesTask::propogateStepProgress);
connect(downloadJob.get(), &NetJob::stepProgress, this, &LibrariesTask::propagateStepProgress);
downloadJob->start();
}

View File

@ -145,7 +145,8 @@ void EnsureMetadataTask::executeTask()
connect(project_task.get(), &Task::finished, this, [=] {
invalidade_leftover();
project_task->deleteLater();
m_current_task = nullptr;
if (m_current_task)
m_current_task.reset();
});
m_current_task = project_task;
@ -154,7 +155,8 @@ void EnsureMetadataTask::executeTask()
connect(version_task.get(), &Task::finished, [=] {
version_task->deleteLater();
m_current_task = nullptr;
if (m_current_task)
m_current_task.reset();
});
if (m_mods.size() > 1)

View File

@ -70,11 +70,17 @@ auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString t
}
QCryptographicHash hash(algo);
if(!hash.addData(device))
if (!hash.addData(device))
qCritical() << "Failed to read JAR to create hash!";
Q_ASSERT(hash.result().length() == hash.hashLength(algo));
return { hash.result().toHex() };
}
QString getMetaURL(ResourceProvider provider, QVariant projectID)
{
return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") +
projectID.toString();
}
} // namespace ModPlatform

View File

@ -128,6 +128,7 @@ struct IndexedPack {
return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; });
}
};
QString getMetaURL(ResourceProvider provider, QVariant projectID);
struct OverrideDep {
QString quilt;
@ -144,6 +145,7 @@ inline auto getOverrideDeps() -> QList<OverrideDep>
{ "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH },
{ "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } };
};
QString getMetaURL(ResourceProvider provider, QVariant projectID);
} // namespace ModPlatform

View File

@ -686,7 +686,7 @@ void PackInstallTask::installConfigs()
abortable = true;
setProgress(current, total);
});
connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress);
connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
connect(jobPtr.get(), &NetJob::aborted, [&]{
abortable = false;
jobPtr.reset();
@ -854,7 +854,7 @@ void PackInstallTask::downloadMods()
abortable = true;
setProgress(current, total);
});
connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress);
connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
connect(jobPtr.get(), &NetJob::aborted, [&]
{
abortable = false;

View File

@ -23,6 +23,10 @@ bool Flame::FileResolvingTask::abort()
void Flame::FileResolvingTask::executeTask()
{
if (m_toProcess.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately
emitSucceeded();
return;
}
setStatus(tr("Resolving mod IDs..."));
setProgress(0, 3);
m_dljob.reset(new NetJob("Mod id resolver", m_network));
@ -50,7 +54,7 @@ void Flame::FileResolvingTask::executeTask()
stepProgress(*step_progress);
emitFailed(reason);
});
connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_dljob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
@ -116,7 +120,7 @@ void Flame::FileResolvingTask::netJobFinished()
stepProgress(*step_progress);
emitFailed(reason);
});
connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_checkJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
@ -130,12 +134,13 @@ void Flame::FileResolvingTask::netJobFinished()
m_checkJob->start();
}
void Flame::FileResolvingTask::modrinthCheckFinished() {
void Flame::FileResolvingTask::modrinthCheckFinished()
{
setProgress(2, 3);
qDebug() << "Finished with blocked mods : " << blockedProjects.size();
for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) {
auto &out = *it;
auto& out = *it;
auto bytes = blockedProjects[out];
if (!out->resolved) {
continue;
@ -155,15 +160,13 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
out->resolved = false;
}
}
//copy to an output list and filter out projects found on modrinth
// copy to an output list and filter out projects found on modrinth
auto block = std::make_shared<QList<File*>>();
auto it = blockedProjects.keys();
std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) {
return !f->resolved;
});
//Display not found mods early
std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File* f) { return !f->resolved; });
// Display not found mods early
if (!block->empty()) {
//blocked mods found, we need the slug for displaying.... we need another job :D !
// blocked mods found, we need the slug for displaying.... we need another job :D !
m_slugJob.reset(new NetJob("Slug Job", m_network));
int index = 0;
for (auto mod : *block) {
@ -175,8 +178,8 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
QObject::connect(dl.get(), &Net::Download::succeeded, [block, index, output]() {
auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done
auto json = QJsonDocument::fromJson(*output);
auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"),
"websiteUrl");
auto base =
Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json), "data"), "links"), "websiteUrl");
auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId));
mod->websiteUrl = link;
});
@ -194,7 +197,7 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
stepProgress(*step_progress);
emitFailed(reason);
});
connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_slugJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);

View File

@ -23,6 +23,8 @@ class FlameAPI : public NetworkResourceAPI {
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt); }
private:
static int getClassId(ModPlatform::ResourceType type)
{

View File

@ -57,17 +57,11 @@
#include <QDebug>
#include <QFileInfo>
#include "meta/Index.h"
#include "meta/VersionList.h"
#include "minecraft/World.h"
#include "minecraft/mod/tasks/LocalResourceParse.h"
#include "net/ApiDownload.h"
const static QMap<QString, QString> forgemap = { { "1.2.5", "3.4.9.171" },
{ "1.4.2", "6.0.1.355" },
{ "1.4.7", "6.6.2.534" },
{ "1.5.2", "7.8.1.737" } };
static const FlameAPI api;
bool FlameCreationTask::abort()
@ -261,6 +255,56 @@ bool FlameCreationTask::updateInstance()
return false;
}
QString FlameCreationTask::getVersionForLoader(QString uid, QString loaderType, QString loaderVersion, QString mcVersion)
{
if (loaderVersion == "recommended") {
auto vlist = APPLICATION->metadataIndex()->get(uid);
if (!vlist) {
setError(tr("Failed to get local metadata index for %1").arg(uid));
return {};
}
if (!vlist->isLoaded()) {
QEventLoop loadVersionLoop;
auto task = vlist->getLoadTask();
connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit);
if (!task->isRunning())
task->start();
loadVersionLoop.exec();
}
for (auto version : vlist->versions()) {
// first recommended build we find, we use.
if (!version->isRecommended())
continue;
auto reqs = version->requiredSet();
// filter by minecraft version, if the loader depends on a certain version.
// not all mod loaders depend on a given Minecraft version, so we won't do this
// filtering for those loaders.
if (loaderType == "forge") {
auto iter = std::find_if(reqs.begin(), reqs.end(), [mcVersion](const Meta::Require& req) {
return req.uid == "net.minecraft" && req.equalsVersion == mcVersion;
});
if (iter == reqs.end())
continue;
}
return version->descriptor();
}
setError(tr("Failed to find version for %1 loader").arg(loaderType));
return {};
}
if (loaderVersion.isEmpty()) {
emitFailed(tr("No loader version set for modpack!"));
return {};
}
return loaderVersion;
}
bool FlameCreationTask::createInstance()
{
QEventLoop loop;
@ -299,22 +343,29 @@ bool FlameCreationTask::createInstance()
}
}
QString forgeVersion;
QString fabricVersion;
// TODO: is Quilt relevant here?
QString loaderType;
QString loaderUid;
QString loaderVersion;
for (auto& loader : m_pack.minecraft.modLoaders) {
auto id = loader.id;
if (id.startsWith("forge-")) {
id.remove("forge-");
forgeVersion = id;
continue;
}
if (id.startsWith("fabric-")) {
loaderType = "forge";
loaderUid = "net.minecraftforge";
} else if (loaderType == "fabric") {
id.remove("fabric-");
fabricVersion = id;
loaderType = "fabric";
loaderUid = "net.fabricmc.fabric-loader";
} else if (loaderType == "quilt") {
id.remove("quilt-");
loaderType = "quilt";
loaderUid = "org.quiltmc.quilt-loader";
} else {
logWarning(tr("Unknown mod loader in manifest: %1").arg(id));
continue;
}
logWarning(tr("Unknown mod loader in manifest: %1").arg(id));
loaderVersion = id;
}
QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
@ -331,19 +382,12 @@ bool FlameCreationTask::createInstance()
auto components = instance.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", mcVersion, true);
if (!forgeVersion.isEmpty()) {
// FIXME: dirty, nasty, hack. Proper solution requires dependency resolution and knowledge of the metadata.
if (forgeVersion == "recommended") {
if (forgemap.contains(mcVersion)) {
forgeVersion = forgemap[mcVersion];
} else {
logWarning(tr("Could not map recommended Forge version for Minecraft %1").arg(mcVersion));
}
}
components->setComponentVersion("net.minecraftforge", forgeVersion);
if (!loaderType.isEmpty()) {
auto version = getVersionForLoader(loaderUid, loaderType, loaderVersion, mcVersion);
if (version.isEmpty())
return false;
components->setComponentVersion(loaderUid, version);
}
if (!fabricVersion.isEmpty())
components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion);
if (m_instIcon != "default") {
instance.setIconKey(m_instIcon);
@ -388,7 +432,7 @@ bool FlameCreationTask::createInstance()
});
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propogateStepProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
m_mod_id_resolver->start();
@ -472,8 +516,9 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever.
// fallthrough intentional, we treat these as plain old mods and dump them wherever.
}
/* fallthrough */
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {
@ -503,11 +548,11 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
m_files_job.reset();
setError(reason);
});
connect(m_files_job.get(), &NetJob::progress, this, [this](qint64 current, qint64 total){
connect(m_files_job.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) {
setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, total);
});
connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propogateStepProgress);
connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
setStatus(tr("Downloading mods..."));
@ -546,7 +591,6 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
setAbortable(true);
}
void FlameCreationTask::validateZIPResouces()
{
qDebug() << "Validating whether resources stored as .zip are in the right place";
@ -564,11 +608,13 @@ void FlameCreationTask::validateZIPResouces()
if (FS::move(localPath, destPath)) {
return destPath;
}
} else {
qDebug() << "Target folder of" << fileName << "is correct at" << targetFolder;
}
return localPath;
};
auto installWorld = [this](QString worldPath){
auto installWorld = [this](QString worldPath) {
qDebug() << "Installing World from" << worldPath;
QFileInfo worldFileInfo(worldPath);
World w(worldFileInfo);
@ -585,29 +631,29 @@ void FlameCreationTask::validateZIPResouces()
QString worldPath;
switch (type) {
case PackedResourceType::ResourcePack :
validatePath(fileName, targetFolder, "resourcepacks");
break;
case PackedResourceType::TexturePack :
validatePath(fileName, targetFolder, "texturepacks");
break;
case PackedResourceType::DataPack :
validatePath(fileName, targetFolder, "datapacks");
break;
case PackedResourceType::Mod :
case PackedResourceType::Mod:
validatePath(fileName, targetFolder, "mods");
break;
case PackedResourceType::ShaderPack :
case PackedResourceType::ResourcePack:
validatePath(fileName, targetFolder, "resourcepacks");
break;
case PackedResourceType::TexturePack:
validatePath(fileName, targetFolder, "texturepacks");
break;
case PackedResourceType::DataPack:
validatePath(fileName, targetFolder, "datapacks");
break;
case PackedResourceType::ShaderPack:
// in theroy flame API can't do this but who knows, that *may* change ?
// better to handle it if it *does* occure in the future
validatePath(fileName, targetFolder, "shaderpacks");
break;
case PackedResourceType::WorldSave :
case PackedResourceType::WorldSave:
worldPath = validatePath(fileName, targetFolder, "saves");
installWorld(worldPath);
break;
case PackedResourceType::UNKNOWN :
default :
case PackedResourceType::UNKNOWN:
default:
qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is.";
break;
}

View File

@ -57,10 +57,7 @@ class FlameCreationTask final : public InstanceCreationTask {
QString id,
QString version_id,
QString original_instance_id = {})
: InstanceCreationTask()
, m_parent(parent)
, m_managed_id(std::move(id))
, m_managed_version_id(std::move(version_id))
: InstanceCreationTask(), m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(version_id))
{
setStagingPath(staging_path);
setParentSettings(global_settings);
@ -78,6 +75,7 @@ class FlameCreationTask final : public InstanceCreationTask {
void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
void validateZIPResouces();
QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
private:
QWidget* m_parent = nullptr;

View File

@ -0,0 +1,473 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "FlamePackExportTask.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QCryptographicHash>
#include <QFileInfo>
#include <QMessageBox>
#include <QtConcurrentRun>
#include <algorithm>
#include <memory>
#include "Json.h"
#include "MMCZip.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/helpers/HashUtils.h"
#include "tasks/Task.h"
const QString FlamePackExportTask::TEMPLATE = "<li><a href=\"{url}\">{name}{authors}</a></li>\n";
const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" });
FlamePackExportTask::FlamePackExportTask(const QString& name,
const QString& version,
const QString& author,
InstancePtr instance,
const QString& output,
MMCZip::FilterFunction filter)
: name(name)
, version(version)
, author(author)
, instance(instance)
, mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))
, gameRoot(instance->gameRoot())
, output(output)
, filter(filter)
{}
void FlamePackExportTask::executeTask()
{
setStatus(tr("Searching for files..."));
setProgress(0, 5);
collectFiles();
}
bool FlamePackExportTask::abort()
{
if (task != nullptr) {
task->abort();
task = nullptr;
emitAborted();
return true;
}
if (buildZipFuture.isRunning()) {
buildZipFuture.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
void FlamePackExportTask::collectFiles()
{
setAbortable(false);
QCoreApplication::processEvents();
files.clear();
if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) {
emitFailed(tr("Could not search for files"));
return;
}
pendingHashes.clear();
resolvedFiles.clear();
if (mcInstance != nullptr) {
mcInstance->loaderModList()->update();
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes);
} else
collectHashes();
}
void FlamePackExportTask::collectHashes()
{
setAbortable(true);
setStatus(tr("Finding file hashes..."));
setProgress(1, 5);
auto allMods = mcInstance->loaderModList()->allMods();
ConcurrentTask::Ptr hashingTask(new ConcurrentTask(this, "MakeHashesTask", 10));
task.reset(hashingTask);
for (const QFileInfo& file : files) {
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
// require sensible file types
if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
}))
continue;
if (relative.startsWith("resourcepacks/") &&
(relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack
auto hashTask = Hashing::createFlameHasher(file.absoluteFilePath());
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") });
}
});
connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
hashingTask->addTask(hashTask);
continue;
}
if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
modIter != allMods.end()) {
const Mod* mod = *modIter;
if (!mod || mod->type() == ResourceType::FOLDER) {
continue;
}
if (mod->metadata() && mod->metadata()->provider == ModPlatform::ResourceProvider::FLAME) {
resolvedFiles.insert(mod->fileinfo().absoluteFilePath(),
{ mod->metadata()->project_id.toInt(), mod->metadata()->file_id.toInt(), mod->enabled(), true,
mod->metadata()->name, mod->metadata()->slug, mod->authors().join(", ") });
continue;
}
auto hashTask = Hashing::createFlameHasher(mod->fileinfo().absoluteFilePath());
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true });
}
});
connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
hashingTask->addTask(hashTask);
}
}
auto progressStep = std::make_shared<TaskStepProgress>();
connect(hashingTask.get(), &Task::finished, this, [this, progressStep] {
progressStep->state = TaskStepState::Succeeded;
stepProgress(*progressStep);
});
connect(hashingTask.get(), &Task::succeeded, this, &FlamePackExportTask::makeApiRequest);
connect(hashingTask.get(), &Task::failed, this, [this, progressStep](QString reason) {
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
emitFailed(reason);
});
connect(hashingTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propagateStepProgress);
connect(hashingTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
progressStep->update(current, total);
stepProgress(*progressStep);
});
connect(hashingTask.get(), &Task::status, this, [this, progressStep](QString status) {
progressStep->status = status;
stepProgress(*progressStep);
});
hashingTask->start();
}
void FlamePackExportTask::makeApiRequest()
{
if (pendingHashes.isEmpty()) {
buildZip();
return;
}
setStatus(tr("Finding versions for hashes..."));
setProgress(2, 5);
auto response = std::make_shared<QByteArray>();
QList<uint> fingerprints;
for (auto& murmur : pendingHashes.keys()) {
fingerprints.push_back(murmur.toUInt());
}
task.reset(api.matchFingerprints(fingerprints, response));
connect(task.get(), &Task::succeeded, this, [this, response] {
QJsonParseError parseError{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at " << parseError.offset
<< " reason: " << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
return;
}
try {
auto docObj = Json::requireObject(doc);
auto dataObj = Json::requireObject(docObj, "data");
auto dataArr = Json::requireArray(dataObj, "exactMatches");
if (dataArr.isEmpty()) {
qWarning() << "No matches found for fingerprint search!";
return;
}
for (auto match : dataArr) {
auto matchObj = Json::ensureObject(match, {});
auto fileObj = Json::ensureObject(matchObj, "file", {});
if (matchObj.isEmpty() || fileObj.isEmpty()) {
qWarning() << "Fingerprint match is empty!";
return;
}
auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt());
auto mod = pendingHashes.find(fingerprint);
if (mod == pendingHashes.end()) {
qWarning() << "Invalid fingerprint from the API response.";
continue;
}
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name));
if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable"))
resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"),
mod->enabled, mod->isMod });
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
pendingHashes.clear();
});
connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo);
connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed);
task->start();
}
void FlamePackExportTask::getProjectsInfo()
{
setStatus(tr("Finding project info from CurseForge..."));
setProgress(3, 5);
QStringList addonIds;
for (const auto& resolved : resolvedFiles) {
if (resolved.slug.isEmpty()) {
addonIds << QString::number(resolved.addonId);
}
}
auto response = std::make_shared<QByteArray>();
Task::Ptr projTask;
if (addonIds.isEmpty()) {
buildZip();
return;
} else if (addonIds.size() == 1) {
projTask = api.getProject(*addonIds.begin(), response);
} else {
projTask = api.getProjects(addonIds, response);
}
connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parseError{};
auto doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset
<< " reason: " << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
return;
}
try {
QJsonArray entries;
if (addonIds.size() == 1)
entries = { Json::requireObject(Json::requireObject(doc), "data") };
else
entries = Json::requireArray(Json::requireObject(doc), "data");
for (auto entry : entries) {
auto entryObj = Json::requireObject(entry);
try {
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(Json::requireString(entryObj, "name")));
ModPlatform::IndexedPack pack;
FlameMod::loadIndexedPack(pack, entryObj);
for (auto key : resolvedFiles.keys()) {
auto val = resolvedFiles.value(key);
if (val.addonId == pack.addonId) {
val.name = pack.name;
val.slug = pack.slug;
QStringList authors;
for (auto author : pack.authors)
authors << author.name;
val.authors = authors.join(", ");
resolvedFiles[key] = val;
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << entries;
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
buildZip();
});
task.reset(projTask);
task->start();
}
void FlamePackExportTask::buildZip()
{
setStatus(tr("Adding files..."));
setProgress(4, 5);
buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() {
QuaZip zip(output);
if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(output);
return BuildZipResult(tr("Could not create file"));
}
if (buildZipFuture.isCanceled())
return BuildZipResult();
QuaZipFile indexFile(&zip);
if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo("manifest.json"))) {
QFile::remove(output);
return BuildZipResult(tr("Could not create index"));
}
indexFile.write(generateIndex());
QuaZipFile modlist(&zip);
if (!modlist.open(QIODevice::WriteOnly, QuaZipNewInfo("modlist.html"))) {
QFile::remove(output);
return BuildZipResult(tr("Could not create index"));
}
QString content = "";
for (auto mod : resolvedFiles) {
if (mod.isMod) {
content += QString(TEMPLATE)
.replace("{name}", mod.name.toHtmlEscaped())
.replace("{url}", ModPlatform::getMetaURL(ModPlatform::ResourceProvider::FLAME, mod.addonId).toHtmlEscaped())
.replace("{authors}", !mod.authors.isEmpty() ? QString(" (by %1)").arg(mod.authors).toHtmlEscaped() : "");
}
}
content = "<ul>" + content + "</ul>";
modlist.write(content.toUtf8());
auto progressStep = std::make_shared<TaskStepProgress>();
size_t progress = 0;
for (const QFileInfo& file : files) {
if (buildZipFuture.isCanceled()) {
QFile::remove(output);
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
return BuildZipResult();
}
progressStep->update(progress, files.length());
stepProgress(*progressStep);
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
if (!resolvedFiles.contains(file.absoluteFilePath()) &&
!JlCompress::compressFile(&zip, file.absoluteFilePath(), "overrides/" + relative)) {
QFile::remove(output);
return BuildZipResult(tr("Could not read and compress %1").arg(relative));
}
progress++;
}
zip.close();
if (zip.getZipError() != 0) {
QFile::remove(output);
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
return BuildZipResult(tr("A zip error occurred"));
}
progressStep->state = TaskStepState::Succeeded;
stepProgress(*progressStep);
return BuildZipResult();
});
connect(&buildZipWatcher, &QFutureWatcher<BuildZipResult>::finished, this, &FlamePackExportTask::finish);
buildZipWatcher.setFuture(buildZipFuture);
}
void FlamePackExportTask::finish()
{
if (buildZipFuture.isCanceled())
emitAborted();
else {
const BuildZipResult result = buildZipFuture.result();
if (result.has_value())
emitFailed(result.value());
else
emitSucceeded();
}
}
QByteArray FlamePackExportTask::generateIndex()
{
QJsonObject obj;
obj["manifestType"] = "minecraftModpack";
obj["manifestVersion"] = 1;
obj["name"] = name;
obj["version"] = version;
obj["author"] = author;
obj["overrides"] = "overrides";
if (mcInstance) {
QJsonObject version;
auto profile = mcInstance->getPackProfile();
// collect all supported components
const ComponentPtr minecraft = profile->getComponent("net.minecraft");
const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
const ComponentPtr forge = profile->getComponent("net.minecraftforge");
// convert all available components to mrpack dependencies
if (minecraft != nullptr)
version["version"] = minecraft->m_version;
QString id;
if (quilt != nullptr)
id = "quilt-" + quilt->getVersion();
else if (fabric != nullptr)
id = "fabric-" + fabric->getVersion();
else if (forge != nullptr)
id = "forge-" + forge->getVersion();
version["modLoaders"] = QJsonArray();
if (!id.isEmpty()) {
QJsonObject loader;
loader["id"] = id;
loader["primary"] = true;
version["modLoaders"] = QJsonArray({ loader });
}
obj["minecraft"] = version;
}
QJsonArray files;
for (auto mod : resolvedFiles) {
QJsonObject file;
file["projectID"] = mod.addonId;
file["fileID"] = mod.version;
file["required"] = mod.enabled;
files << file;
}
obj["files"] = files;
return QJsonDocument(obj).toJson(QJsonDocument::Compact);
}

View File

@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QFuture>
#include <QFutureWatcher>
#include "BaseInstance.h"
#include "MMCZip.h"
#include "minecraft/MinecraftInstance.h"
#include "modplatform/flame/FlameAPI.h"
#include "tasks/Task.h"
class FlamePackExportTask : public Task {
public:
FlamePackExportTask(const QString& name,
const QString& version,
const QString& author,
InstancePtr instance,
const QString& output,
MMCZip::FilterFunction filter);
protected:
void executeTask() override;
bool abort() override;
private:
static const QString TEMPLATE;
static const QStringList FILE_EXTENSIONS;
// inputs
const QString name, version, author;
const InstancePtr instance;
MinecraftInstance* mcInstance;
const QDir gameRoot;
const QString output;
const MMCZip::FilterFunction filter;
typedef std::optional<QString> BuildZipResult;
struct ResolvedFile {
int addonId;
int version;
bool enabled;
bool isMod;
QString name;
QString slug;
QString authors;
};
struct HashInfo {
QString name;
QString path;
bool enabled;
bool isMod;
};
FlameAPI api;
QFileInfoList files;
QMap<QString, HashInfo> pendingHashes{};
QMap<QString, ResolvedFile> resolvedFiles{};
Task::Ptr task;
QFuture<BuildZipResult> buildZipFuture;
QFutureWatcher<BuildZipResult> buildZipWatcher;
void collectFiles();
void collectHashes();
void makeApiRequest();
void getProjectsInfo();
void buildZip();
void finish();
QByteArray generateIndex();
};

View File

@ -76,13 +76,8 @@ bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked
// It is also optional
type = File::Type::SingleFile;
if (fileName.endsWith(".zip")) {
// this is probably a resource pack
targetFolder = "resourcepacks";
} else {
// this is probably a mod, dunno what else could modpacks download
targetFolder = "mods";
}
targetFolder = "mods";
// get the hash
hash = QString();
auto hashes = Json::ensureArray(obj, "hashes");

View File

@ -0,0 +1,200 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ExportToModList.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace ExportToModList {
QString toHTML(QList<Mod*> mods, OptionalData extraData)
{
QStringList lines;
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name().toHtmlEscaped();
if (extraData & Url) {
auto url = mod->metaurl().toHtmlEscaped();
if (!url.isEmpty())
modName = QString("<a href=\"%1\">%2</a>").arg(url, modName);
}
auto line = modName;
if (extraData & Version) {
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
if (!ver.isEmpty())
line += QString(" [%1]").arg(ver.toHtmlEscaped());
}
if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", ").toHtmlEscaped();
lines.append(QString("<li>%1</li>").arg(line));
}
return QString("<html><body><ul>\n\t%1\n</ul></body></html>").arg(lines.join("\n\t"));
}
QString toMarkdown(QList<Mod*> mods, OptionalData extraData)
{
QStringList lines;
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name();
if (extraData & Url) {
auto url = mod->metaurl();
if (!url.isEmpty())
modName = QString("[%1](%2)").arg(modName, url);
}
auto line = modName;
if (extraData & Version) {
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
if (!ver.isEmpty())
line += QString(" [%1]").arg(ver);
}
if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", ");
lines << "- " + line;
}
return lines.join("\n");
}
QString toPlainTXT(QList<Mod*> mods, OptionalData extraData)
{
QStringList lines;
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name();
auto line = modName;
if (extraData & Url) {
auto url = mod->metaurl();
if (!url.isEmpty())
line += QString(" (%1)").arg(url);
}
if (extraData & Version) {
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
if (!ver.isEmpty())
line += QString(" [%1]").arg(ver);
}
if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", ");
lines << line;
}
return lines.join("\n");
}
QString toJSON(QList<Mod*> mods, OptionalData extraData)
{
QJsonArray lines;
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name();
QJsonObject line;
line["name"] = modName;
if (extraData & Url) {
auto url = mod->metaurl();
if (!url.isEmpty())
line["url"] = url;
}
if (extraData & Version) {
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
if (!ver.isEmpty())
line["version"] = ver;
}
if (extraData & Authors && !mod->authors().isEmpty())
line["authors"] = QJsonArray::fromStringList(mod->authors());
lines << line;
}
QJsonDocument doc;
doc.setArray(lines);
return doc.toJson();
}
QString toCSV(QList<Mod*> mods, OptionalData extraData)
{
QStringList lines;
for (auto mod : mods) {
QStringList data;
auto meta = mod->metadata();
auto modName = mod->name();
data << modName;
if (extraData & Url)
data << mod->metaurl();
if (extraData & Version) {
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
data << ver;
}
if (extraData & Authors) {
QString authors;
if (mod->authors().length() == 1)
authors = mod->authors().back();
else if (mod->authors().length() > 1)
authors = QString("\"%1\"").arg(mod->authors().join(","));
data << authors;
}
lines << data.join(",");
}
return lines.join("\n");
}
QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData)
{
switch (format) {
case HTML:
return toHTML(mods, extraData);
case MARKDOWN:
return toMarkdown(mods, extraData);
case PLAINTXT:
return toPlainTXT(mods, extraData);
case JSON:
return toJSON(mods, extraData);
case CSV:
return toCSV(mods, extraData);
default: {
return QString("unknown format:%1").arg(format);
}
}
}
QString exportToModList(QList<Mod*> mods, QString lineTemplate)
{
QStringList lines;
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name();
auto url = mod->metaurl();
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
auto authors = mod->authors().join(", ");
lines << QString(lineTemplate)
.replace("{name}", modName)
.replace("{url}", url)
.replace("{version}", ver)
.replace("{authors}", authors);
}
return lines.join("\n");
}
} // namespace ExportToModList

View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QList>
#include <QString>
#include "minecraft/mod/Mod.h"
namespace ExportToModList {
enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM };
enum OptionalData {
Authors = 1 << 0,
Url = 1 << 1,
Version = 1 << 2,
};
QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData);
QString exportToModList(QList<Mod*> mods, QString lineTemplate);
} // namespace ExportToModList

View File

@ -37,16 +37,16 @@
#include <QtConcurrent>
#include "MMCZip.h"
#include "BaseInstance.h"
#include "FileSystem.h"
#include "settings/INISettingsObject.h"
#include "MMCZip.h"
#include "minecraft/GradleSpecifier.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "minecraft/GradleSpecifier.h"
#include "settings/INISettingsObject.h"
#include "BuildConfig.h"
#include "Application.h"
#include "BuildConfig.h"
#include "net/ApiDownload.h"
@ -67,6 +67,7 @@ void PackInstallTask::executeTask()
void PackInstallTask::downloadPack()
{
setStatus(tr("Downloading zip for %1").arg(m_pack.name));
setProgress(1, 4);
setAbortable(false);
archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
@ -80,11 +81,10 @@ void PackInstallTask::downloadPack()
}
netJobContainer->addNetAction(Net::ApiDownload::makeFile(url, archivePath));
connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress);
connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress);
connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted);
connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip);
connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed);
connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::emitAborted);
netJobContainer->start();
@ -92,27 +92,6 @@ void PackInstallTask::downloadPack()
progress(1, 4);
}
void PackInstallTask::onDownloadSucceeded()
{
unzip();
}
void PackInstallTask::onDownloadFailed(QString reason)
{
emitFailed(reason);
}
void PackInstallTask::onDownloadProgress(qint64 current, qint64 total)
{
progress(current, total * 4);
setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10));
}
void PackInstallTask::onDownloadAborted()
{
emitAborted();
}
void PackInstallTask::unzip()
{
setStatus(tr("Extracting modpack"));
@ -122,16 +101,17 @@ void PackInstallTask::unzip()
QDir extractDir(m_stagingPath);
m_packZip.reset(new QuaZip(archivePath));
if(!m_packZip->open(QuaZip::mdUnzip))
{
if (!m_packZip->open(QuaZip::mdUnzip)) {
emitFailed(tr("Failed to open modpack file %1!").arg(archivePath));
return;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip");
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath,
extractDir.absolutePath() + "/unzip");
#else
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
m_extractFuture =
QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
#endif
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled);
@ -153,11 +133,9 @@ void PackInstallTask::install()
setStatus(tr("Installing modpack"));
progress(3, 4);
QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
if(unzipMcDir.exists())
{
//ok, found minecraft dir, move contents to instance dir
if(!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft"))
{
if (unzipMcDir.exists()) {
// ok, found minecraft dir, move contents to instance dir
if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) {
emitFailed(tr("Failed to move unzipped Minecraft!"));
return;
}
@ -174,23 +152,20 @@ void PackInstallTask::install()
bool fallback = true;
//handle different versions
// handle different versions
QFile packJson(m_stagingPath + "/.minecraft/pack.json");
QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods");
if(packJson.exists())
{
if (packJson.exists()) {
packJson.open(QIODevice::ReadOnly | QIODevice::Text);
QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll());
packJson.close();
//we only care about the libs
// we only care about the libs
QJsonArray libs = doc.object().value("libraries").toArray();
foreach (const QJsonValue &value, libs)
{
foreach (const QJsonValue& value, libs) {
QString nameValue = value.toObject().value("name").toString();
if(!nameValue.startsWith("net.minecraftforge"))
{
if (!nameValue.startsWith("net.minecraftforge")) {
continue;
}
@ -201,16 +176,13 @@ void PackInstallTask::install()
fallback = false;
break;
}
}
if(jarmodDir.exists())
{
if (jarmodDir.exists()) {
qDebug() << "Found jarmods, installing...";
QStringList jarmods;
for (auto info: jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files))
{
for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) {
qDebug() << "Jarmod:" << info.fileName();
jarmods.push_back(info.absoluteFilePath());
}
@ -219,12 +191,11 @@ void PackInstallTask::install()
fallback = false;
}
//just nuke unzip directory, it s not needed anymore
// just nuke unzip directory, it s not needed anymore
FS::deletePath(m_stagingPath + "/unzip");
if(fallback)
{
//TODO: Some fallback mechanism... or just keep failing!
if (fallback) {
// TODO: Some fallback mechanism... or just keep failing!
emitFailed(tr("No installation method found!"));
return;
}
@ -234,8 +205,7 @@ void PackInstallTask::install()
progress(4, 4);
instance.setName(name());
if(m_instIcon == "default")
{
if (m_instIcon == "default") {
m_instIcon = "ftb_logo";
}
instance.setIconKey(m_instIcon);
@ -254,4 +224,4 @@ bool PackInstallTask::abort()
return InstanceTask::abort();
}
}
} // namespace LegacyFTB

View File

@ -1,12 +1,12 @@
#pragma once
#include "InstanceTask.h"
#include "net/NetJob.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include "InstanceTask.h"
#include "PackHelpers.h"
#include "meta/Index.h"
#include "meta/Version.h"
#include "meta/VersionList.h"
#include "PackHelpers.h"
#include "net/NetJob.h"
#include "net/NetJob.h"
@ -14,36 +14,31 @@
namespace LegacyFTB {
class PackInstallTask : public InstanceTask
{
class PackInstallTask : public InstanceTask {
Q_OBJECT
public:
public:
explicit PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, Modpack pack, QString version);
virtual ~PackInstallTask(){}
virtual ~PackInstallTask() {}
bool canAbort() const override { return true; }
bool abort() override;
protected:
protected:
//! Entry point for tasks.
virtual void executeTask() override;
private:
private:
void downloadPack();
void unzip();
void install();
private slots:
void onDownloadSucceeded();
void onDownloadFailed(QString reason);
void onDownloadProgress(qint64 current, qint64 total);
void onDownloadAborted();
private slots:
void onUnzipFinished();
void onUnzipCanceled();
private: /* data */
private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
bool abortable = false;
std::unique_ptr<QuaZip> m_packZip;
@ -56,4 +51,4 @@ private: /* data */
QString m_version;
};
}
} // namespace LegacyFTB

View File

@ -38,7 +38,7 @@ class ModrinthAPI : public NetworkResourceAPI {
static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList
{
QStringList l;
for (auto loader : { Forge, Fabric, Quilt }) {
for (auto loader : { Forge, Fabric, Quilt, LiteLoader }) {
if (types & loader) {
l << getModLoaderString(loader);
}
@ -92,7 +92,7 @@ class ModrinthAPI : public NetworkResourceAPI {
{
if (args.loaders.has_value()) {
if (!validateModLoaders(args.loaders.value())) {
qWarning() << "Modrinth only have Forge and Fabric-compatible mods!";
qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!";
return {};
}
}
@ -141,7 +141,7 @@ class ModrinthAPI : public NetworkResourceAPI {
return s.isEmpty() ? QString() : s;
}
inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { return loaders & (Forge | Fabric | Quilt); }
static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt | LiteLoader); }
[[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
{

View File

@ -268,7 +268,7 @@ bool ModrinthCreationTask::createInstance()
setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, total);
});
connect(m_files_job.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propogateStepProgress);
connect(m_files_job.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress);
setStatus(tr("Downloading mods..."));
m_files_job->start();

View File

@ -64,7 +64,8 @@ bool ModrinthPackExportTask::abort()
if (buildZipFuture.isRunning()) {
buildZipFuture.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur immediately.
// NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur
// immediately.
return true;
}
@ -94,6 +95,7 @@ void ModrinthPackExportTask::collectFiles()
void ModrinthPackExportTask::collectHashes()
{
setStatus(tr("Finding file hashes..."));
for (const QFileInfo& file : files) {
QCoreApplication::processEvents();
@ -157,6 +159,7 @@ void ModrinthPackExportTask::makeApiRequest()
if (pendingHashes.isEmpty())
buildZip();
else {
setStatus(tr("Finding versions for hashes..."));
auto response = std::make_shared<QByteArray>();
task = api.currentVersions(pendingHashes.values(), "sha512", response);
connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); });
@ -263,13 +266,13 @@ void ModrinthPackExportTask::finish()
QByteArray ModrinthPackExportTask::generateIndex()
{
QJsonObject obj;
obj["formatVersion"] = 1;
obj["game"] = "minecraft";
obj["name"] = name;
obj["versionId"] = version;
QJsonObject out;
out["formatVersion"] = 1;
out["game"] = "minecraft";
out["name"] = name;
out["versionId"] = version;
if (!summary.isEmpty())
obj["summary"] = summary;
out["summary"] = summary;
if (mcInstance) {
auto profile = mcInstance->getPackProfile();
@ -290,30 +293,40 @@ QByteArray ModrinthPackExportTask::generateIndex()
if (forge != nullptr)
dependencies["forge"] = forge->m_version;
obj["dependencies"] = dependencies;
out["dependencies"] = dependencies;
}
QJsonArray files;
QMapIterator<QString, ResolvedFile> iterator(resolvedFiles);
while (iterator.hasNext()) {
iterator.next();
QJsonArray filesOut;
for (auto iterator = resolvedFiles.constBegin(); iterator != resolvedFiles.constEnd(); iterator++) {
QJsonObject fileOut;
QString path = iterator.key();
const ResolvedFile& value = iterator.value();
QJsonObject file;
file["path"] = iterator.key();
file["downloads"] = QJsonArray({ iterator.value().url });
// detect disabled mod
const QFileInfo pathInfo(path);
if (pathInfo.suffix() == "disabled") {
// rename it
path = pathInfo.dir().filePath(pathInfo.completeBaseName());
// ...and make it optional
QJsonObject env;
env["client"] = "optional";
env["server"] = "optional";
fileOut["env"] = env;
}
fileOut["path"] = path;
fileOut["downloads"] = QJsonArray{ iterator.value().url };
QJsonObject hashes;
hashes["sha1"] = value.sha1;
hashes["sha512"] = value.sha512;
fileOut["hashes"] = hashes;
file["hashes"] = hashes;
file["fileSize"] = value.size;
files << file;
fileOut["fileSize"] = value.size;
filesOut << fileOut;
}
obj["files"] = files;
out["files"] = filesOut;
return QJsonDocument(obj).toJson(QJsonDocument::Compact);
return QJsonDocument(out).toJson(QJsonDocument::Compact);
}

View File

@ -52,7 +52,7 @@ void Technic::SingleZipPackInstallTask::executeTask()
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded);
connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged);
connect(job, &NetJob::stepProgress, this, &Technic::SingleZipPackInstallTask::propogateStepProgress);
connect(job, &NetJob::stepProgress, this, &Technic::SingleZipPackInstallTask::propagateStepProgress);
connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed);
m_filesNetJob->start();
}

View File

@ -127,7 +127,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded()
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &Technic::SolderPackInstallTask::propogateStepProgress);
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &Technic::SolderPackInstallTask::propagateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted);
m_filesNetJob->start();

View File

@ -37,11 +37,12 @@
#include "settings/INIFile.h"
#include <FileSystem.h>
#include <QFile>
#include <QTextStream>
#include <QStringList>
#include <QSaveFile>
#include <QDebug>
#include <QFile>
#include <QSaveFile>
#include <QStringList>
#include <QTemporaryFile>
#include <QTextStream>
#include <QSettings>
@ -71,6 +72,7 @@ bool INIFile::saveFile(QString fileName)
return true;
}
QString unescape(QString orig)
{
QString out;
@ -185,6 +187,19 @@ bool INIFile::loadFile(QString fileName)
return true;
}
bool INIFile::loadFile(QByteArray data)
{
QTemporaryFile file;
if (!file.open())
return false;
file.write(data);
file.flush();
file.close();
auto loaded = loadFile(file.fileName());
file.remove();
return loaded;
}
QVariant INIFile::get(QString key, QVariant def) const
{
if (!this->contains(key))

View File

@ -50,6 +50,7 @@ public:
explicit INIFile();
bool loadFile(QString fileName);
bool loadFile(QByteArray data);
bool saveFile(QString fileName);
QVariant get(QString key, QVariant def) const;

View File

@ -161,7 +161,7 @@ void Task::emitSucceeded()
emit finished();
}
void Task::propogateStepProgress(TaskStepProgress const& task_progress)
void Task::propagateStepProgress(TaskStepProgress const& task_progress)
{
emit stepProgress(task_progress);
}

View File

@ -167,7 +167,7 @@ class Task : public QObject, public QRunnable {
virtual void emitAborted();
virtual void emitFailed(QString reason = "");
virtual void propogateStepProgress(TaskStepProgress const& task_progress);
virtual void propagateStepProgress(TaskStepProgress const& task_progress);
public slots:
void setStatus(const QString& status);

View File

@ -42,6 +42,7 @@
#include <QDir>
#include <QLibraryInfo>
#include <QDebug>
#include <locale>
#include "FileSystem.h"
#include "net/NetJob.h"
@ -454,6 +455,7 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const
return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1);
}
}
qWarning("TranslationModel::data not implemented when role is DisplayRole");
}
case Qt::ToolTipRole:
{
@ -526,34 +528,34 @@ Language * TranslationsModel::findLanguage(const QString& key)
}
}
void TranslationsModel::setUseSystemLocale(bool useSystemLocale)
{
APPLICATION->settings()->set("UseSystemLocale", useSystemLocale);
QLocale::setDefault(QLocale(useSystemLocale ? QString::fromStdString(std::locale().name()) : defaultLangCode));
}
bool TranslationsModel::selectLanguage(QString key)
{
QString &langCode = key;
QString& langCode = key;
auto langPtr = findLanguage(key);
if (langCode.isEmpty())
{
if (langCode.isEmpty()) {
d->no_language_set = true;
}
if(!langPtr)
{
if (!langPtr) {
qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode;
langCode = defaultLangCode;
}
else
{
} else {
langCode = langPtr->key;
}
// uninstall existing translators if there are any
if (d->m_app_translator)
{
if (d->m_app_translator) {
QCoreApplication::removeTranslator(d->m_app_translator.get());
d->m_app_translator.reset();
}
if (d->m_qt_translator)
{
if (d->m_qt_translator) {
QCoreApplication::removeTranslator(d->m_qt_translator.get());
d->m_qt_translator.reset();
}
@ -563,8 +565,9 @@ bool TranslationsModel::selectLanguage(QString key)
* In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created.
* This function is not reentrant.
*/
QLocale locale = QLocale(langCode);
QLocale::setDefault(locale);
QLocale::setDefault(
QLocale(APPLICATION->settings()->get("UseSystemLocale").toBool() ? QString::fromStdString(std::locale().name()) : langCode));
// if it's the default UI language, finish
if(langCode == defaultLangCode)

View File

@ -20,17 +20,16 @@
struct Language;
class TranslationsModel : public QAbstractListModel
{
class TranslationsModel : public QAbstractListModel {
Q_OBJECT
public:
explicit TranslationsModel(QString path, QObject *parent = 0);
public:
explicit TranslationsModel(QString path, QObject* parent = 0);
virtual ~TranslationsModel();
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex & parent) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex& parent) const override;
bool selectLanguage(QString key);
void updateLanguage(QString key);
@ -38,27 +37,27 @@ public:
QString selectedLanguage();
void downloadIndex();
void setUseSystemLocale(bool useSystemLocale);
private:
Language *findLanguage(const QString & key);
private:
Language* findLanguage(const QString& key);
void reloadLocalFiles();
void downloadTranslation(QString key);
void downloadNext();
// hide copy constructor
TranslationsModel(const TranslationsModel &) = delete;
TranslationsModel(const TranslationsModel&) = delete;
// hide assign op
TranslationsModel &operator=(const TranslationsModel &) = delete;
TranslationsModel& operator=(const TranslationsModel&) = delete;
private slots:
private slots:
void indexReceived();
void indexFailed(QString reason);
void dlFailed(QString reason);
void dlGood();
void translationDirChanged(const QString &path);
void translationDirChanged(const QString& path);
private: /* data */
private: /* data */
struct Private;
std::unique_ptr<Private> d;
};

File diff suppressed because it is too large Load Diff

View File

@ -157,6 +157,8 @@ private slots:
inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); }
void on_actionExportInstanceZip_triggered();
void on_actionExportInstanceMrPack_triggered();
void on_actionExportInstanceFlamePack_triggered();
void on_actionExportInstanceToModList_triggered();
void on_actionRenameInstance_triggered();

View File

@ -479,6 +479,22 @@
<string>Modrinth (mrpack)</string>
</property>
</action>
<action name="actionExportInstanceFlamePack">
<property name="icon">
<iconset theme="flame"/>
</property>
<property name="text">
<string>CurseForge (zip)</string>
</property>
</action>
<action name="actionExportInstanceToModList">
<property name="icon">
<iconset theme="new"/>
</property>
<property name="text">
<string>Mod List</string>
</property>
</action>
<action name="actionCreateInstanceShortcut">
<property name="icon">
<iconset theme="shortcut">

View File

@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -35,24 +36,29 @@
*/
#include "ExportInstanceDialog.h"
#include "ui_ExportInstanceDialog.h"
#include <BaseInstance.h>
#include <MMCZip.h>
#include <QFileDialog>
#include <QMessageBox>
#include <QFileSystemModel>
#include <QMessageBox>
#include "FileIgnoreProxy.h"
#include "QObjectPtr.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui_ExportInstanceDialog.h"
#include <QSortFilterProxyModel>
#include <QDebug>
#include <QSaveFile>
#include <QStack>
#include <QFileInfo>
#include "SeparatorPrefixTree.h"
#include "Application.h"
#include <icons/IconList.h>
#include <FileSystem.h>
#include <icons/IconList.h>
#include <QDebug>
#include <QFileInfo>
#include <QSaveFile>
#include <QSortFilterProxyModel>
#include <QStack>
#include <functional>
#include "Application.h"
#include "SeparatorPrefixTree.h"
ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent)
ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent)
: QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance)
{
ui->setupUi(this);
@ -60,13 +66,19 @@ ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent
model->setIconProvider(&icons);
auto root = instance->instanceRoot();
proxyModel = new FileIgnoreProxy(root, this);
loadPackIgnore();
proxyModel->setSourceModel(model);
auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot());
proxyModel->ignoreFilesWithPath().insert({ FS::PathCombine(prefix, "logs"), FS::PathCombine(prefix, "crash-reports") });
proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" });
proxyModel->ignoreFilesWithPath().insert(
{ FS::PathCombine(prefix, ".cache"), FS::PathCombine(prefix, ".fabric"), FS::PathCombine(prefix, ".quilt") });
loadPackIgnore();
ui->treeView->setModel(proxyModel);
ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
ui->treeView->sortByColumn(0, Qt::AscendingOrder);
connect(proxyModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int)));
connect(proxyModel, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(rowsInserted(QModelIndex, int, int)));
model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden);
model->setRootPath(root);
@ -86,32 +98,26 @@ void SaveIcon(InstancePtr m_instance)
auto iconKey = m_instance->iconKey();
auto iconList = APPLICATION->icons();
auto mmcIcon = iconList->icon(iconKey);
if(!mmcIcon || mmcIcon->isBuiltIn()) {
if (!mmcIcon || mmcIcon->isBuiltIn()) {
return;
}
auto path = mmcIcon->getFilePath();
if(!path.isNull()) {
QFileInfo inInfo (path);
FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), inInfo.fileName())) ();
if (!path.isNull()) {
QFileInfo inInfo(path);
FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), inInfo.fileName()))();
return;
}
auto & image = mmcIcon->m_images[mmcIcon->type()];
auto & icon = image.icon;
auto& image = mmcIcon->m_images[mmcIcon->type()];
auto& icon = image.icon;
auto sizes = icon.availableSizes();
if(sizes.size() == 0)
{
if (sizes.size() == 0) {
return;
}
auto areaOf = [](QSize size)
{
return size.width() * size.height();
};
auto areaOf = [](QSize size) { return size.width() * size.height(); };
QSize largest = sizes[0];
// find variant with largest area
for(auto size: sizes)
{
if(areaOf(largest) < areaOf(size))
{
for (auto size : sizes) {
if (areaOf(largest) < areaOf(size)) {
largest = size;
}
}
@ -119,66 +125,57 @@ void SaveIcon(InstancePtr m_instance)
pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png"));
}
bool ExportInstanceDialog::doExport()
void ExportInstanceDialog::doExport()
{
auto name = FS::RemoveInvalidFilenameChars(m_instance->name());
const QString output = QFileDialog::getSaveFileName(
this, tr("Export %1").arg(m_instance->name()),
FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr);
if (output.isEmpty())
{
return false;
const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(m_instance->name()),
FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr);
if (output.isEmpty()) {
QDialog::done(QDialog::Rejected);
return;
}
SaveIcon(m_instance);
auto & blocked = proxyModel->blockedPaths();
using std::placeholders::_1;
auto files = QFileInfoList();
if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files,
std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) {
std::bind(&FileIgnoreProxy::filterFile, proxyModel, std::placeholders::_1))) {
QMessageBox::warning(this, tr("Error"), tr("Unable to export instance"));
return false;
QDialog::done(QDialog::Rejected);
return;
}
if (!MMCZip::compressDirFiles(output, m_instance->instanceRoot(), files, true))
{
QMessageBox::warning(this, tr("Error"), tr("Unable to export instance"));
return false;
}
return true;
auto task = makeShared<MMCZip::ExportToZipTask>(output, m_instance->instanceRoot(), files, "", true);
connect(task.get(), &Task::failed, this,
[this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
connect(task.get(), &Task::finished, this, [task] { task->deleteLater(); });
ProgressDialog progress(this);
progress.setSkipButton(true, tr("Abort"));
auto result = progress.execWithTask(task.get());
QDialog::done(result);
}
void ExportInstanceDialog::done(int result)
{
savePackIgnore();
if (result == QDialog::Accepted)
{
if (doExport())
{
QDialog::done(QDialog::Accepted);
return;
}
else
{
return;
}
if (result == QDialog::Accepted) {
doExport();
return;
}
QDialog::done(result);
}
void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom)
{
//WARNING: possible off-by-one?
for(int i = top; i < bottom; i++)
{
// WARNING: possible off-by-one?
for (int i = top; i < bottom; i++) {
auto node = proxyModel->index(i, 0, parent);
if(proxyModel->shouldExpand(node))
{
if (proxyModel->shouldExpand(node)) {
auto expNode = node.parent();
if(!expNode.isValid())
{
if (!expNode.isValid()) {
continue;
}
ui->treeView->expand(node);
@ -195,8 +192,7 @@ void ExportInstanceDialog::loadPackIgnore()
{
auto filename = ignoreFileName();
QFile ignoreFile(filename);
if(!ignoreFile.open(QIODevice::ReadOnly))
{
if (!ignoreFile.open(QIODevice::ReadOnly)) {
return;
}
auto data = ignoreFile.readAll();
@ -212,12 +208,9 @@ void ExportInstanceDialog::savePackIgnore()
{
auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8();
auto filename = ignoreFileName();
try
{
try {
FS::write(filename, data);
}
catch (const Exception &e)
{
} catch (const Exception& e) {
qWarning() << e.cause();
}
}

View File

@ -2,6 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -38,39 +39,37 @@
#include <QDialog>
#include <QModelIndex>
#include <memory>
#include "FileIgnoreProxy.h"
#include "FastFileIconProvider.h"
#include "FileIgnoreProxy.h"
class BaseInstance;
typedef std::shared_ptr<BaseInstance> InstancePtr;
namespace Ui
{
namespace Ui {
class ExportInstanceDialog;
}
class ExportInstanceDialog : public QDialog
{
class ExportInstanceDialog : public QDialog {
Q_OBJECT
public:
explicit ExportInstanceDialog(InstancePtr instance, QWidget *parent = 0);
public:
explicit ExportInstanceDialog(InstancePtr instance, QWidget* parent = 0);
~ExportInstanceDialog();
virtual void done(int result);
private:
bool doExport();
private:
void doExport();
void loadPackIgnore();
void savePackIgnore();
QString ignoreFileName();
private:
Ui::ExportInstanceDialog *ui;
private:
Ui::ExportInstanceDialog* ui;
InstancePtr m_instance;
FileIgnoreProxy * proxyModel;
FileIgnoreProxy* proxyModel;
FastFileIconProvider icons;
private slots:
private slots:
void rowsInserted(QModelIndex parent, int top, int bottom);
};

View File

@ -16,11 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ExportMrPackDialog.h"
#include "ExportPackDialog.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlamePackExportTask.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui_ExportMrPackDialog.h"
#include "ui_ExportPackDialog.h"
#include <QFileDialog>
#include <QFileSystemModel>
@ -32,17 +34,24 @@
#include "MMCZip.h"
#include "modplatform/modrinth/ModrinthPackExportTask.h"
ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
: QDialog(parent), instance(instance), ui(new Ui::ExportMrPackDialog)
ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider)
: QDialog(parent), instance(instance), ui(new Ui::ExportPackDialog), m_provider(provider)
{
ui->setupUi(this);
ui->name->setText(instance->name());
ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]);
if (m_provider == ModPlatform::ResourceProvider::MODRINTH) {
ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]);
setWindowTitle("Export Modrinth Pack");
} else {
setWindowTitle("Export CurseForge Pack");
ui->version->setText("");
ui->summaryLabel->setText("Author");
}
// ensure a valid pack is generated
// the name and version fields mustn't be empty
connect(ui->name, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate);
connect(ui->version, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate);
connect(ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate);
connect(ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate);
// the instance name can technically be empty
validate();
@ -52,8 +61,9 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
// use the game root - everything outside cannot be exported
const QDir root(instance->gameRoot());
proxy = new FileIgnoreProxy(instance->gameRoot(), this);
proxy->ignoreFilesWithPath().insert({ "logs", "crash-reports", ".cache", ".fabric", ".quilt" });
proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" });
proxy->setSourceModel(model);
proxy->setFilterRegularExpression("^(?!(\\.DS_Store)|([tT]humbs\\.db)).+$");
const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden);
@ -65,6 +75,7 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
if (mcInstance) {
mcInstance->loaderModList()->update();
const QDir index = mcInstance->loaderModList()->indexDir();
if (index.exists())
proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath()));
@ -82,43 +93,54 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
headerView->setSectionResizeMode(0, QHeaderView::Stretch);
}
ExportMrPackDialog::~ExportMrPackDialog()
ExportPackDialog::~ExportPackDialog()
{
delete ui;
}
void ExportMrPackDialog::done(int result)
void ExportPackDialog::done(int result)
{
if (result == Accepted) {
const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text());
const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
FS::PathCombine(QDir::homePath(), filename + ".mrpack"),
"Modrinth pack (*.mrpack *.zip)", nullptr);
QString output;
if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
FS::PathCombine(QDir::homePath(), filename + ".mrpack"), "Modrinth pack (*.mrpack *.zip)",
nullptr);
else
output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
FS::PathCombine(QDir::homePath(), filename + ".zip"), "CurseForge pack (*.zip)", nullptr);
if (output.isEmpty())
return;
Task* task;
if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
task = new ModrinthPackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1));
else
task = new FlamePackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1));
ModrinthPackExportTask task(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
[this](const QString& path) { return proxy->blockedPaths().covers(path); });
connect(&task, &Task::failed,
connect(task, &Task::failed,
[this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
connect(&task, &Task::aborted, [this] {
connect(task, &Task::aborted, [this] {
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
->show();
});
connect(task, &Task::finished, [task] { task->deleteLater(); });
ProgressDialog progress(this);
progress.setSkipButton(true, tr("Abort"));
if (progress.execWithTask(&task) != QDialog::Accepted)
if (progress.execWithTask(task) != QDialog::Accepted)
return;
}
QDialog::done(result);
}
void ExportMrPackDialog::validate()
void ExportPackDialog::validate()
{
const bool invalid = ui->name->text().isEmpty() || ui->version->text().isEmpty();
const bool invalid =
ui->name->text().isEmpty() || ((m_provider == ModPlatform::ResourceProvider::MODRINTH) && ui->version->text().isEmpty());
ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid);
}

View File

@ -22,24 +22,28 @@
#include "BaseInstance.h"
#include "FastFileIconProvider.h"
#include "FileIgnoreProxy.h"
#include "modplatform/ModIndex.h"
namespace Ui {
class ExportMrPackDialog;
class ExportPackDialog;
}
class ExportMrPackDialog : public QDialog {
class ExportPackDialog : public QDialog {
Q_OBJECT
public:
explicit ExportMrPackDialog(InstancePtr instance, QWidget* parent = nullptr);
~ExportMrPackDialog();
explicit ExportPackDialog(InstancePtr instance,
QWidget* parent = nullptr,
ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH);
~ExportPackDialog();
void done(int result) override;
void validate();
private:
const InstancePtr instance;
Ui::ExportMrPackDialog* ui;
Ui::ExportPackDialog* ui;
FileIgnoreProxy* proxy;
FastFileIconProvider icons;
const ModPlatform::ResourceProvider m_provider;
};

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ExportMrPackDialog</class>
<widget class="QDialog" name="ExportMrPackDialog">
<class>ExportPackDialog</class>
<widget class="QDialog" name="ExportPackDialog">
<property name="geometry">
<rect>
<x>0</x>
@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Export Modrinth Pack</string>
<string>Export Pack</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
@ -24,7 +24,7 @@
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<widget class="QLabel" name="versionLabel">
<widget class="QLabel" name="summaryLabel">
<property name="text">
<string>Summary</string>
</property>
@ -41,7 +41,7 @@
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="summaryLabel">
<widget class="QLabel" name="versionLabel">
<property name="text">
<string>Version</string>
</property>
@ -57,6 +57,7 @@
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -103,7 +104,7 @@
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ExportMrPackDialog</receiver>
<receiver>ExportPackDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
@ -119,7 +120,7 @@
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ExportMrPackDialog</receiver>
<receiver>ExportPackDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">

View File

@ -0,0 +1,223 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ExportToModListDialog.h"
#include <QCheckBox>
#include <QComboBox>
#include <QTextEdit>
#include "FileSystem.h"
#include "Markdown.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/helpers/ExportToModList.h"
#include "ui_ExportToModListDialog.h"
#include <QFileDialog>
#include <QFileSystemModel>
#include <QJsonDocument>
#include <QMessageBox>
#include <QPushButton>
const QHash<ExportToModList::Formats, QString> ExportToModListDialog::exampleLines = {
{ ExportToModList::HTML, "<li><a href=\"{url}\">{name}</a> [{version}] by {authors}</li>" },
{ ExportToModList::MARKDOWN, "[{name}]({url}) [{version}] by {authors}" },
{ ExportToModList::PLAINTXT, "{name} ({url}) [{version}] by {authors}" },
{ ExportToModList::JSON, "{\"name\":\"{name}\",\"url\":\"{url}\",\"version\":\"{version}\",\"authors\":\"{authors}\"}," },
{ ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" },
};
ExportToModListDialog::ExportToModListDialog(InstancePtr instance, QWidget* parent)
: QDialog(parent), m_template_changed(false), name(instance->name()), ui(new Ui::ExportToModListDialog)
{
ui->setupUi(this);
enableCustom(false);
MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
if (mcInstance) {
mcInstance->loaderModList()->update();
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, [this, mcInstance]() {
m_allMods = mcInstance->loaderModList()->allMods();
triggerImp();
});
}
connect(ui->formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged);
connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); });
connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); });
connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); });
connect(ui->templateText, &QTextEdit::textChanged, this, [this] {
if (ui->templateText->toPlainText() != exampleLines[format])
ui->formatComboBox->setCurrentIndex(5);
else
triggerImp();
});
connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) {
this->ui->finalText->selectAll();
this->ui->finalText->copy();
});
}
ExportToModListDialog::~ExportToModListDialog()
{
delete ui;
}
void ExportToModListDialog::formatChanged(int index)
{
switch (index) {
case 0: {
enableCustom(false);
ui->resultText->show();
format = ExportToModList::HTML;
break;
}
case 1: {
enableCustom(false);
ui->resultText->show();
format = ExportToModList::MARKDOWN;
break;
}
case 2: {
enableCustom(false);
ui->resultText->hide();
format = ExportToModList::PLAINTXT;
break;
}
case 3: {
enableCustom(false);
ui->resultText->hide();
format = ExportToModList::JSON;
break;
}
case 4: {
enableCustom(false);
ui->resultText->hide();
format = ExportToModList::CSV;
break;
}
case 5: {
m_template_changed = true;
enableCustom(true);
ui->resultText->hide();
format = ExportToModList::CUSTOM;
break;
}
}
triggerImp();
}
void ExportToModListDialog::triggerImp()
{
if (format == ExportToModList::CUSTOM) {
ui->finalText->setPlainText(ExportToModList::exportToModList(m_allMods, ui->templateText->toPlainText()));
return;
}
auto opt = 0;
if (ui->authorsCheckBox->isChecked())
opt |= ExportToModList::Authors;
if (ui->versionCheckBox->isChecked())
opt |= ExportToModList::Version;
if (ui->urlCheckBox->isChecked())
opt |= ExportToModList::Url;
auto txt = ExportToModList::exportToModList(m_allMods, format, static_cast<ExportToModList::OptionalData>(opt));
ui->finalText->setPlainText(txt);
switch (format) {
case ExportToModList::CUSTOM:
return;
case ExportToModList::HTML:
ui->resultText->setHtml(txt);
break;
case ExportToModList::MARKDOWN:
ui->resultText->setHtml(markdownToHTML(txt));
break;
case ExportToModList::PLAINTXT:
break;
case ExportToModList::JSON:
break;
case ExportToModList::CSV:
break;
}
auto exampleLine = exampleLines[format];
if (!m_template_changed && ui->templateText->toPlainText() != exampleLine)
ui->templateText->setPlainText(exampleLine);
}
void ExportToModListDialog::done(int result)
{
if (result == Accepted) {
const QString filename = FS::RemoveInvalidFilenameChars(name);
const QString output =
QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + extension()),
"File (*.txt *.html *.md *.json *.csv)", nullptr);
if (output.isEmpty())
return;
FS::write(output, ui->finalText->toPlainText().toUtf8());
}
QDialog::done(result);
}
QString ExportToModListDialog::extension()
{
switch (format) {
case ExportToModList::HTML:
return ".html";
case ExportToModList::MARKDOWN:
return ".md";
case ExportToModList::PLAINTXT:
return ".txt";
case ExportToModList::CUSTOM:
return ".txt";
case ExportToModList::JSON:
return ".json";
case ExportToModList::CSV:
return ".csv";
}
return ".txt";
}
void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
{
if (format != ExportToModList::CUSTOM)
return;
switch (option) {
case ExportToModList::Authors:
ui->templateText->insertPlainText("{authors}");
break;
case ExportToModList::Url:
ui->templateText->insertPlainText("{url}");
break;
case ExportToModList::Version:
ui->templateText->insertPlainText("{version}");
break;
}
}
void ExportToModListDialog::enableCustom(bool enabled)
{
ui->authorsCheckBox->setHidden(enabled);
ui->versionCheckBox->setHidden(enabled);
ui->urlCheckBox->setHidden(enabled);
ui->authorsButton->setHidden(!enabled);
ui->versionButton->setHidden(!enabled);
ui->urlButton->setHidden(!enabled);
}

View File

@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDialog>
#include <QList>
#include "BaseInstance.h"
#include "minecraft/mod/Mod.h"
#include "modplatform/helpers/ExportToModList.h"
namespace Ui {
class ExportToModListDialog;
}
class ExportToModListDialog : public QDialog {
Q_OBJECT
public:
explicit ExportToModListDialog(InstancePtr instance, QWidget* parent = nullptr);
~ExportToModListDialog();
void done(int result) override;
protected slots:
void formatChanged(int index);
void triggerImp();
void trigger(int) { triggerImp(); };
void addExtra(ExportToModList::OptionalData option);
private:
QString extension();
void enableCustom(bool enabled);
QList<Mod*> m_allMods;
bool m_template_changed;
QString name;
ExportToModList::Formats format = ExportToModList::Formats::HTML;
Ui::ExportToModListDialog* ui;
static const QHash<ExportToModList::Formats, QString> exampleLines;
};

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