Merge pull request #2583 from Trial97/metadata2

Generate updater metadata for mods added/updated using modpack updater/installer
This commit is contained in:
Alexandru Ionut Tripon 2024-09-17 08:45:09 +03:00 committed by GitHub
commit 926a6bc72a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 297 additions and 246 deletions

View File

@ -42,6 +42,6 @@ class LocalModUpdateTask : public Task {
private:
QDir m_index_dir;
ModPlatform::IndexedPack& m_mod;
ModPlatform::IndexedVersion& m_mod_version;
ModPlatform::IndexedPack m_mod;
ModPlatform::IndexedVersion m_mod_version;
};

View File

@ -42,6 +42,9 @@ EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform:
m_hashing_task->addTask(hash_task);
}
}
EnsureMetadataTask::EnsureMetadataTask(QHash<QString, Mod*>& mods, QDir dir, ModPlatform::ResourceProvider prov)
: Task(nullptr), m_mods(mods), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
{}
Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Mod* mod)
{

View File

@ -1,14 +1,14 @@
#pragma once
#include "ModIndex.h"
#include "net/NetJob.h"
#include "modplatform/helpers/HashUtils.h"
#include "tasks/ConcurrentTask.h"
#include <QDir>
class Mod;
class QDir;
class EnsureMetadataTask : public Task {
Q_OBJECT
@ -16,6 +16,7 @@ class EnsureMetadataTask : public Task {
public:
EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
EnsureMetadataTask(QHash<QString, Mod*>&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
~EnsureMetadataTask() = default;

View File

@ -1,87 +1,101 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 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 "FileResolvingTask.h"
#include <algorithm>
#include "Json.h"
#include "QObjectPtr.h"
#include "modplatform/ModIndex.h"
#include "net/ApiDownload.h"
#include "net/ApiUpload.h"
#include "net/Upload.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
static const FlameAPI flameAPI;
static ModrinthAPI modrinthAPI;
Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess)
: m_network(network), m_toProcess(toProcess)
: m_network(network), m_manifest(toProcess)
{}
bool Flame::FileResolvingTask::abort()
{
bool aborted = true;
if (m_dljob)
aborted &= m_dljob->abort();
if (m_checkJob)
aborted &= m_checkJob->abort();
if (m_task) {
aborted = m_task->abort();
}
return aborted ? Task::abort() : false;
}
void Flame::FileResolvingTask::executeTask()
{
if (m_toProcess.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately
if (m_manifest.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));
result.reset(new QByteArray());
// build json data to send
QJsonObject object;
m_result.reset(new QByteArray());
object["fileIds"] = QJsonArray::fromVariantList(
std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
l.push_back(s.fileId);
return l;
}));
QByteArray data = Json::toText(object);
auto dl = Net::ApiUpload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data);
m_dljob->addNetAction(dl);
QStringList fileIds;
for (auto file : m_manifest.files) {
fileIds.push_back(QString::number(file.fileId));
}
m_task = flameAPI.getFiles(fileIds, m_result);
auto step_progress = std::make_shared<TaskStepProgress>();
connect(m_dljob.get(), &NetJob::finished, this, [this, step_progress]() {
connect(m_task.get(), &Task::finished, this, [this, step_progress]() {
step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress);
netJobFinished();
});
connect(m_dljob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) {
step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress);
emitFailed(reason);
});
connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_dljob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
stepProgress(*step_progress);
});
connect(m_dljob.get(), &NetJob::status, this, [this, step_progress](QString status) {
connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_dljob->start();
m_task->start();
}
void Flame::FileResolvingTask::netJobFinished()
{
setProgress(1, 3);
// job to check modrinth for blocked projects
m_checkJob.reset(new NetJob("Modrinth check", m_network));
m_checkJob->setAskRetry(false);
blockedProjects = QMap<File*, std::shared_ptr<QByteArray>>();
QJsonDocument doc;
QJsonArray array;
try {
doc = Json::requireDocument(*result);
doc = Json::requireDocument(*m_result);
array = Json::requireArray(doc.object()["data"]);
} catch (Json::JsonException& e) {
qCritical() << "Non-JSON data returned from the CF API";
@ -92,125 +106,157 @@ void Flame::FileResolvingTask::netJobFinished()
return;
}
QStringList hashes;
for (QJsonValueRef file : array) {
auto fileid = Json::requireInteger(Json::requireObject(file)["id"]);
auto& out = m_toProcess.files[fileid];
try {
out.parseFromObject(Json::requireObject(file));
} catch ([[maybe_unused]] const JSONValidationError& e) {
qDebug() << "Blocked mod on curseforge" << out.fileName;
auto hash = out.hash;
if (!hash.isEmpty()) {
auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash);
auto output = std::make_shared<QByteArray>();
auto dl = Net::ApiDownload::makeByteArray(QUrl(url), output);
QObject::connect(dl.get(), &Task::succeeded, [&out]() { out.resolved = true; });
m_checkJob->addNetAction(dl);
blockedProjects.insert(&out, output);
auto obj = Json::requireObject(file);
auto version = FlameMod::loadIndexedPackVersion(obj);
auto fileid = version.fileId.toInt();
m_manifest.files[fileid].version = version;
auto url = QUrl(version.downloadUrl, QUrl::TolerantMode);
if (!url.isValid() && "sha1" == version.hash_type && !version.hash.isEmpty()) {
hashes.push_back(version.hash);
}
} catch (Json::JsonException& e) {
qCritical() << "Non-JSON data returned from the CF API";
qCritical() << e.cause();
emitFailed(tr("Invalid data returned from the API."));
return;
}
}
if (hashes.isEmpty()) {
getFlameProjects();
return;
}
m_result.reset(new QByteArray());
m_task = modrinthAPI.currentVersions(hashes, "sha1", m_result);
(dynamic_cast<NetJob*>(m_task.get()))->setAskRetry(false);
auto step_progress = std::make_shared<TaskStepProgress>();
connect(m_checkJob.get(), &NetJob::finished, this, [this, step_progress]() {
connect(m_task.get(), &Task::finished, this, [this, step_progress]() {
step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress);
modrinthCheckFinished();
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*m_result, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *m_result;
failed(parse_error.errorString());
return;
}
try {
auto entries = Json::requireObject(doc);
for (auto& out : m_manifest.files) {
auto url = QUrl(out.version.downloadUrl, QUrl::TolerantMode);
if (!url.isValid() && "sha1" == out.version.hash_type && !out.version.hash.isEmpty()) {
try {
auto entry = Json::requireObject(entries, out.version.hash);
auto file = Modrinth::loadIndexedPackVersion(entry);
// If there's more than one mod loader for this version, we can't know for sure
// which file is relative to each loader, so it's best to not use any one and
// let the user download it manually.
if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) {
out.version.downloadUrl = file.downloadUrl;
qDebug() << "Found alternative on modrinth " << out.version.fileName;
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << entries;
}
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
getFlameProjects();
});
connect(m_checkJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) {
step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress);
});
connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_checkJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
stepProgress(*step_progress);
});
connect(m_checkJob.get(), &NetJob::status, this, [this, step_progress](QString status) {
connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_checkJob->start();
m_task->start();
}
void Flame::FileResolvingTask::modrinthCheckFinished()
void Flame::FileResolvingTask::getFlameProjects()
{
setProgress(2, 3);
qDebug() << "Finished with blocked mods : " << blockedProjects.size();
for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) {
auto& out = *it;
auto bytes = blockedProjects[out];
if (!out->resolved) {
continue;
}
QJsonDocument doc = QJsonDocument::fromJson(*bytes);
auto obj = doc.object();
auto file = Modrinth::loadIndexedPackVersion(obj);
// If there's more than one mod loader for this version, we can't know for sure
// which file is relative to each loader, so it's best to not use any one and
// let the user download it manually.
if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) {
out->url = file.downloadUrl;
qDebug() << "Found alternative on modrinth " << out->fileName;
} else {
out->resolved = false;
}
m_result.reset(new QByteArray());
QStringList addonIds;
for (auto file : m_manifest.files) {
addonIds.push_back(QString::number(file.projectId));
}
// 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
if (!block->empty()) {
// 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) {
auto projectId = mod->projectId;
auto output = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId);
auto dl = Net::ApiDownload::makeByteArray(url, output);
qDebug() << "Fetching url slug for file:" << mod->fileName;
QObject::connect(dl.get(), &Task::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 link = QString("%1/download/%2").arg(base, QString::number(mod->fileId));
mod->websiteUrl = link;
});
m_slugJob->addNetAction(dl);
index++;
}
auto step_progress = std::make_shared<TaskStepProgress>();
connect(m_slugJob.get(), &NetJob::succeeded, this, [this, step_progress]() {
step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress);
emitSucceeded();
});
connect(m_slugJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress);
emitFailed(reason);
});
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);
stepProgress(*step_progress);
});
connect(m_slugJob.get(), &NetJob::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_slugJob->start();
} else {
m_task = flameAPI.getProjects(addonIds, m_result);
auto step_progress = std::make_shared<TaskStepProgress>();
connect(m_task.get(), &Task::succeeded, this, [this, step_progress] {
QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*m_result, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *m_result;
return;
}
try {
QJsonArray entries;
entries = Json::requireArray(Json::requireObject(doc), "data");
for (auto entry : entries) {
auto entry_obj = Json::requireObject(entry);
auto id = Json::requireInteger(entry_obj, "id");
auto file = std::find_if(m_manifest.files.begin(), m_manifest.files.end(),
[id](const Flame::File& file) { return file.projectId == id; });
if (file == m_manifest.files.end()) {
continue;
}
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName));
FlameMod::loadIndexedPack(file->pack, entry_obj);
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress);
emitSucceeded();
}
});
connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) {
step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress);
emitFailed(reason);
});
connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
stepProgress(*step_progress);
});
connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_task->start();
}

