Merge branch 'develop' into curseforge-url-handle
This commit is contained in:
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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
|
473
launcher/modplatform/flame/FlamePackExportTask.cpp
Normal file
473
launcher/modplatform/flame/FlamePackExportTask.cpp
Normal 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);
|
||||
}
|
90
launcher/modplatform/flame/FlamePackExportTask.h
Normal file
90
launcher/modplatform/flame/FlamePackExportTask.h
Normal 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();
|
||||
};
|
@ -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)
|
||||
|
Reference in New Issue
Block a user