aboutsummaryrefslogtreecommitdiff
path: root/launcher/modplatform
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/modplatform')
-rw-r--r--launcher/modplatform/EnsureMetadataTask.cpp6
-rw-r--r--launcher/modplatform/ModIndex.h1
-rw-r--r--launcher/modplatform/flame/FlameAPI.h28
-rw-r--r--launcher/modplatform/flame/FlameInstanceCreationTask.cpp11
-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/legacy_ftb/PackInstallTask.cpp86
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.h27
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h6
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackExportTask.cpp55
11 files changed, 670 insertions, 122 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.h b/launcher/modplatform/ModIndex.h
index 3f51e700..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;
diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h
index a0611957..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)
{
@@ -77,24 +79,28 @@ class FlameAPI : public NetworkResourceAPI {
[[nodiscard]] std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
{
- auto mappedModLoader = getMappedModLoader(args.loaders.value());
auto addonId = args.pack.addonId.toString();
- 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;
- }
- }
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())
+
+ 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('&');
};
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
index f003ada9..b57db288 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
@@ -470,8 +470,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()) {
@@ -562,6 +563,8 @@ void FlameCreationTask::validateZIPResouces()
if (FS::move(localPath, destPath)) {
return destPath;
}
+ } else {
+ qDebug() << "Target folder of" << fileName << "is correct at" << targetFolder;
}
return localPath;
};
@@ -583,6 +586,9 @@ void FlameCreationTask::validateZIPResouces()
QString worldPath;
switch (type) {
+ case PackedResourceType::Mod :
+ validatePath(fileName, targetFolder, "mods");
+ break;
case PackedResourceType::ResourcePack :
validatePath(fileName, targetFolder, "resourcepacks");
break;
@@ -592,9 +598,6 @@ void FlameCreationTask::validateZIPResouces()
case PackedResourceType::DataPack :
validatePath(fileName, targetFolder, "datapacks");
break;
- case PackedResourceType::Mod :
- validatePath(fileName, targetFolder, "mods");
- break;
case PackedResourceType::ShaderPack :
// in theroy flame API can't do this but who knows, that *may* change ?
// better to handle it if it *does* occure in the future
diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp
new file mode 100644
index 00000000..ac0da214
--- /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::propogateStepProgress);
+
+ connect(hashingTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
+ progressStep->update(current, total);
+ stepProgress(*progressStep);
+ });
+ connect(hashingTask.get(), &Task::status, this, [this, progressStep](QString status) {
+ progressStep->status = status;
+ stepProgress(*progressStep);
+ });
+ hashingTask->start();
+}
+
+void FlamePackExportTask::makeApiRequest()
+{
+ if (pendingHashes.isEmpty()) {
+ buildZip();
+ return;
+ }
+
+ setStatus(tr("Finding versions for hashes..."));
+ setProgress(2, 5);
+ auto response = std::make_shared<QByteArray>();
+
+ QList<uint> fingerprints;
+ for (auto& murmur : pendingHashes.keys()) {
+ fingerprints.push_back(murmur.toUInt());
+ }
+
+ task.reset(api.matchFingerprints(fingerprints, response));
+
+ connect(task.get(), &Task::succeeded, this, [this, response] {
+ QJsonParseError parseError{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
+ if (parseError.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at " << parseError.offset
+ << " reason: " << parseError.errorString();
+ qWarning() << *response;
+
+ failed(parseError.errorString());
+ return;
+ }
+
+ try {
+ auto docObj = Json::requireObject(doc);
+ auto dataObj = Json::requireObject(docObj, "data");
+ auto dataArr = Json::requireArray(dataObj, "exactMatches");
+
+ if (dataArr.isEmpty()) {
+ qWarning() << "No matches found for fingerprint search!";
+
+ return;
+ }
+ for (auto match : dataArr) {
+ auto matchObj = Json::ensureObject(match, {});
+ auto fileObj = Json::ensureObject(matchObj, "file", {});
+
+ if (matchObj.isEmpty() || fileObj.isEmpty()) {
+ qWarning() << "Fingerprint match is empty!";
+
+ return;
+ }
+
+ auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt());
+ auto mod = pendingHashes.find(fingerprint);
+ if (mod == pendingHashes.end()) {
+ qWarning() << "Invalid fingerprint from the API response.";
+ continue;
+ }
+
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name));
+ if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable"))
+ resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"),
+ mod->enabled, mod->isMod });
+ }
+
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ pendingHashes.clear();
+ });
+ connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo);
+ connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed);
+ task->start();
+}
+
+void FlamePackExportTask::getProjectsInfo()
+{
+ setStatus(tr("Finding project info from CurseForge..."));
+ setProgress(3, 5);
+ QStringList addonIds;
+ for (const auto& resolved : resolvedFiles) {
+ if (resolved.slug.isEmpty()) {
+ addonIds << QString::number(resolved.addonId);
+ }
+ }
+
+ auto response = std::make_shared<QByteArray>();
+ Task::Ptr projTask;
+
+ if (addonIds.isEmpty()) {
+ buildZip();
+ return;
+ } else if (addonIds.size() == 1) {
+ projTask = api.getProject(*addonIds.begin(), response);
+ } else {
+ projTask = api.getProjects(addonIds, response);
+ }
+
+ connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] {
+ QJsonParseError parseError{};
+ auto doc = QJsonDocument::fromJson(*response, &parseError);
+ if (parseError.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset
+ << " reason: " << parseError.errorString();
+ qWarning() << *response;
+ failed(parseError.errorString());
+ return;
+ }
+
+ try {
+ QJsonArray entries;
+ if (addonIds.size() == 1)
+ entries = { Json::requireObject(Json::requireObject(doc), "data") };
+ else
+ entries = Json::requireArray(Json::requireObject(doc), "data");
+
+ for (auto entry : entries) {
+ auto entryObj = Json::requireObject(entry);
+
+ try {
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(Json::requireString(entryObj, "name")));
+
+ ModPlatform::IndexedPack pack;
+ FlameMod::loadIndexedPack(pack, entryObj);
+ for (auto key : resolvedFiles.keys()) {
+ auto val = resolvedFiles.value(key);
+ if (val.addonId == pack.addonId) {
+ val.name = pack.name;
+ val.slug = pack.slug;
+ QStringList authors;
+ for (auto author : pack.authors)
+ authors << author.name;
+
+ val.authors = authors.join(", ");
+ resolvedFiles[key] = val;
+ }
+ }
+
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+ }
+ }
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ buildZip();
+ });
+ task.reset(projTask);
+ task->start();
+}
+
+void FlamePackExportTask::buildZip()
+{
+ setStatus(tr("Adding files..."));
+ setProgress(4, 5);
+
+ buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() {
+ QuaZip zip(output);
+ if (!zip.open(QuaZip::mdCreate)) {
+ QFile::remove(output);
+ return BuildZipResult(tr("Could not create file"));
+ }
+
+ if (buildZipFuture.isCanceled())
+ return BuildZipResult();
+
+ QuaZipFile indexFile(&zip);
+ if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo("manifest.json"))) {
+ QFile::remove(output);
+ return BuildZipResult(tr("Could not create index"));
+ }
+ indexFile.write(generateIndex());
+
+ QuaZipFile modlist(&zip);
+ if (!modlist.open(QIODevice::WriteOnly, QuaZipNewInfo("modlist.html"))) {
+ QFile::remove(output);
+ return BuildZipResult(tr("Could not create index"));
+ }
+ QString content = "";
+ for (auto mod : resolvedFiles) {
+ if (mod.isMod) {
+ content += QString(TEMPLATE)
+ .replace("{name}", mod.name.toHtmlEscaped())
+ .replace("{url}", ModPlatform::getMetaURL(ModPlatform::ResourceProvider::FLAME, mod.addonId).toHtmlEscaped())
+ .replace("{authors}", !mod.authors.isEmpty() ? QString(" (by %1)").arg(mod.authors).toHtmlEscaped() : "");
+ }
+ }
+ content = "<ul>" + content + "</ul>";
+ modlist.write(content.toUtf8());
+
+ auto progressStep = std::make_shared<TaskStepProgress>();
+
+ size_t progress = 0;
+ for (const QFileInfo& file : files) {
+ if (buildZipFuture.isCanceled()) {
+ QFile::remove(output);
+ progressStep->state = TaskStepState::Failed;
+ stepProgress(*progressStep);
+ return BuildZipResult();
+ }
+ progressStep->update(progress, files.length());
+ stepProgress(*progressStep);
+
+ const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
+ if (!resolvedFiles.contains(file.absoluteFilePath()) &&
+ !JlCompress::compressFile(&zip, file.absoluteFilePath(), "overrides/" + relative)) {
+ QFile::remove(output);
+ return BuildZipResult(tr("Could not read and compress %1").arg(relative));
+ }
+ progress++;
+ }
+
+ zip.close();
+
+ if (zip.getZipError() != 0) {
+ QFile::remove(output);
+ progressStep->state = TaskStepState::Failed;
+ stepProgress(*progressStep);
+ return BuildZipResult(tr("A zip error occurred"));
+ }
+ progressStep->state = TaskStepState::Succeeded;
+ stepProgress(*progressStep);
+ return BuildZipResult();
+ });
+ connect(&buildZipWatcher, &QFutureWatcher<BuildZipResult>::finished, this, &FlamePackExportTask::finish);
+ buildZipWatcher.setFuture(buildZipFuture);
+}
+
+void FlamePackExportTask::finish()
+{
+ if (buildZipFuture.isCanceled())
+ emitAborted();
+ else {
+ const BuildZipResult result = buildZipFuture.result();
+ if (result.has_value())
+ emitFailed(result.value());
+ else
+ emitSucceeded();
+ }
+}
+
+QByteArray FlamePackExportTask::generateIndex()
+{
+ QJsonObject obj;
+ obj["manifestType"] = "minecraftModpack";
+ obj["manifestVersion"] = 1;
+ obj["name"] = name;
+ obj["version"] = version;
+ obj["author"] = author;
+ obj["overrides"] = "overrides";
+ if (mcInstance) {
+ QJsonObject version;
+ auto profile = mcInstance->getPackProfile();
+ // collect all supported components
+ const ComponentPtr minecraft = profile->getComponent("net.minecraft");
+ const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
+ const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
+ const ComponentPtr forge = profile->getComponent("net.minecraftforge");
+
+ // convert all available components to mrpack dependencies
+ if (minecraft != nullptr)
+ version["version"] = minecraft->m_version;
+ QString id;
+ if (quilt != nullptr)
+ id = "quilt-" + quilt->getVersion();
+ else if (fabric != nullptr)
+ id = "fabric-" + fabric->getVersion();
+ else if (forge != nullptr)
+ id = "forge-" + forge->getVersion();
+ version["modLoaders"] = QJsonArray();
+ if (!id.isEmpty()) {
+ QJsonObject loader;
+ loader["id"] = id;
+ loader["primary"] = true;
+ version["modLoaders"] = QJsonArray({ loader });
+ }
+ obj["minecraft"] = version;
+ }
+
+ QJsonArray files;
+ for (auto mod : resolvedFiles) {
+ QJsonObject file;
+ file["projectID"] = mod.addonId;
+ file["fileID"] = mod.version;
+ file["required"] = mod.enabled;
+ files << file;
+ }
+ obj["files"] = files;
+
+ return QJsonDocument(obj).toJson(QJsonDocument::Compact);
+}
diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h
new file mode 100644
index 00000000..3dee0a7e
--- /dev/null
+++ b/launcher/modplatform/flame/FlamePackExportTask.h
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QFuture>
+#include <QFutureWatcher>
+#include "BaseInstance.h"
+#include "MMCZip.h"
+#include "minecraft/MinecraftInstance.h"
+#include "modplatform/flame/FlameAPI.h"
+#include "tasks/Task.h"
+
+class FlamePackExportTask : public Task {
+ public:
+ FlamePackExportTask(const QString& name,
+ const QString& version,
+ const QString& author,
+ InstancePtr instance,
+ const QString& output,
+ MMCZip::FilterFunction filter);
+
+ protected:
+ void executeTask() override;
+ bool abort() override;
+
+ private:
+ static const QString TEMPLATE;
+ static const QStringList FILE_EXTENSIONS;
+
+ // inputs
+ const QString name, version, author;
+ const InstancePtr instance;
+ MinecraftInstance* mcInstance;
+ const QDir gameRoot;
+ const QString output;
+ const MMCZip::FilterFunction filter;
+
+ typedef std::optional<QString> BuildZipResult;
+ struct ResolvedFile {
+ int addonId;
+ int version;
+ bool enabled;
+ bool isMod;
+
+ QString name;
+ QString slug;
+ QString authors;
+ };
+ struct HashInfo {
+ QString name;
+ QString path;
+ bool enabled;
+ bool isMod;
+ };
+
+ FlameAPI api;
+
+ QFileInfoList files;
+ QMap<QString, HashInfo> pendingHashes{};
+ QMap<QString, ResolvedFile> resolvedFiles{};
+ Task::Ptr task;
+ QFuture<BuildZipResult> buildZipFuture;
+ QFutureWatcher<BuildZipResult> buildZipWatcher;
+
+ void collectFiles();
+ void collectHashes();
+ void makeApiRequest();
+ void getProjectsInfo();
+ void buildZip();
+ void finish();
+
+ QByteArray generateIndex();
+};
diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp
index 22008297..ee4d0766 100644
--- a/launcher/modplatform/flame/PackManifest.cpp
+++ b/launcher/modplatform/flame/PackManifest.cpp
@@ -76,13 +76,8 @@ bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked
// It is also optional
type = File::Type::SingleFile;
- if (fileName.endsWith(".zip")) {
- // this is probably a resource pack
- targetFolder = "resourcepacks";
- } else {
- // this is probably a mod, dunno what else could modpacks download
- targetFolder = "mods";
- }
+ targetFolder = "mods";
+
// get the hash
hash = QString();
auto hashes = Json::ensureArray(obj, "hashes");
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
index 36c142ac..a4c78397 100644
--- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
@@ -37,16 +37,16 @@
#include <QtConcurrent>
-#include "MMCZip.h"
#include "BaseInstance.h"
#include "FileSystem.h"
-#include "settings/INISettingsObject.h"
+#include "MMCZip.h"
+#include "minecraft/GradleSpecifier.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
-#include "minecraft/GradleSpecifier.h"
+#include "settings/INISettingsObject.h"
-#include "BuildConfig.h"
#include "Application.h"
+#include "BuildConfig.h"
namespace LegacyFTB {
@@ -65,6 +65,7 @@ void PackInstallTask::executeTask()
void PackInstallTask::downloadPack()
{
setStatus(tr("Downloading zip for %1").arg(m_pack.name));
+ setProgress(1, 4);
setAbortable(false);
archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
@@ -78,11 +79,10 @@ void PackInstallTask::downloadPack()
}
netJobContainer->addNetAction(Net::Download::makeFile(url, archivePath));
- connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
- connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
- connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress);
+ connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip);
+ connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed);
connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress);
- connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted);
+ connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::emitAborted);
netJobContainer->start();
@@ -90,27 +90,6 @@ void PackInstallTask::downloadPack()
progress(1, 4);
}
-void PackInstallTask::onDownloadSucceeded()
-{
- unzip();
-}
-
-void PackInstallTask::onDownloadFailed(QString reason)
-{
- emitFailed(reason);
-}
-
-void PackInstallTask::onDownloadProgress(qint64 current, qint64 total)
-{
- progress(current, total * 4);
- setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10));
-}
-
-void PackInstallTask::onDownloadAborted()
-{
- emitAborted();
-}
-
void PackInstallTask::unzip()
{
setStatus(tr("Extracting modpack"));
@@ -120,16 +99,17 @@ void PackInstallTask::unzip()
QDir extractDir(m_stagingPath);
m_packZip.reset(new QuaZip(archivePath));
- if(!m_packZip->open(QuaZip::mdUnzip))
- {
+ if (!m_packZip->open(QuaZip::mdUnzip)) {
emitFailed(tr("Failed to open modpack file %1!").arg(archivePath));
return;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip");
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath,
+ extractDir.absolutePath() + "/unzip");
#else
- m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
+ m_extractFuture =
+ QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
#endif
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled);
@@ -151,11 +131,9 @@ void PackInstallTask::install()
setStatus(tr("Installing modpack"));
progress(3, 4);
QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
- if(unzipMcDir.exists())
- {
- //ok, found minecraft dir, move contents to instance dir
- if(!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft"))
- {
+ if (unzipMcDir.exists()) {
+ // ok, found minecraft dir, move contents to instance dir
+ if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) {
emitFailed(tr("Failed to move unzipped Minecraft!"));
return;
}
@@ -172,23 +150,20 @@ void PackInstallTask::install()
bool fallback = true;
- //handle different versions
+ // handle different versions
QFile packJson(m_stagingPath + "/.minecraft/pack.json");
QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods");
- if(packJson.exists())
- {
+ if (packJson.exists()) {
packJson.open(QIODevice::ReadOnly | QIODevice::Text);
QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll());
packJson.close();
- //we only care about the libs
+ // we only care about the libs
QJsonArray libs = doc.object().value("libraries").toArray();
- foreach (const QJsonValue &value, libs)
- {
+ foreach (const QJsonValue& value, libs) {
QString nameValue = value.toObject().value("name").toString();
- if(!nameValue.startsWith("net.minecraftforge"))
- {
+ if (!nameValue.startsWith("net.minecraftforge")) {
continue;
}
@@ -199,16 +174,13 @@ void PackInstallTask::install()
fallback = false;
break;
}
-
}
- if(jarmodDir.exists())
- {
+ if (jarmodDir.exists()) {
qDebug() << "Found jarmods, installing...";
QStringList jarmods;
- for (auto info: jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files))
- {
+ for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) {
qDebug() << "Jarmod:" << info.fileName();
jarmods.push_back(info.absoluteFilePath());
}
@@ -217,12 +189,11 @@ void PackInstallTask::install()
fallback = false;
}
- //just nuke unzip directory, it s not needed anymore
+ // just nuke unzip directory, it s not needed anymore
FS::deletePath(m_stagingPath + "/unzip");
- if(fallback)
- {
- //TODO: Some fallback mechanism... or just keep failing!
+ if (fallback) {
+ // TODO: Some fallback mechanism... or just keep failing!
emitFailed(tr("No installation method found!"));
return;
}
@@ -232,8 +203,7 @@ void PackInstallTask::install()
progress(4, 4);
instance.setName(name());
- if(m_instIcon == "default")
- {
+ if (m_instIcon == "default") {
m_instIcon = "ftb_logo";
}
instance.setIconKey(m_instIcon);
@@ -252,4 +222,4 @@ bool PackInstallTask::abort()
return InstanceTask::abort();
}
-}
+} // namespace LegacyFTB
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h
index da791e06..30ff4859 100644
--- a/launcher/modplatform/legacy_ftb/PackInstallTask.h
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h
@@ -1,12 +1,12 @@
#pragma once
-#include "InstanceTask.h"
-#include "net/NetJob.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
+#include "InstanceTask.h"
+#include "PackHelpers.h"
#include "meta/Index.h"
#include "meta/Version.h"
#include "meta/VersionList.h"
-#include "PackHelpers.h"
+#include "net/NetJob.h"
#include "net/NetJob.h"
@@ -14,36 +14,31 @@
namespace LegacyFTB {
-class PackInstallTask : public InstanceTask
-{
+class PackInstallTask : public InstanceTask {
Q_OBJECT
-public:
+ public:
explicit PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, Modpack pack, QString version);
- virtual ~PackInstallTask(){}
+ virtual ~PackInstallTask() {}
bool canAbort() const override { return true; }
bool abort() override;
-protected:
+ protected:
//! Entry point for tasks.
virtual void executeTask() override;
-private:
+ private:
void downloadPack();
void unzip();
void install();
-private slots:
- void onDownloadSucceeded();
- void onDownloadFailed(QString reason);
- void onDownloadProgress(qint64 current, qint64 total);
- void onDownloadAborted();
+ private slots:
void onUnzipFinished();
void onUnzipCanceled();
-private: /* data */
+ private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
bool abortable = false;
std::unique_ptr<QuaZip> m_packZip;
@@ -56,4 +51,4 @@ private: /* data */
QString m_version;
};
-}
+} // namespace LegacyFTB
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
index e83ed2bf..58af14cc 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.h
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -38,7 +38,7 @@ class ModrinthAPI : public NetworkResourceAPI {
static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList
{
QStringList l;
- for (auto loader : { Forge, Fabric, Quilt }) {
+ for (auto loader : { Forge, Fabric, Quilt, LiteLoader }) {
if (types & loader) {
l << getModLoaderString(loader);
}
@@ -92,7 +92,7 @@ class ModrinthAPI : public NetworkResourceAPI {
{
if (args.loaders.has_value()) {
if (!validateModLoaders(args.loaders.value())) {
- qWarning() << "Modrinth only have Forge and Fabric-compatible mods!";
+ qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!";
return {};
}
}
@@ -141,7 +141,7 @@ class ModrinthAPI : public NetworkResourceAPI {
return s.isEmpty() ? QString() : s;
}
- inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { return loaders & (Forge | Fabric | Quilt); }
+ static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt | LiteLoader); }
[[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
{
diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
index 4cd88aa6..30fe566d 100644
--- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
@@ -64,7 +64,8 @@ bool ModrinthPackExportTask::abort()
if (buildZipFuture.isRunning()) {
buildZipFuture.cancel();
- // NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur immediately.
+ // NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur
+ // immediately.
return true;
}
@@ -94,6 +95,7 @@ void ModrinthPackExportTask::collectFiles()
void ModrinthPackExportTask::collectHashes()
{
+ setStatus(tr("Finding file hashes..."));
for (const QFileInfo& file : files) {
QCoreApplication::processEvents();
@@ -157,6 +159,7 @@ void ModrinthPackExportTask::makeApiRequest()
if (pendingHashes.isEmpty())
buildZip();
else {
+ setStatus(tr("Finding versions for hashes..."));
auto response = std::make_shared<QByteArray>();
task = api.currentVersions(pendingHashes.values(), "sha512", response);
connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); });
@@ -263,13 +266,13 @@ void ModrinthPackExportTask::finish()
QByteArray ModrinthPackExportTask::generateIndex()
{
- QJsonObject obj;
- obj["formatVersion"] = 1;
- obj["game"] = "minecraft";
- obj["name"] = name;
- obj["versionId"] = version;
+ QJsonObject out;
+ out["formatVersion"] = 1;
+ out["game"] = "minecraft";
+ out["name"] = name;
+ out["versionId"] = version;
if (!summary.isEmpty())
- obj["summary"] = summary;
+ out["summary"] = summary;
if (mcInstance) {
auto profile = mcInstance->getPackProfile();
@@ -290,30 +293,40 @@ QByteArray ModrinthPackExportTask::generateIndex()
if (forge != nullptr)
dependencies["forge"] = forge->m_version;
- obj["dependencies"] = dependencies;
+ out["dependencies"] = dependencies;
}
- QJsonArray files;
- QMapIterator<QString, ResolvedFile> iterator(resolvedFiles);
- while (iterator.hasNext()) {
- iterator.next();
+ QJsonArray filesOut;
+ for (auto iterator = resolvedFiles.constBegin(); iterator != resolvedFiles.constEnd(); iterator++) {
+ QJsonObject fileOut;
+ QString path = iterator.key();
const ResolvedFile& value = iterator.value();
- QJsonObject file;
- file["path"] = iterator.key();
- file["downloads"] = QJsonArray({ iterator.value().url });
+ // detect disabled mod
+ const QFileInfo pathInfo(path);
+ if (pathInfo.suffix() == "disabled") {
+ // rename it
+ path = pathInfo.dir().filePath(pathInfo.completeBaseName());
+ // ...and make it optional
+ QJsonObject env;
+ env["client"] = "optional";
+ env["server"] = "optional";
+ fileOut["env"] = env;
+ }
+
+ fileOut["path"] = path;
+ fileOut["downloads"] = QJsonArray{ iterator.value().url };
QJsonObject hashes;
hashes["sha1"] = value.sha1;
hashes["sha512"] = value.sha512;
+ fileOut["hashes"] = hashes;
- file["hashes"] = hashes;
- file["fileSize"] = value.size;
-
- files << file;
+ fileOut["fileSize"] = value.size;
+ filesOut << fileOut;
}
- obj["files"] = files;
+ out["files"] = filesOut;
- return QJsonDocument(obj).toJson(QJsonDocument::Compact);
+ return QJsonDocument(out).toJson(QJsonDocument::Compact);
}