Merge branch 'develop' into chore/add-compiler-warnings

Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
Rachel Powers 2023-06-26 01:57:23 -07:00
commit 671d3c1c80
No known key found for this signature in database
GPG Key ID: E10E321EB160949B
215 changed files with 5304 additions and 3627 deletions

4
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,4 @@
# .git-blame-ignore-revs
# tabs -> spaces
bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9

View File

@ -86,11 +86,11 @@ jobs:
- os: macos-12
name: macOS
macosx_deployment_target: 10.15
macosx_deployment_target: 11.0
qt_ver: 6
qt_host: mac
qt_arch: ''
qt_version: '6.5.1'
qt_version: '6.5.0'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
@ -191,7 +191,7 @@ jobs:
if: runner.os == 'Linux'
run: |
sudo apt-get -y update
sudo apt-get -y install ninja-build extra-cmake-modules scdoc
sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream
- name: Install Dependencies (macOS)
if: runner.os == 'macOS'
@ -250,6 +250,7 @@ jobs:
wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
${{ github.workspace }}/.github/scripts/prepare_JREs.sh
sudo apt install libopengl0
- name: Add QT_HOST_PATH var (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
@ -375,6 +376,8 @@ jobs:
shell: msys2 {0}
run: |
cmake --install ${{ env.BUILD_DIR }}
touch ${{ env.INSTALL_DIR }}/manifest.txt
for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt
- name: Package (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == ''
@ -387,6 +390,10 @@ jobs:
Copy-Item D:/a/PrismLauncher/Qt/Tools/OpenSSL/Win_x86/bin/libcrypto-1_1.dll -Destination libcrypto-1_1.dll
Copy-Item D:/a/PrismLauncher/Qt/Tools/OpenSSL/Win_x86/bin/libssl-1_1.dll -Destination libssl-1_1.dll
}
cd ${{ github.workspace }}
Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
- name: Fetch codesign certificate (Windows)
if: runner.os == 'Windows'
@ -411,6 +418,7 @@ jobs:
run: |
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
- name: Package (Windows MSVC, portable)
if: runner.os == 'Windows' && matrix.msystem == ''
@ -418,6 +426,8 @@ jobs:
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
- name: Package (Windows, installer)
if: runner.os == 'Windows'
run: |
@ -437,6 +447,7 @@ jobs:
if: runner.os == 'Linux'
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }}
for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_DIR }}/manifest.txt
cd ${{ env.INSTALL_DIR }}
tar --owner root --group root -czf ../PrismLauncher.tar.gz *
@ -446,6 +457,8 @@ jobs:
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }}
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
cd ${{ env.INSTALL_PORTABLE_DIR }}
tar -czf ../PrismLauncher-portable.tar.gz *
@ -455,7 +468,8 @@ jobs:
shell: bash
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr
mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml
export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated
export OUTPUT="PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage"
chmod +x linuxdeploy-*.AppImage
@ -470,7 +484,8 @@ jobs:
cp -r /home/runner/work/PrismLauncher/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}//usr/lib/
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server"
@ -587,7 +602,7 @@ jobs:
submodules: 'true'
- name: Install nix
if: inputs.build_type == 'Debug'
uses: cachix/install-nix-action@v21
uses: cachix/install-nix-action@v22
with:
install_url: https://nixos.org/nix/install
extra_nix_config: |

View File

@ -47,7 +47,7 @@ jobs:
mv PrismLauncher-macOS-Legacy*/PrismLauncher.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz
mv PrismLauncher-macOS*/PrismLauncher.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.tar.gz
tar -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
for d in PrismLauncher-Windows-MSVC*; do
cd "${d}" || continue

View File

@ -138,7 +138,7 @@ set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL th
set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help")
######## Set version numbers ########
set(Launcher_VERSION_MAJOR 7)
set(Launcher_VERSION_MAJOR 8)
set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")

View File

@ -82,6 +82,7 @@ Config::Config()
{
GIT_REFSPEC = "refs/heads/stable";
GIT_TAG = versionString();
GIT_COMMIT = "";
}
if (GIT_REFSPEC.startsWith("refs/heads/"))

View File

@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -36,6 +37,7 @@
#pragma once
#include <QString>
#include <QList>
/**
* \brief The Config class holds all the build-time information passed from the build system.
@ -160,6 +162,7 @@ class Config {
QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2";
QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2";
QStringList MODRINTH_MRPACK_HOSTS{"cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com"};
QString FLAME_BASE_URL = "https://api.curseforge.com/v1";

View File

@ -1 +1,14 @@
(import nix/flake-compat.nix).defaultNix
(
import
(
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}
)
{src = ./.;}
)
.defaultNix

View File

@ -16,29 +16,31 @@
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"lastModified": 1683560683,
"narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
@ -86,11 +88,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1678693419,
"narHash": "sha256-bbSv5yqZAW6dz+3f3f3pOUZbxpPN+3OgCljgn7P+nnQ=",
"lastModified": 1685012353,
"narHash": "sha256-U3oOge4cHnav8OLGdRVhL45xoRj4Ppd+It6nPC9nNIU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "8e3fad82be64c06fbfb9fd43993aec9ef4623936",
"rev": "aeb75dba965e790de427b73315d5addf91a54955",
"type": "github"
},
"original": {
@ -100,40 +102,44 @@
"type": "github"
}
},
"nixpkgs-stable": {
"nixpkgs-lib": {
"locked": {
"lastModified": 1673800717,
"narHash": "sha256-SFHraUqLSu5cC6IxTprex/nTsI81ZQAtDvlBvGDWfnA=",
"dir": "lib",
"lastModified": 1682879489,
"narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2f9fd351ec37f5d479556cd48be4ca340da59b8f",
"rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-22.11",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat_2",
"flake-utils": [
"flake-utils"
"flake-compat": [
"flake-compat"
],
"flake-utils": "flake-utils",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
"nixpkgs-stable": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1678376203,
"narHash": "sha256-3tyYGyC8h7fBwncLZy5nCUjTJPrHbmNwp47LlNLOHSM=",
"lastModified": 1684842236,
"narHash": "sha256-rYWsIXHvNhVQ15RQlBUv67W3YnM+Pd+DuXGMvCBq2IE=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "1a20b9708962096ec2481eeb2ddca29ed747770a",
"rev": "61e567d6497bc9556f391faebe5e410e6623217f",
"type": "github"
},
"original": {
@ -145,7 +151,7 @@
"root": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"flake-parts": "flake-parts",
"libnbtplusplus": "libnbtplusplus",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"

View File

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

View File

@ -376,33 +376,33 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// init the logger
{
static const QString logBase = BuildConfig.LAUNCHER_NAME + "-%0.log";
auto moveFile = [](const QString &oldName, const QString &newName)
{
static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log";
static const QString logBase = FS::PathCombine("logs", baseLogFile);
auto moveFile = [](const QString& oldName, const QString& newName) {
QFile::remove(newName);
QFile::copy(oldName, newName);
QFile::remove(oldName);
};
if (FS::ensureFolderPathExists("logs")) { // if this did not fail
for (auto i = 0; i <= 4; i++)
if (auto oldName = baseLogFile.arg(i);
QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there
moveFile(oldName, logBase.arg(i));
}
moveFile(logBase.arg(3), logBase.arg(4));
moveFile(logBase.arg(2), logBase.arg(3));
moveFile(logBase.arg(1), logBase.arg(2));
moveFile(logBase.arg(0), logBase.arg(1));
for (auto i = 4; i > 0; i--)
moveFile(logBase.arg(i - 1), logBase.arg(i));
logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0)));
if(!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
{
showFatalErrorMessage(
"The launcher data folder is not writable!",
QString(
"The launcher couldn't create a log file - the data folder is not writable.\n"
if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
showFatalErrorMessage("The launcher data folder is not writable!",
QString("The launcher couldn't create a log file - the data folder is not writable.\n"
"\n"
"Make sure you have write permissions to the data folder.\n"
"(%1)\n"
"\n"
"The launcher cannot continue until you fix this problem."
).arg(dataPath)
);
"The launcher cannot continue until you fix this problem.")
.arg(dataPath));
return;
}
qInstallMessageHandler(appDebugOutput);
@ -594,7 +594,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Java Settings
m_settings->registerSetting("JavaPath", "");
m_settings->registerSetting("JavaTimestamp", 0);
m_settings->registerSetting("JavaSignature", "");
m_settings->registerSetting("JavaArchitecture", "");
m_settings->registerSetting("JavaRealArchitecture", "");
m_settings->registerSetting("JavaVersion", "");
@ -1699,6 +1699,7 @@ bool Application::handleDataMigration(const QString& currentData,
matcher->add(std::make_shared<SimplePrefixMatcher>(configFile));
matcher->add(std::make_shared<SimplePrefixMatcher>(
BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before
matcher->add(std::make_shared<SimplePrefixMatcher>("logs/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("accounts.json"));
matcher->add(std::make_shared<SimplePrefixMatcher>("accounts/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("assets/"));

View File

@ -362,6 +362,8 @@ set(MINECRAFT_SOURCES
minecraft/mod/tasks/LocalWorldSaveParseTask.cpp
minecraft/mod/tasks/LocalResourceParse.h
minecraft/mod/tasks/LocalResourceParse.cpp
minecraft/mod/tasks/GetModDependenciesTask.h
minecraft/mod/tasks/GetModDependenciesTask.cpp
# Assets
minecraft/AssetsUtils.h
@ -375,8 +377,6 @@ set(MINECRAFT_SOURCES
minecraft/services/SkinDelete.cpp
minecraft/services/SkinDelete.h
mojang/PackageManifest.h
mojang/PackageManifest.cpp
minecraft/Agent.h)
# the screenshots feature
@ -525,6 +525,8 @@ set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthCheckUpdate.h
modplatform/modrinth/ModrinthInstanceCreationTask.cpp
modplatform/modrinth/ModrinthInstanceCreationTask.h
modplatform/modrinth/ModrinthPackExportTask.cpp
modplatform/modrinth/ModrinthPackExportTask.h
)
set(PACKWIZ_SOURCES
@ -680,6 +682,7 @@ SET(LAUNCHER_SOURCES
VersionProxyModel.h
VersionProxyModel.cpp
Markdown.h
Markdown.cpp
# Super secret!
KonamiCode.h
@ -720,6 +723,10 @@ SET(LAUNCHER_SOURCES
# FIXME: maybe find a better home for this.
SkinUtils.cpp
SkinUtils.h
FileIgnoreProxy.cpp
FileIgnoreProxy.h
FastFileIconProvider.cpp
FastFileIconProvider.h
# GUI - setup wizard
ui/setupwizard/SetupWizard.h
@ -819,8 +826,8 @@ SET(LAUNCHER_SOURCES
ui/pages/global/APIPage.h
# GUI - platform pages
ui/pages/modplatform/VanillaPage.cpp
ui/pages/modplatform/VanillaPage.h
ui/pages/modplatform/CustomPage.cpp
ui/pages/modplatform/CustomPage.h
ui/pages/modplatform/ResourcePage.cpp
ui/pages/modplatform/ResourcePage.h
@ -900,6 +907,8 @@ 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/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourceDialog.cpp
@ -1024,7 +1033,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/instance/ScreenshotsPage.ui
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
ui/pages/modplatform/atlauncher/AtlPage.ui
ui/pages/modplatform/VanillaPage.ui
ui/pages/modplatform/CustomPage.ui
ui/pages/modplatform/ResourcePage.ui
ui/pages/modplatform/flame/FlamePage.ui
ui/pages/modplatform/legacy_ftb/Page.ui
@ -1046,6 +1055,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/ExportMrPackDialog.ui
ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui

View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "FastFileIconProvider.h"
#include <QApplication>
#include <QStyle>
QIcon FastFileIconProvider::icon(const QFileInfo& info) const
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
bool link = info.isSymbolicLink() || info.isAlias() || info.isShortcut();
#else
// in versions prior to 6.4 we don't have access to isAlias
bool link = info.isSymLink();
#endif
QStyle::StandardPixmap icon;
if (info.isDir()) {
if (link)
icon = QStyle::SP_DirLinkIcon;
else
icon = QStyle::SP_DirIcon;
} else {
if (link)
icon = QStyle::SP_FileLinkIcon;
else
icon = QStyle::SP_FileIcon;
}
return QApplication::style()->standardIcon(icon);
}

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QFileIconProvider>
class FastFileIconProvider : public QFileIconProvider {
public:
QIcon icon(const QFileInfo& info) const override;
};

View File

@ -0,0 +1,279 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "FileIgnoreProxy.h"
#include <QDebug>
#include <QFileSystemModel>
#include <QSortFilterProxyModel>
#include <QStack>
#include <algorithm>
#include "FileSystem.h"
#include "SeparatorPrefixTree.h"
#include "StringUtils.h"
FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), root(root) {}
// NOTE: Sadly, we have to do sorting ourselves.
bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
if (!fsm) {
return QSortFilterProxyModel::lessThan(left, right);
}
bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
QFileInfo leftFileInfo = fsm->fileInfo(left);
QFileInfo rightFileInfo = fsm->fileInfo(right);
if (!leftFileInfo.isDir() && rightFileInfo.isDir()) {
return !asc;
}
if (leftFileInfo.isDir() && !rightFileInfo.isDir()) {
return asc;
}
// sort and proxy model breaks the original model...
if (sortColumn() == 0) {
return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0;
}
if (sortColumn() == 1) {
auto leftSize = leftFileInfo.size();
auto rightSize = rightFileInfo.size();
if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) {
return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0 ? asc : !asc;
}
return leftSize < rightSize;
}
return QSortFilterProxyModel::lessThan(left, right);
}
Qt::ItemFlags FileIgnoreProxy::flags(const QModelIndex& index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
auto sourceIndex = mapToSource(index);
Qt::ItemFlags flags = sourceIndex.flags();
if (index.column() == 0) {
flags |= Qt::ItemIsUserCheckable;
if (sourceIndex.model()->hasChildren(sourceIndex)) {
flags |= Qt::ItemIsAutoTristate;
}
}
return flags;
}
QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const
{
QModelIndex sourceIndex = mapToSource(index);
if (index.column() == 0 && role == Qt::CheckStateRole) {
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
auto blockedPath = relPath(fsm->filePath(sourceIndex));
auto cover = blocked.cover(blockedPath);
if (!cover.isNull()) {
return QVariant(Qt::Unchecked);
} else if (blocked.exists(blockedPath)) {
return QVariant(Qt::PartiallyChecked);
} else {
return QVariant(Qt::Checked);
}
}
return sourceIndex.data(role);
}
bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (index.column() == 0 && role == Qt::CheckStateRole) {
Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
return setFilterState(index, state);
}
QModelIndex sourceIndex = mapToSource(index);
return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role);
}
QString FileIgnoreProxy::relPath(const QString& path) const
{
return QDir(root).relativeFilePath(path);
}
bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state)
{
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
if (!fsm) {
return false;
}
QModelIndex sourceIndex = mapToSource(index);
auto blockedPath = relPath(fsm->filePath(sourceIndex));
bool changed = false;
if (state == Qt::Unchecked) {
// blocking a path
auto& node = blocked.insert(blockedPath);
// get rid of all blocked nodes below
node.clear();
changed = true;
} else if (state == Qt::Checked || state == Qt::PartiallyChecked) {
if (!blocked.remove(blockedPath)) {
auto cover = blocked.cover(blockedPath);
qDebug() << "Blocked by cover" << cover;
// uncover
blocked.remove(cover);
// block all contents, except for any cover
QModelIndex rootIndex = fsm->index(FS::PathCombine(root, cover));
QModelIndex doing = rootIndex;
int row = 0;
QStack<QModelIndex> todo;
while (1) {
auto node = fsm->index(row, 0, doing);
if (!node.isValid()) {
if (!todo.size()) {
break;
} else {
doing = todo.pop();
row = 0;
continue;
}
}
auto relpath = relPath(fsm->filePath(node));
if (blockedPath.startsWith(relpath)) // cover found?
{
// continue processing cover later
todo.push(node);
} else {
// or just block this one.
blocked.insert(relpath);
}
row++;
}
}
changed = true;
}
if (changed) {
// update the thing
emit dataChanged(index, index, { Qt::CheckStateRole });
// update everything above index
QModelIndex up = index.parent();
while (1) {
if (!up.isValid())
break;
emit dataChanged(up, up, { Qt::CheckStateRole });
up = up.parent();
}
// and everything below the index
QModelIndex doing = index;
int row = 0;
QStack<QModelIndex> todo;
while (1) {
auto node = this->index(row, 0, doing);
if (!node.isValid()) {
if (!todo.size()) {
break;
} else {
doing = todo.pop();
row = 0;
continue;
}
}
emit dataChanged(node, node, { Qt::CheckStateRole });
todo.push(node);
row++;
}
// siblings and unrelated nodes are ignored
}
return true;
}
bool FileIgnoreProxy::shouldExpand(QModelIndex index)
{
QModelIndex sourceIndex = mapToSource(index);
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
if (!fsm) {
return false;
}
auto blockedPath = relPath(fsm->filePath(sourceIndex));
auto found = blocked.find(blockedPath);
if (found) {
return !found->leaf();
}
return false;
}
void FileIgnoreProxy::setBlockedPaths(QStringList paths)
{
beginResetModel();
blocked.clear();
blocked.insert(paths);
endResetModel();
}
bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const
{
Q_UNUSED(source_parent)
// adjust the columns you want to filter out here
// return false for those that will be hidden
if (source_column == 2 || source_column == 3)
return false;
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

@ -0,0 +1,85 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QFileInfo>
#include <QSortFilterProxyModel>
#include "SeparatorPrefixTree.h"
class FileIgnoreProxy : public QSortFilterProxyModel {
Q_OBJECT
public:
FileIgnoreProxy(QString root, QObject* parent);
// NOTE: Sadly, we have to do sorting ourselves.
bool lessThan(const QModelIndex& left, const QModelIndex& right) const;
virtual Qt::ItemFlags flags(const QModelIndex& index) const;
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole);
QString relPath(const QString& path) const;
bool setFilterState(QModelIndex index, Qt::CheckState state);
bool shouldExpand(QModelIndex index);
void setBlockedPaths(QStringList paths);
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

@ -102,7 +102,7 @@ namespace fs = ghc::filesystem;
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <unistd.h>
#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
#include <sys/attr.h>
#include <sys/clonefile.h>
#elif defined(Q_OS_WIN)
@ -769,6 +769,9 @@ QString getDesktopDir()
// Cross-platform Shortcut creation
bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon)
{
if (destination.isEmpty()) {
destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name));
}
#if defined(Q_OS_MACOS)
destination += ".command";
@ -791,6 +794,8 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
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
destination += ".desktop";
QFile f(destination);
f.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream stream(&f);
@ -1151,7 +1156,7 @@ bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
return false;
}
#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
if (!macos_bsd_clonefile(src_path, dst_path, ec)) {
qDebug() << "failed macos_bsd_clonefile:";
@ -1380,7 +1385,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_FREEBSD) || defined(Q_OS_OPENBSD)
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
{

View File

@ -39,7 +39,16 @@ void InstanceCopyTask::executeTask()
setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
auto copySaves = [&]() {
FS::copy savesCopy(FS::PathCombine(m_origInstance->instanceRoot(), "saves"), FS::PathCombine(m_stagingPath, "saves"));
QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
QString staging_mc_dir;
if (mcDir.exists() && !dotMCDir.exists())
staging_mc_dir = mcDir.filePath();
else
staging_mc_dir = dotMCDir.filePath();
FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves"));
savesCopy.followSymlinks(true);
return savesCopy();
@ -123,6 +132,7 @@ void InstanceCopyTask::copyFinished()
emitFailed(tr("Instance folder copy failed."));
return;
}
// FIXME: shouldn't this be able to report errors?
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
@ -134,6 +144,24 @@ void InstanceCopyTask::copyFinished()
}
if (m_useLinks)
inst->addLinkedInstanceId(m_origInstance->id());
if (m_useLinks) {
auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt"));
QByteArray allowed_symlinks;
if (allowed_symlinks_file.exists()) {
allowed_symlinks.append(FS::read(allowed_symlinks_file.filePath()));
if (allowed_symlinks.right(1) != "\n")
allowed_symlinks.append("\n"); // we want to be on a new line
}
allowed_symlinks.append(m_origInstance->gameRoot().toUtf8());
allowed_symlinks.append("\n");
if (allowed_symlinks_file.isSymLink())
FS::deletePath(allowed_symlinks_file
.filePath()); // we dont want to modify the original. also make sure the resulting file is not itself a link.
FS::write(allowed_symlinks_file.filePath(), allowed_symlinks);
}
emitSucceeded();
}

View File

@ -45,7 +45,10 @@ QString InstanceName::name() const
{
if (!m_modified_name.isEmpty())
return modifiedName();
if (!m_original_version.isEmpty())
return QString("%1 %2").arg(m_original_name, m_original_version);
return m_original_name;
}
QString InstanceName::originalName() const

View File

@ -3,6 +3,8 @@
#include <QCoreApplication>
#include <QPixmapCache>
#include <QThread>
#include <QTime>
#include <QDebug>
#define GET_TYPE() \
Qt::ConnectionType type; \
@ -60,6 +62,8 @@ class PixmapCache final : public QObject {
DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&)
DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&)
DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int)
DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool)
DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, int)
// NOTE: Every function returns something non-void to simplify the macros.
private slots:
@ -90,6 +94,43 @@ class PixmapCache final : public QObject {
return true;
}
/**
* Mark that a cache miss occurred because of a eviction if too many of these occur too fast the cache size is increased
* @return if the cache size was increased
*/
bool _markCacheMissByEviciton()
{
auto now = QTime::currentTime();
if (!m_last_cache_miss_by_eviciton.isNull()) {
auto diff = m_last_cache_miss_by_eviciton.msecsTo(now);
if (diff < 1000) { // less than a second ago
++m_consecutive_fast_evicitons;
} else {
m_consecutive_fast_evicitons = 0;
}
}
m_last_cache_miss_by_eviciton = now;
if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) {
// double the cache size
auto newSize = _cacheLimit() * 2;
qDebug() << m_consecutive_fast_evicitons << "pixmap cache misses by eviction happened too fast, doubling cache size to"
<< newSize;
_setCacheLimit(newSize);
m_consecutive_fast_evicitons = 0;
return true;
}
return false;
}
bool _setFastEvictionThreshold(int threshold)
{
m_consecutive_fast_evicitons_threshold = threshold;
return true;
}
private:
static PixmapCache* s_instance;
QTime m_last_cache_miss_by_eviciton;
int m_consecutive_fast_evicitons = 0;
int m_consecutive_fast_evicitons_threshold = 15;
};

