Merge pull request #2583 from Trial97/metadata2
Generate updater metadata for mods added/updated using modpack updater/installer
This commit is contained in:
commit
926a6bc72a
@ -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;
|
||||
};
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user