From 0e52112016fe9942f7448cd83914f6266904c311 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:04:49 -0300 Subject: feat: add some api calls to modrinth Calls added: - Get version from hash - Get versions from hashes - Latest version of a project from a hash, loader(s), and game version(s) - Latest versions of multiple project from hashes, loader(s), and game version(s) Some of those are not used yet, but may be of use later on, so we have it if we need it :) Signed-off-by: flow --- launcher/modplatform/ModIndex.h | 1 + launcher/modplatform/modrinth/ModrinthAPI.cpp | 97 ++++++++++++++++++++++ launcher/modplatform/modrinth/ModrinthAPI.h | 22 +++++ .../modplatform/modrinth/ModrinthPackIndex.cpp | 26 ++++-- launcher/modplatform/modrinth/ModrinthPackIndex.h | 2 +- 5 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 launcher/modplatform/modrinth/ModrinthAPI.cpp (limited to 'launcher/modplatform') diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index c27643af..459eb261 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -61,6 +61,7 @@ struct IndexedVersion { QVector loaders = {}; QString hash_type; QString hash; + bool is_preferred = true; }; struct ExtraPackData { diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp new file mode 100644 index 00000000..0d5d4bab --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -0,0 +1,97 @@ +#include "ModrinthAPI.h" + +#include "Application.h" +#include "Json.h" +#include "net/Upload.h" + +auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); + + netJob->addNetAction(Net::Download::makeByteArray( + QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); + + QJsonObject body_obj; + + Json::writeStringList(body_obj, "hashes", hashes); + Json::writeString(body_obj, "algorithm", hash_format); + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto ModrinthAPI::latestVersion(QString hash, + QString hash_format, + std::list mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); + + QJsonObject body_obj; + + Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders)); + + QStringList game_versions; + for (auto& ver : mcVersions) { + game_versions.append(ver.toString()); + } + Json::writeStringList(body_obj, "game_versions", game_versions); + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray( + QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto ModrinthAPI::latestVersions(const QStringList& hashes, + QString hash_format, + std::list mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); + + QJsonObject body_obj; + + Json::writeStringList(body_obj, "hashes", hashes); + Json::writeString(body_obj, "algorithm", hash_format); + + Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders)); + + QStringList game_versions; + for (auto& ver : mcVersions) { + game_versions.append(ver.toString()); + } + Json::writeStringList(body_obj, "game_versions", game_versions); + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray( + QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 89e52d6c..9694b85e 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -22,10 +22,32 @@ #include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" +#include "net/NetJob.h" #include class ModrinthAPI : public NetworkModAPI { + public: + auto currentVersion(QString hash, + QString hash_format, + QByteArray* response) -> NetJob::Ptr; + + auto currentVersions(const QStringList& hashes, + QString hash_format, + QByteArray* response) -> NetJob::Ptr; + + auto latestVersion(QString hash, + QString hash_format, + std::list mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr; + + auto latestVersions(const QStringList& hashes, + QString hash_format, + std::list mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr; + public: inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index b6f5490a..4e738819 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -111,7 +111,7 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versionsLoaded = true; } -auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedVersion +auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_type, QString preferred_file_name) -> ModPlatform::IndexedVersion { ModPlatform::IndexedVersion file; @@ -142,6 +142,11 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV auto parent = files[i].toObject(); auto fileName = Json::requireString(parent, "filename"); + if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name)) { + file.is_preferred = true; + break; + } + // Grab the primary file, if available if (Json::requireBoolean(parent, "primary")) break; @@ -153,13 +158,20 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); + file.is_preferred = Json::requireBoolean(parent, "primary"); auto hash_list = Json::requireObject(parent, "hashes"); - auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH); - for (auto& hash_type : hash_types) { - if (hash_list.contains(hash_type)) { - file.hash = Json::requireString(hash_list, hash_type); - file.hash_type = hash_type; - break; + + if (hash_list.contains(preferred_hash_type)) { + file.hash = Json::requireString(hash_list, preferred_hash_type); + file.hash_type = preferred_hash_type; + } else { + auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH); + for (auto& hash_type : hash_types) { + if (hash_list.contains(hash_type)) { + file.hash = Json::requireString(hash_list, hash_type); + file.hash_type = hash_type; + break; + } } } diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index b7936204..31881414 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -30,6 +30,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, BaseInstance* inst); -auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; +auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; } // namespace Modrinth -- cgit From 4bcf8e6975d4f314654062dde01d627ed7495866 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:06:51 -0300 Subject: feat: add api call to flame Call added: - Get Fingerprints Matches - Get Mod File Changelog Signed-off-by: flow --- launcher/modplatform/flame/FlameAPI.cpp | 125 +++++++++++++++++++++++++++ launcher/modplatform/flame/FlameAPI.h | 8 ++ launcher/modplatform/flame/FlameModIndex.cpp | 2 +- 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 launcher/modplatform/flame/FlameAPI.cpp (limited to 'launcher/modplatform') diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp new file mode 100644 index 00000000..983e09fd --- /dev/null +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -0,0 +1,125 @@ +#include "FlameAPI.h" +#include "FlameModIndex.h" + +#include "Application.h" +#include "BuildConfig.h" +#include "Json.h" + +#include "net/Upload.h" + +auto FlameAPI::matchFingerprints(const std::list& fingerprints, QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Flame::MatchFingerprints"), APPLICATION->network()); + + QJsonObject body_obj; + QJsonArray fingerprints_arr; + for (auto& fp : fingerprints) { + fingerprints_arr.append(QString("%1").arg(fp)); + } + + body_obj["fingerprints"] = fingerprints_arr; + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString +{ + QEventLoop lock; + QString changelog; + + auto* netJob = new NetJob(QString("Flame::FileChangelog"), APPLICATION->network()); + auto* response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray( + QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog") + .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))), + response)); + + QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &changelog] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame::FileChangelog at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + netJob->failed(parse_error.errorString()); + return; + } + + changelog = Json::ensureString(doc.object(), "data"); + }); + + QObject::connect(netJob, &NetJob::finished, [response, &lock] { + delete response; + lock.quit(); + }); + + netJob->start(); + lock.exec(); + + return changelog; +} + +auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion +{ + QEventLoop loop; + + auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network()); + auto response = new QByteArray(); + ModPlatform::IndexedVersion ver; + + netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response)); + + QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + auto obj = Json::requireObject(doc); + auto arr = Json::requireArray(obj, "data"); + + QJsonObject latest_file_obj; + ModPlatform::IndexedVersion ver_tmp; + + for (auto file : arr) { + auto file_obj = Json::requireObject(file); + auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj); + if(file_tmp.date > ver_tmp.date) { + ver_tmp = file_tmp; + latest_file_obj = file_obj; + } + } + + ver = FlameMod::loadIndexedPackVersion(latest_file_obj); + } catch (Json::JsonException& e) { + qCritical() << "Failed to parse response from a version request."; + qCritical() << e.what(); + qDebug() << doc; + } + }); + + QObject::connect(netJob, &NetJob::finished, [response, netJob, &loop] { + netJob->deleteLater(); + delete response; + loop.quit(); + }); + + netJob->start(); + + loop.exec(); + + return ver; +} diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index aea76ff1..e45b5cb1 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -3,7 +3,15 @@ #include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" +#include "net/NetJob.h" + class FlameAPI : public NetworkModAPI { + public: + auto matchFingerprints(const std::list& fingerprints, QByteArray* response) -> NetJob::Ptr; + auto getModFileChangelog(int modId, int fileId) -> QString; + + auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; + private: inline auto getSortFieldInt(QString sortString) const -> int { diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index b99bfb26..0ad1d4ba 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -110,7 +110,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV file.fileId = Json::requireInteger(obj, "id"); file.date = Json::requireString(obj, "fileDate"); file.version = Json::requireString(obj, "displayName"); - file.downloadUrl = Json::requireString(obj, "downloadUrl"); + file.downloadUrl = Json::ensureString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); auto hash_list = Json::ensureArray(obj, "hashes"); -- cgit From 844b2457769d61131f97b5e82bb134568dfd42ed Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:08:01 -0300 Subject: 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/modplatform/EnsureMetadataTask.cpp | 244 ++++++++++++++++++++++++++++ launcher/modplatform/EnsureMetadataTask.h | 41 +++++ 2 files changed, 285 insertions(+) create mode 100644 launcher/modplatform/EnsureMetadataTask.cpp create mode 100644 launcher/modplatform/EnsureMetadataTask.h (limited to 'launcher/modplatform') 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; +}; -- cgit From c3f6c3dd8228b23b403af6b091a888ea3cc436ca Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 20:45:44 -0300 Subject: feat: add changelog to mod providers The Modrinth changelog is fairly straight-forward, as it's given to us directly with the API call we already did. Flame, on the other hand, requires us to do another call to get the changelog, so it can introduce quite a heavy performance impact. This way, we make it optional to get such changelog. Signed-off-by: flow --- launcher/modplatform/ModIndex.h | 1 + launcher/modplatform/flame/FlameModIndex.cpp | 7 ++++++- launcher/modplatform/flame/FlameModIndex.h | 2 +- launcher/modplatform/modrinth/ModrinthPackIndex.cpp | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) (limited to 'launcher/modplatform') diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 459eb261..966082ab 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -62,6 +62,7 @@ struct IndexedVersion { QString hash_type; QString hash; bool is_preferred = true; + QString changelog; }; struct ExtraPackData { diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 0ad1d4ba..a3222f44 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -7,6 +7,7 @@ #include "net/NetJob.h" static ModPlatform::ProviderCapabilities ProviderCaps; +static FlameAPI api; void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { @@ -91,7 +92,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versionsLoaded = true; } -auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion +auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion { auto versionArray = Json::requireArray(obj, "gameVersions"); if (versionArray.isEmpty()) { @@ -124,5 +125,9 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV break; } } + + if(load_changelog) + file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt()); + return file; } diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index 9c6c1c6c..a839dd83 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -17,6 +17,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, BaseInstance* inst); -auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; +auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion; } // namespace FlameMod diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 4e738819..9736e861 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -130,6 +130,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_t file.loaders.append(loader.toString()); } file.version = Json::requireString(obj, "name"); + file.changelog = Json::requireString(obj, "changelog"); auto files = Json::requireArray(obj, "files"); int i = 0; -- cgit From b8b71c7dd29fbdc6c98d60ec54c57cff74f4cbfd Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 21:26:26 -0300 Subject: feat: add mod update check tasks Those tasks take a list of mods and check on the mod providers for updates. They assume that the mods have metadata already. Signed-off-by: flow --- launcher/modplatform/CheckUpdateTask.h | 51 +++++ launcher/modplatform/ModIndex.h | 1 + launcher/modplatform/flame/FlameCheckUpdate.cpp | 228 +++++++++++++++++++++ launcher/modplatform/flame/FlameCheckUpdate.h | 25 +++ .../modplatform/modrinth/ModrinthCheckUpdate.cpp | 163 +++++++++++++++ .../modplatform/modrinth/ModrinthCheckUpdate.h | 23 +++ .../modplatform/modrinth/ModrinthPackIndex.cpp | 3 +- 7 files changed, 493 insertions(+), 1 deletion(-) create mode 100644 launcher/modplatform/CheckUpdateTask.h create mode 100644 launcher/modplatform/flame/FlameCheckUpdate.cpp create mode 100644 launcher/modplatform/flame/FlameCheckUpdate.h create mode 100644 launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp create mode 100644 launcher/modplatform/modrinth/ModrinthCheckUpdate.h (limited to 'launcher/modplatform') diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h new file mode 100644 index 00000000..8c701e46 --- /dev/null +++ b/launcher/modplatform/CheckUpdateTask.h @@ -0,0 +1,51 @@ +#pragma once + +#include "minecraft/mod/Mod.h" +#include "modplatform/ModAPI.h" +#include "modplatform/ModIndex.h" +#include "tasks/Task.h" + +class ModDownloadTask; +class ModFolderModel; + +class CheckUpdateTask : public Task { + Q_OBJECT + + public: + CheckUpdateTask(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + : m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; + + struct UpdatableMod { + QString name; + QString old_hash; + QString old_version; + QString new_version; + QString changelog; + ModPlatform::Provider provider; + ModDownloadTask* download; + + public: + UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::Provider p, ModDownloadTask* t) + : name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t) + {} + }; + + auto getUpdatable() -> std::vector&& { return std::move(m_updatable); } + + public slots: + bool abort() override = 0; + + protected slots: + void executeTask() override = 0; + + signals: + void checkFailed(Mod failed, QString reason, QUrl recover_url = {}); + + protected: + std::list& m_mods; + std::list& m_game_versions; + ModAPI::ModLoaderTypes m_loaders; + std::shared_ptr m_mods_folder; + + std::vector m_updatable; +}; diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 966082ab..f8ef211e 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -54,6 +54,7 @@ struct IndexedVersion { QVariant addonId; QVariant fileId; QString version; + QString version_number = {}; QVector mcVersion; QString downloadUrl; QString date; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp new file mode 100644 index 00000000..f1983fa4 --- /dev/null +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -0,0 +1,228 @@ +#include "FlameCheckUpdate.h" +#include "FlameAPI.h" +#include "FlameModIndex.h" + +#include + +#include "FileSystem.h" +#include "Json.h" + +#include "ModDownloadTask.h" + +static FlameAPI api; +static ModPlatform::ProviderCapabilities ProviderCaps; + +bool FlameCheckUpdate::abort() +{ + m_was_aborted = true; + if (m_net_job) + return m_net_job->abort(); + return true; +} + +ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info) +{ + ModPlatform::IndexedPack pack; + + QEventLoop loop; + + auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network()); + + auto response = new QByteArray(); + auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString()); + auto dl = Net::Download::makeByteArray(url, response); + get_project_job->addNetAction(dl); + + QObject::connect(get_project_job, &NetJob::succeeded, [response, &pack]() { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::requireObject(doc_obj, "data"); + FlameMod::loadIndexedPack(pack, data_obj); + } catch (Json::JsonException& e) { + qWarning() << e.cause(); + qDebug() << doc; + } + }); + + QObject::connect(get_project_job, &NetJob::finished, [&loop, get_project_job] { + get_project_job->deleteLater(); + loop.quit(); + }); + + get_project_job->start(); + loop.exec(); + + return pack; +} + +/* Check for update: + * - Get latest version available + * - Compare hash of the latest version with the current hash + * - If equal, no updates, else, there's updates, so add to the list + * */ +void FlameCheckUpdate::executeTask() +{ + setStatus(tr("Preparing mods for CurseForge...")); + setProgress(0, 5); + + QHash mappings; + + // Create all hashes + QStringList hashes; + std::list murmur_hashes; + + auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::FLAME).first(); + for (auto mod : m_mods) { + auto hash = mod.metadata()->hash; + + QByteArray jar_data; + + try { + jar_data = FS::read(mod.fileinfo().absoluteFilePath()); + } catch (FS::FileSystemException& e) { + qCritical() << QString("Failed to open / read JAR file of %1").arg(mod.name()); + qCritical() << QString("Reason: ") << e.cause(); + + failed(e.what()); + return; + } + + 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 murmur_hash = MurmurHash2(jar_data_treated, jar_data_treated.length()); + murmur_hashes.emplace_back(murmur_hash); + + // Sadly the API can only handle one hash type per call, se we + // need to generate a new hash if the current one is innadequate + // (though it will rarely happen, if at all) + if (mod.metadata()->hash_format != best_hash_type) + hash = QString(ProviderCaps.hash(ModPlatform::Provider::FLAME, jar_data, best_hash_type).toHex()); + + hashes.append(hash); + mappings.insert(hash, mod); + } + + auto* response = new QByteArray(); + auto job = api.matchFingerprints(murmur_hashes, response); + + QEventLoop lock; + + connect(job.get(), &Task::succeeded, this, [this, response, &mappings] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + failed(parse_error.errorString()); + return; + } + + setStatus(tr("Parsing the first API response from CurseForge...")); + setProgress(2, 5); + + try { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::ensureObject(doc_obj, "data"); + auto match_arr = Json::ensureArray(data_obj, "exactMatches"); + for (auto match : match_arr) { + auto match_obj = Json::ensureObject(match); + + ModPlatform::IndexedVersion current_ver; + try { + auto file_obj = Json::requireObject(match_obj, "file"); + current_ver = FlameMod::loadIndexedPackVersion(file_obj); + } catch (Json::JsonException& e) { + qCritical() << "Error while parsing Flame indexed version"; + qCritical() << e.what(); + failed(tr("An error occured while parsing a CurseForge indexed version!")); + return; + } + + auto mod_iter = mappings.find(current_ver.hash); + if (mod_iter == mappings.end()) { + qCritical() << "Failed to remap mod from Flame!"; + continue; + } + + auto mod = mod_iter.value(); + + setStatus(tr("Waiting for the API response from CurseForge for '%1'...").arg(mod.name())); + setProgress(3, 5); + + auto latest_ver = api.getLatestVersion({ current_ver.addonId.toString(), m_game_versions, m_loaders }); + + // Check if we were aborted while getting the latest version + if (m_was_aborted) { + aborted(); + return; + } + + setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod.name())); + setProgress(4, 5); + + if (!latest_ver.addonId.isValid()) { + emit checkFailed( + mod, + tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader.")); + continue; + } + + if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != current_ver.fileId) { + auto pack = getProjectInfo(latest_ver); + auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString()); + emit checkFailed(mod, tr("Mod has a new update available, but is opted-out on CurseForge"), recover_url); + + continue; + } + + if (!latest_ver.hash.isEmpty() && current_ver.hash != latest_ver.hash) { + // Fake pack with the necessary info to pass to the download task :) + ModPlatform::IndexedPack pack; + pack.name = mod.name(); + pack.addonId = mod.metadata()->project_id; + pack.websiteUrl = mod.homeurl(); + for (auto& author : mod.authors()) + pack.authors.append({ author }); + pack.description = mod.description(); + pack.provider = ModPlatform::Provider::FLAME; + + auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder); + m_updatable.emplace_back(mod.name(), current_ver.hash, current_ver.version, latest_ver.version, + api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), + ModPlatform::Provider::FLAME, download_task); + } + } + + } catch (Json::JsonException& e) { + failed(e.cause() + " : " + e.what()); + } + }); + + connect(job.get(), &Task::finished, &lock, &QEventLoop::quit); + + setStatus(tr("Waiting for the first API response from CurseForge...")); + setProgress(1, 5); + + m_net_job = job.get(); + job->start(); + + lock.exec(); + + emitSucceeded(); +} diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h new file mode 100644 index 00000000..f068f08f --- /dev/null +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Application.h" +#include "modplatform/CheckUpdateTask.h" +#include "net/NetJob.h" + +class FlameCheckUpdate : public CheckUpdateTask { + Q_OBJECT + + public: + FlameCheckUpdate(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) + {} + + public slots: + bool abort() override; + + protected slots: + void executeTask() override; + + private: + NetJob* m_net_job = nullptr; + + bool m_was_aborted = false; +}; diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp new file mode 100644 index 00000000..81a2652a --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -0,0 +1,163 @@ +#include "ModrinthCheckUpdate.h" +#include "ModrinthAPI.h" +#include "ModrinthPackIndex.h" + +#include "FileSystem.h" +#include "Json.h" + +#include "ModDownloadTask.h" + +static ModrinthAPI api; +static ModPlatform::ProviderCapabilities ProviderCaps; + +bool ModrinthCheckUpdate::abort() +{ + if (m_net_job) + return m_net_job->abort(); + return true; +} + +/* Check for update: + * - Get latest version available + * - Compare hash of the latest version with the current hash + * - If equal, no updates, else, there's updates, so add to the list + * */ +void ModrinthCheckUpdate::executeTask() +{ + setStatus(tr("Preparing mods for Modrinth...")); + setProgress(0, 3); + + QHash mappings; + + // Create all hashes + QStringList hashes; + auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); + for (auto mod : m_mods) { + auto hash = mod.metadata()->hash; + + // Sadly the API can only handle one hash type per call, se we + // need to generate a new hash if the current one is innadequate + // (though it will rarely happen, if at all) + if (mod.metadata()->hash_format != best_hash_type) { + QByteArray jar_data; + + try { + jar_data = FS::read(mod.fileinfo().absoluteFilePath()); + } catch (FS::FileSystemException& e) { + qCritical() << QString("Failed to open / read JAR file of %1").arg(mod.name()); + qCritical() << QString("Reason: ") << e.cause(); + + failed(e.what()); + return; + } + + hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, best_hash_type).toHex()); + } + + hashes.append(hash); + mappings.insert(hash, mod); + } + + auto* response = new QByteArray(); + auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response); + + QEventLoop lock; + + connect(job.get(), &Task::succeeded, this, [this, response, &mappings, best_hash_type, job] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + failed(parse_error.errorString()); + return; + } + + setStatus(tr("Parsing the API response from Modrinth...")); + setProgress(2, 3); + + try { + for (auto hash : mappings.keys()) { + auto project_obj = doc[hash].toObject(); + if (project_obj.isEmpty()) { + qDebug() << "Mod " << mappings.find(hash).value().name() << " got an empty response."; + qDebug() << "Hash: " << hash; + + emit checkFailed(mappings.find(hash).value(), tr("Couldn't find mod in Modrinth")); + + continue; + } + + // Sometimes a version may have multiple files, one with "forge" and one with "fabric", + // so we may want to filter it + QString loader_filter; + static auto flags = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt }; + for (auto flag : flags) { + if (m_loaders.testFlag(flag)) { + loader_filter = api.getModLoaderString(flag); + break; + } + } + + // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: + // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter + // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case) + // Such is the pain of having arbitrary files for a given version .-. + + auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, best_hash_type, loader_filter); + if (project_ver.downloadUrl.isEmpty()) { + qCritical() << "Modrinth mod without download url!"; + qCritical() << project_ver.fileName; + + emit checkFailed(mappings.find(hash).value(), tr("Mod has an empty download URL")); + + continue; + } + + auto mod_iter = mappings.find(hash); + if (mod_iter == mappings.end()) { + qCritical() << "Failed to remap mod from Modrinth!"; + continue; + } + auto mod = *mod_iter; + + auto key = project_ver.hash; + if ((key != hash && project_ver.is_preferred) || (mod.status() == ModStatus::NotInstalled)) { + if (mod.version() == project_ver.version_number) + continue; + + // Fake pack with the necessary info to pass to the download task :) + ModPlatform::IndexedPack pack; + pack.name = mod.name(); + pack.addonId = mod.metadata()->project_id; + pack.websiteUrl = mod.homeurl(); + for (auto& author : mod.authors()) + pack.authors.append({ author }); + pack.description = mod.description(); + pack.provider = ModPlatform::Provider::MODRINTH; + + auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder); + + m_updatable.emplace_back(mod.name(), hash, mod.version(), project_ver.version_number, project_ver.changelog, + ModPlatform::Provider::MODRINTH, download_task); + } + } + } catch (Json::JsonException& e) { + failed(e.cause() + " : " + e.what()); + } + }); + + connect(job.get(), &Task::finished, &lock, &QEventLoop::quit); + + setStatus(tr("Waiting for the API response from Modrinth...")); + setProgress(1, 3); + + m_net_job = job.get(); + job->start(); + + lock.exec(); + + emitSucceeded(); +} diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h new file mode 100644 index 00000000..7e685a6d --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Application.h" +#include "modplatform/CheckUpdateTask.h" +#include "net/NetJob.h" + +class ModrinthCheckUpdate : public CheckUpdateTask { + Q_OBJECT + + public: + ModrinthCheckUpdate(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) + {} + + public slots: + bool abort() override; + + protected slots: + void executeTask() override; + + private: + NetJob* m_net_job = nullptr; +}; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 9736e861..1910c9be 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -130,6 +130,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_t file.loaders.append(loader.toString()); } file.version = Json::requireString(obj, "name"); + file.version_number = Json::requireString(obj, "version_number"); file.changelog = Json::requireString(obj, "changelog"); auto files = Json::requireArray(obj, "files"); @@ -159,7 +160,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_t if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); - file.is_preferred = Json::requireBoolean(parent, "primary"); + file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1); auto hash_list = Json::requireObject(parent, "hashes"); if (hash_list.contains(preferred_hash_type)) { -- cgit From 6e2869834f59ae4863e63a16de97aa3019723b26 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 4 Jun 2022 16:22:46 -0300 Subject: feat: add mod update dialog This subclasses the Review mods dialog to make a "Update review" one. Also, all the necessary components built until now are put together in a coherent unity that checks and generates metadata on-the-fly and checks for mod updates, while giving and receiving feedback to the user. Signed-off-by: flow --- launcher/modplatform/flame/FlameCheckUpdate.cpp | 19 +++++-------------- launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp | 5 ++++- 2 files changed, 9 insertions(+), 15 deletions(-) (limited to 'launcher/modplatform') diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index f1983fa4..12d99029 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -74,16 +74,13 @@ void FlameCheckUpdate::executeTask() setStatus(tr("Preparing mods for CurseForge...")); setProgress(0, 5); - QHash mappings; + QHash mappings; // Create all hashes - QStringList hashes; std::list murmur_hashes; auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::FLAME).first(); for (auto mod : m_mods) { - auto hash = mod.metadata()->hash; - QByteArray jar_data; try { @@ -106,14 +103,7 @@ void FlameCheckUpdate::executeTask() auto murmur_hash = MurmurHash2(jar_data_treated, jar_data_treated.length()); murmur_hashes.emplace_back(murmur_hash); - // Sadly the API can only handle one hash type per call, se we - // need to generate a new hash if the current one is innadequate - // (though it will rarely happen, if at all) - if (mod.metadata()->hash_format != best_hash_type) - hash = QString(ProviderCaps.hash(ModPlatform::Provider::FLAME, jar_data, best_hash_type).toHex()); - - hashes.append(hash); - mappings.insert(hash, mod); + mappings.insert(mod.metadata()->mod_id().toInt(), mod); } auto* response = new QByteArray(); @@ -154,9 +144,10 @@ void FlameCheckUpdate::executeTask() return; } - auto mod_iter = mappings.find(current_ver.hash); + auto mod_iter = mappings.find(current_ver.addonId.toInt()); if (mod_iter == mappings.end()) { qCritical() << "Failed to remap mod from Flame!"; + qDebug() << match_obj; continue; } @@ -191,7 +182,7 @@ void FlameCheckUpdate::executeTask() continue; } - if (!latest_ver.hash.isEmpty() && current_ver.hash != latest_ver.hash) { + if (!latest_ver.hash.isEmpty() && (current_ver.hash != latest_ver.hash || mod.status() == ModStatus::NotInstalled)) { // Fake pack with the necessary info to pass to the download task :) ModPlatform::IndexedPack pack; pack.name = mod.name(); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 81a2652a..981c4216 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -81,11 +81,14 @@ void ModrinthCheckUpdate::executeTask() try { for (auto hash : mappings.keys()) { auto project_obj = doc[hash].toObject(); + + // If the returned project is empty, but we have Modrinth metadata, + // it means this specific version is not available if (project_obj.isEmpty()) { qDebug() << "Mod " << mappings.find(hash).value().name() << " got an empty response."; qDebug() << "Hash: " << hash; - emit checkFailed(mappings.find(hash).value(), tr("Couldn't find mod in Modrinth")); + emit checkFailed(mappings.find(hash).value(), tr("Couldn't find the latest version of this mod with the correct mod loader and game version.")); continue; } -- cgit From 91a5c4bdcbd3ae18139b85899f051fb3d9cbd1fc Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 11 Jun 2022 17:19:34 -0300 Subject: feat: add metadata get/delete via mod id This is, in many cases, more reliable than name comparisons, so it's useful specially in cases where a mod changes name between versions Signed-off-by: flow --- launcher/modplatform/packwiz/Packwiz.cpp | 33 +++++++++++++++++++++++++++++--- launcher/modplatform/packwiz/Packwiz.h | 8 ++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) (limited to 'launcher/modplatform') diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 0782b9f4..8bd66088 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -144,6 +144,9 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) QFile index_file(index_dir.absoluteFilePath(real_fname)); + if (real_fname != normalized_fname) + index_file.rename(normalized_fname); + // There's already data on there! // TODO: We should do more stuff here, as the user is likely trying to // override a file. In this case, check versions and ask the user what @@ -196,16 +199,28 @@ void V1::deleteModIndex(QDir& index_dir, QString& mod_name) QFile index_file(index_dir.absoluteFilePath(real_fname)); - if(!index_file.exists()){ + if (!index_file.exists()) { qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_name); return; } - if(!index_file.remove()){ + if (!index_file.remove()) { qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_name); } } +void V1::deleteModIndex(QDir& index_dir, QVariant& mod_id) +{ + for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { + auto mod = getIndexForMod(index_dir, file_name); + + if (mod.mod_id() == mod_id) { + deleteModIndex(index_dir, mod.name); + break; + } + } +} + auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod { Mod mod; @@ -286,4 +301,16 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod return mod; } -} // namespace Packwiz +auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod +{ + for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { + auto mod = getIndexForMod(index_dir, file_name); + + if (mod.mod_id() == mod_id) + return mod; + } + + return {}; +} + +} // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 3c99769c..9d643703 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -84,10 +84,18 @@ class V1 { /* Deletes the metadata for the mod with the given name. If the metadata doesn't exist, it does nothing. */ static void deleteModIndex(QDir& index_dir, QString& mod_name); + /* Deletes the metadata for the mod with the given id. If the metadata doesn't exist, it does nothing. */ + static void deleteModIndex(QDir& index_dir, QVariant& mod_id); + /* Gets the metadata for a mod with a particular name. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ static auto getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod; + + /* Gets the metadata for a mod with a particular id. + * If the mod doesn't have a metadata, it simply returns an empty Mod object. + * */ + static auto getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod; }; } // namespace Packwiz -- cgit From 4e6978ff6f61777a2e2e989cba58a9f0c48d2782 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 16 Jun 2022 11:45:29 -0300 Subject: feat: improve metadata gen. networking and performance This makes the metadata generation code a lot messier and harder to use, but there's not really much else that can be done about it while preserving all it's capabilities :( At least we now have speed Signed-off-by: flow --- launcher/modplatform/CheckUpdateTask.h | 2 +- launcher/modplatform/EnsureMetadataTask.cpp | 550 ++++++++++++++++----- launcher/modplatform/EnsureMetadataTask.h | 35 +- launcher/modplatform/flame/FlameCheckUpdate.cpp | 209 +++----- .../modplatform/modrinth/ModrinthCheckUpdate.cpp | 5 +- 5 files changed, 524 insertions(+), 277 deletions(-) (limited to 'launcher/modplatform') diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 8c701e46..d96fc340 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -13,7 +13,7 @@ class CheckUpdateTask : public Task { public: CheckUpdateTask(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) - : m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; + : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; struct UpdatableMod { QString name; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index dc92d8ab..cf4e55b9 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -19,226 +19,502 @@ 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() +EnsureMetadataTask::EnsureMetadataTask(Mod& mod, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov) { - return m_task_handler->abort(); + auto hash = getHash(mod); + if (hash.isEmpty()) + emitFail(mod); + else + m_mods.insert(hash, mod); } -void EnsureMetadataTask::executeTask() +EnsureMetadataTask::EnsureMetadataTask(std::list& mods, QDir dir, ModPlatform::Provider prov) + : Task(nullptr), m_index_dir(dir), m_provider(prov) { - // They already have the right metadata :o - if (m_mod.status() != ModStatus::NoMetadata && m_mod.metadata() && m_mod.metadata()->provider == m_provider) { - emitReady(); - return; - } + for (auto& mod : mods) { + if (!mod.valid()) { + emitFail(mod); + continue; + } - // Folders don't have metadata - if (m_mod.type() == Mod::MOD_FOLDER) { - emitReady(); - return; - } + auto hash = getHash(mod); + if (hash.isEmpty()) { + emitFail(mod); + continue; + } - setStatus(tr("Generating %1's metadata...").arg(m_mod.name())); - qDebug() << QString("Generating %1's metadata...").arg(m_mod.name()); + m_mods.insert(hash, mod); + } +} +QString EnsureMetadataTask::getHash(Mod& mod) +{ + /* Here we create a mapping hash -> mod, because we need that relationship to parse the API routes */ QByteArray jar_data; - try { - jar_data = FS::read(m_mod.fileinfo().absoluteFilePath()); + jar_data = FS::read(mod.fileinfo().absoluteFilePath()); } catch (FS::FileSystemException& e) { - qCritical() << QString("Failed to open / read JAR file of %1").arg(m_mod.name()); +