31
launcher/Markdown.cpp Normal file
View File

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

View File

@ -21,14 +21,4 @@
#include <QString>
#include <cmark.h>
static QString markdownToHTML(const QString& markdown)
{
const QByteArray markdownData = markdown.toUtf8();
char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE);
QString htmlStr(buffer);
free(buffer);
return htmlStr;
}
QString markdownToHTML(const QString& markdown);

View File

@ -24,14 +24,15 @@
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourceFolderModel.h"
ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack,
ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion version,
const std::shared_ptr<ResourceFolderModel> packs,
bool is_indexed)
: m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs)
bool is_indexed,
QString custom_target_folder)
: m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs), m_custom_target_folder(custom_target_folder)
{
if (auto model = dynamic_cast<ModFolderModel*>(m_pack_model.get()); model && is_indexed) {
m_update_task.reset(new LocalModUpdateTask(model->indexDir(), m_pack, m_pack_version));
m_update_task.reset(new LocalModUpdateTask(model->indexDir(), *m_pack, m_pack_version));
connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource);
addTask(m_update_task);
@ -44,9 +45,9 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack,
{
// FIXME: Make this more generic. May require adding additional info to IndexedVersion,
// or adquiring a reference to the base instance.
if (!m_pack_version.custom_target_folder.isEmpty()) {
if (!m_custom_target_folder.isEmpty()) {
dir.cdUp();
dir.cd(m_pack_version.custom_target_folder);
dir.cd(m_custom_target_folder);
}
}

View File

@ -22,23 +22,32 @@
#include "net/NetJob.h"
#include "tasks/SequentialTask.h"
#include "modplatform/ModIndex.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
#include "modplatform/ModIndex.h"
class ResourceFolderModel;
class ResourceDownloadTask : public SequentialTask {
Q_OBJECT
public:
explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr<ResourceFolderModel> packs, bool is_indexed = true);
explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion version,
const std::shared_ptr<ResourceFolderModel> packs,
bool is_indexed = true,
QString custom_target_folder = {});
const QString& getFilename() const { return m_pack_version.fileName; }
const QString& getCustomPath() const { return m_pack_version.custom_target_folder; }
const QString& getCustomPath() const { return m_custom_target_folder; }
const QVariant& getVersionID() const { return m_pack_version.fileId; }
const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; }
const ModPlatform::ResourceProvider& getProvider() const { return m_pack->provider; }
const QString& getName() const { return m_pack->name; }
ModPlatform::IndexedPack::Ptr getPack() { return m_pack; }
private:
ModPlatform::IndexedPack m_pack;
ModPlatform::IndexedPack::Ptr m_pack;
ModPlatform::IndexedVersion m_pack_version;
const std::shared_ptr<ResourceFolderModel> m_pack_model;
QString m_custom_target_folder;
NetJob::Ptr m_filesNetJob;
LocalModUpdateTask::Ptr m_update_task;
@ -52,6 +61,3 @@ private:
private slots:
void hasOldResource(QString name, QString filename);
};

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 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -54,9 +55,14 @@ public:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
const auto &filters = m_parent->filters();
const QString &search = m_parent->search();
const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
if (!search.isEmpty() && !sourceModel()->data(idx, BaseVersionList::VersionRole).toString().contains(search, Qt::CaseInsensitive))
return false;
for (auto it = filters.begin(); it != filters.end(); ++it)
{
auto idx = sourceModel()->index(source_row, 0, source_parent);
auto data = sourceModel()->data(idx, it.key());
auto match = data.toString();
if(!it.value()->accepts(match))
@ -206,10 +212,6 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
return tr("Latest");
}
}
else if(index.row() == 0)
{
return tr("Latest");
}
}
}
default:
@ -239,10 +241,6 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
return APPLICATION->getThemedIcon("bug");
}
}
else if(index.row() == 0)
{
return APPLICATION->getThemedIcon("bug");
}
QPixmap pixmap;
QPixmapCache::find("placeholder", &pixmap);
if(!pixmap)
@ -431,6 +429,7 @@ QModelIndex VersionProxyModel::getVersion(const QString& version) const
void VersionProxyModel::clearFilters()
{
m_filters.clear();
m_search.clear();
filterModel->filterChanged();
}
@ -440,11 +439,21 @@ void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filt
filterModel->filterChanged();
}
void VersionProxyModel::setSearch(const QString &search) {
m_search = search;
filterModel->filterChanged();
}
const VersionProxyModel::FilterMap &VersionProxyModel::filters() const
{
return m_filters;
}
const QString &VersionProxyModel::search() const
{
return m_search;
}
void VersionProxyModel::sourceAboutToBeReset()
{
beginResetModel();

View File

@ -38,7 +38,9 @@ public:
virtual void setSourceModel(QAbstractItemModel *sourceModel) override;
const FilterMap &filters() const;
const QString &search() const;
void setFilter(const BaseVersionList::ModelRoles column, Filter * filter);
void setSearch(const QString &search);
void clearFilters();
QModelIndex getRecommended() const;
QModelIndex getVersion(const QString & version) const;
@ -59,6 +61,7 @@ private slots:
private:
QList<Column> m_columns;
FilterMap m_filters;
QString m_search;
BaseVersionList::RoleList roles;
VersionFilterModel * filterModel;
bool hasRecommended = false;

View File

@ -85,7 +85,7 @@ void JavaChecker::performCheck()
process->setProgram(m_path);
process->setProcessChannelMode(QProcess::SeparateChannels);
process->setProcessEnvironment(CleanEnviroment());
qDebug() << "Running java checker: " + m_path + args.join(" ");;
qDebug() << "Running java checker:" << m_path << args.join(" ");
connect(process.get(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &JavaChecker::finished);
connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error);

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 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -98,6 +99,8 @@ QVariant JavaInstallList::data(const QModelIndex &index, int role) const
auto version = std::dynamic_pointer_cast<JavaInstall>(m_vlist[index.row()]);
switch (role)
{
case SortRole:
return -index.row();
case VersionPointerRole:
return QVariant::fromValue(m_vlist[index.row()]);
case VersionIdRole:

View File

@ -81,15 +81,20 @@ void CheckJava::executeTask()
}
QFileInfo javaInfo(realJavaPath);
qlonglong javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch();
auto storedUnixTime = settings->get("JavaTimestamp").toLongLong();
qint64 javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch();
auto storedSignature = settings->get("JavaSignature").toString();
auto storedArchitecture = settings->get("JavaArchitecture").toString();
auto storedRealArchitecture = settings->get("JavaRealArchitecture").toString();
auto storedVersion = settings->get("JavaVersion").toString();
auto storedVendor = settings->get("JavaVendor").toString();
m_javaUnixTime = javaUnixTime;
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(QByteArray::number(javaUnixTime));
hash.addData(m_javaPath.toUtf8());
m_javaSignature = hash.result().toHex();
// if timestamps are not the same, or something is missing, check!
if (javaUnixTime != storedUnixTime || storedVersion.size() == 0
if (m_javaSignature != storedSignature || storedVersion.size() == 0
|| storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0
|| storedVendor.size() == 0)
{
@ -140,7 +145,7 @@ void CheckJava::checkJavaFinished(JavaCheckResult result)
instance->settings()->set("JavaArchitecture", result.mojangPlatform);
instance->settings()->set("JavaRealArchitecture", result.realPlatform);
instance->settings()->set("JavaVendor", result.javaVendor);
instance->settings()->set("JavaTimestamp", m_javaUnixTime);
instance->settings()->set("JavaSignature", m_javaSignature);
emitSucceeded();
return;
}

View File

@ -40,6 +40,6 @@ private:
private:
QString m_javaPath;
qlonglong m_javaUnixTime;
QString m_javaSignature;
JavaCheckerPtr m_JavaChecker;
};

View File

@ -56,10 +56,10 @@ static Version::Ptr parseCommonVersion(const QString &uid, const QJsonObject &ob
version->setType(ensureString(obj, "type", QString()));
version->setRecommended(ensureBoolean(obj, QString("recommended"), false));
version->setVolatile(ensureBoolean(obj, QString("volatile"), false));
RequireSet requires, conflicts;
parseRequires(obj, &requires, "requires");
RequireSet reqs, conflicts;
parseRequires(obj, &reqs, "requires");
parseRequires(obj, &conflicts, "conflicts");
version->setRequires(requires, conflicts);
version->setRequires(reqs, conflicts);
return version;
}
@ -176,7 +176,6 @@ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char * keyName
{
if(obj.contains(keyName))
{
QSet<QString> requires;
auto reqArray = requireArray(obj, keyName);
auto iter = reqArray.begin();
while(iter != reqArray.end())

View File

@ -116,9 +116,9 @@ void Meta::Version::setTime(const qint64 time)
emit timeChanged();
}
void Meta::Version::setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts)
void Meta::Version::setRequires(const Meta::RequireSet &reqs, const Meta::RequireSet &conflicts)
{
m_requires = requires;
m_requires = reqs;
m_conflicts = conflicts;
emit requiresChanged();
}

View File

@ -63,7 +63,7 @@ public:
{
return m_time;
}
const Meta::RequireSet &requires() const
const Meta::RequireSet &requiredSet() const
{
return m_requires;
}
@ -91,7 +91,7 @@ public:
public: // for usage by format parsers only
void setType(const QString &type);
void setTime(const qint64 time);
void setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts);
void setRequires(const Meta::RequireSet &reqs, const Meta::RequireSet &conflicts);
void setVolatile(bool volatile_);
void setRecommended(bool recommended);
void setProvidesRecommendations();

View File

@ -77,7 +77,7 @@ QVariant VersionList::data(const QModelIndex &index, int role) const
case ParentVersionRole:
{
// FIXME: HACK: this should be generic and be replaced by something else. Anything that is a hard 'equals' dep is a 'parent uid'.
auto & reqs = version->requires();
auto & reqs = version->requiredSet();
auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Require & req)
{
return req.uid == "net.minecraft";
@ -92,7 +92,7 @@ QVariant VersionList::data(const QModelIndex &index, int role) const
case UidRole: return version->uid();
case TimeRole: return version->time();
case RequiresRole: return QVariant::fromValue(version->requires());
case RequiresRole: return QVariant::fromValue(version->requiredSet());
case SortRole: return version->rawTime();
case VersionPtrRole: return QVariant::fromValue(version);
case RecommendedRole: return version->isRecommended();

View File

@ -451,9 +451,9 @@ void Component::updateCachedData()
m_cachedVolatile = file->m_volatile;
changed = true;
}
if(!deepCompare(m_cachedRequires, file->requires))
if(!deepCompare(m_cachedRequires, file->m_requires))
{
m_cachedRequires = file->requires;
m_cachedRequires = file->m_requires;
changed = true;
}
if(!deepCompare(m_cachedConflicts, file->conflicts))

View File

@ -148,10 +148,11 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), javaOrLocation);
// special!
m_settings->registerPassthrough(global_settings->getSetting("JavaTimestamp"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), javaOrLocation);
// Window Size
auto windowSetting = m_settings->registerSetting("OverrideWindow", false);

View File

