aboutsummaryrefslogtreecommitdiff
path: root/launcher/modplatform
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/modplatform')
-rw-r--r--launcher/modplatform/CheckUpdateTask.h2
-rw-r--r--launcher/modplatform/EnsureMetadataTask.cpp550
-rw-r--r--launcher/modplatform/EnsureMetadataTask.h35
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.cpp209
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp5
5 files changed, 524 insertions, 277 deletions
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<Mod>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> 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<Mod>& 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());
+ qCritical() << QString("Failed to open / read JAR file of %1").arg(mod.name());
qCritical() << QString("Reason: ") << e.cause();
- emitFail();
- return;
+ return {};
}
- auto tsk = new MultipleOptionsTask(nullptr, "GetMetadataTask");
+ switch (m_provider) {
+ case ModPlatform::Provider::MODRINTH: {
+ auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+
+ return QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex());
+ }
+ case ModPlatform::Provider::FLAME: {
+ 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);
+ }
+
+ return QString::number(MurmurHash2(jar_data_treated, jar_data_treated.length()));
+ }
+ }
+
+ return {};
+}
+
+bool EnsureMetadataTask::abort()
+{
+ // Prevent sending signals to a dead object
+ disconnect(this, 0, 0, 0);
+
+ if (m_current_task)
+ return m_current_task->abort();
+ return true;
+}
+
+void EnsureMetadataTask::executeTask()
+{
+ setStatus(tr("Checking if mods have metadata..."));
+
+ for (auto mod : m_mods) {
+ if (!mod.valid())
+ continue;
+
+ // They already have the right metadata :o
+ if (mod.status() != ModStatus::NoMetadata && mod.metadata() && mod.metadata()->provider == m_provider) {
+ qDebug() << "Mod" << mod.name() << "already has metadata!";
+ emitReady(mod);
+ return;
+ }
+
+ // Folders don't have metadata
+ if (mod.type() == Mod::MOD_FOLDER) {
+ emitReady(mod);
+ return;
+ }
+ }
+
+ NetJob::Ptr version_task;
switch (m_provider) {
case (ModPlatform::Provider::MODRINTH):
- modrinthEnsureMetadata(*tsk, jar_data);
- if (m_try_all)
- flameEnsureMetadata(*tsk, jar_data);
-
+ version_task = modrinthVersionsTask();
break;
case (ModPlatform::Provider::FLAME):
- flameEnsureMetadata(*tsk, jar_data);
- if (m_try_all)
- modrinthEnsureMetadata(*tsk, jar_data);
-
+ version_task = flameVersionsTask();
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());
+ auto invalidade_leftover = [this] {
+ QMutableHashIterator<QString, Mod> mods_iter(m_mods);
+ while (mods_iter.hasNext()) {
+ auto mod = mods_iter.next();
+ emitFail(mod.value());
+ }
+
+ emitSucceeded();
+ };
+
+ connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] {
+ NetJob::Ptr project_task;
+
+ switch (m_provider) {
+ case (ModPlatform::Provider::MODRINTH):
+ project_task = modrinthProjectsTask();
+ break;
+ case (ModPlatform::Provider::FLAME):
+ project_task = flameProjectsTask();
+ break;
+ }
+
+ if (!project_task) {
+ invalidade_leftover();
+ return;
+ }
+
+ connect(project_task.get(), &Task::finished, this, [=] {
+ invalidade_leftover();
+ project_task->deleteLater();
+ m_current_task = nullptr;
+ });
- emitFail();
+ m_current_task = project_task.get();
+ project_task->start();
});
- connect(tsk, &MultipleOptionsTask::succeeded, this, &EnsureMetadataTask::emitReady);
- m_task_handler = tsk;
+ connect(version_task.get(), &Task::finished, [=] {
+ version_task->deleteLater();
+ m_current_task = nullptr;
+ });
+
+ if (m_mods.size() > 1)
+ setStatus(tr("Requesting metadata information from %1...").arg(ProviderCaps.readableName(m_provider)));
+ else if (!m_mods.empty())
+ setStatus(tr("Requesting metadata information from %1 for '%2'...")
+ .arg(ProviderCaps.readableName(m_provider), m_mods.begin().value().name()));
- tsk->start();
+ m_current_task = version_task.get();
+ version_task->start();
}
-void EnsureMetadataTask::emitReady()
+void EnsureMetadataTask::emitReady(Mod& m)
{
- emit metadataReady();
- emitSucceeded();
+ qDebug() << QString("Generated metadata for %1").arg(m.name());
+ emit metadataReady(m);
+
+ m_mods.remove(getHash(m));
}
-void EnsureMetadataTask::emitFail()
+void EnsureMetadataTask::emitFail(Mod& m)
{
- 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();
+ qDebug() << QString("Failed to generate metadata for %1").arg(m.name());
+ emit metadataFailed(m);
+
+ m_mods.remove(getHash(m));
}
-void EnsureMetadataTask::modrinthEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data)
+// Modrinth
+
+NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
{
- // 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;
+ auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
- 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;
+ auto* response = new QByteArray();
+ auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);
- ver_task->failed(parse_error.errorString());
- return;
+ // Prevents unfortunate timings when aborting the task
+ if (!ver_task)
+ return {};
+
+ connect(ver_task.get(), &NetJob::succeeded, this, [this, response] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &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() << *response;
+
+ failed(parse_error.errorString());
+ return;
+ }
+
+ try {
+ auto entries = Json::requireObject(doc);
+ for (auto& hash : m_mods.keys()) {
+ auto mod = m_mods.find(hash).value();
+ try {
+ auto entry = Json::requireObject(entries, hash);
+
+ setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod.name()));
+ qDebug() << "Getting version for" << mod.name() << "from Modrinth";
+
+ m_temp_versions.insert(hash, Modrinth::loadIndexedPackVersion(entry));
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(mod);
+ }
}
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
- auto doc_obj = Json::requireObject(doc);
- auto ver = Modrinth::loadIndexedPackVersion(doc_obj, {}, m_mod.fileinfo().fileName());
+ return ver_task;
+}
+
+NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
+{
+ QHash<QString, QString> addonIds;
+ for (auto const& data : m_temp_versions)
+ addonIds.insert(data.addonId.toString(), data.hash);
+
+ auto response = new QByteArray();
+ NetJob::Ptr proj_task;
+
+ if (addonIds.isEmpty()) {
+ qWarning() << "No addonId found!";
+ } else if (addonIds.size() == 1) {
+ proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response);
+ } else {
+ proj_task = modrinth_api.getProjects(addonIds.keys(), response);
+ }
+
+ // Prevents unfortunate timings when aborting the task
+ if (!proj_task)
+ return {};
+
+ connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*response, &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() << *response;
+ return;
+ }
- // Minimal IndexedPack to create the metadata
- ModPlatform::IndexedPack pack;
- pack.name = m_mod.name();
- pack.provider = ModPlatform::Provider::MODRINTH;
- pack.addonId = ver.addonId;
+ try {
+ QJsonArray entries;
+ if (addonIds.size() == 1)
+ entries = { doc.object() };
+ else
+ entries = Json::requireArray(doc);
- // Prevent file name mismatch
- ver.fileName = m_mod.fileinfo().fileName();
+ for (auto entry : entries) {
+ auto entry_obj = Json::requireObject(entry);
+ auto entry_id = Json::requireString(entry_obj, "id");
- QDir tmp_index_dir(m_index_dir);
+ auto hash = addonIds.find(entry_id).value();
- {
- LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
- QEventLoop loop;
- QTimer timeout;
+ auto mod = m_mods.find(hash).value();
- QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
- QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
+ try {
+ setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod.name()));
- update_metadata.start();
- timeout.start(100);
+ ModPlatform::IndexedPack pack;
+ Modrinth::loadIndexedPack(pack, entry_obj);
- loop.exec();
- }
+ modrinthCallback(pack, m_temp_versions.find(hash).value(), mod);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
- auto mod_name = m_mod.name();
- auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name));
- m_mod.setMetadata(meta);
- });
+ emitFail(mod);
+ }
+ }
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
- tsk.addTask(ver_task);
- }
+ return proj_task;
}
-void EnsureMetadataTask::flameEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data)
+// Flame
+NetJob::Ptr EnsureMetadataTask::flameVersionsTask()
{
- 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);
+ for (auto& murmur : m_mods.keys()) {
+ fingerprints.push_back(murmur.toUInt());
+ }
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);
-
+ connect(ver_task.get(), &Task::succeeded, this, [this, 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
+ qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
- ver_task->failed(parse_error.errorString());
+ 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!";
+ auto data_obj = Json::requireObject(doc_obj, "data");
+ auto data_arr = Json::requireArray(data_obj, "exactMatches");
+
+ if (data_arr.isEmpty()) {
+ qWarning() << "No matches found for fingerprint search!";
- ver_task->failed(parse_error.errorString());
return;
}
- auto file_obj = Json::ensureObject(match_obj, "file");
+ for (auto match : data_arr) {
+ auto match_obj = Json::ensureObject(match, {});
+ 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");
+ if (match_obj.isEmpty() || file_obj.isEmpty()) {
+ qWarning() << "Fingerprint match is empty!";
- ModPlatform::IndexedVersion ver = FlameMod::loadIndexedPackVersion(file_obj);
+ return;
+ }
- // Prevent file name mismatch
- ver.fileName = m_mod.fileinfo().fileName();
+ auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt());
+ auto mod = m_mods.find(fingerprint);
+ if (mod == m_mods.end()) {
+ qWarning() << "Invalid fingerprint from the API response.";
+ continue;
+ }
- {
- LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
- QEventLoop loop;
- QTimer timeout;
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name()));
- QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
- QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
+ m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj));
+ }
- update_metadata.start();
- timeout.start(100);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
- loop.exec();
- }
+ return ver_task;
+}
+
+NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
+{
+ QHash<QString, QString> addonIds;
+ for (auto const& hash : m_mods.keys()) {
+ if (m_temp_versions.contains(hash)) {
+ auto const& data = m_temp_versions.find(hash).value();
+ addonIds.insert(data.addonId.toString(), hash);
+ }
+ }
+
+ auto response = new QByteArray();
+ NetJob::Ptr proj_task;
+
+ if (addonIds.isEmpty()) {
+ qWarning() << "No addonId found!";
+ } else if (addonIds.size() == 1) {
+ proj_task = flame_api.getProject(*addonIds.keyBegin(), response);
+ } else {
+ proj_task = flame_api.getProjects(addonIds.keys(), response);
+ }
+
+ // Prevents unfortunate timings when aborting the task
+ if (!proj_task)
+ return {};
+
+ connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*response, &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() << *response;
+ return;
+ }
+
+ try {
+ QJsonArray entries;
+ if (addonIds.size() == 1)
+ entries = { Json::requireObject(Json::requireObject(doc), "data") };
+ else
+ entries = Json::requireArray(Json::requireObject(doc), "data");
+
+ for (auto entry : entries) {
+ auto entry_obj = Json::requireObject(entry);
+
+ auto id = QString::number(Json::requireInteger(entry_obj, "id"));
+ auto hash = addonIds.find(id).value();
+ auto mod = m_mods.find(hash).value();
+
+ try {
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod.name()));
- auto mod_name = m_mod.name();
- auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name));
- m_mod.setMetadata(meta);
+ ModPlatform::IndexedPack pack;
+ FlameMod::loadIndexedPack(pack, entry_obj);
+ flameCallback(pack, m_temp_versions.find(hash).value(), mod);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(mod);
+ }
+ }
} catch (Json::JsonException& e) {
- emitFailed(e.cause() + " : " + e.what());
+ qDebug() << e.cause();
+ qDebug() << doc;
}
});
- tsk.addTask(ver_task);
+ return proj_task;
+}
+
+void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod& mod)
+{
+ // Prevent file name mismatch
+ ver.fileName = mod.fileinfo().fileName();
+
+ QDir tmp_index_dir(m_index_dir);
+
+ {
+ LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
+ QEventLoop loop;
+
+ QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
+
+ update_metadata.start();
+
+ if (!update_metadata.isFinished())
+ loop.exec();
+ }
+
+ auto metadata = Metadata::get(tmp_index_dir, pack.slug);
+ if (!metadata.isValid()) {
+ qCritical() << "Failed to generate metadata at last step!";
+ emitFail(mod);
+ return;
+ }
+
+ mod.setMetadata(metadata);
+
+ emitReady(mod);
+}
+
+void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod& mod)
+{
+ try {
+ // Prevent file name mismatch
+ ver.fileName = mod.fileinfo().fileName();
+
+ QDir tmp_index_dir(m_index_dir);
+
+ {
+ LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
+ QEventLoop loop;
+
+ QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
+
+ update_metadata.start();
+
+ if (!update_metadata.isFinished())
+ loop.exec();
+ }
+
+ auto metadata = Metadata::get(tmp_index_dir, pack.slug);
+ if (!metadata.isValid()) {
+ qCritical() << "Failed to generate metadata at last step!";
+ emitFail(mod);
+ return;
+ }
+
+ mod.setMetadata(metadata);
+
+ emitReady(mod);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+
+ emitFail(mod);
+ }
}
diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h
index 624e253a..880503b9 100644
--- a/launcher/modplatform/EnsureMetadataTask.h
+++ b/launcher/modplatform/EnsureMetadataTask.h
@@ -2,6 +2,7 @@
#include "ModIndex.h"
#include "tasks/SequentialTask.h"
+#include "net/NetJob.h"
class Mod;
class QDir;
@@ -11,7 +12,10 @@ class EnsureMetadataTask : public Task {
Q_OBJECT
public:
- EnsureMetadataTask(Mod&, QDir&, bool try_all, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
+ EnsureMetadataTask(Mod&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
+ EnsureMetadataTask(std::list<Mod>&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
+
+ ~EnsureMetadataTask() = default;
public slots:
bool abort() override;
@@ -20,22 +24,31 @@ class EnsureMetadataTask : public Task {
private:
// FIXME: Move to their own namespace
- void modrinthEnsureMetadata(SequentialTask&, QByteArray&);
- void flameEnsureMetadata(SequentialTask&, QByteArray&);
+ auto modrinthVersionsTask() -> NetJob::Ptr;
+ auto modrinthProjectsTask() -> NetJob::Ptr;
+
+ auto flameVersionsTask() -> NetJob::Ptr;
+ auto flameProjectsTask() -> NetJob::Ptr;
// Helpers
- void emitReady();
- void emitFail();
+ void emitReady(Mod&);
+ void emitFail(Mod&);
+
+ auto getHash(Mod&) -> QString;
+
+ private slots:
+ void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod&);
+ void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod&);
signals:
- void metadataReady();
- void metadataFailed();
+ void metadataReady(Mod&);
+ void metadataFailed(Mod&);
private:
- Mod& m_mod;
- QDir& m_index_dir;
+ QHash<QString, Mod> m_mods;
+ QDir m_index_dir;
ModPlatform::Provider m_provider;
- bool m_try_all;
- MultipleOptionsTask* m_task_handler = nullptr;
+ QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
+ NetJob* m_current_task;
};
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp
index 12d99029..3658bf8d 100644
--- a/launcher/modplatform/flame/FlameCheckUpdate.cpp
+++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp
@@ -10,7 +10,6 @@
#include "ModDownloadTask.h"
static FlameAPI api;
-static ModPlatform::ProviderCapabilities ProviderCaps;
bool FlameCheckUpdate::abort()
{
@@ -64,156 +63,112 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info)
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()
+ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId)
{
- setStatus(tr("Preparing mods for CurseForge..."));
- setProgress(0, 5);
-
- QHash<int, Mod> mappings;
+ ModPlatform::IndexedVersion ver;
- // Create all hashes
- std::list<uint> murmur_hashes;
+ QEventLoop loop;
- auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::FLAME).first();
- for (auto mod : m_mods) {
- QByteArray jar_data;
+ auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network());
- 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();
+ auto response = new QByteArray();
+ auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId));
+ auto dl = Net::Download::makeByteArray(url, response);
+ get_file_info_job->addNetAction(dl);
- failed(e.what());
+ QObject::connect(get_file_info_job, &NetJob::succeeded, [response, &ver]() {
+ 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;
}
- 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);
+ try {
+ auto doc_obj = Json::requireObject(doc);
+ auto data_obj = Json::requireObject(doc_obj, "data");
+ ver = FlameMod::loadIndexedPackVersion(data_obj);
+ } catch (Json::JsonException& e) {
+ qWarning() << e.cause();
+ qDebug() << doc;
}
+ });
- auto murmur_hash = MurmurHash2(jar_data_treated, jar_data_treated.length());
- murmur_hashes.emplace_back(murmur_hash);
+ QObject::connect(get_file_info_job, &NetJob::finished, [&loop, get_file_info_job] {
+ get_file_info_job->deleteLater();
+ loop.quit();
+ });
- mappings.insert(mod.metadata()->mod_id().toInt(), mod);
- }
+ get_file_info_job->start();
+ loop.exec();
- auto* response = new QByteArray();
- auto job = api.matchFingerprints(murmur_hashes, response);
+ return ver;
+}
- QEventLoop lock;
+/* 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..."));
- 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;
+ int i = 0;
+ for (auto mod : m_mods) {
+ setStatus(tr("Getting API response from CurseForge for '%1'").arg(mod.name()));
+ setProgress(i++, m_mods.size());
+
+ auto latest_ver = api.getLatestVersion({ mod.metadata()->project_id.toString(), m_game_versions, m_loaders });
- failed(parse_error.errorString());
+ // Check if we were aborted while getting the latest version
+ if (m_was_aborted) {
+ aborted();
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.addonId.toInt());
- if (mod_iter == mappings.end()) {
- qCritical() << "Failed to remap mod from Flame!";
- qDebug() << match_obj;
- 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 || mod.status() == ModStatus::NotInstalled)) {
- // 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);
- }
- }
+ setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod.name()));
- } catch (Json::JsonException& e) {
- failed(e.cause() + " : " + e.what());
+ 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;
}
- });
- connect(job.get(), &Task::finished, &lock, &QEventLoop::quit);
+ if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != mod.metadata()->file_id) {
+ 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);
- setStatus(tr("Waiting for the first API response from CurseForge..."));
- setProgress(1, 5);
+ continue;
+ }
- m_net_job = job.get();
- job->start();
+ if (!latest_ver.hash.isEmpty() && (mod.metadata()->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();
+ pack.slug = mod.metadata()->slug;
+ 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 old_version = mod.version();
+ if (old_version.isEmpty() && mod.status() != ModStatus::NotInstalled) {
+ auto current_ver = getFileInfo(latest_ver.addonId.toInt(), mod.metadata()->file_id.toInt());
+ old_version = current_ver.version;
+ }
- lock.exec();
+ auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder);
+ m_updatable.emplace_back(mod.name(), mod.metadata()->hash, old_version, latest_ver.version,
+ api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
+ ModPlatform::Provider::FLAME, download_task);
+ }
+ }
emitSucceeded();
}
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
index 981c4216..78275cf0 100644
--- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -88,7 +88,9 @@ void ModrinthCheckUpdate::executeTask()
qDebug() << "Mod " << mappings.find(hash).value().name() << " got an empty response.";
qDebug() << "Hash: " << hash;
- emit checkFailed(mappings.find(hash).value(), tr("Couldn't find the latest version of this mod with the correct mod loader and game version."));
+ emit checkFailed(
+ mappings.find(hash).value(),
+ tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader."));
continue;
}
@@ -134,6 +136,7 @@ void ModrinthCheckUpdate::executeTask()
// Fake pack with the necessary info to pass to the download task :)
ModPlatform::IndexedPack pack;
pack.name = mod.name();
+ pack.slug = mod.metadata()->slug;
pack.addonId = mod.metadata()->project_id;
pack.websiteUrl = mod.homeurl();
for (auto& author : mod.authors())