Merge branch 'develop' into curseforge-url-handle

This commit is contained in:
Rachel Powers
2023-05-28 11:25:58 -07:00
279 changed files with 7065 additions and 4476 deletions

View File

@ -25,17 +25,40 @@ void Flame::FileResolvingTask::executeTask()
setProgress(0, 3);
m_dljob.reset(new NetJob("Mod id resolver", m_network));
result.reset(new QByteArray());
//build json data to send
// build json data to send
QJsonObject object;
object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
l.push_back(s.fileId);
return l;
}));
object["fileIds"] = QJsonArray::fromVariantList(
std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
l.push_back(s.fileId);
return l;
}));
QByteArray data = Json::toText(object);
auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result.get(), data);
auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data);
m_dljob->addNetAction(dl);
connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
auto step_progress = std::make_shared<TaskStepProgress>();
connect(m_dljob.get(), &NetJob::finished, this, [this, step_progress]() {
step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress);
netJobFinished();
});
connect(m_dljob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress);
emitFailed(reason);
});
connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
connect(m_dljob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
stepProgress(*step_progress);
});
connect(m_dljob.get(), &NetJob::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_dljob->start();
}
@ -44,7 +67,7 @@ void Flame::FileResolvingTask::netJobFinished()
setProgress(1, 3);
// job to check modrinth for blocked projects
m_checkJob.reset(new NetJob("Modrinth check", m_network));
blockedProjects = QMap<File *,QByteArray *>();
blockedProjects = QMap<File*, std::shared_ptr<QByteArray>>();
QJsonDocument doc;
QJsonArray array;
@ -65,24 +88,42 @@ void Flame::FileResolvingTask::netJobFinished()
auto fileid = Json::requireInteger(Json::requireObject(file)["id"]);
auto& out = m_toProcess.files[fileid];
try {
out.parseFromObject(Json::requireObject(file));
out.parseFromObject(Json::requireObject(file));
} catch (const JSONValidationError& e) {
qDebug() << "Blocked mod on curseforge" << out.fileName;
auto hash = out.hash;
if(!hash.isEmpty()) {
if (!hash.isEmpty()) {
auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash);
auto output = new QByteArray();
auto output = std::make_shared<QByteArray>();
auto dl = Net::Download::makeByteArray(QUrl(url), output);
QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() {
out.resolved = true;
});
QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() { out.resolved = true; });
m_checkJob->addNetAction(dl);
blockedProjects.insert(&out, output);
}
}
}
connect(m_checkJob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished);
auto step_progress = std::make_shared<TaskStepProgress>();
connect(m_checkJob.get(), &NetJob::finished, this, [this, step_progress]() {
step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress);
modrinthCheckFinished();
});
connect(m_checkJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress);
emitFailed(reason);
});
connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
connect(m_checkJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
stepProgress(*step_progress);
});
connect(m_checkJob.get(), &NetJob::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_checkJob->start();
}
@ -95,7 +136,6 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
auto &out = *it;
auto bytes = blockedProjects[out];
if (!out->resolved) {
delete bytes;
continue;
}
@ -112,11 +152,9 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
} else {
out->resolved = false;
}
delete bytes;
}
//copy to an output list and filter out projects found on modrinth
auto block = new QList<File *>();
auto block = std::make_shared<QList<File*>>();
auto it = blockedProjects.keys();
std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) {
return !f->resolved;
@ -124,32 +162,48 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
//Display not found mods early
if (!block->empty()) {
//blocked mods found, we need the slug for displaying.... we need another job :D !
auto slugJob = new NetJob("Slug Job", m_network);
auto slugs = QVector<QByteArray>(block->size());
auto index = 0;
for (auto fileInfo: *block) {
auto projectId = fileInfo->projectId;
slugs[index] = QByteArray();
m_slugJob.reset(new NetJob("Slug Job", m_network));
int index = 0;
for (auto mod : *block) {
auto projectId = mod->projectId;
auto output = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId);
auto dl = Net::Download::makeByteArray(url, &slugs[index]);
slugJob->addNetAction(dl);
index++;
}
connect(slugJob, &NetJob::succeeded, this, [slugs, this, slugJob, block]() {
slugJob->deleteLater();
auto index = 0;
for (const auto &slugResult: slugs) {
auto json = QJsonDocument::fromJson(slugResult);
auto dl = Net::Download::makeByteArray(url, output);
qDebug() << "Fetching url slug for file:" << mod->fileName;
QObject::connect(dl.get(), &Net::Download::succeeded, [block, index, output]() {
auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done
auto json = QJsonDocument::fromJson(*output);
auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"),
"websiteUrl");
auto mod = block->at(index);
auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId));
mod->websiteUrl = link;
index++;
}
});
m_slugJob->addNetAction(dl);
index++;
}
auto step_progress = std::make_shared<TaskStepProgress>();
connect(m_slugJob.get(), &NetJob::succeeded, this, [this, step_progress]() {
step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress);
emitSucceeded();
});
slugJob->start();
connect(m_slugJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress);
emitFailed(reason);
});
connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propogateStepProgress);
connect(m_slugJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
stepProgress(*step_progress);
});
connect(m_slugJob.get(), &NetJob::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_slugJob->start();
} else {
emitSucceeded();
}