@ -276,7 +276,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
if (root.contains("requires"))
{
Meta::parseRequires(root, &out->requires);
Meta::parseRequires(root, &out->m_requires);
}
QString dependsOnMinecraftVersion = root.value("mcVersion").toString();
if(!dependsOnMinecraftVersion.isEmpty())
@ -284,9 +284,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
Meta::Require mcReq;
mcReq.uid = "net.minecraft";
mcReq.equalsVersion = dependsOnMinecraftVersion;
if (out->requires.count(mcReq) == 0)
if (out->m_requires.count(mcReq) == 0)
{
out->requires.insert(mcReq);
out->m_requires.insert(mcReq);
}
}
if (root.contains("conflicts"))
@ -392,9 +392,9 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch
}
root.insert("mods", array);
}
if(!patch->requires.empty())
if(!patch->m_requires.empty())
{
Meta::serializeRequires(root, &patch->requires, "requires");
Meta::serializeRequires(root, &patch->m_requires, "requires");
}
if(!patch->conflicts.empty())
{

View File

@ -138,7 +138,7 @@ public: /* data */
* Prism Launcher: set of packages this depends on
* NOTE: this is shared with the meta format!!!
*/
Meta::RequireSet requires;
Meta::RequireSet m_requires;
/**
* Prism Launcher: set of packages this conflicts with

View File

@ -333,13 +333,13 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
case MigrationColumn: {
if(account->isMSA() || account->isOffline()) {
return tr("N/A", "Can Migrate?");
return tr("N/A", "Can Migrate");
}
if (account->canMigrate()) {
return tr("Yes", "Can Migrate?");
return tr("Yes", "Can Migrate");
}
else {
return tr("No", "Can Migrate?");
return tr("No", "Can Migrate");
}
}

View File

@ -33,7 +33,9 @@ static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } },
{ 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } },
{ 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } },
{ 10, { Version("1.19"), Version("1.19.3") } },
{ 10, { Version("1.19"), Version("1.19.3") } }, { 11, { Version("23w03a"), Version("23w05a") } },
{ 12, { Version("1.19.4"), Version("1.19.4") } }, { 13, { Version("23w12a"), Version("23w14a") } },
{ 14, { Version("23w16a"), Version("23w17a") } }, { 15, { Version("1.20"), Version("1.20") } },
};
void DataPack::setPackFormat(int new_format_id)

View File

@ -41,9 +41,11 @@
#include <QString>
#include <QRegularExpression>
#include "MTPixmapCache.h"
#include "MetadataHandler.h"
#include "Version.h"
#include "minecraft/mod/ModDetails.h"
#include "minecraft/mod/tasks/LocalModParseTask.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
@ -201,7 +203,10 @@ void Mod::finishResolvingWithDetails(ModDetails&& details)
m_local_details = std::move(details);
if (metadata)
setMetadata(std::move(metadata));
};
if (!iconPath().isEmpty()) {
m_pack_image_cache_key.was_read_attempt = false;
}
}
auto Mod::provider() const -> std::optional<QString>
{
@ -210,6 +215,56 @@ auto Mod::provider() const -> std::optional<QString>
return {};
}
auto Mod::licenses() const -> const QList<ModLicense>&
{
return details().licenses;
}
auto Mod::issueTracker() const -> QString
{
return details().issue_tracker;
}
void Mod::setIcon(QImage new_image) const
{
QMutexLocker locker(&m_data_lock);
Q_ASSERT(!new_image.isNull());
if (m_pack_image_cache_key.key.isValid())
PixmapCache::remove(m_pack_image_cache_key.key);
// scale the image to avoid flooding the pixmapcache
auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding));
m_pack_image_cache_key.key = PixmapCache::insert(pixmap);
m_pack_image_cache_key.was_ever_used = true;
m_pack_image_cache_key.was_read_attempt = true;
}
QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const
{
QPixmap cached_image;
if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {
if (size.isNull())
return cached_image;
return cached_image.scaled(size, mode);
}
// No valid image we can get
if ((!m_pack_image_cache_key.was_ever_used && m_pack_image_cache_key.was_read_attempt) || iconPath().isEmpty())
return {};
if (m_pack_image_cache_key.was_ever_used) {
qDebug() << "Mod" << name() << "Had it's icon evicted form the cache. reloading...";
PixmapCache::markCacheMissByEviciton();
}
// Image got evicted from the cache or an attempt to load it has not been made. load it and retry.
m_pack_image_cache_key.was_read_attempt = true;
ModUtils::loadIconFile(*this);
return icon(size);
}
bool Mod::valid() const
{
return !m_local_details.mod_id.isEmpty();

View File

@ -38,6 +38,10 @@
#include <QDateTime>
#include <QFileInfo>
#include <QList>
#include <QImage>
#include <QMutex>
#include <QPixmap>
#include <QPixmapCache>
#include <optional>
@ -64,6 +68,15 @@ public:
auto authors() const -> QStringList;
auto status() const -> ModStatus;
auto provider() const -> std::optional<QString>;
auto licenses() const -> const QList<ModLicense>&;
auto issueTracker() const -> QString;
/** Get the intneral path to the mod's icon file*/
QString iconPath() const { return m_local_details.icon_file; };
/** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */
[[nodiscard]] QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const;
/** Thread-safe. */
void setIcon(QImage new_image) const;
auto metadata() -> std::shared_ptr<Metadata::ModStruct>;
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
@ -85,4 +98,13 @@ public:
protected:
ModDetails m_local_details;
mutable QMutex m_data_lock;
struct {
QPixmapCache::Key key;
bool was_ever_used = false;
bool was_read_attempt = false;
} mutable m_pack_image_cache_key;
};

View File

@ -39,6 +39,7 @@
#include <QString>
#include <QStringList>
#include <QUrl>
#include "minecraft/mod/MetadataHandler.h"
@ -49,6 +50,84 @@ enum class ModStatus {
Unknown, // Default status
};
struct ModLicense {
QString name = {};
QString id = {};
QString url = {};
QString description = {};
ModLicense() {}
ModLicense(const QString license) {
// FIXME: come up with a better license parseing.
// handle SPDX identifiers? https://spdx.org/licenses/
auto parts = license.split(' ');
QStringList notNameParts = {};
for (auto part : parts) {
auto url = QUrl(part);
if (part.startsWith("(") && part.endsWith(")"))
url = QUrl(part.mid(1, part.size() - 2));
if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
this->url = url.toString();
notNameParts.append(part);
continue;
}
}
for (auto part : notNameParts) {
parts.removeOne(part);
}
auto licensePart = parts.join(' ');
this->name = licensePart;
this->description = licensePart;
if (parts.size() == 1) {
this->id = parts.first();
}
}
ModLicense(const QString name, const QString id, const QString url, const QString description) {
this->name = name;
this->id = id;
this->url = url;
this->description = description;
}
ModLicense(const ModLicense& other)
: name(other.name)
, id(other.id)
, url(other.url)
, description(other.description)
{}
ModLicense& operator=(const ModLicense& other)
{
this->name = other.name;
this->id = other.id;
this->url = other.url;
this->description = other.description;
return *this;
}
ModLicense& operator=(const ModLicense&& other)
{
this->name = other.name;
this->id = other.id;
this->url = other.url;
this->description = other.description;
return *this;
}
bool isEmpty() {
return this->name.isEmpty() && this->id.isEmpty() && this->url.isEmpty() && this->description.isEmpty();
}
};
struct ModDetails
{
/* Mod ID as defined in the ModLoader-specific metadata */
@ -72,6 +151,15 @@ struct ModDetails
/* List of the author's names */
QStringList authors = {};
/* Issue Tracker URL */
QString issue_tracker = {};
/* License */
QList<ModLicense> licenses = {};
/* Path of mod logo */
QString icon_file = {};
/* Installation status of the mod */
ModStatus status = ModStatus::Unknown;
@ -89,6 +177,9 @@ struct ModDetails
, homeurl(other.homeurl)
, description(other.description)
, authors(other.authors)
, issue_tracker(other.issue_tracker)
, licenses(other.licenses)
, icon_file(other.icon_file)
, status(other.status)
{}
@ -101,6 +192,9 @@ struct ModDetails
this->homeurl = other.homeurl;
this->description = other.description;
this->authors = other.authors;
this->issue_tracker = other.issue_tracker;
this->licenses = other.licenses;
this->icon_file = other.icon_file;
this->status = other.status;
return *this;
@ -115,6 +209,9 @@ struct ModDetails
this->homeurl = other.homeurl;
this->description = other.description;
this->authors = other.authors;
this->issue_tracker = other.issue_tracker;
this->licenses = other.licenses;
this->icon_file = other.icon_file;
this->status = other.status;
return *this;

View File

@ -37,6 +37,7 @@
#include "ModFolderModel.h"
#include <FileSystem.h>
#include <qheaderview.h>
#include <QDebug>
#include <QFileSystemWatcher>
#include <QIcon>
@ -52,12 +53,14 @@
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
#include "modplatform/ModIndex.h"
ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir)
: ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed)
{
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER };
m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME , SortType::VERSION, SortType::DATE, SortType::PROVIDER};
m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents};
}
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
@ -118,7 +121,9 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
case Qt::DecorationRole: {
if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
return APPLICATION->getThemedIcon("status-yellow");
if (column == ImageColumn) {
return at(row)->icon({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding);
}
return {};
}
case Qt::CheckStateRole:
@ -142,15 +147,12 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
switch (section)
{
case ActiveColumn:
return QString();
case NameColumn:
return tr("Name");
case VersionColumn:
return tr("Version");
case DateColumn:
return tr("Last changed");
case ProviderColumn:
return tr("Provider");
case ImageColumn:
return columnNames().at(section);
default:
return QVariant();
}

View File

@ -64,6 +64,7 @@ public:
enum Columns
{
ActiveColumn = 0,
ImageColumn,
NameColumn,
VersionColumn,
DateColumn,
@ -77,6 +78,8 @@ public:
};
ModFolderModel(const QString &dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true);
virtual QString id() const override { return "mods"; }
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;

View File

@ -8,12 +8,15 @@
#include <QStyle>
#include <QThreadPool>
#include <QUrl>
#include <QMenu>
#include "Application.h"
#include "FileSystem.h"
#include "QVariantUtils.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "settings/Setting.h"
#include "tasks/Task.h"
ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir)
@ -471,10 +474,10 @@ QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientatio
switch (role) {
case Qt::DisplayRole:
switch (section) {
case ACTIVE_COLUMN:
case NAME_COLUMN:
return tr("Name");
case DATE_COLUMN:
return tr("Last modified");
return columnNames().at(section);
default:
return {};
}
@ -500,6 +503,75 @@ QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientatio
return {};
}
void ResourceFolderModel::setupHeaderAction(QAction* act, int column)
{
Q_ASSERT(act);
act->setText(columnNames().at(column));
}
void ResourceFolderModel::saveHiddenColumn(int column, bool hidden)
{
auto const setting_name = QString("UI/%1_Page/HiddenColumns").arg(id());
auto setting = (m_instance->settings()->contains(setting_name)) ?
m_instance->settings()->getSetting(setting_name) : m_instance->settings()->registerSetting(setting_name);
auto hiddenColumns = setting->get().toStringList();
auto name = columnNames(false).at(column);
auto index = hiddenColumns.indexOf(name);
if (index >= 0 && !hidden) {
hiddenColumns.removeAt(index);
} else if ( index < 0 && hidden) {
hiddenColumns.append(name);
}
setting->set(hiddenColumns);
}
void ResourceFolderModel::loadHiddenColumns(QTreeView *tree)
{
auto const setting_name = QString("UI/%1_Page/HiddenColumns").arg(id());
auto setting = (m_instance->settings()->contains(setting_name)) ?
m_instance->settings()->getSetting(setting_name) : m_instance->settings()->registerSetting(setting_name);
auto hiddenColumns = setting->get().toStringList();
auto col_names = columnNames(false);
for (auto col_name : hiddenColumns) {
auto index = col_names.indexOf(col_name);
if (index >= 0)
tree->setColumnHidden(index, true);
}
}
QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree)
{
auto menu = new QMenu(tree);
menu->addSeparator()->setText(tr("Show / Hide Columns"));
for (int col = 0; col < columnCount(); ++col) {
auto act = new QAction(menu);
setupHeaderAction(act, col);
act->setCheckable(true);
act->setChecked(!tree->isColumnHidden(col));
connect(act, &QAction::toggled, tree, [this, col, tree](bool toggled){
tree->setColumnHidden(col, !toggled);
for(int c = 0; c < columnCount(); ++c) {
if (m_column_resize_modes.at(c) == QHeaderView::ResizeToContents)
tree->resizeColumnToContents(c);
}
saveHiddenColumn(col, !toggled);
});
menu->addAction(act);
}
return menu;
}
QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent)
{
return new ProxyModel(parent);

View File

@ -1,5 +1,8 @@
#pragma once
#include <QHeaderView>
#include <QAction>
#include <QTreeView>
#include <QAbstractListModel>
#include <QDir>
#include <QFileSystemWatcher>
@ -29,6 +32,8 @@ class ResourceFolderModel : public QAbstractListModel {
ResourceFolderModel(QDir, BaseInstance* instance, QObject* parent = nullptr, bool create_dir = true);
~ResourceFolderModel() override;
virtual QString id() const { return "resource"; }
/** Starts watching the paths for changes.
*
* Returns whether starting to watch all the paths was successful.
@ -92,6 +97,7 @@ class ResourceFolderModel : public QAbstractListModel {
/* Basic columns */
enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS };
QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; };
[[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast<int>(size()); }
[[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; }
@ -110,6 +116,11 @@ class ResourceFolderModel : public QAbstractListModel {
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
void setupHeaderAction(QAction* act, int column);
void saveHiddenColumn(int column, bool hidden);
void loadHiddenColumns(QTreeView* tree);
QMenu* createHeaderContextMenu(QTreeView* tree);
/** This creates a proxy model to filter / sort the model for a UI.
*
* The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead!
@ -117,6 +128,7 @@ class ResourceFolderModel : public QAbstractListModel {
QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr);
[[nodiscard]] SortType columnToSortKey(size_t column) const;
[[nodiscard]] QList<QHeaderView::ResizeMode> columnResizeModes() const { return m_column_resize_modes; }
class ProxyModel : public QSortFilterProxyModel {
public:
@ -187,6 +199,9 @@ class ResourceFolderModel : public QAbstractListModel {
// Represents the relationship between a column's index (represented by the list index), and it's sorting key.
// As such, the order in with they appear is very important!
QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE };
QStringList m_column_names = {"Enable", "Name", "Last Modified"};
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;

View File

@ -18,7 +18,8 @@ static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
{ 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
{ 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } },
{ 12, { Version("1.19.3"), Version("1.19.3") } },
{ 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } },
{ 14, { Version("1.20"), Version("1.20") } }
};
void ResourcePack::setPackFormat(int new_format_id)
@ -39,7 +40,7 @@ void ResourcePack::setDescription(QString new_description)
m_description = new_description;
}
void ResourcePack::setImage(QImage new_image)
void ResourcePack::setImage(QImage new_image) const
{
QMutexLocker locker(&m_data_lock);
@ -48,7 +49,10 @@ void ResourcePack::setImage(QImage new_image)
if (m_pack_image_cache_key.key.isValid())
PixmapCache::instance().remove(m_pack_image_cache_key.key);
m_pack_image_cache_key.key = PixmapCache::instance().insert(QPixmap::fromImage(new_image));
// scale the image to avoid flooding the pixmapcache
auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding));
m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap);
m_pack_image_cache_key.was_ever_used = true;
// This can happen if the pixmap is too big to fit in the cache :c
@ -58,21 +62,25 @@ void ResourcePack::setImage(QImage new_image)
}
}
QPixmap ResourcePack::image(QSize size)
QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const
{
QPixmap cached_image;
if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) {
if (size.isNull())
return cached_image;
return cached_image.scaled(size);
return cached_image.scaled(size, mode);
}
// No valid image we can get
if (!m_pack_image_cache_key.was_ever_used)
if (!m_pack_image_cache_key.was_ever_used) {
return {};
} else {
qDebug() << "Resource Pack" << name() << "Had it's image evicted from the cache. reloading...";
PixmapCache::markCacheMissByEviciton();
}
// Imaged got evicted from the cache. Re-process it and retry.
ResourcePackUtils::process(*this);
ResourcePackUtils::processPackPNG(*this);
return image(size);
}

View File

@ -31,7 +31,7 @@ class ResourcePack : public Resource {
[[nodiscard]] QString description() const { return m_description; }
/** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */
[[nodiscard]] QPixmap image(QSize size);
[[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const;
/** Thread-safe. */
void setPackFormat(int new_format_id);
@ -40,7 +40,7 @@ class ResourcePack : public Resource {
void setDescription(QString new_description);
/** Thread-safe. */
void setImage(QImage new_image);
void setImage(QImage new_image) const;
bool valid() const override;
@ -67,5 +67,5 @@ class ResourcePack : public Resource {
struct {
QPixmapCache::Key key;
bool was_ever_used = false;
} m_pack_image_cache_key;
} mutable m_pack_image_cache_key;
};

View File

@ -35,6 +35,8 @@
*/
#include "ResourcePackFolderModel.h"
#include <qnamespace.h>
#include <qsize.h>
#include <QIcon>
#include <QStyle>
@ -48,7 +50,11 @@
ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance)
: ResourceFolderModel(QDir(dir), instance)
{
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE };
m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE};
m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents };
}
QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
@ -84,9 +90,11 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
return {};
}
case Qt::DecorationRole: {
if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
return APPLICATION->getThemedIcon("status-yellow");
if (column == ImageColumn) {
return at(row)->image({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding);
}
return {};
}
case Qt::ToolTipRole: {
@ -94,7 +102,7 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
//: The string being explained by this is in the format: ID (Lower version - Upper version)
return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
}
if (column == NAME_COLUMN) {
if (column == NameColumn) {
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."
@ -126,13 +134,11 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient
case Qt::DisplayRole:
switch (section) {
case ActiveColumn:
return QString();
case NameColumn:
return tr("Name");
case PackFormatColumn:
return tr("Pack Format");
case DateColumn:
return tr("Last changed");
case ImageColumn:
return columnNames().at(section);
default:
return {};
}
@ -151,6 +157,11 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient
default:
return {};
}
case Qt::SizeHintRole:
if (section == ImageColumn) {
return QSize(64,0);
}
return {};
default:
return {};
}

View File

@ -11,6 +11,7 @@ public:
enum Columns
{
ActiveColumn = 0,
ImageColumn,
NameColumn,
PackFormatColumn,
DateColumn,
@ -19,6 +20,8 @@ public:
explicit ResourcePackFolderModel(const QString &dir, BaseInstance* instance);
virtual QString id() const override { return "resourcepacks"; }
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;

View File

@ -9,4 +9,6 @@ class ShaderPackFolderModel : public ResourceFolderModel {
explicit ShaderPackFolderModel(const QString& dir, BaseInstance* instance)
: ResourceFolderModel(QDir(dir), instance)
{}
virtual QString id() const override { return "shaderpacks"; }
};

View File

@ -23,6 +23,8 @@
#include <QMap>
#include <QRegularExpression>
#include "MTPixmapCache.h"
#include "minecraft/mod/tasks/LocalTexturePackParseTask.h"
void TexturePack::setDescription(QString new_description)
@ -32,34 +34,41 @@ void TexturePack::setDescription(QString new_description)
m_description = new_description;
}
void TexturePack::setImage(QImage new_image)
void TexturePack::setImage(QImage new_image) const
{
QMutexLocker locker(&m_data_lock);
Q_ASSERT(!new_image.isNull());
if (m_pack_image_cache_key.key.isValid())
QPixmapCache::remove(m_pack_image_cache_key.key);
PixmapCache::remove(m_pack_image_cache_key.key);
m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image));
// scale the image to avoid flooding the pixmapcache
auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding));
m_pack_image_cache_key.key = PixmapCache::insert(pixmap);
m_pack_image_cache_key.was_ever_used = true;
}
QPixmap TexturePack::image(QSize size)
QPixmap TexturePack::image(QSize size, Qt::AspectRatioMode mode) const
{
QPixmap cached_image;
if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {
if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {
if (size.isNull())
return cached_image;
return cached_image.scaled(size);
return cached_image.scaled(size, mode);
}
// No valid image we can get
if (!m_pack_image_cache_key.was_ever_used)
if (!m_pack_image_cache_key.was_ever_used) {
return {};
} else {
qDebug() << "Texture Pack" << name() << "Had it's image evicted from the cache. reloading...";
PixmapCache::markCacheMissByEviciton();
}
// Imaged got evicted from the cache. Re-process it and retry.
TexturePackUtils::process(*this);
TexturePackUtils::processPackPNG(*this);
return image(size);
}

View File

@ -40,13 +40,13 @@ class TexturePack : public Resource {
[[nodiscard]] QString description() const { return m_description; }
/** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */
[[nodiscard]] QPixmap image(QSize size);
[[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const;
/** Thread-safe. */
void setDescription(QString new_description);
/** Thread-safe. */
void setImage(QImage new_image);
void setImage(QImage new_image) const;
bool valid() const override;
@ -65,5 +65,5 @@ class TexturePack : public Resource {
struct {
QPixmapCache::Key key;
bool was_ever_used = false;
} m_pack_image_cache_key;
} mutable m_pack_image_cache_key;
};

View File

@ -33,6 +33,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <QCoreApplication>
#include "Application.h"
#include "TexturePackFolderModel.h"
@ -41,7 +44,13 @@
TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance)
: ResourceFolderModel(QDir(dir), instance)
{}
{
m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE };
m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents};
}
Task* TexturePackFolderModel::createUpdateTask()
{
@ -52,3 +61,96 @@ Task* TexturePackFolderModel::createParseTask(Resource& resource)
{
return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast<TexturePack&>(resource));
}
QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const
{
if (!validateIndex(index))
return {};
int row = index.row();
int column = index.column();
switch (role) {
case Qt::DisplayRole:
switch (column) {
case NameColumn:
return m_resources[row]->name();
case DateColumn:
return m_resources[row]->dateTimeChanged();
default:
return {};
}
case Qt::ToolTipRole:
if (column == NameColumn) {
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());;
}
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.");
}
}
return m_resources[row]->internal_id();
case Qt::DecorationRole: {
if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
return APPLICATION->getThemedIcon("status-yellow");
if (column == ImageColumn) {
return at(row)->image({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding);
}
return {};
}
case Qt::CheckStateRole:
if (column == ActiveColumn) {
return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
}
return {};
default:
return {};
}
}
QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
{
switch (role) {
case Qt::DisplayRole:
switch (section) {
case ActiveColumn:
case NameColumn:
case DateColumn:
case ImageColumn:
return columnNames().at(section);
default:
return {};
}
case Qt::ToolTipRole: {
switch (section) {
case ActiveColumn:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("Is the resource enabled?");
case NameColumn:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("The name of the resource.");
case DateColumn:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("The date and time this resource was last changed (or added).");
default:
return {};
}
}
default:
break;
}
return {};
}
int TexturePackFolderModel::columnCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : NUM_COLUMNS;
}

