From 844b2457769d61131f97b5e82bb134568dfd42ed Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:08:01 -0300 Subject: [PATCH] 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 --- launcher/CMakeLists.txt | 3 + launcher/modplatform/EnsureMetadataTask.cpp | 244 ++++++++++++++++++++ launcher/modplatform/EnsureMetadataTask.h | 41 ++++ 3 files changed, 288 insertions(+) create mode 100644 launcher/modplatform/EnsureMetadataTask.cpp create mode 100644 launcher/modplatform/EnsureMetadataTask.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6dcea8e2..25546c38 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -482,6 +482,9 @@ set(API_SOURCES modplatform/ModAPI.h + modplatform/EnsureMetadataTask.h + modplatform/EnsureMetadataTask.cpp + modplatform/flame/FlameAPI.h modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp new file mode 100644 index 00000000..dc92d8ab --- /dev/null +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -0,0 +1,244 @@ +#include "EnsureMetadataTask.h" + +#include +#include + +#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 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); +} diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h new file mode 100644 index 00000000..624e253a --- /dev/null +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -0,0 +1,41 @@ +#pragma once + +#include "ModIndex.h" +#include "tasks/SequentialTask.h" + +class Mod; +class QDir; +class MultipleOptionsTask; + +class EnsureMetadataTask : public Task { + Q_OBJECT + + public: + EnsureMetadataTask(Mod&, QDir&, bool try_all, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + + public slots: + bool abort() override; + protected slots: + void executeTask() override; + + private: + // FIXME: Move to their own namespace + void modrinthEnsureMetadata(SequentialTask&, QByteArray&); + void flameEnsureMetadata(SequentialTask&, QByteArray&); + + // Helpers + void emitReady(); + void emitFail(); + + signals: + void metadataReady(); + void metadataFailed(); + + private: + Mod& m_mod; + QDir& m_index_dir; + ModPlatform::Provider m_provider; + bool m_try_all; + + MultipleOptionsTask* m_task_handler = nullptr; +};