View File

@ -1,41 +1,37 @@
#pragma once
#include "tasks/Task.h"
#include "net/NetJob.h"
#include "PackManifest.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
namespace Flame
{
class FileResolvingTask : public Task
{
namespace Flame {
class FileResolvingTask : public Task {
Q_OBJECT
public:
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess);
virtual ~FileResolvingTask() {};
public:
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess);
virtual ~FileResolvingTask(){};
bool canAbort() const override { return true; }
bool abort() override;
const Flame::Manifest &getResults() const
{
return m_toProcess;
}
const Flame::Manifest& getResults() const { return m_toProcess; }
protected:
protected:
virtual void executeTask() override;
protected slots:
protected slots:
void netJobFinished();
private: /* data */
private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
Flame::Manifest m_toProcess;
std::shared_ptr<QByteArray> result;
std::shared_ptr<QByteArray> result;
NetJob::Ptr m_dljob;
NetJob::Ptr m_checkJob;
NetJob::Ptr m_checkJob;
NetJob::Ptr m_slugJob;
void modrinthCheckFinished();
QMap<File *, QByteArray *> blockedProjects;
QMap<File*, std::shared_ptr<QByteArray>> blockedProjects;
};
}
} // namespace Flame

View File

@ -11,7 +11,7 @@
#include "net/NetJob.h"
#include "net/Upload.h"
Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* response)
Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Flame::MatchFingerprints"), APPLICATION->network());
@ -28,8 +28,6 @@ Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArra
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw));
QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
return netJob;
}
@ -38,14 +36,14 @@ 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();
auto netJob = makeShared<NetJob>(QString("Flame::FileChangelog"), APPLICATION->network());
auto response = std::make_shared<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] {
QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &changelog] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@ -60,10 +58,7 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
changelog = Json::ensureString(doc.object(), "data");
});
QObject::connect(netJob, &NetJob::finished, [response, &lock] {
delete response;
lock.quit();
});
QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); });
netJob->start();
lock.exec();
@ -76,13 +71,12 @@ auto FlameAPI::getModDescription(int modId) -> QString
QEventLoop lock;
QString description;
auto* netJob = new NetJob(QString("Flame::ModDescription"), APPLICATION->network());
auto* response = new QByteArray();
netJob->addNetAction(Net::Download::makeByteArray(
QString("https://api.curseforge.com/v1/mods/%1/description")
.arg(QString::number(modId)), response));
auto netJob = makeShared<NetJob>(QString("Flame::ModDescription"), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
netJob->addNetAction(
Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response));
QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &description] {
QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &description] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@ -97,10 +91,7 @@ auto FlameAPI::getModDescription(int modId) -> QString
description = Json::ensureString(doc.object(), "data");
});
QObject::connect(netJob, &NetJob::finished, [response, &lock] {
delete response;
lock.quit();
});
QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); });
netJob->start();
lock.exec();
@ -118,13 +109,13 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
QEventLoop loop;
auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network());
auto response = new QByteArray();
auto netJob = makeShared<NetJob>(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
ModPlatform::IndexedVersion ver;
netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] {
QObject::connect(netJob.get(), &NetJob::succeeded, [response, args, &ver] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@ -144,7 +135,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
for (auto file : arr) {
auto file_obj = Json::requireObject(file);
auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj);
if(file_tmp.date > ver_tmp.date) {
if (file_tmp.date > ver_tmp.date) {
ver_tmp = file_tmp;
latest_file_obj = file_obj;
}
@ -158,11 +149,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
}
});
QObject::connect(netJob, &NetJob::finished, [response, netJob, &loop] {
netJob->deleteLater();
delete response;
loop.quit();
});
QObject::connect(netJob.get(), &NetJob::finished, [&loop] { loop.quit(); });
netJob->start();
@ -171,7 +158,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
return ver;
}
Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const
Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const
{
auto netJob = makeShared<NetJob>(QString("Flame::GetProjects"), APPLICATION->network());
@ -188,13 +175,12 @@ Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) cons
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw));
QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob;
}
Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const
Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const
{
auto netJob = makeShared<NetJob>(QString("Flame::GetFiles"), APPLICATION->network());
@ -211,7 +197,6 @@ Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) c
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw));
QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; });
QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob;