View File

@ -38,12 +38,35 @@
#include "ResourceFolderModel.h"
#include "TexturePack.h"
class TexturePackFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
enum Columns
{
ActiveColumn = 0,
ImageColumn,
NameColumn,
DateColumn,
NUM_COLUMNS
};
explicit TexturePackFolderModel(const QString &dir, std::shared_ptr<const BaseInstance> instance);
virtual QString id() const override { return "texturepacks"; }
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
[[nodiscard]] int columnCount(const QModelIndex &parent) const override;
explicit TexturePackFolderModel(const QString &dir, BaseInstance* instance);
[[nodiscard]] Task* createUpdateTask() override;
[[nodiscard]] Task* createParseTask(Resource&) override;
RESOURCE_HELPERS(TexturePack)
};

View File

@ -0,0 +1,252 @@
// 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 "GetModDependenciesTask.h"
#include <QDebug>
#include <algorithm>
#include <memory>
#include "Json.h"
#include "QObjectPtr.h"
#include "minecraft/mod/MetadataHandler.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "tasks/ConcurrentTask.h"
#include "tasks/SequentialTask.h"
#include "ui/pages/modplatform/ModModel.h"
#include "ui/pages/modplatform/flame/FlameResourceModels.h"
#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h"
static Version mcVersion(BaseInstance* inst)
{
return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion();
}
static ResourceAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
{
return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders().value();
}
GetModDependenciesTask::GetModDependenciesTask(QObject* parent,
BaseInstance* instance,
ModFolderModel* folder,
QList<std::shared_ptr<PackDependency>> selected)
: SequentialTask(parent, tr("Get dependencies"))
, m_selected(selected)
, m_flame_provider{ ModPlatform::ResourceProvider::FLAME, std::make_shared<ResourceDownload::FlameModModel>(*instance),
std::make_shared<FlameAPI>() }
, m_modrinth_provider{ ModPlatform::ResourceProvider::MODRINTH, std::make_shared<ResourceDownload::ModrinthModModel>(*instance),
std::make_shared<ModrinthAPI>() }
, m_version(mcVersion(instance))
, m_loaderType(mcLoaders(instance))
{
for (auto mod : folder->allMods())
if (auto meta = mod->metadata(); meta)
m_mods.append(meta);
prepare();
};
void GetModDependenciesTask::prepare()
{
for (auto sel : m_selected) {
for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) {
addTask(prepareDependencyTask(dep, sel->pack->provider, 20));
}
}
}
ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::Dependency& dep,
const ModPlatform::ResourceProvider providerName)
{
if (auto isQuilt = m_loaderType & ResourceAPI::Quilt; isQuilt || m_loaderType & ResourceAPI::Fabric) {
auto overide = ModPlatform::getOverrideDeps();
auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](auto o) {
return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt);
});
if (over != overide.cend()) {
return { isQuilt ? over->quilt : over->fabric, dep.type };
}
}
return dep;
}
QList<ModPlatform::Dependency> GetModDependenciesTask::getDependenciesForVersion(const ModPlatform::IndexedVersion& version,
const ModPlatform::ResourceProvider providerName)
{
QList<ModPlatform::Dependency> c_dependencies;
for (auto ver_dep : version.dependencies) {
if (ver_dep.type != ModPlatform::DependencyType::REQUIRED)
continue;
auto isOnlyVersion = providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty();
if (auto dep = std::find_if(c_dependencies.begin(), c_dependencies.end(),
[&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) {
return isOnlyVersion ? i.version == ver_dep.version : i.addonId == ver_dep.addonId;
});
dep != c_dependencies.end())
continue; // check the current dependency list
if (auto dep = std::find_if(m_selected.begin(), m_selected.end(),
[&ver_dep, providerName, isOnlyVersion](std::shared_ptr<PackDependency> i) {
return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.version
: i->pack->addonId == ver_dep.addonId);
});
dep != m_selected.end())
continue; // check the selected versions
if (auto dep = std::find_if(m_mods.begin(), m_mods.end(),
[&ver_dep, providerName, isOnlyVersion](std::shared_ptr<Metadata::ModStruct> i) {
return i->provider == providerName &&
(isOnlyVersion ? i->file_id == ver_dep.version : i->project_id == ver_dep.addonId);
});
dep != m_mods.end())
continue; // check the existing mods
if (auto dep = std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(),
[&ver_dep, providerName, isOnlyVersion](std::shared_ptr<PackDependency> i) {
return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.addonId
: i->pack->addonId == ver_dep.addonId);
});
dep != m_pack_dependencies.end()) // check loaded dependencies
continue;
c_dependencies.append(getOverride(ver_dep, providerName));
}
return c_dependencies;
};
Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr<PackDependency> pDep)
{
auto provider = pDep->pack->provider == m_flame_provider.name ? m_flame_provider : m_modrinth_provider;
auto responseInfo = std::make_shared<QByteArray>();
auto info = provider.api->getProject(pDep->pack->addonId.toString(), responseInfo);
QObject::connect(info.get(), &NetJob::succeeded, [responseInfo, provider, pDep] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qDebug() << *responseInfo;
return;
}
try {
auto obj = provider.name == ModPlatform::ResourceProvider::FLAME ? Json::requireObject(Json::requireObject(doc), "data")
: Json::requireObject(doc);
provider.mod->loadIndexedPack(*pDep->pack, obj);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading mod info: " << e.cause();
}
});
return info;
}
Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Dependency& dep,
const ModPlatform::ResourceProvider providerName,
int level)
{
auto pDep = std::make_shared<PackDependency>();
pDep->dependency = dep;
pDep->pack = std::make_shared<ModPlatform::IndexedPack>();
pDep->pack->addonId = dep.addonId;
pDep->pack->provider = providerName;
m_pack_dependencies.append(pDep);
auto provider = providerName == m_flame_provider.name ? m_flame_provider : m_modrinth_provider;
auto tasks = makeShared<SequentialTask>(
this, QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString()));
if (!dep.addonId.toString().isEmpty()) {
tasks->addTask(getProjectInfoTask(pDep));
}
ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType };
ResourceAPI::DependencySearchCallbacks callbacks;
callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, auto& pack) {
try {
QJsonArray arr;
if (dep.version.length() != 0 && doc.isObject()) {
arr.append(doc.object());
} else {
arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
}
pDep->version = provider.mod->loadDependencyVersions(dep, arr);
if (!pDep->version.addonId.isValid()) {
if (m_loaderType & ResourceAPI::Quilt) { // falback for quilt
auto overide = ModPlatform::getOverrideDeps();
auto over = std::find_if(overide.cbegin(), overide.cend(),
[dep, provider](auto o) { return o.provider == provider.name && dep.addonId == o.quilt; });
if (over != overide.cend()) {
removePack(dep.addonId);
addTask(prepareDependencyTask({ over->fabric, dep.type }, provider.name, level));
return;
}
}
qWarning() << "Error while reading mod version empty ";
qDebug() << doc;
return;
}
pDep->version.is_currently_selected = true;
pDep->pack->versions = { pDep->version };
pDep->pack->versionsLoaded = true;
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading mod version: " << e.cause();
return;
}
if (level == 0) {
qWarning() << "Dependency cycle exeeded";
return;
}
if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) {
pDep->pack->addonId = pDep->version.addonId;
auto dep = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider.name);
if (dep.addonId != pDep->version.addonId) {
removePack(pDep->version.addonId);
addTask(prepareDependencyTask(dep, provider.name, level));
} else
addTask(getProjectInfoTask(pDep));
}
for (auto dep : getDependenciesForVersion(pDep->version, provider.name)) {
addTask(prepareDependencyTask(dep, provider.name, level - 1));
}
};
auto version = provider.api->getDependencyVersion(std::move(args), std::move(callbacks));
tasks->addTask(version);
return tasks;
};
void GetModDependenciesTask::removePack(const QVariant addonId)
{
auto pred = [addonId](const std::shared_ptr<PackDependency>& v) { return v->pack->addonId == addonId; };
#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
m_pack_dependencies.removeIf(pred);
#else
for (auto it = m_pack_dependencies.begin(); it != m_pack_dependencies.end();)
if (pred(*it))
it = m_pack_dependencies.erase(it);
else
++it;
#endif
}

View File

@ -0,0 +1,84 @@
// 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 <QDir>
#include <QEventLoop>
#include <QList>
#include <QVariant>
#include <functional>
#include <memory>
#include "minecraft/mod/MetadataHandler.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "tasks/SequentialTask.h"
#include "tasks/Task.h"
#include "ui/pages/modplatform/ModModel.h"
class GetModDependenciesTask : public SequentialTask {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<GetModDependenciesTask>;
struct PackDependency {
ModPlatform::Dependency dependency;
ModPlatform::IndexedPack::Ptr pack;
ModPlatform::IndexedVersion version;
PackDependency() = default;
PackDependency(const ModPlatform::IndexedPack::Ptr p, const ModPlatform::IndexedVersion& v)
{
pack = p;
version = v;
}
};
struct Provider {
ModPlatform::ResourceProvider name;
std::shared_ptr<ResourceDownload::ModModel> mod;
std::shared_ptr<ResourceAPI> api;
};
explicit GetModDependenciesTask(QObject* parent,
BaseInstance* instance,
ModFolderModel* folder,
QList<std::shared_ptr<PackDependency>> selected);
auto getDependecies() const -> QList<std::shared_ptr<PackDependency>> { return m_pack_dependencies; }
protected slots:
Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, const ModPlatform::ResourceProvider, int);
QList<ModPlatform::Dependency> getDependenciesForVersion(const ModPlatform::IndexedVersion&,
const ModPlatform::ResourceProvider providerName);
void prepare();
Task::Ptr getProjectInfoTask(std::shared_ptr<PackDependency> pDep);
ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, const ModPlatform::ResourceProvider providerName);
void removePack(const QVariant addonId);
private:
QList<std::shared_ptr<PackDependency>> m_pack_dependencies;
QList<std::shared_ptr<Metadata::ModStruct>> m_mods;
QList<std::shared_ptr<PackDependency>> m_selected;
Provider m_flame_provider;
Provider m_modrinth_provider;
Version m_version;
ResourceAPI::ModLoaderTypes m_loaderType;
};

View File

@ -52,6 +52,10 @@ ModDetails ReadMCModInfo(QByteArray contents)
authors = firstObj.value("authors").toArray();
}
if (firstObj.contains("logoFile")) {
details.icon_file = firstObj.value("logoFile").toString();
}
for (auto author : authors) {
details.authors.append(author.toString());
}
@ -166,6 +170,31 @@ ModDetails ReadMCModTOML(QByteArray contents)
}
details.homeurl = homeurl;
QString issueTrackerURL = "";
if (auto issueTrackerURLDatum = tomlData["issueTrackerURL"].as_string()) {
issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get());
} else if (auto issueTrackerURLDatum = (*modsTable)["issueTrackerURL"].as_string()) {
issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get());
}
details.issue_tracker = issueTrackerURL;
QString license = "";
if (auto licenseDatum = tomlData["license"].as_string()) {
license = QString::fromStdString(licenseDatum->get());
} else if (auto licenseDatum =(*modsTable)["license"].as_string()) {
license = QString::fromStdString(licenseDatum->get());
}
if (!license.isEmpty())
details.licenses.append(ModLicense(license));
QString logoFile = "";
if (auto logoFileDatum = tomlData["logoFile"].as_string()) {
logoFile = QString::fromStdString(logoFileDatum->get());
} else if (auto logoFileDatum =(*modsTable)["logoFile"].as_string()) {
logoFile = QString::fromStdString(logoFileDatum->get());
}
details.icon_file = logoFile;
return details;
}
@ -201,6 +230,57 @@ ModDetails ReadFabricModInfo(QByteArray contents)
if (contact.contains("homepage")) {
details.homeurl = contact.value("homepage").toString();
}
if (contact.contains("issues")) {
details.issue_tracker = contact.value("issues").toString();
}
}
if (object.contains("license")) {
auto license = object.value("license");
if (license.isArray()) {
for (auto l : license.toArray()) {
if (l.isString()) {
details.licenses.append(ModLicense(l.toString()));
} else if (l.isObject()) {
auto obj = l.toObject();
details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(),
obj.value("url").toString(), obj.value("description").toString()));
}
}
} else if (license.isString()) {
details.licenses.append(ModLicense(license.toString()));
} else if (license.isObject()) {
auto obj = license.toObject();
details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(),
obj.value("description").toString()));
}
}
if (object.contains("icon")) {
auto icon = object.value("icon");
if (icon.isObject()) {
auto obj = icon.toObject();
// take the largest icon
int largest = 0;
for (auto key : obj.keys()) {
auto size = key.split('x').first().toInt();
if (size > largest) {
largest = size;
}
}
if (largest > 0) {
auto key = QString::number(largest) + "x" + QString::number(largest);
details.icon_file = obj.value(key).toString();
} else { // parsing the sizes failed
// take the first
for (auto i : obj) {
details.icon_file = i.toString();
break;
}
}
} else if (icon.isString()) {
details.icon_file = icon.toString();
}
}
}
return details;
@ -238,6 +318,58 @@ ModDetails ReadQuiltModInfo(QByteArray contents)
if (modContact.contains("homepage")) {
details.homeurl = Json::requireString(modContact.value("homepage"));
}
if (modContact.contains("issues")) {
details.issue_tracker = Json::requireString(modContact.value("issues"));
}
if (modMetadata.contains("license")) {
auto license = modMetadata.value("license");
if (license.isArray()) {
for (auto l : license.toArray()) {
if (l.isString()) {
details.licenses.append(ModLicense(l.toString()));
} else if (l.isObject()) {
auto obj = l.toObject();
details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(),
obj.value("url").toString(), obj.value("description").toString()));
}
}
} else if (license.isString()) {
details.licenses.append(ModLicense(license.toString()));
} else if (license.isObject()) {
auto obj = license.toObject();
details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(),
obj.value("description").toString()));
}
}
if (modMetadata.contains("icon")) {
auto icon = modMetadata.value("icon");
if (icon.isObject()) {
auto obj = icon.toObject();
// take the largest icon
int largest = 0;
for (auto key : obj.keys()) {
auto size = key.split('x').first().toInt();
if (size > largest) {
largest = size;
}
}
if (largest > 0) {
auto key = QString::number(largest) + "x" + QString::number(largest);
details.icon_file = obj.value(key).toString();
} else { // parsing the sizes failed
// take the first
for (auto i : obj) {
details.icon_file = i.toString();
break;
}
}
} else if (icon.isString()) {
details.icon_file = icon.toString();
}
}
}
return details;
}
@ -515,6 +647,85 @@ bool validate(QFileInfo file)
return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid();
}
bool processIconPNG(const Mod& mod, QByteArray&& raw_data)
{
auto img = QImage::fromData(raw_data);
if (!img.isNull()) {
mod.setIcon(img);
} else {
qWarning() << "Failed to parse mod logo:" << mod.iconPath() << "from" << mod.name();
return false;
}
return true;
}
bool loadIconFile(const Mod& mod) {
if (mod.iconPath().isEmpty()) {
qWarning() << "No Iconfile set, be sure to parse the mod first";
return false;
}
auto png_invalid = [&mod]() {
qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon";
return false;
};
switch (mod.type()) {
case ResourceType::FOLDER:
{
QFileInfo icon_info(FS::PathCombine(mod.fileinfo().filePath(), mod.iconPath()));
if (icon_info.exists() && icon_info.isFile()) {
QFile icon(icon_info.filePath());
if (!icon.open(QIODevice::ReadOnly))
return false;
auto data = icon.readAll();
bool icon_result = ModUtils::processIconPNG(mod, std::move(data));
icon.close();
if (!icon_result) {
return png_invalid(); // icon invalid
}
}
}
case ResourceType::ZIPFILE:
{
QuaZip zip(mod.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false;
QuaZipFile file(&zip);
if (zip.setCurrentFile(mod.iconPath())) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid();
}
auto data = file.readAll();
bool icon_result = ModUtils::processIconPNG(mod, std::move(data));
file.close();
if (!icon_result) {
return png_invalid(); // icon png invalid
}
} else {
return png_invalid(); // could not set icon as current file.
}
}
case ResourceType::LITEMOD:
{
return false; // can lightmods even have icons?
}
default:
qWarning() << "Invalid type for mod, can not load icon.";
return false;
}
}
} // namespace ModUtils
LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile)