View File

@ -1,7 +1,25 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 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 <QNetworkAccessManager>
#include "PackManifest.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
namespace Flame {
@ -9,12 +27,12 @@ class FileResolvingTask : public Task {
Q_OBJECT
public:
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess);
virtual ~FileResolvingTask() {};
virtual ~FileResolvingTask() = default;
bool canAbort() const override { return true; }
bool abort() override;
const Flame::Manifest& getResults() const { return m_toProcess; }
const Flame::Manifest& getResults() const { return m_manifest; }
protected:
virtual void executeTask() override;
@ -22,16 +40,13 @@ class FileResolvingTask : public Task {
protected slots:
void netJobFinished();
private:
void getFlameProjects();
private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
Flame::Manifest m_toProcess;
std::shared_ptr<QByteArray> result;
NetJob::Ptr m_dljob;
NetJob::Ptr m_checkJob;
NetJob::Ptr m_slugJob;
void modrinthCheckFinished();
QMap<File*, std::shared_ptr<QByteArray>> blockedProjects;
Flame::Manifest m_manifest;
std::shared_ptr<QByteArray> m_result;
Task::Ptr m_task;
};
} // namespace Flame

View File

@ -35,8 +35,11 @@
#include "FlameInstanceCreationTask.h"
#include "QObjectPtr.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
#include "modplatform/flame/FileResolvingTask.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/flame/PackManifest.h"
#include "Application.h"
@ -51,6 +54,7 @@
#include "settings/INISettingsObject.h"
#include "tasks/ConcurrentTask.h"
#include "ui/dialogs/BlockedModsDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
@ -58,7 +62,6 @@
#include <QFileInfo>
#include "meta/Index.h"
#include "meta/VersionList.h"
#include "minecraft/World.h"
#include "minecraft/mod/tasks/LocalResourceParse.h"
#include "net/ApiDownload.h"
@ -208,8 +211,7 @@ bool FlameCreationTask::updateInstance()
Flame::File file;
// We don't care about blocked mods, we just need local data to delete the file
file.parseFromObject(entry_obj, false);
file.version = FlameMod::loadIndexedPackVersion(entry_obj);
auto id = Json::requireInteger(entry_obj, "id");
old_files.insert(id, file);
}
@ -219,10 +221,10 @@ bool FlameCreationTask::updateInstance()
// Delete the files
for (auto& file : old_files) {
if (file.fileName.isEmpty() || file.targetFolder.isEmpty())
if (file.version.fileName.isEmpty() || file.targetFolder.isEmpty())
continue;
QString relative_path(FS::PathCombine(file.targetFolder, file.fileName));
QString relative_path(FS::PathCombine(file.targetFolder, file.version.fileName));
qDebug() << "Scheduling" << relative_path << "for removal";
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path));
}
@ -471,15 +473,15 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
QList<BlockedMod> blocked_mods;
auto anyBlocked = false;
for (const auto& result : results.files.values()) {
if (result.fileName.endsWith(".zip")) {
m_ZIP_resources.append(std::make_pair(result.fileName, result.targetFolder));
if (result.version.fileName.endsWith(".zip")) {
m_ZIP_resources.append(std::make_pair(result.version.fileName, result.targetFolder));
}
if (!result.resolved || result.url.isEmpty()) {
if (result.version.downloadUrl.isEmpty()) {
BlockedMod blocked_mod;
blocked_mod.name = result.fileName;
blocked_mod.websiteUrl = result.websiteUrl;
blocked_mod.hash = result.hash;
blocked_mod.name = result.version.fileName;
blocked_mod.websiteUrl = QString("%1/download/%2").arg(result.pack.websiteUrl, QString::number(result.fileId));
blocked_mod.hash = result.version.hash;
blocked_mod.matched = false;
blocked_mod.localPath = "";
blocked_mod.targetFolder = result.targetFolder;
@ -521,7 +523,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
QStringList optionalFiles;
for (auto& result : results) {
if (!result.required) {
optionalFiles << FS::PathCombine(result.targetFolder, result.fileName);
optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName);
}
}
@ -537,7 +539,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
selectedOptionalMods = optionalModDialog.getResult();
}
for (const auto& result : results) {
auto fileName = result.fileName;
auto fileName = result.version.fileName;
fileName = FS::RemoveInvalidPathChars(fileName);
auto relpath = FS::PathCombine(result.targetFolder, fileName);
@ -548,36 +550,16 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
relpath = FS::PathCombine("minecraft", relpath);
auto path = FS::PathCombine(m_stagingPath, relpath);
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// 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()) {
qDebug() << "Will download" << result.url << "to" << path;
auto dl = Net::ApiDownload::makeFile(result.url, path);
m_files_job->addNetAction(dl);
}
break;
}
case Flame::File::Type::Modpack:
logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath));
break;
case Flame::File::Type::Cmod2:
case Flame::File::Type::Ctoc:
case Flame::File::Type::Unknown:
logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
break;
if (!result.version.downloadUrl.isEmpty()) {
qDebug() << "Will download" << result.version.downloadUrl << "to" << path;
auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path);
m_files_job->addNetAction(dl);
}
}
m_mod_id_resolver.reset();
connect(m_files_job.get(), &NetJob::succeeded, this, [&]() {
connect(m_files_job.get(), &NetJob::finished, this, [this, &loop]() {
m_files_job.reset();
validateZIPResources();
validateZIPResources(loop);
});
connect(m_files_job.get(), &NetJob::failed, [&](QString reason) {
m_files_job.reset();
@ -588,7 +570,6 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
setProgress(current, total);
});
connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
setStatus(tr("Downloading mods..."));
m_files_job->start();
@ -626,9 +607,10 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
setAbortable(true);
}
void FlameCreationTask::validateZIPResources()
void FlameCreationTask::validateZIPResources(QEventLoop& loop)
{
qDebug() << "Validating whether resources stored as .zip are in the right place";
QStringList zipMods;
for (auto [fileName, targetFolder] : m_ZIP_resources) {
qDebug() << "Checking" << fileName << "...";
auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName);
@ -668,6 +650,7 @@ void FlameCreationTask::validateZIPResources()
switch (type) {
case PackedResourceType::Mod:
validatePath(fileName, targetFolder, "mods");
zipMods.push_back(fileName);
break;
case PackedResourceType::ResourcePack:
validatePath(fileName, targetFolder, "resourcepacks");
@ -693,4 +676,16 @@ void FlameCreationTask::validateZIPResources()
break;
}
}
auto task = makeShared<ConcurrentTask>(this, "CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
auto results = m_mod_id_resolver->getResults().files;
auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index");
for (auto file : results) {
if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) {
continue;
}
task->addTask(makeShared<LocalModUpdateTask>(folder, file.pack, file.version));
}
connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
m_process_update_file_info_job = task;
task->start();
}