View File

@ -4,7 +4,10 @@
#pragma once
#include <algorithm>
#include <memory>
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "modplatform/helpers/NetworkResourceAPI.h"
class FlameAPI : public NetworkResourceAPI {
@ -14,13 +17,15 @@ class FlameAPI : public NetworkResourceAPI {
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
Task::Ptr matchFingerprints(const QList<uint>& fingerprints, QByteArray* response);
Task::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const;
Task::Ptr getFile(const QString& addonId, const QString& fileId, QByteArray* response) const;
Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
Task::Ptr matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response);
Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const;
Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr<QByteArray> response) const;
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt); }
private:
static int getClassId(ModPlatform::ResourceType type)
{
@ -42,14 +47,15 @@ class FlameAPI : public NetworkResourceAPI {
return 4;
// TODO: remove this once Quilt drops official Fabric support
if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently*
return 4; // Quilt would probably be 5
return 4; // Quilt would probably be 5
return 0;
}
private:
[[nodiscard]] std::optional<QString> getSearchURL(SearchArgs const& args) const override
{
auto gameVersionStr = args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString();
auto gameVersionStr =
args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString();
QStringList get_arguments;
get_arguments.append(QString("classId=%1").arg(getClassId(args.type)));
@ -74,14 +80,48 @@ class FlameAPI : public NetworkResourceAPI {
[[nodiscard]] std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
{
QString url{QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(args.pack.addonId.toString())};
auto addonId = args.pack.addonId.toString();
QString url{ QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(addonId) };
QStringList get_parameters;
if (args.mcVersions.has_value())
get_parameters.append(QString("gameVersion=%1").arg(args.mcVersions.value().front().toString()));
if (args.loaders.has_value())
get_parameters.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value())));
if (args.loaders.has_value()) {
int mappedModLoader = getMappedModLoader(args.loaders.value());
if (args.loaders.value() & Quilt) {
auto overide = ModPlatform::getOverrideDeps();
auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) {
return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt;
});
if (over != overide.cend()) {
mappedModLoader = 5;
}
}
get_parameters.append(QString("modLoaderType=%1").arg(mappedModLoader));
}
return url + get_parameters.join('&');
};
[[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
{
auto mappedModLoader = getMappedModLoader(args.loader);
auto addonId = args.dependency.addonId.toString();
if (args.loader & Quilt) {
auto overide = ModPlatform::getOverrideDeps();
auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) {
return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt;
});
if (over != overide.cend()) {
mappedModLoader = 5;
}
}
return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&gameVersion=%2&modLoaderType=%3")
.arg(addonId)
.arg(args.mcVersion.toString())
.arg(mappedModLoader);
};
};

View File

@ -3,6 +3,7 @@
#include "FlameModIndex.h"
#include <MurmurHash2.h>
#include <memory>
#include "FileSystem.h"
#include "Json.h"
@ -30,7 +31,7 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info)
auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network());
auto response = new QByteArray();
auto response = std::make_shared<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);
@ -74,7 +75,7 @@ ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId)
auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network());
auto response = new QByteArray();
auto response = std::make_shared<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);
@ -129,8 +130,7 @@ void FlameCheckUpdate::executeTask()
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name()));
setProgress(i++, m_mods.size());
ModPlatform::IndexedPack pack{ mod->metadata()->project_id.toString() };
auto latest_ver = api.getLatestVersion({ pack, m_game_versions, m_loaders });
auto latest_ver = api.getLatestVersion({ { mod->metadata()->project_id.toString() }, m_game_versions, m_loaders });
// Check if we were aborted while getting the latest version
if (m_was_aborted) {
@ -156,15 +156,15 @@ void FlameCheckUpdate::executeTask()
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();
auto pack = std::make_shared<ModPlatform::IndexedPack>();
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::ResourceProvider::FLAME;
pack->authors.append({ author });
pack->description = mod->description();
pack->provider = ModPlatform::ResourceProvider::FLAME;
auto old_version = mod->version();
if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
@ -173,7 +173,7 @@ void FlameCheckUpdate::executeTask()
}
auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver, m_mods_folder);
m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version,
m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver.version,
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
ModPlatform::ResourceProvider::FLAME, download_task);
}