View File

@ -25,6 +25,9 @@ bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full);
/** Checks whether a file is valid as a mod or not. */
bool validate(QFileInfo file);
bool processIconPNG(const Mod& mod, QByteArray&& raw_data);
bool loadIconFile(const Mod& mod);
} // namespace ModUtils
class LocalModParseTask : public Task {

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
@ -19,7 +19,6 @@
#include "LocalModUpdateTask.h"
#include "Application.h"
#include "FileSystem.h"
#include "minecraft/mod/MetadataHandler.h"

View File

@ -165,15 +165,16 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level)
bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
file.close();
zip.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
zip.close();
return png_invalid(); // could not set pack.mcmeta as current file.
}
zip.close();
return true;
}
@ -193,7 +194,7 @@ bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
return true;
}
bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data)
bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data)
{
auto img = QImage::fromData(raw_data);
if (!img.isNull()) {
@ -205,6 +206,68 @@ bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data)
return true;
}
bool processPackPNG(const ResourcePack& pack)
{
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
return false;
};
switch (pack.type()) {
case ResourceType::FOLDER:
{
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
if (image_file_info.exists() && image_file_info.isFile()) {
QFile pack_png_file(image_file_info.filePath());
if (!pack_png_file.open(QIODevice::ReadOnly))
return png_invalid(); // can't open pack.png file
auto data = pack_png_file.readAll();
bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
pack_png_file.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
return png_invalid(); // pack.png does not exists or is not a valid file.
}
}
case ResourceType::ZIPFILE:
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
QuaZip zip(pack.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file
QuaZipFile file(&zip);
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid();
}
auto data = file.readAll();
bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
file.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
return png_invalid(); // could not set pack.mcmeta as current file.
}
}
default:
qWarning() << "Invalid type for resource pack parse task!";
return false;
}
}
bool validate(QFileInfo file)
{
ResourcePack rp{ file };

View File

@ -35,7 +35,10 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Ful
bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data);
bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data);
bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data);
/// processes ONLY the pack.png (rest of the pack may be invalid)
bool processPackPNG(const ResourcePack& pack);
/** Checks whether a file is valid as a resource pack or not. */
bool validate(QFileInfo file);

View File

@ -131,6 +131,7 @@ bool processZIP(TexturePack& pack, ProcessingLevel level)
bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data));
file.close();
zip.close();
if (!packPNG_result) {
return false;
}
@ -147,7 +148,7 @@ bool processPackTXT(TexturePack& pack, QByteArray&& raw_data)
return true;
}
bool processPackPNG(TexturePack& pack, QByteArray&& raw_data)
bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data)
{
auto img = QImage::fromData(raw_data);
if (!img.isNull()) {
@ -159,6 +160,70 @@ bool processPackPNG(TexturePack& pack, QByteArray&& raw_data)
return true;
}
bool processPackPNG(const TexturePack& pack)
{
auto png_invalid = [&pack]() {
qWarning() << "Texture pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
return false;
};
switch (pack.type()) {
case ResourceType::FOLDER:
{
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
if (image_file_info.exists() && image_file_info.isFile()) {
QFile pack_png_file(image_file_info.filePath());
if (!pack_png_file.open(QIODevice::ReadOnly))
return png_invalid(); // can't open pack.png file
auto data = pack_png_file.readAll();
bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data));
pack_png_file.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
return png_invalid(); // pack.png does not exists or is not a valid file.
}
}
case ResourceType::ZIPFILE:
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
QuaZip zip(pack.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file
QuaZipFile file(&zip);
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid();
}
auto data = file.readAll();
bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data));
file.close();
if (!pack_png_result) {
zip.close();
return png_invalid(); // pack.png invalid
}
} else {
zip.close();
return png_invalid(); // could not set pack.mcmeta as current file.
}
}
default:
qWarning() << "Invalid type for resource pack parse task!";
return false;
}
}
bool validate(QFileInfo file)
{
TexturePack rp{ file };

View File

@ -36,7 +36,10 @@ bool processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full
bool processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processPackTXT(TexturePack& pack, QByteArray&& raw_data);
bool processPackPNG(TexturePack& pack, QByteArray&& raw_data);
bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data);
/// processes ONLY the pack.png (rest of the pack may be invalid)
bool processPackPNG(const TexturePack& pack);
/** Checks whether a file is valid as a texture pack or not. */
bool validate(QFileInfo file);

View File

@ -10,6 +10,7 @@
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/helpers/HashUtils.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
@ -24,8 +25,8 @@ EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Resource
auto hash_task = createNewHash(mod);
if (!hash_task)
return;
connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); });
connect(hash_task.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); });
hash_task->start();
}
@ -37,8 +38,8 @@ EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform:
auto hash_task = createNewHash(mod);
if (!hash_task)
continue;
connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); });
connect(hash_task.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); });
m_hashing_task->addTask(hash_task);
}
}
@ -212,7 +213,7 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask()
{
auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
auto* response = new QByteArray();
auto response = std::make_shared<QByteArray>();
auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);
// Prevents unfortunate timings when aborting the task
@ -264,7 +265,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask()
for (auto const& data : m_temp_versions)
addonIds.insert(data.addonId.toString(), data.hash);
auto response = new QByteArray();
auto response = std::make_shared<QByteArray>();
Task::Ptr proj_task;
if (addonIds.isEmpty()) {
@ -345,7 +346,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask()
// Flame
Task::Ptr EnsureMetadataTask::flameVersionsTask()
{
auto* response = new QByteArray();
auto response = std::make_shared<QByteArray>();
QList<uint> fingerprints;
for (auto& murmur : m_mods.keys()) {
@ -413,7 +414,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask()
QHash<QString, QString> addonIds;
for (auto const& hash : m_mods.keys()) {
if (m_temp_versions.contains(hash)) {
auto const& data = m_temp_versions.find(hash).value();
auto data = m_temp_versions.find(hash).value();
auto id_str = data.addonId.toString();
if (!id_str.isEmpty())
@ -421,7 +422,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask()
}
}
auto response = new QByteArray();
auto response = std::make_shared<QByteArray>();
Task::Ptr proj_task;
if (addonIds.isEmpty()) {

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* 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,6 +34,8 @@ enum class ResourceProvider { MODRINTH, FLAME };
enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK };
enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN };
class ProviderCapabilities {
public:
auto name(ResourceProvider) -> const char*;
@ -52,6 +55,12 @@ struct DonationData {
QString url;
};
struct Dependency {
QVariant addonId;
DependencyType type;
QString version;
};
struct IndexedVersion {
QVariant addonId;
QVariant fileId;
@ -66,10 +75,10 @@ struct IndexedVersion {
QString hash;
bool is_preferred = true;
QString changelog;
QList<Dependency> dependencies;
// For internal use, not provided by APIs
bool is_currently_selected = false;
QString custom_target_folder;
};
struct ExtraPackData {
@ -116,12 +125,28 @@ struct IndexedPack {
if (!versionsLoaded)
return false;
return std::any_of(versions.constBegin(), versions.constEnd(),
[](auto const& v) { return v.is_currently_selected; });
return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; });
}
};
struct OverrideDep {
QString quilt;
QString fabric;
QString slug;
ModPlatform::ResourceProvider provider;
};
inline auto getOverrideDeps() -> QList<OverrideDep>
{
return { { "634179", "306612", "API", ModPlatform::ResourceProvider::FLAME },
{ "720410", "308769", "KotlinLibraries", ModPlatform::ResourceProvider::FLAME },
{ "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH },
{ "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } };
};
} // namespace ModPlatform
Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
Q_DECLARE_METATYPE(ModPlatform::IndexedPack::Ptr)
Q_DECLARE_METATYPE(ModPlatform::ResourceProvider)

View File

@ -111,6 +111,16 @@ class ResourceAPI {
std::function<void(QJsonDocument&, ModPlatform::IndexedPack)> on_succeed;
};
struct DependencySearchArgs {
ModPlatform::Dependency dependency;
Version mcVersion;
ModLoaderTypes loader;
};
struct DependencySearchCallbacks {
std::function<void(QJsonDocument&, const ModPlatform::Dependency&)> on_succeed;
};
public:
/** Gets a list of available sorting methods for this API. */
[[nodiscard]] virtual auto getSortingMethods() const -> QList<SortingMethod> = 0;
@ -121,14 +131,12 @@ class ResourceAPI {
qWarning() << "TODO: ResourceAPI::searchProjects";
return nullptr;
}
/** getProject(QString addonId, QByteArray* responce)*/
[[nodiscard]] virtual Task::Ptr getProject(QString, QByteArray*) const
[[nodiscard]] virtual Task::Ptr getProject(QString addonId, std::shared_ptr<QByteArray> response) const
{
qWarning() << "TODO: ResourceAPI::getProject";
return nullptr;
}
/** getProjects(QStringList addonIds, QByteArray* responce) */
[[nodiscard]] virtual Task::Ptr getProjects(QStringList, QByteArray*) const
[[nodiscard]] virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const
{
qWarning() << "TODO: ResourceAPI::getProjects";
return nullptr;
@ -145,6 +153,12 @@ class ResourceAPI {
return nullptr;
}
[[nodiscard]] virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const
{
qWarning() << "TODO";
return nullptr;
}
static auto getModLoaderString(ModLoaderType type) -> const QString
{
switch (type) {

View File

@ -82,9 +82,9 @@ void PackInstallTask::executeTask()
{
qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId();
NetJob::Ptr netJob{ new NetJob("ATLauncher::VersionFetch", APPLICATION->network()) };
auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json")
.arg(m_pack_safe_name).arg(m_version_name);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
auto searchUrl =
QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json").arg(m_pack_safe_name).arg(m_version_name);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
QObject::connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
@ -100,10 +100,11 @@ void PackInstallTask::onDownloadSucceeded()
jobPtr.reset();
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << response;
qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response.get();
return;
}
auto obj = doc.object();
@ -352,7 +353,7 @@ QString PackInstallTask::getVersionForLoader(QString uid)
if(m_version.loader.recommended || m_version.loader.latest) {
for (int i = 0; i < vlist->versions().size(); i++) {
auto version = vlist->versions().at(i);
auto reqs = version->requires();
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

View File

@ -40,12 +40,13 @@
#include "ATLPackManifest.h"
#include "InstanceTask.h"
#include "net/NetJob.h"
#include "settings/INISettingsObject.h"
#include "meta/Version.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "meta/Version.h"
#include "net/NetJob.h"
#include "settings/INISettingsObject.h"
#include <memory>
#include <optional>
namespace ATLauncher {
@ -57,7 +58,6 @@ enum class InstallMode {
};
class UserInteractionSupport {
public:
/**
* Requests a user interaction to select which optional mods should be installed.
@ -74,15 +74,19 @@ public:
* Requests a user interaction to display a message.
*/
virtual void displayMessage(QString message) = 0;
virtual ~UserInteractionSupport() = default;
};
class PackInstallTask : public InstanceTask
{
class PackInstallTask : public InstanceTask {
Q_OBJECT
public:
explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version, InstallMode installMode = InstallMode::Install);
virtual ~PackInstallTask(){}
explicit PackInstallTask(UserInteractionSupport* support,
QString packName,
QString version,
InstallMode installMode = InstallMode::Install);
virtual ~PackInstallTask() { delete m_support; }
bool canAbort() const override { return true; }
bool abort() override;
@ -110,11 +114,9 @@ private:
void installConfigs();
void extractConfigs();
void downloadMods();
bool extractMods(
const QMap<QString, VersionMod> &toExtract,
bool extractMods(const QMap<QString, VersionMod>& toExtract,
const QMap<QString, VersionMod>& toDecomp,
const QMap<QString, QString> &toCopy
);
const QMap<QString, QString>& toCopy);
void install();
private:
@ -123,7 +125,7 @@ private:
bool abortable = false;
NetJob::Ptr jobPtr;
QByteArray response;
std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
InstallMode m_install_mode;
QString m_pack_name;
@ -145,7 +147,6 @@ private:
QFuture<bool> m_modExtractFuture;
QFutureWatcher<bool> m_modExtractFutureWatcher;
};
}
} // namespace ATLauncher

View File

@ -28,12 +28,13 @@ void Flame::FileResolvingTask::executeTask()
// build json data to send
QJsonObject object;
object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
object["fileIds"] = QJsonArray::fromVariantList(
std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
l.push_back(s.fileId);
return l;
}));
QByteArray data = Json::toText(object);
auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result.get(), data);
auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data);
m_dljob->addNetAction(dl);
auto step_progress = std::make_shared<TaskStepProgress>();
@ -94,10 +95,8 @@ void Flame::FileResolvingTask::netJobFinished()
if (!hash.isEmpty()) {
auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash);
auto output = std::make_shared<QByteArray>();
auto dl = Net::Download::makeByteArray(QUrl(url), output.get());
QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() {
out.resolved = true;
});
auto dl = Net::Download::makeByteArray(QUrl(url), output);
QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() { out.resolved = true; });
m_checkJob->addNetAction(dl);
blockedProjects.insert(&out, output);
@ -169,7 +168,7 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
auto projectId = mod->projectId;
auto output = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId);
auto dl = Net::Download::makeByteArray(url, output.get());
auto dl = Net::Download::makeByteArray(url, output);
qDebug() << "Fetching url slug for file:" << mod->fileName;
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

View File

@ -11,7 +11,7 @@
#include "net/NetJob.h"
#include "net/Upload.h"
Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* response)
Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Flame::MatchFingerprints"), APPLICATION->network());
@ -28,8 +28,6 @@ Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArra
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw));
QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
return netJob;
}
@ -43,7 +41,7 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
netJob->addNetAction(Net::Download::makeByteArray(
QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog")
.arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))),
response.get()));
response));
QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &changelog] {
QJsonParseError parse_error{};
@ -75,8 +73,8 @@ auto FlameAPI::getModDescription(int modId) -> QString
auto netJob = makeShared<NetJob>(QString("Flame::ModDescription"), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
netJob->addNetAction(Net::Download::makeByteArray(
QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response.get()));
netJob->addNetAction(
Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response));
QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &description] {
QJsonParseError parse_error{};
@ -115,7 +113,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
auto response = std::make_shared<QByteArray>();
ModPlatform::IndexedVersion ver;
netJob->addNetAction(Net::Download::makeByteArray(versions_url, response.get()));
netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
QObject::connect(netJob.get(), &NetJob::succeeded, [response, args, &ver] {
QJsonParseError parse_error{};
@ -160,7 +158,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
return ver;
}
Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const
Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const
{
auto netJob = makeShared<NetJob>(QString("Flame::GetProjects"), APPLICATION->network());
@ -177,13 +175,12 @@ Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) cons
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw));
QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob;
}
Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const
Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const
{
auto netJob = makeShared<NetJob>(QString("Flame::GetFiles"), APPLICATION->network());
@ -200,7 +197,6 @@ Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) c
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw));
QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob;

View File

