diff options
| author | Rachel Powers <508861+Ryex@users.noreply.github.com> | 2023-06-04 19:01:50 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-06-04 19:01:50 -0700 |
| commit | 3dccc38f42bf64c922e96b9a224d87ca1cee98c6 (patch) | |
| tree | 717d036c1466d175f57c8f4f18b7d29e48b7e480 | |
| parent | 1840505a0f887ebfc2c719113873ea3345b133fb (diff) | |
| parent | 3c87e5d31eb8fb33d6e63a38377c9729a130af45 (diff) | |
| download | PrismLauncher-3dccc38f42bf64c922e96b9a224d87ca1cee98c6.tar.gz PrismLauncher-3dccc38f42bf64c922e96b9a224d87ca1cee98c6.tar.bz2 PrismLauncher-3dccc38f42bf64c922e96b9a224d87ca1cee98c6.zip | |
Merge pull request #904 from TheKodeToad/mrpack-export
mrpack export
| -rw-r--r-- | buildconfig/BuildConfig.h | 5 | ||||
| -rw-r--r-- | launcher/CMakeLists.txt | 9 | ||||
| -rw-r--r-- | launcher/FastFileIconProvider.cpp | 47 | ||||
| -rw-r--r-- | launcher/FastFileIconProvider.h | 26 | ||||
| -rw-r--r-- | launcher/FileIgnoreProxy.cpp | 256 | ||||
| -rw-r--r-- | launcher/FileIgnoreProxy.h | 72 | ||||
| -rw-r--r-- | launcher/modplatform/modrinth/ModrinthPackExportTask.cpp | 323 | ||||
| -rw-r--r-- | launcher/modplatform/modrinth/ModrinthPackExportTask.h | 77 | ||||
| -rw-r--r-- | launcher/ui/MainWindow.cpp | 16 | ||||
| -rw-r--r-- | launcher/ui/MainWindow.h | 7 | ||||
| -rw-r--r-- | launcher/ui/MainWindow.ui | 17 | ||||
| -rw-r--r-- | launcher/ui/dialogs/ExportInstanceDialog.cpp | 304 | ||||
| -rw-r--r-- | launcher/ui/dialogs/ExportInstanceDialog.h | 46 | ||||
| -rw-r--r-- | launcher/ui/dialogs/ExportMrPackDialog.cpp | 109 | ||||
| -rw-r--r-- | launcher/ui/dialogs/ExportMrPackDialog.h | 44 | ||||
| -rw-r--r-- | launcher/ui/dialogs/ExportMrPackDialog.ui | 132 |
16 files changed, 1172 insertions, 318 deletions
diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index a05d7a9e..8543d724 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -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"; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 273b5449..ce2771a4 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -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 @@ -720,6 +722,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 @@ -900,6 +906,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 @@ -1046,6 +1054,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 diff --git a/launcher/FastFileIconProvider.cpp b/launcher/FastFileIconProvider.cpp new file mode 100644 index 00000000..f2b6f442 --- /dev/null +++ b/launcher/FastFileIconProvider.cpp @@ -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); +}
\ No newline at end of file diff --git a/launcher/FastFileIconProvider.h b/launcher/FastFileIconProvider.h new file mode 100644 index 00000000..20853404 --- /dev/null +++ b/launcher/FastFileIconProvider.h @@ -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; +};
\ No newline at end of file diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp new file mode 100644 index 00000000..a3b7d505 --- /dev/null +++ b/launcher/FileIgnoreProxy.cpp @@ -0,0 +1,256 @@ +// 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 "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; +} diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h new file mode 100644 index 00000000..a5a1153d --- /dev/null +++ b/launcher/FileIgnoreProxy.h @@ -0,0 +1,72 @@ +// 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 <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; } + + protected: + bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const; + + private: + const QString root; + SeparatorPrefixTree<'/'> blocked; +}; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp new file mode 100644 index 00000000..29df90dd --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -0,0 +1,323 @@ +// 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 + QDir::separator()); })) + 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 file{ sha1.result().toHex(), sha512.result().toHex(), url.toString(), openFile.size() }; + resolvedFiles[relative] = file; + + // 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 { + QByteArray* response = new 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 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; + QString path = iterator.key(); + path.replace(QDir::separator(), "/"); + file["path"] = path; + 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); +} diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h new file mode 100644 index 00000000..af00ffaa --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -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; + }; |