View File

@ -153,6 +153,9 @@ bool FlameCreationTask::updateInstance()
old_files.remove(file.key());
files_iterator = files.erase(files_iterator);
if (files_iterator != files.begin())
files_iterator--;
}
}
@ -179,7 +182,7 @@ bool FlameCreationTask::updateInstance()
fileIds.append(QString::number(file.fileId));
}
auto* raw_response = new QByteArray;
auto raw_response = std::make_shared<QByteArray>();
auto job = api.getFiles(fileIds, raw_response);
QEventLoop loop;
@ -384,6 +387,7 @@ bool FlameCreationTask::createInstance()
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propogateStepProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
m_mod_id_resolver->start();
loop.exec();
@ -466,8 +470,9 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever.
// fallthrough intentional, we treat these as plain old mods and dump them wherever.
}
/* fallthrough */
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {

View File

@ -39,15 +39,15 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj)
auto links_obj = Json::ensureObject(obj, "links");
pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl");
if(pack.extraData.issuesUrl.endsWith('/'))
if (pack.extraData.issuesUrl.endsWith('/'))
pack.extraData.issuesUrl.chop(1);
pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl");
if(pack.extraData.sourceUrl.endsWith('/'))
if (pack.extraData.sourceUrl.endsWith('/'))
pack.extraData.sourceUrl.chop(1);
pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl");
if(pack.extraData.wikiUrl.endsWith('/'))
if (pack.extraData.wikiUrl.endsWith('/'))
pack.extraData.wikiUrl.chop(1);
if (!pack.extraData.body.isEmpty())
@ -56,7 +56,7 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj)
void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.extraData.body = api.getModDescription(pack.addonId.toInt());
pack.extraData.body = api.getModDescription(pack.addonId.toInt());
if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty())
pack.extraDataLoaded = true;
@ -64,12 +64,12 @@ void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj)
static QString enumToString(int hash_algorithm)
{
switch(hash_algorithm){
default:
case 1:
return "sha1";
case 2:
return "md5";
switch (hash_algorithm) {
default:
case 1:
return "sha1";
case 2:
return "md5";
}
}
@ -84,12 +84,12 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj);
if(!file.addonId.isValid())
if (!file.addonId.isValid())
file.addonId = pack.addonId;
if(file.fileId.isValid()) // Heuristic to check if the returned value is valid
if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
unsortedVersions.append(file);
}
@ -136,8 +136,61 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
}
}
if(load_changelog)
auto dependencies = Json::ensureArray(obj, "dependencies");
for (auto d : dependencies) {
auto dep = Json::ensureObject(d);
ModPlatform::Dependency dependency;
dependency.addonId = Json::requireInteger(dep, "modId");
switch (Json::requireInteger(dep, "relationType")) {
case 1: // EmbeddedLibrary
dependency.type = ModPlatform::DependencyType::EMBEDDED;
break;
case 2: // OptionalDependency
dependency.type = ModPlatform::DependencyType::OPTIONAL;
break;
case 3: // RequiredDependency
dependency.type = ModPlatform::DependencyType::REQUIRED;
break;
case 4: // Tool
dependency.type = ModPlatform::DependencyType::TOOL;
break;
case 5: // Incompatible
dependency.type = ModPlatform::DependencyType::INCOMPATIBLE;
break;
case 6: // Include
dependency.type = ModPlatform::DependencyType::INCLUDE;
break;
default:
dependency.type = ModPlatform::DependencyType::UNKNOWN;
break;
}
file.dependencies.append(dependency);
}
if (load_changelog)
file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt());
return file;
}
ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr)
{
QVector<ModPlatform::IndexedVersion> versions;
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj);
if (!file.addonId.isValid())
file.addonId = m.addonId;
if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
versions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
// dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(versions.begin(), versions.end(), orderSortPredicate);
return versions.front();
};

View File