@ -4,7 +4,10 @@
#pragma once
#include <algorithm>
#include <memory>
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "modplatform/helpers/NetworkResourceAPI.h"
class FlameAPI : public NetworkResourceAPI {
@ -14,9 +17,9 @@ class FlameAPI : public NetworkResourceAPI {
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
Task::Ptr matchFingerprints(const QList<uint>& fingerprints, QByteArray* response);
Task::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const;
Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
Task::Ptr matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response);
Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const;
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
@ -48,7 +51,8 @@ class FlameAPI : public NetworkResourceAPI {
private:
[[nodiscard]] std::optional<QString> getSearchURL(SearchArgs const& args) const override
{
auto gameVersionStr = args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString();
auto gameVersionStr =
args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString();
QStringList get_arguments;
get_arguments.append(QString("classId=%1").arg(getClassId(args.type)));
@ -73,14 +77,48 @@ class FlameAPI : public NetworkResourceAPI {
[[nodiscard]] std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
{
QString url{QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(args.pack.addonId.toString())};
auto addonId = args.pack.addonId.toString();
QString url{ QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(addonId) };
QStringList get_parameters;
if (args.mcVersions.has_value())
get_parameters.append(QString("gameVersion=%1").arg(args.mcVersions.value().front().toString()));
if (args.loaders.has_value())
get_parameters.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value())));
if (args.loaders.has_value()) {
int mappedModLoader = getMappedModLoader(args.loaders.value());
if (args.loaders.value() & Quilt) {
auto overide = ModPlatform::getOverrideDeps();
auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) {
return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt;
});
if (over != overide.cend()) {
mappedModLoader = 5;
}
}
get_parameters.append(QString("modLoaderType=%1").arg(mappedModLoader));
}
return url + get_parameters.join('&');
};
[[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
{
auto mappedModLoader = getMappedModLoader(args.loader);
auto addonId = args.dependency.addonId.toString();
if (args.loader & Quilt) {
auto overide = ModPlatform::getOverrideDeps();
auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) {
return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt;
});
if (over != overide.cend()) {
mappedModLoader = 5;
}
}
return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&gameVersion=%2&modLoaderType=%3")
.arg(addonId)
.arg(args.mcVersion.toString())
.arg(mappedModLoader);
};
};

View File

@ -3,6 +3,7 @@
#include "FlameModIndex.h"
#include <MurmurHash2.h>
#include <memory>
#include "FileSystem.h"
#include "Json.h"
@ -30,7 +31,7 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info)
auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network());
auto response = new QByteArray();
auto response = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString());
auto dl = Net::Download::makeByteArray(url, response);
get_project_job->addNetAction(dl);
@ -74,7 +75,7 @@ ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId)
auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network());
auto response = new QByteArray();
auto response = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId));
auto dl = Net::Download::makeByteArray(url, response);
get_file_info_job->addNetAction(dl);
@ -129,8 +130,7 @@ void FlameCheckUpdate::executeTask()
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name()));
setProgress(i++, m_mods.size());
ModPlatform::IndexedPack pack{ mod->metadata()->project_id.toString() };
auto latest_ver = api.getLatestVersion({ pack, m_game_versions, m_loaders });
auto latest_ver = api.getLatestVersion({ { mod->metadata()->project_id.toString() }, m_game_versions, m_loaders });
// Check if we were aborted while getting the latest version
if (m_was_aborted) {
@ -156,15 +156,15 @@ void FlameCheckUpdate::executeTask()
if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) {
// Fake pack with the necessary info to pass to the download task :)
ModPlatform::IndexedPack pack;
pack.name = mod->name();
pack.slug = mod->metadata()->slug;
pack.addonId = mod->metadata()->project_id;
pack.websiteUrl = mod->homeurl();
auto pack = std::make_shared<ModPlatform::IndexedPack>();
pack->name = mod->name();
pack->slug = mod->metadata()->slug;
pack->addonId = mod->metadata()->project_id;
pack->websiteUrl = mod->homeurl();
for (auto& author : mod->authors())
pack.authors.append({ author });
pack.description = mod->description();
pack.provider = ModPlatform::ResourceProvider::FLAME;
pack->authors.append({ author });
pack->description = mod->description();
pack->provider = ModPlatform::ResourceProvider::FLAME;
auto old_version = mod->version();
if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
@ -173,7 +173,7 @@ void FlameCheckUpdate::executeTask()
}
auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver, m_mods_folder);
m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version,
m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver.version,
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
ModPlatform::ResourceProvider::FLAME, download_task);
}

View File

@ -153,6 +153,9 @@ bool FlameCreationTask::updateInstance()
old_files.remove(file.key());
files_iterator = files.erase(files_iterator);
if (files_iterator != files.begin())
files_iterator--;
}
}
@ -179,7 +182,7 @@ bool FlameCreationTask::updateInstance()
fileIds.append(QString::number(file.fileId));
}
auto* raw_response = new QByteArray;
auto raw_response = std::make_shared<QByteArray>();
auto job = api.getFiles(fileIds, raw_response);
QEventLoop loop;

View File

@ -136,8 +136,61 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
}
}
auto dependencies = Json::ensureArray(obj, "dependencies");
for (auto d : dependencies) {
auto dep = Json::ensureObject(d);
ModPlatform::Dependency dependency;
dependency.addonId = Json::requireInteger(dep, "modId");
switch (Json::requireInteger(dep, "relationType")) {
case 1: // EmbeddedLibrary
dependency.type = ModPlatform::DependencyType::EMBEDDED;
break;
case 2: // OptionalDependency
dependency.type = ModPlatform::DependencyType::OPTIONAL;
break;
case 3: // RequiredDependency
dependency.type = ModPlatform::DependencyType::REQUIRED;
break;
case 4: // Tool
dependency.type = ModPlatform::DependencyType::TOOL;
break;
case 5: // Incompatible
dependency.type = ModPlatform::DependencyType::INCOMPATIBLE;
break;
case 6: // Include
dependency.type = ModPlatform::DependencyType::INCLUDE;
break;
default:
dependency.type = ModPlatform::DependencyType::UNKNOWN;
break;
}
file.dependencies.append(dependency);
}
if (load_changelog)
file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt());
return file;
}
ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr)
{
QVector<ModPlatform::IndexedVersion> versions;
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj);
if (!file.addonId.isValid())
file.addonId = m.addonId;
if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
versions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
// dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(versions.begin(), versions.end(), orderSortPredicate);
return versions.front();
};

View File

@ -6,8 +6,8 @@
#include "modplatform/ModIndex.h"
#include "BaseInstance.h"
#include <QNetworkAccessManager>
#include "BaseInstance.h"
namespace FlameMod {
@ -19,5 +19,5 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
const shared_qobject_ptr<QNetworkAccessManager>& network,
const BaseInstance* inst);
auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion;
auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion;
} // namespace FlameMod

View File

@ -4,6 +4,7 @@
#include <QMetaType>
#include <QString>
#include <QVector>
#include "modplatform/ModIndex.h"
namespace Flame {
@ -27,8 +28,7 @@ struct ModpackExtra {
QString sourceUrl;
};
struct IndexedPack
{
struct IndexedPack {
int addonId;
QString name;
QString description;
@ -46,6 +46,6 @@ struct IndexedPack
void loadIndexedPack(IndexedPack& m, QJsonObject& obj);
void loadIndexedInfo(IndexedPack&, QJsonObject&);
void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr);
}
} // namespace Flame
Q_DECLARE_METATYPE(Flame::IndexedPack)

View File

@ -71,6 +71,7 @@ void ModrinthHasher::executeTask()
emitFailed("Empty hash!");
} else {
emitSucceeded();
emit resultsReady(m_hash);
}
}
@ -88,12 +89,12 @@ void FlameHasher::executeTask()
emitFailed("Empty hash!");
} else {
emitSucceeded();
emit resultsReady(m_hash);
}
}
BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider)
: Hasher(file_path), provider(provider) {
BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) : Hasher(file_path), provider(provider)
{
setObjectName(QString("BlockedModHasher: %1").arg(file_path));
hash_type = ProviderCaps.hashType(provider).first();
}
@ -120,14 +121,17 @@ void BlockedModHasher::executeTask()
emitFailed("Empty hash!");
} else {
emitSucceeded();
emit resultsReady(m_hash);
}
}
QStringList BlockedModHasher::getHashTypes() {
QStringList BlockedModHasher::getHashTypes()
{
return ProviderCaps.hashType(provider);
}
bool BlockedModHasher::useHashType(QString type) {
bool BlockedModHasher::useHashType(QString type)
{
auto types = ProviderCaps.hashType(provider);
if (types.contains(type)) {
hash_type = type;

View File

@ -8,6 +8,7 @@
namespace Hashing {
class Hasher : public Task {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Hasher>;
@ -21,6 +22,9 @@ class Hasher : public Task {
QString getResult() const { return m_hash; };
QString getPath() const { return m_path; };
signals:
void resultsReady(QString hash);
protected:
QString m_hash;
QString m_path;
@ -48,6 +52,7 @@ class BlockedModHasher : public Hasher {
QStringList getHashTypes();
bool useHashType(QString type);
private:
ModPlatform::ResourceProvider provider;
QString hash_type;

View File

@ -3,6 +3,7 @@
// SPDX-License-Identifier: GPL-3.0-only
#include "NetworkResourceAPI.h"
#include <memory>
#include "Application.h"
#include "net/NetJob.h"
@ -19,7 +20,7 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
auto search_url = search_url_optional.value();
auto response = new QByteArray();
auto response = std::make_shared<QByteArray>();
auto netJob = makeShared<NetJob>(QString("%1::Search").arg(debugName()), APPLICATION->network());
netJob->addNetAction(Net::Download::makeByteArray(QUrl(search_url), response));
@ -47,20 +48,14 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
callbacks.on_fail(reason, network_error_code);
});
QObject::connect(netJob.get(), &NetJob::aborted, [callbacks]{
callbacks.on_abort();
});
QObject::connect(netJob.get(), &NetJob::finished, [response] {
delete response;
});
QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });
return netJob;
}
Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const
{
auto response = new QByteArray();
auto response = std::make_shared<QByteArray>();
auto job = getProject(args.pack.addonId.toString(), response);
QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] {
@ -88,7 +83,7 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
auto versions_url = versions_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::Versions").arg(args.pack.name), APPLICATION->network());
auto response = new QByteArray();
auto response = std::make_shared<QByteArray>();
netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
@ -105,14 +100,10 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
callbacks.on_succeed(doc, args.pack);
});
QObject::connect(netJob.get(), &NetJob::finished, [response] {
delete response;
});
return netJob;
}
Task::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) const
Task::Ptr NetworkResourceAPI::getProject(QString addonId, std::shared_ptr<QByteArray> response) const
{
auto project_url_optional = getInfoURL(addonId);
if (!project_url_optional.has_value())
@ -124,9 +115,34 @@ Task::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response)
netJob->addNetAction(Net::Download::makeByteArray(QUrl(project_url), response));
QObject::connect(netJob.get(), &NetJob::finished, [response] {
delete response;
return netJob;
}
Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, DependencySearchCallbacks&& callbacks) const
{
auto versions_url_optional = getDependencyURL(args);
if (!versions_url_optional.has_value())
return nullptr;
auto versions_url = versions_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
QObject::connect(netJob.get(), &NetJob::succeeded, [=] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
callbacks.on_succeed(doc, args.dependency);
});
return netJob;
}
};

View File

@ -4,19 +4,22 @@
#pragma once
#include <memory>
#include "modplatform/ResourceAPI.h"
class NetworkResourceAPI : public ResourceAPI {
public:
Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override;
Task::Ptr getProject(QString addonId, QByteArray* response) const override;
Task::Ptr getProject(QString addonId, std::shared_ptr<QByteArray> response) const override;
Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override;
Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override;
Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const override;
protected:
[[nodiscard]] virtual auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> = 0;
[[nodiscard]] virtual auto getInfoURL(QString const& id) const -> std::optional<QString> = 0;
[[nodiscard]] virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> = 0;
[[nodiscard]] virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional<QString> = 0;
};

View File

@ -51,11 +51,11 @@ void PackFetchTask::fetch()
QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml");
qDebug() << "Downloading public version info from" << publicPacksUrl.toString();
jobPtr->addNetAction(Net::Download::makeByteArray(publicPacksUrl, &publicModpacksXmlFileData));
jobPtr->addNetAction(Net::Download::makeByteArray(publicPacksUrl, publicModpacksXmlFileData));
QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml");
qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString();
jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, &thirdPartyModpacksXmlFileData));
jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, thirdPartyModpacksXmlFileData));
QObject::connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished);
QObject::connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed);
@ -68,18 +68,15 @@ void PackFetchTask::fetchPrivate(const QStringList & toFetch)
{
QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml";
for (auto &packCode: toFetch)
{
QByteArray *data = new QByteArray();
for (auto& packCode : toFetch) {
auto data = std::make_shared<QByteArray>();
NetJob* job = new NetJob("Fetching private pack", m_network);
job->addNetAction(Net::Download::makeByteArray(privatePackBaseUrl.arg(packCode), data));
QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode]
{
QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] {
ModpackList packs;
parseAndAddPacks(*data, PackType::Private, packs);
foreach(Modpack currentPack, packs)
{
foreach (Modpack currentPack, packs) {
currentPack.packCode = packCode;
emit privateFileDownloadFinished(currentPack);
}
@ -87,16 +84,13 @@ void PackFetchTask::fetchPrivate(const QStringList & toFetch)
job->deleteLater();
data->clear();
delete data;
});
QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason)
{
QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) {
emit privateFileDownloadFailed(reason, packCode);
job->deleteLater();
data->clear();
delete data;
});
QObject::connect(job, &NetJob::aborted, this, [this, job, data] {
@ -104,7 +98,6 @@ void PackFetchTask::fetchPrivate(const QStringList & toFetch)
job->deleteLater();
data->clear();
delete data;
});
job->start();
@ -117,22 +110,17 @@ void PackFetchTask::fileDownloadFinished()
QStringList failedLists;
if(!parseAndAddPacks(publicModpacksXmlFileData, PackType::Public, publicPacks))
{
if (!parseAndAddPacks(*publicModpacksXmlFileData, PackType::Public, publicPacks)) {
failedLists.append(tr("Public Packs"));
}
if(!parseAndAddPacks(thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks))
{
if (!parseAndAddPacks(*thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks)) {
failedLists.append(tr("Third Party Packs"));
}
if(failedLists.size() > 0)
{
if (failedLists.size() > 0) {
emit failed(tr("Failed to download some pack lists: %1").arg(failedLists.join("\n- ")));
}
else
{
} else {
emit finished(publicPacks, thirdPartyPacks);
}
}
@ -145,8 +133,7 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac
int errorLine = -1;
int errorCol = -1;
if(!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol))
{
if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) {
auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:%3!").arg(errorMsg).arg(errorLine).arg(errorCol);
qWarning() << fullErrMsg;
data.clear();
@ -154,8 +141,7 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac
}
QDomNodeList nodes = doc.elementsByTagName("modpack");
for(int i = 0; i < nodes.length(); i++)
{
for (int i = 0; i < nodes.length(); i++) {
QDomElement element = nodes.at(i).toElement();
Modpack modpack;
@ -170,25 +156,19 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac
modpack.bugged = false;
// remove empty if the xml is bugged
for(QString curr : modpack.oldVersions)
{
if(curr.isNull() || curr.isEmpty())
{
for (QString curr : modpack.oldVersions) {
if (curr.isNull() || curr.isEmpty()) {
modpack.oldVersions.removeAll(curr);
modpack.bugged = true;
qWarning() << "Removed some empty versions from" << modpack.name;
}
}
if(modpack.oldVersions.size() < 1)
{
if(!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty())
{
if (modpack.oldVersions.size() < 1) {
if (!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty()) {
modpack.oldVersions.append(modpack.currentVersion);
qWarning() << "Added current version to oldVersions because oldVersions was empty! (" + modpack.name + ")";
}
else
{
} else {
modpack.broken = true;
qWarning() << "Broken pack:" << modpack.name << " => No valid version!";
}
@ -218,4 +198,4 @@ void PackFetchTask::fileDownloadAborted()
emit aborted();
}
}
} // namespace LegacyFTB

View File

@ -1,15 +1,15 @@
#pragma once
#include "net/NetJob.h"
#include <QTemporaryDir>
#include <QByteArray>
#include <QObject>
#include <QTemporaryDir>
#include <memory>
#include "PackHelpers.h"
#include "net/NetJob.h"
namespace LegacyFTB {
class PackFetchTask : public QObject {
Q_OBJECT
public:
@ -23,8 +23,8 @@ private:
shared_qobject_ptr<QNetworkAccessManager> m_network;
NetJob::Ptr jobPtr;
QByteArray publicModpacksXmlFileData;
QByteArray thirdPartyModpacksXmlFileData;
std::shared_ptr<QByteArray> publicModpacksXmlFileData = std::make_shared<QByteArray>();
std::shared_ptr<QByteArray> thirdPartyModpacksXmlFileData = std::make_shared<QByteArray>();
bool parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list);
ModpackList publicPacks;
@ -44,4 +44,4 @@ signals:
void privateFileDownloadFailed(QString reason, QString packCode);
};
}
} // namespace LegacyFTB

View File

@ -9,19 +9,17 @@
#include "net/NetJob.h"
#include "net/Upload.h"
Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response)
Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Modrinth::GetCurrentVersion"), APPLICATION->network());
netJob->addNetAction(Net::Download::makeByteArray(
QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response));
QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
return netJob;
}
Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response)
Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Modrinth::GetCurrentVersions"), APPLICATION->network());
@ -35,8 +33,6 @@ Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_f
netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw));
QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
return netJob;
}
@ -44,7 +40,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash,
QString hash_format,
std::optional<std::list<Version>> mcVersions,
std::optional<ModLoaderTypes> loaders,
QByteArray* response)
std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
@ -67,8 +63,6 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash,
netJob->addNetAction(Net::Upload::makeByteArray(
QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw));
QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
return netJob;
}
@ -76,7 +70,7 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes,
QString hash_format,
std::optional<std::list<Version>> mcVersions,
std::optional<ModLoaderTypes> loaders,
QByteArray* response)
std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Modrinth::GetLatestVersions"), APPLICATION->network());
@ -101,22 +95,16 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes,
netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw));
QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
return netJob;
}
Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const
Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const
{
auto netJob = makeShared<NetJob>(QString("Modrinth::GetProjects"), APPLICATION->network());
auto searchUrl = getMultipleModInfoURL(addonIds);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
QObject::connect(netJob.get(), &NetJob::finished, [response, netJob] {
delete response;
});
return netJob;
}