View File

@ -74,7 +74,7 @@ class FlameCreationTask final : public InstanceCreationTask {
void idResolverSucceeded(QEventLoop&);
void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
void validateZIPResources();
void validateZIPResources(QEventLoop& loop);
QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
private:

View File

@ -68,35 +68,3 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
}
loadManifestV1(m, obj);
}
bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked)
{
fileName = Json::requireString(obj, "fileName");
// This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
// It is also optional
type = File::Type::SingleFile;
targetFolder = "mods";
// get the hash
hash = QString();
auto hashes = Json::ensureArray(obj, "hashes");
for (QJsonValueRef item : hashes) {
auto hobj = Json::requireObject(item);
auto algo = Json::requireInteger(hobj, "algo");
auto value = Json::requireString(hobj, "value");
if (algo == 1) {
hash = value;
}
}
// may throw, if the project is blocked
QString rawUrl = Json::ensureString(obj, "downloadUrl");
url = QUrl(rawUrl, QUrl::TolerantMode);
if (!url.isValid() && throw_on_blocked) {
throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
}
resolved = true;
return true;
}

View File

@ -40,26 +40,20 @@
#include <QString>
#include <QUrl>
#include <QVector>
#include "modplatform/ModIndex.h"
namespace Flame {
struct File {
// NOTE: throws JSONValidationError
bool parseFromObject(const QJsonObject& object, bool throw_on_blocked = true);
int projectId = 0;
int fileId = 0;
// NOTE: the opposite to 'optional'
bool required = true;
QString hash;
// NOTE: only set on blocked files ! Empty otherwise.
QString websiteUrl;
ModPlatform::IndexedPack pack;
ModPlatform::IndexedVersion version;
// our
bool resolved = false;
QString fileName;
QUrl url;
QString targetFolder = QStringLiteral("mods");
enum class Type { Unknown, Folder, Ctoc, SingleFile, Cmod2, Modpack, Mod } type = Type::Mod;
};
struct Modloader {

View File

@ -5,8 +5,12 @@
#include "InstanceList.h"
#include "Json.h"
#include "QObjectPtr.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/Mod.h"
#include "modplatform/EnsureMetadataTask.h"
#include "modplatform/helpers/OverrideUtils.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
@ -21,6 +25,7 @@
#include <QAbstractButton>
#include <QFileInfo>
#include <QHash>
#include <vector>
bool ModrinthCreationTask::abort()
@ -29,8 +34,8 @@ bool ModrinthCreationTask::abort()
return false;
m_abort = true;
if (m_files_job)
m_files_job->abort();
if (m_task)
m_task->abort();
return Task::abort();
}
@ -234,11 +239,11 @@ bool ModrinthCreationTask::createInstance()
instance.setName(name());
instance.saveNow();
m_files_job.reset(new NetJob(tr("Mod Download Modrinth"), APPLICATION->network()));
auto downloadMods = makeShared<NetJob>(tr("Mod Download Modrinth"), APPLICATION->network());
auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path);
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
QHash<QString, Mod*> mods;
for (auto file : m_files) {
auto fileName = file.path;
fileName = FS::RemoveInvalidPathChars(fileName);
@ -249,20 +254,27 @@ bool ModrinthCreationTask::createInstance()
.arg(fileName));
return false;
}
if (fileName.startsWith("mods/")) {
auto mod = new Mod(file_path);
ModDetails d;
d.mod_id = file_path;
mod->setDetails(d);
mods[file.hash.toHex()] = mod;
}
qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path;
auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_files_job->addNetAction(dl);
downloadMods->addNetAction(dl);
if (!file.downloads.empty()) {
// FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :)
auto param = dl.toWeakRef();
connect(dl.get(), &Task::failed, [this, &file, file_path, param] {
connect(dl.get(), &Task::failed, [&file, file_path, param, downloadMods] {
auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_files_job->addNetAction(ndl);
downloadMods->addNetAction(ndl);
if (auto shared = param.lock())
shared->succeeded();
});
@ -271,23 +283,44 @@ bool ModrinthCreationTask::createInstance()
bool ended_well = false;
connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { ended_well = true; });
connect(m_files_job.get(), &NetJob::failed, [&](const QString& reason) {
connect(downloadMods.get(), &NetJob::succeeded, this, [&]() { ended_well = true; });
connect(downloadMods.get(), &NetJob::failed, [&](const QString& reason) {
ended_well = false;
setError(reason);
});
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit);
connect(downloadMods.get(), &NetJob::progress, [&](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, &ModrinthCreationTask::propagateStepProgress);
connect(downloadMods.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress);
setStatus(tr("Downloading mods..."));
m_files_job->start();
downloadMods->start();
m_task = downloadMods;
loop.exec();
QEventLoop ensureMetaLoop;
QDir folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index");
auto ensureMetadataTask = makeShared<EnsureMetadataTask>(mods, folder, ModPlatform::ResourceProvider::MODRINTH);
connect(ensureMetadataTask.get(), &Task::succeeded, this, [&]() { ended_well = true; });
connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit);
connect(ensureMetadataTask.get(), &Task::progress, [&](qint64 current, qint64 total) {
setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, total);
});
connect(ensureMetadataTask.get(), &Task::stepProgress, this, &ModrinthCreationTask::propagateStepProgress);
ensureMetadataTask->start();
m_task = ensureMetadataTask;
ensureMetaLoop.exec();
for (auto m : mods) {
delete m;
}
mods.clear();
// Update information of the already installed instance, if any.
if (m_instance && ended_well) {
setAbortable(false);

View File

@ -1,15 +1,11 @@
#pragma once
#include <optional>
#include "BaseInstance.h"
#include "InstanceCreationTask.h"
#include <optional>
#include "minecraft/MinecraftInstance.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "net/NetJob.h"
class ModrinthCreationTask final : public InstanceCreationTask {
Q_OBJECT
@ -43,7 +39,7 @@ class ModrinthCreationTask final : public InstanceCreationTask {
QString m_managed_id, m_managed_version_id, m_managed_name;
std::vector<Modrinth::File> m_files;
NetJob::Ptr m_files_job;
Task::Ptr m_task;
std::optional<InstancePtr> m_instance;