@ -6,8 +6,8 @@
#include "modplatform/ModIndex.h"
#include "BaseInstance.h"
#include <QNetworkAccessManager>
#include "BaseInstance.h"
namespace FlameMod {
@ -19,5 +19,5 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
const shared_qobject_ptr<QNetworkAccessManager>& network,
const BaseInstance* inst);
auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion;
} // namespace FlameMod
auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion;
} // namespace FlameMod

View File

@ -0,0 +1,473 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "FlamePackExportTask.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QCryptographicHash>
#include <QFileInfo>
#include <QMessageBox>
#include <QtConcurrentRun>
#include <algorithm>
#include <memory>
#include "Json.h"
#include "MMCZip.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/helpers/HashUtils.h"
#include "tasks/Task.h"
const QString FlamePackExportTask::TEMPLATE = "<li><a href=\"{url}\">{name}{authors}</a></li>\n";
const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" });
FlamePackExportTask::FlamePackExportTask(const QString& name,
const QString& version,
const QString& author,
InstancePtr instance,
const QString& output,
MMCZip::FilterFunction filter)
: name(name)
, version(version)
, author(author)
, instance(instance)
, mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))
, gameRoot(instance->gameRoot())
, output(output)
, filter(filter)
{}
void FlamePackExportTask::executeTask()
{
setStatus(tr("Searching for files..."));
setProgress(0, 5);
collectFiles();
}
bool FlamePackExportTask::abort()
{
if (task != nullptr) {
task->abort();
task = nullptr;
emitAborted();
return true;
}
if (buildZipFuture.isRunning()) {
buildZipFuture.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
void FlamePackExportTask::collectFiles()
{
setAbortable(false);
QCoreApplication::processEvents();
files.clear();
if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) {
emitFailed(tr("Could not search for files"));
return;
}
pendingHashes.clear();
resolvedFiles.clear();
if (mcInstance != nullptr) {
mcInstance->loaderModList()->update();
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes);
} else
collectHashes();
}
void FlamePackExportTask::collectHashes()
{
setAbortable(true);
setStatus(tr("Finding file hashes..."));
setProgress(1, 5);
auto allMods = mcInstance->loaderModList()->allMods();
ConcurrentTask::Ptr hashingTask(new ConcurrentTask(this, "MakeHashesTask", 10));
task.reset(hashingTask);
for (const QFileInfo& file : files) {
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
// require sensible file types
if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
}))
continue;
if (relative.startsWith("resourcepacks/") &&
(relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack
auto hashTask = Hashing::createFlameHasher(file.absoluteFilePath());
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") });
}
});
connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
hashingTask->addTask(hashTask);
continue;
}
if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
modIter != allMods.end()) {
const Mod* mod = *modIter;
if (!mod || mod->type() == ResourceType::FOLDER) {
continue;
}
if (mod->metadata() && mod->metadata()->provider == ModPlatform::ResourceProvider::FLAME) {
resolvedFiles.insert(mod->fileinfo().absoluteFilePath(),
{ mod->metadata()->project_id.toInt(), mod->metadata()->file_id.toInt(), mod->enabled(), true,
mod->metadata()->name, mod->metadata()->slug, mod->authors().join(", ") });
continue;
}
auto hashTask = Hashing::createFlameHasher(mod->fileinfo().absoluteFilePath());
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true });
}
});
connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
hashingTask->addTask(hashTask);
}
}
auto progressStep = std::make_shared<TaskStepProgress>();
connect(hashingTask.get(), &Task::finished, this, [this, progressStep] {
progressStep->state = TaskStepState::Succeeded;
stepProgress(*progressStep);
});
connect(hashingTask.get(), &Task::succeeded, this, &FlamePackExportTask::makeApiRequest);
connect(hashingTask.get(), &Task::failed, this, [this, progressStep](QString reason) {
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
emitFailed(reason);
});
connect(hashingTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propogateStepProgress);
connect(hashingTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
progressStep->update(current, total);
stepProgress(*progressStep);
});
connect(hashingTask.get(), &Task::status, this, [this, progressStep](QString status) {
progressStep->status = status;
stepProgress(*progressStep);
});
hashingTask->start();
}
void FlamePackExportTask::makeApiRequest()
{
if (pendingHashes.isEmpty()) {
buildZip();
return;
}
setStatus(tr("Finding versions for hashes..."));
setProgress(2, 5);
auto response = std::make_shared<QByteArray>();
QList<uint> fingerprints;
for (auto& murmur : pendingHashes.keys()) {
fingerprints.push_back(murmur.toUInt());
}
task.reset(api.matchFingerprints(fingerprints, response));
connect(task.get(), &Task::succeeded, this, [this, response] {
QJsonParseError parseError{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at " << parseError.offset
<< " reason: " << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
return;
}
try {
auto docObj = Json::requireObject(doc);
auto dataObj = Json::requireObject(docObj, "data");
auto dataArr = Json::requireArray(dataObj, "exactMatches");
if (dataArr.isEmpty()) {
qWarning() << "No matches found for fingerprint search!";
return;
}
for (auto match : dataArr) {
auto matchObj = Json::ensureObject(match, {});
auto fileObj = Json::ensureObject(matchObj, "file", {});
if (matchObj.isEmpty() || fileObj.isEmpty()) {
qWarning() << "Fingerprint match is empty!";
return;
}
auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt());
auto mod = pendingHashes.find(fingerprint);
if (mod == pendingHashes.end()) {
qWarning() << "Invalid fingerprint from the API response.";
continue;
}
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name));
if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable"))
resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"),
mod->enabled, mod->isMod });
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
pendingHashes.clear();
});
connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo);
connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed);
task->start();
}
void FlamePackExportTask::getProjectsInfo()
{
setStatus(tr("Finding project info from CurseForge..."));
setProgress(3, 5);
QStringList addonIds;
for (const auto& resolved : resolvedFiles) {
if (resolved.slug.isEmpty()) {
addonIds << QString::number(resolved.addonId);
}
}
auto response = std::make_shared<QByteArray>();
Task::Ptr projTask;
if (addonIds.isEmpty()) {
buildZip();
return;
} else if (addonIds.size() == 1) {
projTask = api.getProject(*addonIds.begin(), response);
} else {
projTask = api.getProjects(addonIds, response);
}
connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parseError{};
auto doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset
<< " reason: " << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
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 entryObj = Json::requireObject(entry);
try {
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(Json::requireString(entryObj, "name")));
ModPlatform::IndexedPack pack;
FlameMod::loadIndexedPack(pack, entryObj);
for (auto key : resolvedFiles.keys()) {
auto val = resolvedFiles.value(key);
if (val.addonId == pack.addonId) {
val.name = pack.name;
val.slug = pack.slug;
QStringList authors;
for (auto author : pack.authors)
authors << author.name;
val.authors = authors.join(", ");
resolvedFiles[key] = val;
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << entries;
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
buildZip();
});
task.reset(projTask);
task->start();
}
void FlamePackExportTask::buildZip()
{
setStatus(tr("Adding files..."));
setProgress(4, 5);
buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() {
QuaZip zip(output);
if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(output);
return BuildZipResult(tr("Could not create file"));
}
if (buildZipFuture.isCanceled())
return BuildZipResult();
QuaZipFile indexFile(&zip);
if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo("manifest.json"))) {
QFile::remove(output);
return BuildZipResult(tr("Could not create index"));
}
indexFile.write(generateIndex());
QuaZipFile modlist(&zip);
if (!modlist.open(QIODevice::WriteOnly, QuaZipNewInfo("modlist.html"))) {
QFile::remove(output);
return BuildZipResult(tr("Could not create index"));
}
QString content = "";
for (auto mod : resolvedFiles) {
if (mod.isMod) {
content += QString(TEMPLATE)
.replace("{name}", mod.name.toHtmlEscaped())
.replace("{url}", ModPlatform::getMetaURL(ModPlatform::ResourceProvider::FLAME, mod.addonId).toHtmlEscaped())
.replace("{authors}", !mod.authors.isEmpty() ? QString(" (by %1)").arg(mod.authors).toHtmlEscaped() : "");
}
}
content = "<ul>" + content + "</ul>";
modlist.write(content.toUtf8());
auto progressStep = std::make_shared<TaskStepProgress>();
size_t progress = 0;
for (const QFileInfo& file : files) {
if (buildZipFuture.isCanceled()) {
QFile::remove(output);
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
return BuildZipResult();
}
progressStep->update(progress, files.length());
stepProgress(*progressStep);
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
if (!resolvedFiles.contains(file.absoluteFilePath()) &&
!JlCompress::compressFile(&zip, file.absoluteFilePath(), "overrides/" + relative)) {
QFile::remove(output);
return BuildZipResult(tr("Could not read and compress %1").arg(relative));
}
progress++;
}
zip.close();
if (zip.getZipError() != 0) {
QFile::remove(output);
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
return BuildZipResult(tr("A zip error occurred"));
}
progressStep->state = TaskStepState::Succeeded;
stepProgress(*progressStep);
return BuildZipResult();
});
connect(&buildZipWatcher, &QFutureWatcher<BuildZipResult>::finished, this, &FlamePackExportTask::finish);
buildZipWatcher.setFuture(buildZipFuture);
}
void FlamePackExportTask::finish()
{
if (buildZipFuture.isCanceled())
emitAborted();
else {
const BuildZipResult result = buildZipFuture.result();
if (result.has_value())
emitFailed(result.value());
else
emitSucceeded();
}
}
QByteArray FlamePackExportTask::generateIndex()
{
QJsonObject obj;
obj["manifestType"] = "minecraftModpack";
obj["manifestVersion"] = 1;
obj["name"] = name;
obj["version"] = version;
obj["author"] = author;
obj["overrides"] = "overrides";
if (mcInstance) {
QJsonObject version;
auto profile = mcInstance->getPackProfile();
// collect all supported components
const ComponentPtr minecraft = profile->getComponent("net.minecraft");
const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
const ComponentPtr forge = profile->getComponent("net.minecraftforge");
// convert all available components to mrpack dependencies
if (minecraft != nullptr)
version["version"] = minecraft->m_version;
QString id;
if (quilt != nullptr)
id = "quilt-" + quilt->getVersion();
else if (fabric != nullptr)
id = "fabric-" + fabric->getVersion();
else if (forge != nullptr)
id = "forge-" + forge->getVersion();
version["modLoaders"] = QJsonArray();
if (!id.isEmpty()) {
QJsonObject loader;
loader["id"] = id;
loader["primary"] = true;
version["modLoaders"] = QJsonArray({ loader });
}
obj["minecraft"] = version;
}
QJsonArray files;
for (auto mod : resolvedFiles) {
QJsonObject file;
file["projectID"] = mod.addonId;
file["fileID"] = mod.version;
file["required"] = mod.enabled;
files << file;
}
obj["files"] = files;
return QJsonDocument(obj).toJson(QJsonDocument::Compact);
}