View File

@ -12,27 +12,23 @@
class ModrinthAPI : public NetworkResourceAPI {
public:
auto currentVersion(QString hash,
QString hash_format,
QByteArray* response) -> Task::Ptr;
auto currentVersion(QString hash, QString hash_format, std::shared_ptr<QByteArray> response) -> Task::Ptr;
auto currentVersions(const QStringList& hashes,
QString hash_format,
QByteArray* response) -> Task::Ptr;
auto currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr<QByteArray> response) -> Task::Ptr;
auto latestVersion(QString hash,
QString hash_format,
std::optional<std::list<Version>> mcVersions,
std::optional<ModLoaderTypes> loaders,
QByteArray* response) -> Task::Ptr;
std::shared_ptr<QByteArray> response) -> Task::Ptr;
auto latestVersions(const QStringList& hashes,
QString hash_format,
std::optional<std::list<Version>> mcVersions,
std::optional<ModLoaderTypes> loaders,
QByteArray* response) -> Task::Ptr;
std::shared_ptr<QByteArray> response) -> Task::Ptr;
Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
public:
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
@ -55,8 +51,7 @@ class ModrinthAPI : public NetworkResourceAPI {
static auto getModLoaderFilters(ModLoaderTypes types) -> const QString
{
QStringList l;
for (auto loader : getModLoaderStrings(types))
{
for (auto loader : getModLoaderStrings(types)) {
l << QString("\"categories:%1\"").arg(loader);
}
return l.join(',');
@ -146,9 +141,15 @@ class ModrinthAPI : public NetworkResourceAPI {
return s.isEmpty() ? QString() : s;
}
inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool
{
return loaders & (Forge | Fabric | Quilt);
}
inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { return loaders & (Forge | Fabric | Quilt); }
[[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
{
return args.dependency.version.length() != 0 ? QString("%1/version/%2").arg(BuildConfig.MODRINTH_PROD_URL, args.dependency.version)
: QString("%1/project/%2/version?game_versions=[\"%3\"]&loaders=[\"%4\"]")
.arg(BuildConfig.MODRINTH_PROD_URL)
.arg(args.dependency.addonId.toString())
.arg(args.mcVersion.toString())
.arg(getModLoaderStrings(args.loader).join("\",\""));
};
};

View File

@ -53,12 +53,11 @@ void ModrinthCheckUpdate::executeTask()
// (though it will rarely happen, if at all)
if (mod->metadata()->hash_format != best_hash_type) {
auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath());
connect(hash_task.get(), &Task::succeeded, [&] {
QString hash (hash_task->getResult());
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [&hashes, &mappings, mod](QString hash) {
hashes.append(hash);
mappings.insert(hash, mod);
});
connect(hash_task.get(), &Task::failed, [this, hash_task] { failed("Failed to generate hash"); });
connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); });
hashing_task.addTask(hash_task);
} else {
hashes.append(hash);
@ -71,7 +70,7 @@ void ModrinthCheckUpdate::executeTask()
hashing_task.start();
loop.exec();
auto* response = new QByteArray();
auto response = std::make_shared<QByteArray>();
auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
QEventLoop lock;
@ -112,7 +111,8 @@ void ModrinthCheckUpdate::executeTask()
// so we may want to filter it
QString loader_filter;
if (m_loaders.has_value()) {
static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, ResourceAPI::ModLoaderType::Quilt };
static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric,
ResourceAPI::ModLoaderType::Quilt };
for (auto flag : flags) {
if (m_loaders.value().testFlag(flag)) {
loader_filter = api.getModLoaderString(flag);
@ -122,7 +122,8 @@ void ModrinthCheckUpdate::executeTask()
}
// Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
// - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter
// - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the
// loader_filter
// - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
// Such is the pain of having arbitrary files for a given version .-.
@ -149,19 +150,19 @@ void ModrinthCheckUpdate::executeTask()
continue;
// Fake pack with the necessary info to pass to the download task :)
ModPlatform::IndexedPack pack;
pack.name = mod->name();
pack.slug = mod->metadata()->slug;
pack.addonId = mod->metadata()->project_id;
pack.websiteUrl = mod->homeurl();
auto pack = std::make_shared<ModPlatform::IndexedPack>();
pack->name = mod->name();
pack->slug = mod->metadata()->slug;
pack->addonId = mod->metadata()->project_id;
pack->websiteUrl = mod->homeurl();
for (auto& author : mod->authors())
pack.authors.append({ author });
pack.description = mod->description();
pack.provider = ModPlatform::ResourceProvider::MODRINTH;
pack->authors.append({ author });
pack->description = mod->description();
pack->provider = ModPlatform::ResourceProvider::MODRINTH;
auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_mods_folder);
m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
ModPlatform::ResourceProvider::MODRINTH, download_task);
}
}

View File

@ -214,7 +214,7 @@ bool ModrinthCreationTask::createInstance()
if (m_instIcon != "default") {
instance.setIconKey(m_instIcon);
} else {
} else if (!m_managed_id.isEmpty()) {
instance.setIconKey("modrinth");
}

View File

@ -0,0 +1,319 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ModrinthPackExportTask.h"
#include <QCryptographicHash>
#include <QFileInfo>
#include <QMessageBox>
#include <QtConcurrentRun>
#include "Json.h"
#include "MMCZip.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/ModFolderModel.h"
const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" });
const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" });
ModrinthPackExportTask::ModrinthPackExportTask(const QString& name,
const QString& version,
const QString& summary,
InstancePtr instance,
const QString& output,
MMCZip::FilterFunction filter)
: name(name)
, version(version)
, summary(summary)
, instance(instance)
, mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))
, gameRoot(instance->gameRoot())
, output(output)
, filter(filter)
{}
void ModrinthPackExportTask::executeTask()
{
setStatus(tr("Searching for files..."));
setProgress(0, 0);
collectFiles();
}
bool ModrinthPackExportTask::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 ModrinthPackExportTask::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) {
mcInstance->loaderModList()->update();
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &ModrinthPackExportTask::collectHashes);
} else
collectHashes();
}
void ModrinthPackExportTask::collectHashes()
{
for (const QFileInfo& file : files) {
QCoreApplication::processEvents();
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
// require sensible file types
if (!std::any_of(PREFIXES.begin(), PREFIXES.end(), [&relative](const QString& prefix) { return relative.startsWith(prefix); }))
continue;
if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
}))
continue;
QCryptographicHash sha512(QCryptographicHash::Algorithm::Sha512);
QFile openFile(file.absoluteFilePath());
if (!openFile.open(QFile::ReadOnly)) {
qWarning() << "Could not open" << file << "for hashing";
continue;
}
const QByteArray data = openFile.readAll();
if (openFile.error() != QFileDevice::NoError) {
qWarning() << "Could not read" << file;
continue;
}
sha512.addData(data);
auto allMods = mcInstance->loaderModList()->allMods();
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->metadata() != nullptr) {
QUrl& url = mod->metadata()->url;
// ensure the url is permitted on modrinth.com
if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) {
qDebug() << "Resolving" << relative << "from index";
QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1);
sha1.addData(data);
ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size() };
resolvedFiles[relative] = resolvedFile;
// nice! we've managed to resolve based on local metadata!
// no need to enqueue it
continue;
}
}
}
qDebug() << "Enqueueing" << relative << "for Modrinth query";
pendingHashes[relative] = sha512.result().toHex();
}
setAbortable(true);
makeApiRequest();
}
void ModrinthPackExportTask::makeApiRequest()
{
if (pendingHashes.isEmpty())
buildZip();
else {
auto response = std::make_shared<QByteArray>();
task = api.currentVersions(pendingHashes.values(), "sha512", response);
connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); });
connect(task.get(), &NetJob::failed, this, &ModrinthPackExportTask::emitFailed);
task->start();
}
}
void ModrinthPackExportTask::parseApiResponse(const std::shared_ptr<QByteArray> response)
{
task = nullptr;
try {
const QJsonDocument doc = Json::requireDocument(*response);
QMapIterator<QString, QString> iterator(pendingHashes);
while (iterator.hasNext()) {
iterator.next();
const QJsonObject obj = doc[iterator.value()].toObject();
if (obj.isEmpty())
continue;
const QJsonArray files = obj["files"].toArray();
if (auto fileIter = std::find_if(files.begin(), files.end(),
[&iterator](const QJsonValue& file) { return file["hashes"]["sha512"] == iterator.value(); });
fileIter != files.end()) {
// map the file to the url
resolvedFiles[iterator.key()] =
ResolvedFile{ fileIter->toObject()["hashes"].toObject()["sha1"].toString(), iterator.value(),
fileIter->toObject()["url"].toString(), fileIter->toObject()["size"].toInt() };
}
}
} catch (const Json::JsonException& e) {
emitFailed(tr("Failed to parse versions response: %1").arg(e.what()));
return;
}
pendingHashes.clear();
buildZip();
}
void ModrinthPackExportTask::buildZip()
{
setStatus(tr("Adding files..."));
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("modrinth.index.json"))) {
QFile::remove(output);
return BuildZipResult(tr("Could not create index"));
}
indexFile.write(generateIndex());
size_t progress = 0;
for (const QFileInfo& file : files) {
if (buildZipFuture.isCanceled()) {
QFile::remove(output);
return BuildZipResult();
}
setProgress(progress, files.length());
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
if (!resolvedFiles.contains(relative) && !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);
return BuildZipResult(tr("A zip error occurred"));
}
return BuildZipResult();
});
connect(&buildZipWatcher, &QFutureWatcher<BuildZipResult>::finished, this, &ModrinthPackExportTask::finish);
buildZipWatcher.setFuture(buildZipFuture);
}
void ModrinthPackExportTask::finish()
{
if (buildZipFuture.isCanceled())
emitAborted();
else {
const BuildZipResult result = buildZipFuture.result();
if (result.has_value())
emitFailed(result.value());
else
emitSucceeded();
}
}
QByteArray ModrinthPackExportTask::generateIndex()
{
QJsonObject obj;
obj["formatVersion"] = 1;
obj["game"] = "minecraft";
obj["name"] = name;
obj["versionId"] = version;
if (!summary.isEmpty())
obj["summary"] = summary;
if (mcInstance) {
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
QJsonObject dependencies;
if (minecraft != nullptr)
dependencies["minecraft"] = minecraft->m_version;
if (quilt != nullptr)
dependencies["quilt-loader"] = quilt->m_version;
if (fabric != nullptr)
dependencies["fabric-loader"] = fabric->m_version;
if (forge != nullptr)
dependencies["forge"] = forge->m_version;
obj["dependencies"] = dependencies;
}
QJsonArray files;
QMapIterator<QString, ResolvedFile> iterator(resolvedFiles);
while (iterator.hasNext()) {
iterator.next();
const ResolvedFile& value = iterator.value();
QJsonObject file;
file["path"] = iterator.key();
file["downloads"] = QJsonArray({ iterator.value().url });
QJsonObject hashes;
hashes["sha1"] = value.sha1;
hashes["sha512"] = value.sha512;
file["hashes"] = hashes;
file["fileSize"] = value.size;
files << file;
}
obj["files"] = files;
return QJsonDocument(obj).toJson(QJsonDocument::Compact);
}

View File

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

View File

@ -22,7 +22,7 @@
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "net/NetJob.h"
#include "modplatform/ModIndex.h"
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
@ -140,6 +140,28 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
file.version_number = Json::requireString(obj, "version_number");
file.changelog = Json::requireString(obj, "changelog");
auto dependencies = Json::ensureArray(obj, "dependencies");
for (auto d : dependencies) {
auto dep = Json::ensureObject(d);
ModPlatform::Dependency dependency;
dependency.addonId = Json::ensureString(dep, "project_id");
dependency.version = Json::ensureString(dep, "version_id");
auto depType = Json::requireString(dep, "dependency_type");
if (depType == "required")
dependency.type = ModPlatform::DependencyType::REQUIRED;
else if (depType == "optional")
dependency.type = ModPlatform::DependencyType::OPTIONAL;
else if (depType == "incompatible")
dependency.type = ModPlatform::DependencyType::INCOMPATIBLE;
else if (depType == "embedded")
dependency.type = ModPlatform::DependencyType::EMBEDDED;
else
dependency.type = ModPlatform::DependencyType::UNKNOWN;
file.dependencies.append(dependency);
}
auto files = Json::requireArray(obj, "files");
int i = 0;
@ -195,3 +217,22 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
return {};
}
auto Modrinth::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion
{
QVector<ModPlatform::IndexedVersion> versions;
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj);
if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
versions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
// dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(versions.begin(), versions.end(), orderSortPredicate);
return versions.length() != 0 ? versions.front() : ModPlatform::IndexedVersion();
}

View File

@ -19,8 +19,8 @@
#include "modplatform/ModIndex.h"
#include "BaseInstance.h"
#include <QNetworkAccessManager>
#include "BaseInstance.h"
namespace Modrinth {
@ -31,5 +31,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
const shared_qobject_ptr<QNetworkAccessManager>& network,
const BaseInstance* inst);
auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion;
} // namespace Modrinth

View File

@ -37,20 +37,19 @@
#include <FileSystem.h>
#include <Json.h>
#include <QtConcurrentRun>
#include <MMCZip.h>
#include <QtConcurrentRun>
#include "TechnicPackProcessor.h"
#include "SolderPackManifest.h"
#include "TechnicPackProcessor.h"
#include "net/ChecksumValidator.h"
Technic::SolderPackInstallTask::SolderPackInstallTask(
shared_qobject_ptr<QNetworkAccessManager> network,
Technic::SolderPackInstallTask::SolderPackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network,
const QUrl& solderUrl,
const QString& pack,
const QString& version,
const QString &minecraftVersion
) {
const QString& minecraftVersion)
{
m_solderUrl = solderUrl;
m_pack = pack;
m_version = version;
@ -58,9 +57,9 @@ Technic::SolderPackInstallTask::SolderPackInstallTask(
m_minecraftVersion = minecraftVersion;
}
bool Technic::SolderPackInstallTask::abort() {
if(m_abortable)
bool Technic::SolderPackInstallTask::abort()
{
if (m_abortable) {
return m_filesNetJob->abort();
}
return false;
@ -72,7 +71,7 @@ void Technic::SolderPackInstallTask::executeTask()
m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"), m_network));
auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version);
m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, &m_response));
m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, m_response));
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded);
@ -86,10 +85,10 @@ void Technic::SolderPackInstallTask::fileListSucceeded()
setStatus(tr("Downloading modpack"));
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error);
QJsonDocument doc = QJsonDocument::fromJson(*m_response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << m_response;
qWarning() << *m_response;
return;
}
auto obj = doc.object();

View File

@ -40,14 +40,17 @@
#include <tasks/Task.h>
#include <QUrl>
#include <memory>
namespace Technic
{
class SolderPackInstallTask : public InstanceTask
{
namespace Technic {
class SolderPackInstallTask : public InstanceTask {
Q_OBJECT
public:
explicit SolderPackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, const QUrl &solderUrl, const QString& pack, const QString& version, const QString &minecraftVersion);
explicit SolderPackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network,
const QUrl& solderUrl,
const QString& pack,
const QString& version,
const QString& minecraftVersion);
bool canAbort() const override { return true; }
bool abort() override;
@ -75,10 +78,10 @@ namespace Technic
QString m_pack;
QString m_version;
QString m_minecraftVersion;
QByteArray m_response;
std::shared_ptr<QByteArray> m_response = std::make_shared<QByteArray>();
QTemporaryDir m_outputDir;
int m_modCount;
QFuture<bool> m_extractFuture;
QFutureWatcher<bool> m_extractFutureWatcher;
};
}
} // namespace Technic

View File

