aboutsummaryrefslogtreecommitdiff
path: root/launcher/modplatform
diff options
context:
space:
mode:
authorRachel Powers <508861+Ryex@users.noreply.github.com>2023-07-31 22:26:20 -0700
committerGitHub <noreply@github.com>2023-07-31 22:26:20 -0700
commit2d1197f59f824a3093ad71b9ee3466bd5e2825c8 (patch)
tree368a2ba5ab9a6ae9f3b3fd0841ea5ee1d0283add /launcher/modplatform
parentb47993b736ba40a75b5b63290f9d7b5911f8150e (diff)
parent9137721e8e5b44f2338a36874a393435cbc6daa3 (diff)
downloadPrismLauncher-2d1197f59f824a3093ad71b9ee3466bd5e2825c8.tar.gz
PrismLauncher-2d1197f59f824a3093ad71b9ee3466bd5e2825c8.tar.bz2
PrismLauncher-2d1197f59f824a3093ad71b9ee3466bd5e2825c8.zip
Merge branch 'develop' into refactor/net-split-headers-to-proxy-class
Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
Diffstat (limited to 'launcher/modplatform')
-rw-r--r--launcher/modplatform/EnsureMetadataTask.cpp6
-rw-r--r--launcher/modplatform/ModIndex.cpp8
-rw-r--r--launcher/modplatform/ModIndex.h2
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp4
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp29
-rw-r--r--launcher/modplatform/flame/FlameAPI.h2
-rw-r--r--launcher/modplatform/flame/FlameInstanceCreationTask.cpp136
-rw-r--r--launcher/modplatform/flame/FlameInstanceCreationTask.h6
-rw-r--r--launcher/modplatform/flame/FlamePackExportTask.cpp473
-rw-r--r--launcher/modplatform/flame/FlamePackExportTask.h90
-rw-r--r--launcher/modplatform/flame/PackManifest.cpp9
-rw-r--r--launcher/modplatform/helpers/ExportToModList.cpp200
-rw-r--r--launcher/modplatform/helpers/ExportToModList.h33
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.cpp88
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.h27
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h6
-rw-r--r--launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp2
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackExportTask.cpp55
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.cpp2
-rw-r--r--launcher/modplatform/technic/SolderPackInstallTask.cpp2
20 files changed, 1004 insertions, 176 deletions
diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp
index 93b5ce76..c3eadd06 100644
--- a/launcher/modplatform/EnsureMetadataTask.cpp
+++ b/launcher/modplatform/EnsureMetadataTask.cpp
@@ -145,7 +145,8 @@ void EnsureMetadataTask::executeTask()
connect(project_task.get(), &Task::finished, this, [=] {
invalidade_leftover();
project_task->deleteLater();
- m_current_task = nullptr;
+ if (m_current_task)
+ m_current_task.reset();
});
m_current_task = project_task;
@@ -154,7 +155,8 @@ void EnsureMetadataTask::executeTask()
connect(version_task.get(), &Task::finished, [=] {
version_task->deleteLater();
- m_current_task = nullptr;
+ if (m_current_task)
+ m_current_task.reset();
});
if (m_mods.size() > 1)
diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp
index 6a507caf..a1c4d891 100644
--- a/launcher/modplatform/ModIndex.cpp
+++ b/launcher/modplatform/ModIndex.cpp
@@ -70,11 +70,17 @@ auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString t
}
QCryptographicHash hash(algo);
- if(!hash.addData(device))
+ if (!hash.addData(device))
qCritical() << "Failed to read JAR to create hash!";
Q_ASSERT(hash.result().length() == hash.hashLength(algo));
return { hash.result().toHex() };
}
+QString getMetaURL(ResourceProvider provider, QVariant projectID)
+{
+ return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") +
+ projectID.toString();
+}
+
} // namespace ModPlatform
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index 3b0a03a1..2aa91602 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -128,6 +128,7 @@ struct IndexedPack {
return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; });
}
};
+QString getMetaURL(ResourceProvider provider, QVariant projectID);
struct OverrideDep {
QString quilt;
@@ -144,6 +145,7 @@ inline auto getOverrideDeps() -> QList<OverrideDep>
{ "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH },
{ "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } };
};
+QString getMetaURL(ResourceProvider provider, QVariant projectID);
} // namespace ModPlatform
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index b72ce137..8260a25c 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -686,7 +686,7 @@ void PackInstallTask::installConfigs()
abortable = true;
setProgress(current, total);
});
- connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress);
+ connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
connect(jobPtr.get(), &NetJob::aborted, [&]{
abortable = false;
jobPtr.reset();
@@ -854,7 +854,7 @@ void PackInstallTask::downloadMods()
abortable = true;
setProgress(current, total);
});
- connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress);
+ connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
connect(jobPtr.get(), &NetJob::aborted, [&]
{
abortable = false;
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
index 28cda162..fcf8517f 100644
--- a/launcher/modplatform/flame/FileResolvingTask.cpp
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -23,6 +23,10 @@ bool Flame::FileResolvingTask::abort()
void Flame::FileResolvingTask::executeTask()
{
+ if (m_toProcess.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately
+ emitSucceeded();
+ return;
+ }
setStatus(tr("Resolving mod IDs..."));
setProgress(0, 3);
m_dljob.reset(new NetJob("Mod id resolver", m_network));
@@ -50,7 +54,7 @@ void Flame::FileResolvingTask::executeTask()
stepProgress(*step_progress);
emitFailed(reason);
});
- connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
+ connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_dljob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
@@ -116,7 +120,7 @@ void Flame::FileResolvingTask::netJobFinished()
stepProgress(*step_progress);
emitFailed(reason);
});
- connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
+ connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_checkJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
@@ -130,12 +134,13 @@ void Flame::FileResolvingTask::netJobFinished()
m_checkJob->start();
}
-void Flame::FileResolvingTask::modrinthCheckFinished() {
+void Flame::FileResolvingTask::modrinthCheckFinished()
+{
setProgress(2, 3);
qDebug() << "Finished with blocked mods : " << blockedProjects.size();
for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) {
- auto &out = *it;
+ auto& out = *it;
auto bytes = blockedProjects[out];
if (!out->resolved) {
continue;
@@ -155,15 +160,13 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
out->resolved = false;
}
}
- //copy to an output list and filter out projects found on modrinth
+ // copy to an output list and filter out projects found on modrinth
auto block = std::make_shared<QList<File*>>();
auto it = blockedProjects.keys();
- std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) {
- return !f->resolved;
- });
- //Display not found mods early
+ std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File* f) { return !f->resolved; });
+ // Display not found mods early
if (!block->empty()) {
- //blocked mods found, we need the slug for displaying.... we need another job :D !
+ // blocked mods found, we need the slug for displaying.... we need another job :D !
m_slugJob.reset(new NetJob("Slug Job", m_network));
int index = 0;
for (auto mod : *block) {
@@ -175,8 +178,8 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
QObject::connect(dl.get(), &Net::Download::succeeded, [block, index, output]() {
auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done
auto json = QJsonDocument::fromJson(*output);
- auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"),
- "websiteUrl");
+ auto base =
+ Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json), "data"), "links"), "websiteUrl");
auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId));
mod->websiteUrl = link;
});
@@ -194,7 +197,7 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
stepProgress(*step_progress);
emitFailed(reason);
});
- connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
+ connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_slugJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h
index 0a6dc78f..49bc316f 100644
--- a/launcher/modplatform/flame/FlameAPI.h
+++ b/launcher/modplatform/flame/FlameAPI.h
@@ -23,6 +23,8 @@ class FlameAPI : public NetworkResourceAPI {
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
+ static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt); }
+
private:
static int getClassId(ModPlatform::ResourceType type)
{
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
index d44e1499..c2c7080f 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
@@ -57,17 +57,11 @@
#include <QDebug>
#include <QFileInfo>
+#include "meta/Index.h"
+#include "meta/VersionList.h"
#include "minecraft/World.h"
#include "minecraft/mod/tasks/LocalResourceParse.h"
-#include "net/ApiDownload.h"
-
-
-const static QMap<QString, QString> forgemap = { { "1.2.5", "3.4.9.171" },
- { "1.4.2", "6.0.1.355" },
- { "1.4.7", "6.6.2.534" },
- { "1.5.2", "7.8.1.737" } };
-
static const FlameAPI api;
bool FlameCreationTask::abort()
@@ -261,6 +255,56 @@ bool FlameCreationTask::updateInstance()
return false;
}
+QString FlameCreationTask::getVersionForLoader(QString uid, QString loaderType, QString loaderVersion, QString mcVersion)
+{
+ if (loaderVersion == "recommended") {
+ auto vlist = APPLICATION->metadataIndex()->get(uid);
+ if (!vlist) {
+ setError(tr("Failed to get local metadata index for %1").arg(uid));
+ return {};
+ }
+
+ if (!vlist->isLoaded()) {
+ QEventLoop loadVersionLoop;
+ auto task = vlist->getLoadTask();
+ connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit);
+ if (!task->isRunning())
+ task->start();
+
+ loadVersionLoop.exec();
+ }
+
+ for (auto version : vlist->versions()) {
+ // first recommended build we find, we use.
+ if (!version->isRecommended())
+ continue;
+ auto reqs = version->requiredSet();
+
+ // filter by minecraft version, if the loader depends on a certain version.
+ // not all mod loaders depend on a given Minecraft version, so we won't do this
+ // filtering for those loaders.
+ if (loaderType == "forge") {
+ auto iter = std::find_if(reqs.begin(), reqs.end(), [mcVersion](const Meta::Require& req) {
+ return req.uid == "net.minecraft" && req.equalsVersion == mcVersion;
+ });
+ if (iter == reqs.end())
+ continue;
+ }
+ return version->descriptor();
+ }
+
+ setError(tr("Failed to find version for %1 loader").arg(loaderType));
+ return {};
+ }
+
+ if (loaderVersion.isEmpty()) {
+ emitFailed(tr("No loader version set for modpack!"));
+ return {};
+ }
+
+ return loaderVersion;
+}
+
bool FlameCreationTask::createInstance()
{
QEventLoop loop;
@@ -299,22 +343,29 @@ bool FlameCreationTask::createInstance()
}
}
- QString forgeVersion;
- QString fabricVersion;
- // TODO: is Quilt relevant here?
+ QString loaderType;
+ QString loaderUid;
+ QString loaderVersion;
+
for (auto& loader : m_pack.minecraft.modLoaders) {
auto id = loader.id;
if (id.startsWith("forge-")) {
id.remove("forge-");
- forgeVersion = id;
- continue;
- }
- if (id.startsWith("fabric-")) {
+ loaderType = "forge";
+ loaderUid = "net.minecraftforge";
+ } else if (loaderType == "fabric") {
id.remove("fabric-");
- fabricVersion = id;
+ loaderType = "fabric";
+ loaderUid = "net.fabricmc.fabric-loader";
+ } else if (loaderType == "quilt") {
+ id.remove("quilt-");
+ loaderType = "quilt";
+ loaderUid = "org.quiltmc.quilt-loader";
+ } else {
+ logWarning(tr("Unknown mod loader in manifest: %1").arg(id));
continue;
}
- logWarning(tr("Unknown mod loader in manifest: %1").arg(id));
+ loaderVersion = id;
}
QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
@@ -331,19 +382,12 @@ bool FlameCreationTask::createInstance()
auto components = instance.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", mcVersion, true);
- if (!forgeVersion.isEmpty()) {
- // FIXME: dirty, nasty, hack. Proper solution requires dependency resolution and knowledge of the metadata.
- if (forgeVersion == "recommended") {
- if (forgemap.contains(mcVersion)) {
- forgeVersion = forgemap[mcVersion];
- } else {
- logWarning(tr("Could not map recommended Forge version for Minecraft %1").arg(mcVersion));
- }
- }
- components->setComponentVersion("net.minecraftforge", forgeVersion);
+ if (!loaderType.isEmpty()) {
+ auto version = getVersionForLoader(loaderUid, loaderType, loaderVersion, mcVersion);
+ if (version.isEmpty())
+ return false;
+ components->setComponentVersion(loaderUid, version);
}
- if (!fabricVersion.isEmpty())
- components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion);
if (m_instIcon != "default") {
instance.setIconKey(m_instIcon);
@@ -388,7 +432,7 @@ bool FlameCreationTask::createInstance()
});
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
- connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propogateStepProgress);
+ connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
m_mod_id_resolver->start();
@@ -472,8 +516,9 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
- // fall-through intentional, we treat these as plain old mods and dump them wherever.
+ // fallthrough intentional, we treat these as plain old mods and dump them wherever.
}
+ /* fallthrough */
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {
@@ -503,11 +548,11 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
m_files_job.reset();
setError(reason);
});
- connect(m_files_job.get(), &NetJob::progress, this, [this](qint64 current, qint64 total){
+ connect(m_files_job.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) {
setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, total);
});
- connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propogateStepProgress);
+ connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
setStatus(tr("Downloading mods..."));
@@ -546,7 +591,6 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
setAbortable(true);
}
-
void FlameCreationTask::validateZIPResouces()
{
qDebug() << "Validating whether resources stored as .zip are in the right place";
@@ -564,11 +608,13 @@ void FlameCreationTask::validateZIPResouces()
if (FS::move(localPath, destPath)) {
return destPath;
}
+ } else {
+ qDebug() << "Target folder of" << fileName << "is correct at" << targetFolder;
}
return localPath;
};
- auto installWorld = [this](QString worldPath){
+ auto installWorld = [this](QString worldPath) {
qDebug() << "Installing World from" << worldPath;
QFileInfo worldFileInfo(worldPath);
World w(worldFileInfo);
@@ -585,29 +631,29 @@ void FlameCreationTask::validateZIPResouces()
QString worldPath;
switch (type) {
- case PackedResourceType::ResourcePack :
+ case PackedResourceType::Mod:
+ validatePath(fileName, targetFolder, "mods");
+ break;
+ case PackedResourceType::ResourcePack:
validatePath(fileName, targetFolder, "resourcepacks");
break;
- case PackedResourceType::TexturePack :
+ case PackedResourceType::TexturePack:
validatePath(fileName, targetFolder, "texturepacks");
break;
- case PackedResourceType::DataPack :
+ case PackedResourceType::DataPack:
validatePath(fileName, targetFolder, "datapacks");
break;
- case PackedResourceType::Mod :
- validatePath(fileName, targetFolder, "mods");
- break;
- case PackedResourceType::ShaderPack :
+ case PackedResourceType::ShaderPack:
// in theroy flame API can't do this but who knows, that *may* change ?
// better to handle it if it *does* occure in the future
validatePath(fileName, targetFolder, "shaderpacks");
break;
- case PackedResourceType::WorldSave :
+ case PackedResourceType::WorldSave:
worldPath = validatePath(fileName, targetFolder, "saves");
installWorld(worldPath);
break;
- case PackedResourceType::UNKNOWN :
- default :
+ case PackedResourceType::UNKNOWN:
+ default:
qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is.";
break;
}
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h
index 0ae4735b..603d3693 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.h
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h
@@ -57,10 +57,7 @@ class FlameCreationTask final : public InstanceCreationTask {
QString id,
QString version_id,
QString original_instance_id = {})
- : InstanceCreationTask()
- , m_parent(parent)
- , m_managed_id(std::move(id))
- , m_managed_version_id(std::move(version_id))
+ : InstanceCreationTask(), m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(version_id))
{
setStagingPath(staging_path);
setParentSettings(global_settings);
@@ -78,6 +75,7 @@ class FlameCreationTask final : public InstanceCreationTask {
void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
void validateZIPResouces();
+ QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
private:
QWidget* m_parent = nullptr;
diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp
new file mode 100644
index 00000000..87bf780c
--- /dev/null
+++ b/launcher/modplatform/flame/FlamePackExportTask.cpp
@@ -0,0 +1,473 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "FlamePackExportTask.h"
+#include <QJsonArray>
+#include <QJsonObject>
+
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QMessageBox>
+#include <QtConcurrentRun>
+#include <algorithm>
+#include <memory>
+#include "Json.h"
+#include "MMCZip.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "modplatform/ModIndex.h"
+#include "modplatform/flame/FlameModIndex.h"
+#include "modplatform/helpers/HashUtils.h"
+#include "tasks/Task.h"
+
+const QString FlamePackExportTask::TEMPLATE = "<li><a href=\"{url}\">{name}{authors}</a></li>\n";
+const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" });
+
+FlamePackExportTask::FlamePackExportTask(const QString& name,
+ const QString& version,
+ const QString& author,
+ InstancePtr instance,
+ const QString& output,
+ MMCZip::FilterFunction filter)
+ : name(name)
+ , version(version)
+ , author(author)
+ , instance(instance)
+ , mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))
+ , gameRoot(instance->gameRoot())
+ , output(output)
+ , filter(filter)
+{}
+
+void FlamePackExportTask::executeTask()
+{
+ setStatus(tr("Searching for files..."));
+ setProgress(0, 5);
+ collectFiles();
+}
+
+bool FlamePackExportTask::abort()
+{
+ if (task != nullptr) {
+ task->abort();
+ task = nullptr;
+ emitAborted();
+ return true;
+ }
+
+ if (buildZipFuture.isRunning()) {
+ buildZipFuture.cancel();
+ // NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur
+ // immediately.
+ return true;
+ }
+
+ return false;
+}
+
+void FlamePackExportTask::collectFiles()
+{
+ setAbortable(false);
+ QCoreApplication::processEvents();
+
+ files.clear();
+ if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) {
+ emitFailed(tr("Could not search for files"));
+ return;
+ }
+
+ pendingHashes.clear();
+ resolvedFiles.clear();
+
+ if (mcInstance != nullptr) {
+ mcInstance->loaderModList()->update();
+ connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes);
+ } else
+ collectHashes();
+}
+
+void FlamePackExportTask::collectHashes()
+{
+ setAbortable(true);
+ setStatus(tr("Finding file hashes..."));
+ setProgress(1, 5);
+ auto allMods = mcInstance->loaderModList()->allMods();
+ ConcurrentTask::Ptr hashingTask(new ConcurrentTask(this, "MakeHashesTask", 10));
+ task.reset(hashingTask);
+ for (const QFileInfo& file : files) {
+ const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
+ // require sensible file types
+ if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
+ return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
+ }))
+ continue;
+
+ if (relative.startsWith("resourcepacks/") &&
+ (relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack
+ auto hashTask = Hashing::createFlameHasher(file.absoluteFilePath());
+ connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) {
+ if (m_state == Task::State::Running) {
+ pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") });
+ }
+ });
+ connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
+ hashingTask->addTask(hashTask);
+ continue;
+ }
+
+ if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
+ modIter != allMods.end()) {
+ const Mod* mod = *modIter;
+ if (!mod || mod->type() == ResourceType::FOLDER) {
+ continue;
+ }
+ if (mod->metadata() && mod->metadata()->provider == ModPlatform::ResourceProvider::FLAME) {
+ resolvedFiles.insert(mod->fileinfo().absoluteFilePath(),
+ { mod->metadata()->project_id.toInt(), mod->metadata()->file_id.toInt(), mod->enabled(), true,
+ mod->metadata()->name, mod->metadata()->slug, mod->authors().join(", ") });
+ continue;
+ }
+
+ auto hashTask = Hashing::createFlameHasher(mod->fileinfo().absoluteFilePath());
+ connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) {
+ if (m_state == Task::State::Running) {
+ pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true });
+ }
+ });
+ connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
+ hashingTask->addTask(hashTask);
+ }
+ }
+ auto progressStep = std::make_shared<TaskStepProgress>();
+ connect(hashingTask.get(), &Task::finished, this, [this, progressStep] {
+ progressStep->state = TaskStepState::Succeeded;
+ stepProgress(*progressStep);
+ });
+
+ connect(hashingTask.get(), &Task::succeeded, this, &FlamePackExportTask::makeApiRequest);
+ connect(hashingTask.get(), &Task::failed, this, [this, progressStep](QString reason) {
+ progressStep->state = TaskStepState::Failed;
+ stepProgress(*progressStep);
+ emitFailed(reason);
+ });
+ connect(hashingTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propagateStepProgress);
+
+ connect(hashingTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
+ progressStep->update(current, total);
+ stepProgress(*progressStep);
+ });
+ connect(hashingTask.get(), &Task::status, this, [this, progressStep](QString status) {
+ progressStep->status = status;
+ stepProgress(*progressStep);
+ });
+ hashingTask->start();
+}
+
+void FlamePackExportTask::makeApiRequest()
+{
+ if (pendingHashes.isEmpty()) {
+ buildZip();
+ return;
+ }
+
+ setStatus(tr("Finding versions for hashes..."));
+ setProgress(2, 5);
+ auto response = std::make_shared<QByteArray>();
+
+ QList<uint> fingerprints;
+ for (auto& murmur : pendingHashes.keys()) {
+ fingerprints.push_back(murmur.toUInt());
+ }
+
+ task.reset(api.matchFingerprints(fingerprints, response));
+
+ connect(task.get(), &Task::succeeded, this, [this, response] {
+ QJsonParseError parseError{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
+ if (parseError.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at " << parseError.offset
+ << " reason: " << parseError.errorString();
+ qWarning() << *response;
+
+ failed(parseError.errorString());
+ return;
+ }
+
+ try {
+ auto docObj = Json::requireObject(doc);
+ auto dataObj = Json::requireObject(docObj, "data");
+ auto dataArr = Json::requireArray(dataObj, "exactMatches");
+
+ if (dataArr.isEmpty()) {
+ qWarning() << "No matches found for fingerprint search!";
+
+ return;
+ }
+ for (auto match : dataArr) {
+ auto matchObj = Json::ensureObject(match, {});
+ auto fileObj = Json::ensureObject(matchObj, "file", {});
+
+ if (matchObj.isEmpty() || fileObj.isEmpty()) {
+ qWarning() << "Fingerprint match is empty!";
+
+ return;
+ }
+
+ auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt());
+ auto mod = pendingHashes.find(fingerprint);
+ if (mod == pendingHashes.end()) {
+ qWarning() << "Invalid fingerprint from the API response.";
+ continue;
+ }
+
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name));
+ if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable"))
+ resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fi