View File

@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QFuture>
#include <QFutureWatcher>
#include "BaseInstance.h"
#include "MMCZip.h"
#include "minecraft/MinecraftInstance.h"
#include "modplatform/flame/FlameAPI.h"
#include "tasks/Task.h"
class FlamePackExportTask : public Task {
public:
FlamePackExportTask(const QString& name,
const QString& version,
const QString& author,
InstancePtr instance,
const QString& output,
MMCZip::FilterFunction filter);
protected:
void executeTask() override;
bool abort() override;
private:
static const QString TEMPLATE;
static const QStringList FILE_EXTENSIONS;
// inputs
const QString name, version, author;
const InstancePtr instance;
MinecraftInstance* mcInstance;
const QDir gameRoot;
const QString output;
const MMCZip::FilterFunction filter;
typedef std::optional<QString> BuildZipResult;
struct ResolvedFile {
int addonId;
int version;
bool enabled;
bool isMod;
QString name;
QString slug;
QString authors;
};
struct HashInfo {
QString name;
QString path;
bool enabled;
bool isMod;
};
FlameAPI api;
QFileInfoList files;
QMap<QString, HashInfo> pendingHashes{};
QMap<QString, ResolvedFile> resolvedFiles{};
Task::Ptr task;
QFuture<BuildZipResult> buildZipFuture;
QFutureWatcher<BuildZipResult> buildZipWatcher;
void collectFiles();
void collectHashes();
void makeApiRequest();
void getProjectsInfo();
void buildZip();
void finish();
QByteArray generateIndex();
};

View File

@ -4,6 +4,7 @@
#include <QMetaType>
#include <QString>
#include <QVector>
#include "modplatform/ModIndex.h"
namespace Flame {
@ -27,8 +28,7 @@ struct ModpackExtra {
QString sourceUrl;
};
struct IndexedPack
{
struct IndexedPack {
int addonId;
QString name;
QString description;
@ -43,9 +43,9 @@ struct IndexedPack
ModpackExtra extra;
};
void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
void loadIndexedPack(IndexedPack& m, QJsonObject& obj);
void loadIndexedInfo(IndexedPack&, QJsonObject&);
void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr);
}
void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr);
} // namespace Flame
Q_DECLARE_METATYPE(Flame::IndexedPack)