9cb6200081
for FileSystem.cpp: Instead of checking if Linux or FreeBSD, check if its not Windows and not OSX. Chances are other operating systems run a DE that adheres to the XDG Desktop standard (.desktop). The check isn't good enough anyways since alternative shells for Windows exist, it will never be an accurate check. In any case this function is unused. WorldListPage.cpp: Redo confusing switch statement plagued with fall throughs, now well defined. LaunchController.cpp: Remove cringe. Also fix warning and make the unimplemented case(s) more explicit. VersionProxyModel.cpp: Add fallthrough for warning suppression. WorldListPage.cpp: redo `mceditState` TranslationsModel.cpp: Move up definition of `column` variable to when it is needed, clear up switch cases FlameInstanceCreationTask.cpp: Fallthrough intentionally SkinUpload.cpp: Make `getVariant` ResourcePack.cpp: Add new values for 1.19.3+ meta/Index.cpp: Make clear switch statement behavior JavaWizardPage.cpp: Fix case fallthrough Yggdrasil.cpp: Fix case fallthrough AccountList.cpp: Fix case fallthrough, WinDarkmode.cpp: Add an explanation and fix warnings due to FARPROC casts. Signed-off-by: jdp_ <42700985+jdpatdiscord@users.noreply.github.com>
550 lines
17 KiB
C++
550 lines
17 KiB
C++
#include "EnsureMetadataTask.h"
|
|
|
|
#include <MurmurHash2.h>
|
|
#include <QDebug>
|
|
|
|
#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"
|
|
|
|
static ModPlatform::ProviderCapabilities ProviderCaps;
|
|
|
|
static ModrinthAPI modrinth_api;
|
|
static FlameAPI flame_api;
|
|
|
|
EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov)
|
|
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr)
|
|
{
|
|
auto hash_task = createNewHash(mod);
|
|
if (!hash_task)
|
|
return;
|
|
connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
|
|
connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
|
|
hash_task->start();
|
|
}
|
|
|
|
EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov)
|
|
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
|
|
{
|
|
m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10);
|
|
for (auto* mod : mods) {
|
|
auto hash_task = createNewHash(mod);
|
|
if (!hash_task)
|
|
continue;
|
|
connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
|
|
connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
|
|
m_hashing_task->addTask(hash_task);
|
|
}
|
|
}
|
|
|
|
Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Mod* mod)
|
|
{
|
|
if (!mod || !mod->valid() || mod->type() == ResourceType::FOLDER)
|
|
return nullptr;
|
|
|
|
return Hashing::createHasher(mod->fileinfo().absoluteFilePath(), m_provider);
|
|
}
|
|
|
|
QString EnsureMetadataTask::getExistingHash(Mod* mod)
|
|
{
|
|
// Check for already computed hashes
|
|
// (linear on the number of mods vs. linear on the size of the mod's JAR)
|
|
auto it = m_mods.keyValueBegin();
|
|
while (it != m_mods.keyValueEnd()) {
|
|
if ((*it).second == mod)
|
|
break;
|
|
it++;
|
|
}
|
|
|
|
// We already have the hash computed
|
|
if (it != m_mods.keyValueEnd()) {
|
|
return (*it).first;
|
|
}
|
|
|
|
// No existing hash
|
|
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()) {
|
|
qDebug() << "Mod" << mod->name() << "is invalid!";
|
|
emitFail(mod);
|
|
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);
|
|
continue;
|
|
}
|
|
|
|
// Folders don't have metadata
|
|
if (mod->type() == ResourceType::FOLDER) {
|
|
emitReady(mod);
|
|
}
|
|
}
|
|
|
|
NetJob::Ptr version_task;
|
|
|
|
switch (m_provider) {
|
|
case (ModPlatform::Provider::MODRINTH):
|
|
version_task = modrinthVersionsTask();
|
|
break;
|
|
case (ModPlatform::Provider::FLAME):
|
|
version_task = flameVersionsTask();
|
|
break;
|
|
}
|
|
|
|
auto invalidade_leftover = [this] {
|
|
for (auto mod = m_mods.constBegin(); mod != m_mods.constEnd(); mod++)
|
|
emitFail(mod.value(), mod.key(), RemoveFromList::No);
|
|
m_mods.clear();
|
|
|
|
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;
|
|
});
|
|
|
|
m_current_task = project_task.get();
|
|
project_task->start();
|
|
});
|
|
|
|
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()));
|
|
|
|
m_current_task = version_task.get();
|
|
version_task->start();
|
|
}
|
|
|
|
void EnsureMetadataTask::emitReady(Mod* m, QString key, RemoveFromList remove)
|
|
{
|
|
if (!m) {
|
|
qCritical() << "Tried to mark a null mod as ready.";
|
|
if (!key.isEmpty())
|
|
m_mods.remove(key);
|
|
|
|
return;
|
|
}
|
|
|
|
qDebug() << QString("Generated metadata for %1").arg(m->name());
|
|
emit metadataReady(m);
|
|
|
|
if (remove == RemoveFromList::Yes) {
|
|
if (key.isEmpty())
|
|
key = getExistingHash(m);
|
|
m_mods.remove(key);
|
|
}
|
|
}
|
|
|
|
void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove)
|
|
{
|
|
if (!m) {
|
|
qCritical() << "Tried to mark a null mod as failed.";
|
|
if (!key.isEmpty())
|
|
m_mods.remove(key);
|
|
|
|
return;
|
|
}
|
|
|
|
qDebug() << QString("Failed to generate metadata for %1").arg(m->name());
|
|
emit metadataFailed(m);
|
|
|
|
if (remove == RemoveFromList::Yes) {
|
|
if (key.isEmpty())
|
|
key = getExistingHash(m);
|
|
m_mods.remove(key);
|
|
}
|
|
}
|
|
|
|
// Modrinth
|
|
|
|
NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
|
|
{
|
|
auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
|
|
|
|
auto* response = new QByteArray();
|
|
auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);
|
|
|
|
// 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;
|
|
}
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
try {
|
|
QJsonArray entries;
|
|
if (addonIds.size() == 1)
|
|
entries = { doc.object() };
|
|
else
|
|
entries = Json::requireArray(doc);
|
|
|
|
for (auto entry : entries) {
|
|
auto entry_obj = Json::requireObject(entry);
|
|
|
|
ModPlatform::IndexedPack pack;
|
|
Modrinth::loadIndexedPack(pack, entry_obj);
|
|
|
|
auto hash = addonIds.find(pack.addonId.toString()).value();
|
|
|
|
auto mod_iter = m_mods.find(hash);
|
|
if (mod_iter == m_mods.end()) {
|
|
qWarning() << "Invalid project id from the API response.";
|
|
continue;
|
|
}
|
|
|
|
auto* mod = mod_iter.value();
|
|
|
|
try {
|
|
setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
|
|
|
|
modrinthCallback(pack, m_temp_versions.find(hash).value(), mod);
|
|
} catch (Json::JsonException& e) {
|
|
qDebug() << e.cause();
|
|
qDebug() << entries;
|
|
|
|
emitFail(mod);
|
|
}
|
|
}
|
|
} catch (Json::JsonException& e) {
|
|
qDebug() << e.cause();
|
|
qDebug() << doc;
|
|
}
|
|
});
|
|
|
|
return proj_task;
|
|
}
|
|
|
|
// Flame
|
|
NetJob::Ptr EnsureMetadataTask::flameVersionsTask()
|
|
{
|
|
auto* response = new QByteArray();
|
|
|
|
QList<uint> fingerprints;
|
|
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, 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 doc_obj = Json::requireObject(doc);
|
|
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!";
|
|
|
|
return;
|
|
}
|
|
|
|
for (auto match : data_arr) {
|
|
auto match_obj = Json::ensureObject(match, {});
|
|
auto file_obj = Json::ensureObject(match_obj, "file", {});
|
|
|
|
if (match_obj.isEmpty() || file_obj.isEmpty()) {
|
|
qWarning() << "Fingerprint match is empty!";
|
|
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*mod)->name()));
|
|
|
|
m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj));
|
|
}
|
|
|
|
} catch (Json::JsonException& e) {
|
|
qDebug() << e.cause();
|
|
qDebug() << doc;
|
|
}
|
|
});
|
|
|
|
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& dataObj = m_temp_versions.find(hash);
|
|
auto const& data = dataObj.value();
|
|
|
|
auto id_str = data.addonId.toString();
|
|
if (!id_str.isEmpty())
|
|
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()));
|
|
|
|
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) {
|
|
qDebug() << e.cause();
|
|
qDebug() << doc;
|
|
}
|
|
});
|
|
|
|
return proj_task;
|
|
}
|
|
|
|
void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod)
|
|
{
|
|
// Prevent file name mismatch
|
|
ver.fileName = mod->fileinfo().fileName();
|
|
if (ver.fileName.endsWith(".disabled"))
|
|
ver.fileName.chop(9);
|
|
|
|
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();
|
|
if (ver.fileName.endsWith(".disabled"))
|
|
ver.fileName.chop(9);
|
|
|
|
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);
|
|
}
|
|
}
|