PrismLauncher/launcher/modplatform/EnsureMetadataTask.cpp
flow 844b245776
feat: add EnsureMetadataTask
This task is responsible for checking if the mod has metadata for a
specific provider, and create it if it doesn't.

In the context of the mod updater, this is not the best architecture,
since we do a single task for each mod. However, this way of structuring
it allows us to use it later on in more diverse scenarios.

This way we decouple this task from the mod updater, trading off some performance
(though that will be mitigated when we have a way of running arbitrary tasks
concurrently).

Signed-off-by: flow <flowlnlnln@gmail.com>
2022-07-17 11:33:42 -03:00

245 lines
8.0 KiB
C++

#include "EnsureMetadataTask.h"
#include <MurmurHash2.h>
#include <QDebug>
#include "FileSystem.h"
#include "Json.h"
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.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/MultipleOptionsTask.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
static ModrinthAPI modrinth_api;
static FlameAPI flame_api;
EnsureMetadataTask::EnsureMetadataTask(Mod& mod, QDir& dir, bool try_all, ModPlatform::Provider prov)
: m_mod(mod), m_index_dir(dir), m_provider(prov), m_try_all(try_all)
{}
bool EnsureMetadataTask::abort()
{
return m_task_handler->abort();
}
void EnsureMetadataTask::executeTask()
{
// They already have the right metadata :o
if (m_mod.status() != ModStatus::NoMetadata && m_mod.metadata() && m_mod.metadata()->provider == m_provider) {
emitReady();
return;
}
// Folders don't have metadata
if (m_mod.type() == Mod::MOD_FOLDER) {
emitReady();
return;
}
setStatus(tr("Generating %1's metadata...").arg(m_mod.name()));
qDebug() << QString("Generating %1's metadata...").arg(m_mod.name());
QByteArray jar_data;
try {
jar_data = FS::read(m_mod.fileinfo().absoluteFilePath());
} catch (FS::FileSystemException& e) {
qCritical() << QString("Failed to open / read JAR file of %1").arg(m_mod.name());
qCritical() << QString("Reason: ") << e.cause();
emitFail();
return;
}
auto tsk = new MultipleOptionsTask(nullptr, "GetMetadataTask");
switch (m_provider) {
case (ModPlatform::Provider::MODRINTH):
modrinthEnsureMetadata(*tsk, jar_data);
if (m_try_all)
flameEnsureMetadata(*tsk, jar_data);
break;
case (ModPlatform::Provider::FLAME):
flameEnsureMetadata(*tsk, jar_data);
if (m_try_all)
modrinthEnsureMetadata(*tsk, jar_data);
break;
}
connect(tsk, &MultipleOptionsTask::finished, this, [tsk] { tsk->deleteLater(); });
connect(tsk, &MultipleOptionsTask::failed, [this] {
qCritical() << QString("Download of %1's metadata failed").arg(m_mod.name());
emitFail();
});
connect(tsk, &MultipleOptionsTask::succeeded, this, &EnsureMetadataTask::emitReady);
m_task_handler = tsk;
tsk->start();
}
void EnsureMetadataTask::emitReady()
{
emit metadataReady();
emitSucceeded();
}
void EnsureMetadataTask::emitFail()
{
qDebug() << QString("Failed to generate metadata for %1").arg(m_mod.name());
emit metadataFailed();
//emitFailed(tr("Failed to generate metadata for %1").arg(m_mod.name()));
emitSucceeded();
}
void EnsureMetadataTask::modrinthEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data)
{
// Modrinth currently garantees that some hash types will always be present.
// But let's be sure and cover all cases anyways :)
for (auto hash_type : ProviderCaps.hashType(ModPlatform::Provider::MODRINTH)) {
auto* response = new QByteArray();
auto hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex());
auto ver_task = modrinth_api.currentVersion(hash, hash_type, response);
// Prevents unfortunate timings when aborting the task
if (!ver_task)
return;
connect(ver_task.get(), &NetJob::succeeded, this, [this, ver_task, response] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from " << m_mod.name() << " at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
ver_task->failed(parse_error.errorString());
return;
}
auto doc_obj = Json::requireObject(doc);
auto ver = Modrinth::loadIndexedPackVersion(doc_obj, {}, m_mod.fileinfo().fileName());
// Minimal IndexedPack to create the metadata
ModPlatform::IndexedPack pack;
pack.name = m_mod.name();
pack.provider = ModPlatform::Provider::MODRINTH;
pack.addonId = ver.addonId;
// Prevent file name mismatch
ver.fileName = m_mod.fileinfo().fileName();
QDir tmp_index_dir(m_index_dir);
{
LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
QEventLoop loop;
QTimer timeout;
QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
update_metadata.start();
timeout.start(100);
loop.exec();
}
auto mod_name = m_mod.name();
auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name));
m_mod.setMetadata(meta);
});
tsk.addTask(ver_task);
}
}
void EnsureMetadataTask::flameEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data)
{
QByteArray jar_data_treated;
for (char c : jar_data) {
// CF-specific
if (!(c == 9 || c == 10 || c == 13 || c == 32))
jar_data_treated.push_back(c);
}
auto* response = new QByteArray();
std::list<uint> fingerprints;
auto murmur = MurmurHash2(jar_data_treated, jar_data_treated.length());
fingerprints.push_back(murmur);
auto ver_task = flame_api.matchFingerprints(fingerprints, response);
connect(ver_task.get(), &Task::succeeded, this, [this, ver_task, response] {
QDir tmp_index_dir(m_index_dir);
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from " << m_mod.name() << " at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
ver_task->failed(parse_error.errorString());
return;
}
try {
auto doc_obj = Json::requireObject(doc);
auto data_obj = Json::ensureObject(doc_obj, "data");
auto match_obj = Json::ensureObject(Json::ensureArray(data_obj, "exactMatches")[0], {});
if (match_obj.isEmpty()) {
qCritical() << "Fingerprint match is empty!";
ver_task->failed(parse_error.errorString());
return;
}
auto file_obj = Json::ensureObject(match_obj, "file");
ModPlatform::IndexedPack pack;
pack.name = m_mod.name();
pack.provider = ModPlatform::Provider::FLAME;
pack.addonId = Json::requireInteger(file_obj, "modId");
ModPlatform::IndexedVersion ver = FlameMod::loadIndexedPackVersion(file_obj);
// Prevent file name mismatch
ver.fileName = m_mod.fileinfo().fileName();
{
LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
QEventLoop loop;
QTimer timeout;
QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
update_metadata.start();
timeout.start(100);
loop.exec();
}
auto mod_name = m_mod.name();
auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name));
m_mod.setMetadata(meta);
} catch (Json::JsonException& e) {
emitFailed(e.cause() + " : " + e.what());
}
});
tsk.addTask(ver_task);
}