diff options
Diffstat (limited to 'launcher/modplatform/flame')
-rw-r--r-- | launcher/modplatform/flame/FileResolvingTask.cpp | 23 | ||||
-rw-r--r-- | launcher/modplatform/flame/FlameAPI.h | 2 | ||||
-rw-r--r-- | launcher/modplatform/flame/FlameInstanceCreationTask.cpp | 130 | ||||
-rw-r--r-- | launcher/modplatform/flame/FlameInstanceCreationTask.h | 6 | ||||
-rw-r--r-- | launcher/modplatform/flame/FlamePackExportTask.cpp | 473 | ||||
-rw-r--r-- | launcher/modplatform/flame/FlamePackExportTask.h | 90 | ||||
-rw-r--r-- | launcher/modplatform/flame/PackManifest.cpp | 9 |
7 files changed, 671 insertions, 62 deletions
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index ce7a6055..34bd401d 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -21,6 +21,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)); @@ -128,12 +132,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; @@ -153,15 +158,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) { @@ -173,8 +176,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; }); 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 f003ada9..29145d89 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -57,15 +57,11 @@ #include <QDebug> #include <QFileInfo> +#include "meta/Index.h" +#include "meta/VersionList.h" #include "minecraft/World.h" #include "minecraft/mod/tasks/LocalResourceParse.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() @@ -259,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; @@ -297,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"); @@ -329,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); @@ -470,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()) { @@ -501,7 +548,7 @@ 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); }); @@ -544,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"; @@ -562,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); @@ -583,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..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"); |