@ -1,427 +0,0 @@
#include "PackageManifest.h"
#include <Json.h>
#include <QDir>
#include <QDirIterator>
#include <QCryptographicHash>
#include <QDebug>
#ifndef Q_OS_WIN32
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#endif
namespace mojang_files {
const Hash hash_of_empty_string = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
int Path::compare(const Path& rhs) const
{
auto left_cursor = begin();
auto left_end = end();
auto right_cursor = rhs.begin();
auto right_end = rhs.end();
while (left_cursor != left_end && right_cursor != right_end)
{
if(*left_cursor < *right_cursor)
{
return -1;
}
else if(*left_cursor > *right_cursor)
{
return 1;
}
left_cursor++;
right_cursor++;
}
if(left_cursor == left_end)
{
if(right_cursor == right_end)
{
return 0;
}
return -1;
}
return 1;
}
void Package::addFile(const Path& path, const File& file) {
addFolder(path.parent_path());
files[path] = file;
}
void Package::addFolder(Path folder) {
if(!folder.has_parent_path()) {
return;
}
do {
folders.insert(folder);
folder = folder.parent_path();
} while(folder.has_parent_path());
}
void Package::addLink(const Path& path, const Path& target) {
addFolder(path.parent_path());
symlinks[path] = target;
}
void Package::addSource(const FileSource& source) {
sources[source.hash] = source;
}
namespace {
void fromJson(QJsonDocument & doc, Package & out) {
std::set<Path> seen_paths;
if (!doc.isObject())
{
throw JSONValidationError("file manifest is not an object");
}
QJsonObject root = doc.object();
auto filesObj = Json::ensureObject(root, "files");
auto iter = filesObj.begin();
while (iter != filesObj.end())
{
Path objectPath = Path(iter.key());
auto value = iter.value();
iter++;
if(seen_paths.count(objectPath)) {
throw JSONValidationError("duplicate path inside manifest, the manifest is invalid");
}
if (!value.isObject())
{
throw JSONValidationError("file entry inside manifest is not an an object");
}
seen_paths.insert(objectPath);
auto fileObject = value.toObject();
auto type = Json::requireString(fileObject, "type");
if(type == "directory") {
out.addFolder(objectPath);
continue;
}
else if(type == "file") {
FileSource bestSource;
File file;
file.executable = Json::ensureBoolean(fileObject, QString("executable"), false);
auto downloads = Json::requireObject(fileObject, "downloads");
for(auto iter2 = downloads.begin(); iter2 != downloads.end(); iter2++) {
FileSource source;
auto downloadObject = Json::requireObject(iter2.value());
source.hash = Json::requireString(downloadObject, "sha1");
source.size = Json::requireInteger(downloadObject, "size");
source.url = Json::requireString(downloadObject, "url");
auto compression = iter2.key();
if(compression == "raw") {
file.hash = source.hash;
file.size = source.size;
source.compression = Compression::Raw;
}
else if (compression == "lzma") {
source.compression = Compression::Lzma;
}
else {
continue;
}
bestSource.upgrade(source);
}
if(bestSource.isBad()) {
throw JSONValidationError("No valid compression method for file " + iter.key());
}
out.addFile(objectPath, file);
out.addSource(bestSource);
}
else if(type == "link") {
auto target = Json::requireString(fileObject, "target");
out.symlinks[objectPath] = target;
out.addLink(objectPath, target);
}
else {
throw JSONValidationError("Invalid item type in manifest: " + type);
}
}
// make sure the containing folder exists
out.folders.insert(Path());
}
}
Package Package::fromManifestContents(const QByteArray& contents)
{
Package out;
try
{
auto doc = Json::requireDocument(contents, "Manifest");
fromJson(doc, out);
return out;
}
catch (const Exception &e)
{
qDebug() << QString("Unable to parse manifest: %1").arg(e.cause());
out.valid = false;
return out;
}
}
Package Package::fromManifestFile(const QString & filename) {
Package out;
try
{
auto doc = Json::requireDocument(filename, filename);
fromJson(doc, out);
return out;
}
catch (const Exception &e)
{
qDebug() << QString("Unable to parse manifest file %1: %2").arg(filename, e.cause());
out.valid = false;
return out;
}
}
#ifndef Q_OS_WIN32
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
namespace {
// FIXME: Qt obscures symlink targets by making them absolute. that is useless. this is the workaround - we do it ourselves
bool actually_read_symlink_target(const QString & filepath, Path & out)
{
struct ::stat st;
// FIXME: here, we assume the native filesystem encoding. May the Gods have mercy upon our Souls.
QByteArray nativePath = filepath.toUtf8();
const char * filepath_cstr = nativePath.data();
if (lstat(filepath_cstr, &st) != 0)
{
return false;
}
auto size = st.st_size ? st.st_size + 1 : PATH_MAX;
std::string temp(size, '\0');
// because we don't realiably know how long the damn thing actually is, we loop and expand. POSIX is naff
do
{
auto link_length = ::readlink(filepath_cstr, &temp[0], temp.size());
if(link_length == -1)
{
return false;
}
if(std::string::size_type(link_length) < temp.size())
{
// buffer was long enough and we managed to read the link target. RETURN here.
temp.resize(link_length);
out = Path(QString::fromUtf8(temp.c_str()));
return true;
}
temp.resize(temp.size() * 2);
} while (true);
}
}
#endif
// FIXME: Qt filesystem abstraction is bad, but ... let's hope it doesn't break too much?
// FIXME: The error handling is just DEFICIENT
Package Package::fromInspectedFolder(const QString& folderPath)
{
QDir root(folderPath);
Package out;
QDirIterator iterator(folderPath, QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden, QDirIterator::Subdirectories);
while(iterator.hasNext()) {
iterator.next();
auto fileInfo = iterator.fileInfo();
auto relPath = root.relativeFilePath(fileInfo.filePath());
// FIXME: this is probably completely busted on Windows anyway, so just disable it.
// Qt makes shit up and doesn't understand the platform details
// TODO: Actually use a filesystem library that isn't terrible and has decen license.
// I only know one, and I wrote it. Sadly, currently proprietary. PAIN.
#ifndef Q_OS_WIN32
if(fileInfo.isSymLink()) {
Path targetPath;
if(!actually_read_symlink_target(fileInfo.filePath(), targetPath)) {
qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath();
out.valid = false;
}
out.addLink(relPath, targetPath);
}
else
#endif
if(fileInfo.isDir()) {
out.addFolder(relPath);
}
else if(fileInfo.isFile()) {
File f;
f.executable = fileInfo.isExecutable();
f.size = fileInfo.size();
// FIXME: async / optimize the hashing
QFile input(fileInfo.absoluteFilePath());
if(!input.open(QIODevice::ReadOnly)) {
qCritical() << "Folder inspection: Failed to open file:" << fileInfo.absoluteFilePath();
out.valid = false;
break;
}
f.hash = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1).toHex().constData();
out.addFile(relPath, f);
}
else {
// Something else... oh my
qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath();
out.valid = false;
break;
}
}
out.folders.insert(Path("."));
out.valid = true;
return out;
}
namespace {
struct shallow_first_sort
{
bool operator()(const Path &lhs, const Path &rhs) const
{
auto lhs_depth = lhs.length();
auto rhs_depth = rhs.length();
if(lhs_depth < rhs_depth)
{
return true;
}
else if(lhs_depth == rhs_depth)
{
if(lhs < rhs)
{
return true;
}
}
return false;
}
};
struct deep_first_sort
{
bool operator()(const Path &lhs, const Path &rhs) const
{
auto lhs_depth = lhs.length();
auto rhs_depth = rhs.length();
if(lhs_depth > rhs_depth)
{
return true;
}
else if(lhs_depth == rhs_depth)
{
if(lhs < rhs)
{
return true;
}
}
return false;
}
};
}
UpdateOperations UpdateOperations::resolve(const Package& from, const Package& to)
{
UpdateOperations out;
if(!from.valid || !to.valid) {
out.valid = false;
return out;
}
// Files
for(auto iter = from.files.begin(); iter != from.files.end(); iter++) {
const auto &current_hash = iter->second.hash;
const auto &current_executable = iter->second.executable;
const auto &path = iter->first;
auto iter2 = to.files.find(path);
if(iter2 == to.files.end()) {
// removed
out.deletes.push_back(path);
continue;
}
auto new_hash = iter2->second.hash;
auto new_executable = iter2->second.executable;
if (current_hash != new_hash) {
out.deletes.push_back(path);
out.downloads.emplace(
std::pair<Path, FileDownload>{
path,
FileDownload(to.sources.at(iter2->second.hash), iter2->second.executable)
}
);
}
else if (current_executable != new_executable) {
out.executable_fixes[path] = new_executable;
}
}
for(auto iter = to.files.begin(); iter != to.files.end(); iter++) {
auto path = iter->first;
if(!from.files.count(path)) {
out.downloads.emplace(
std::pair<Path, FileDownload>{
path,
FileDownload(to.sources.at(iter->second.hash), iter->second.executable)
}
);
}
}
// Folders
std::set<Path, deep_first_sort> remove_folders;
std::set<Path, shallow_first_sort> make_folders;
for(auto from_path: from.folders) {
auto iter = to.folders.find(from_path);
if(iter == to.folders.end()) {
remove_folders.insert(from_path);
}
}
for(auto & rmdir: remove_folders) {
out.rmdirs.push_back(rmdir);
}
for(auto to_path: to.folders) {
auto iter = from.folders.find(to_path);
if(iter == from.folders.end()) {
make_folders.insert(to_path);
}
}
for(auto & mkdir: make_folders) {
out.mkdirs.push_back(mkdir);
}
// Symlinks
for(auto iter = from.symlinks.begin(); iter != from.symlinks.end(); iter++) {
const auto &current_target = iter->second;
const auto &path = iter->first;
auto iter2 = to.symlinks.find(path);
if(iter2 == to.symlinks.end()) {
// removed
out.deletes.push_back(path);
continue;
}
const auto &new_target = iter2->second;
if (current_target != new_target) {
out.deletes.push_back(path);
out.mklinks[path] = iter2->second;
}
}
for(auto iter = to.symlinks.begin(); iter != to.symlinks.end(); iter++) {
auto path = iter->first;
if(!from.symlinks.count(path)) {
out.mklinks[path] = iter->second;
}
}
out.valid = true;
return out;
}
}

View File

@ -1,171 +0,0 @@
#pragma once
#include <QString>
#include <map>
#include <set>
#include <QStringList>
#include "tasks/Task.h"
namespace mojang_files {
using Hash = QString;
extern const Hash empty_hash;
// simple-ish path implementation. assumes always relative and does not allow '..' entries
class Path
{
public:
using parts_type = QStringList;
Path() = default;
Path(QString string) {
auto parts_in = string.split('/');
for(auto & part: parts_in) {
if(part.isEmpty() || part == ".") {
continue;
}
if(part == "..") {
if(parts.size()) {
parts.pop_back();
}
continue;
}
parts.push_back(part);
}
}
bool has_parent_path() const
{
return parts.size() > 0;
}
Path parent_path() const
{
if (parts.empty())
return Path();
return Path(parts.begin(), std::prev(parts.end()));
}
bool empty() const
{
return parts.empty();
}
int length() const
{
return parts.length();
}
bool operator==(const Path & rhs) const {
return parts == rhs.parts;
}
bool operator!=(const Path & rhs) const {
return parts != rhs.parts;
}
inline bool operator<(const Path& rhs) const
{
return compare(rhs) < 0;
}
parts_type::const_iterator begin() const
{
return parts.begin();
}
parts_type::const_iterator end() const
{
return parts.end();
}
QString toString() const {
return parts.join("/");
}
private:
Path(const parts_type::const_iterator & start, const parts_type::const_iterator & end) {
auto cursor = start;
while(cursor != end) {
parts.push_back(*cursor);
cursor++;
}
}
int compare(const Path& p) const;
parts_type parts;
};
enum class Compression {
Raw,
Lzma,
Unknown
};
struct FileSource
{
Compression compression = Compression::Unknown;
Hash hash;
QString url;
std::size_t size = 0;
void upgrade(const FileSource & other) {
if(compression == Compression::Unknown || other.size < size) {
*this = other;
}
}
bool isBad() const {
return compression == Compression::Unknown;
}
};
struct File
{
Hash hash;
bool executable;
std::uint64_t size = 0;
};
struct Package {
static Package fromInspectedFolder(const QString &folderPath);
static Package fromManifestFile(const QString &path);
static Package fromManifestContents(const QByteArray& contents);
explicit operator bool() const
{
return valid;
}
void addFolder(Path folder);
void addFile(const Path & path, const File & file);
void addLink(const Path & path, const Path & target);
void addSource(const FileSource & source);
std::map<Hash, FileSource> sources;
bool valid = true;
std::set<Path> folders;
std::map<Path, File> files;
std::map<Path, Path> symlinks;
};
struct FileDownload : FileSource
{
FileDownload(const FileSource& source, bool executable) {
static_cast<FileSource &> (*this) = source;
this->executable = executable;
}
bool executable = false;
};
struct UpdateOperations {
static UpdateOperations resolve(const Package & from, const Package & to);
bool valid = false;
std::vector<Path> deletes;
std::vector<Path> rmdirs;
std::vector<Path> mkdirs;
std::map<Path, FileDownload> downloads;
std::map<Path, Path> mklinks;
std::map<Path, bool> executable_fixes;
};
}

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* 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
@ -46,14 +47,17 @@ namespace Net {
*/
class ByteArraySink : public Sink {
public:
ByteArraySink(QByteArray* output) : m_output(output){};
ByteArraySink(std::shared_ptr<QByteArray> output) : m_output(output){};
virtual ~ByteArraySink() = default;
public:
auto init(QNetworkRequest& request) -> Task::State override
{
if (m_output)
m_output->clear();
else
qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable";
if (initAllValidators(request))
return Task::State::Running;
return Task::State::Failed;
@ -61,7 +65,10 @@ class ByteArraySink : public Sink {
auto write(QByteArray& data) -> Task::State override
{
if (m_output)
m_output->append(data);
else
qWarning() << "ByteArraySink did not write the buffer because it's not addressable";
if (writeAllValidators(data))
return Task::State::Running;
return Task::State::Failed;
@ -69,7 +76,10 @@ class ByteArraySink : public Sink {
auto abort() -> Task::State override
{
if (m_output)
m_output->clear();
else
qWarning() << "ByteArraySink did not clear the buffer because it's not addressable";
failAllValidators();
return Task::State::Failed;
}
@ -84,6 +94,6 @@ class ByteArraySink : public Sink {
auto hasLocalData() -> bool override { return false; }
private:
QByteArray* m_output;
std::shared_ptr<QByteArray> m_output;
};
} // namespace Net

View File

@ -41,6 +41,7 @@
#include <QDateTime>
#include <QFileInfo>
#include <memory>
#include "ByteArraySink.h"
#include "ChecksumValidator.h"
@ -69,7 +70,7 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down
return dl;
}
auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> Download::Ptr
auto Download::makeByteArray(QUrl url, std::shared_ptr<QByteArray> output, Options options) -> Download::Ptr
{
auto dl = makeShared<Download>();
dl->m_url = url;

View File

@ -60,7 +60,7 @@ class Download : public NetAction {
~Download() override = default;
static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr;
static auto makeByteArray(QUrl url, QByteArray* output, Options options = Option::NoOptions) -> Download::Ptr;
static auto makeByteArray(QUrl url, std::shared_ptr<QByteArray> output, Options options = Option::NoOptions) -> Download::Ptr;
static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr;
public:

View File

@ -39,9 +39,9 @@
#include "Upload.h"
#include <utility>
#include "ByteArraySink.h"
#include "BuildConfig.h"
#include "Application.h"
#include "BuildConfig.h"
#include "ByteArraySink.h"
#include "net/Logging.h"
@ -57,11 +57,13 @@ namespace Net {
return true;
}
void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
setProgress(bytesReceived, bytesTotal);
}
void Upload::downloadError(QNetworkReply::NetworkError error) {
void Upload::downloadError(QNetworkReply::NetworkError error)
{
if (error == QNetworkReply::OperationCanceledError) {
qCCritical(taskUploadLogC) << getUid().toString() << "Aborted " << m_url.toString();
m_state = State::AbortedByUser;
@ -72,10 +74,12 @@ namespace Net {
}
}
void Upload::sslErrors(const QList<QSslError> &errors) {
void Upload::sslErrors(const QList<QSslError>& errors)
{
int i = 1;
for (const auto& error : errors) {
qCCritical(taskUploadLogC) << getUid().toString() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString();
qCCritical(taskUploadLogC) << getUid().toString() << "Upload" << m_url.toString() << "SSL Error #" << i << " : "
<< error.errorString();
auto cert = error.certificate();
qCCritical(taskUploadLogC) << getUid().toString() << "Certificate in question:\n" << cert.toText();
i++;
@ -135,7 +139,8 @@ namespace Net {
return true;
}
void Upload::downloadFinished() {
void Upload::downloadFinished()
{
// handle HTTP redirection first
// very unlikely for post requests, still can happen
if (handleRedirect()) {
@ -185,14 +190,16 @@ namespace Net {
emit succeeded();
}
void Upload::downloadReadyRead() {
void Upload::downloadReadyRead()
{
if (m_state == State::Running) {
auto data = m_reply->readAll();
m_state = m_sink->write(data);
}
}
void Upload::executeTask() {
void Upload::executeTask()
{
setStatus(tr("Uploading %1").arg(m_url.toString()));
if (m_state == State::AbortedByUser) {
@ -246,11 +253,12 @@ namespace Net {
connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead);
}
Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data) {
Upload::Ptr Upload::makeByteArray(QUrl url, std::shared_ptr<QByteArray> output, QByteArray m_post_data)
{
auto up = makeShared<Upload>();
up->m_url = std::move(url);
up->m_sink.reset(new ByteArraySink(output));
up->m_post_data = std::move(m_post_data);
return up;
}
} // Net
} // namespace Net

View File

@ -48,7 +48,7 @@ namespace Net {
public:
using Ptr = shared_qobject_ptr<Upload>;
static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data);
static Upload::Ptr makeByteArray(QUrl url, std::shared_ptr<QByteArray> output, QByteArray m_post_data);
auto abort() -> bool override;
auto canAbort() const -> bool override { return true; };
@ -61,6 +61,7 @@ namespace Net {
public slots:
void executeTask() override;
private:
std::unique_ptr<Sink> m_sink;
QByteArray m_post_data;
@ -68,5 +69,4 @@ namespace Net {
bool handleRedirect();
};
} // Net
} // namespace Net

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