removed some duplicate code

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97
2025-03-30 00:32:59 +02:00
parent ab3bfb0f74
commit 29cff14fd6
52 changed files with 928 additions and 2318 deletions

View File

@@ -128,6 +128,23 @@ struct IndexedVersion {
// For internal use, not provided by APIs
bool is_currently_selected = false;
QString getVersionDisplayString() const
{
auto release_type = version_type.isValid() ? QString(" [%1]").arg(version_type.toString()) : "";
auto versionStr = !version.contains(version_number) ? version_number : "";
QString gameVersion = "";
for (auto v : mcVersion) {
if (version.contains(v)) {
gameVersion = "";
break;
}
if (gameVersion.isEmpty()) {
gameVersion = QObject::tr(" for %1").arg(v);
}
}
return QString("%1%2 — %3%4").arg(version, gameVersion, versionStr, release_type);
}
};
struct ExtraPackData {

View File

@@ -1,18 +1,14 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "NetworkResourceAPI.h"
#include <memory>
#include "modplatform/ResourceAPI.h"
#include "Application.h"
#include "Json.h"
#include "net/NetJob.h"
#include "modplatform/ModIndex.h"
#include "net/ApiDownload.h"
Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&& callbacks) const
Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args, Callback<QList<ModPlatform::IndexedPack::Ptr>>&& callbacks) const
{
auto search_url_optional = getSearchURL(args);
if (!search_url_optional.has_value()) {
@@ -40,7 +36,23 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
return;
}
callbacks.on_succeed(doc);
QList<ModPlatform::IndexedPack::Ptr> newList;
auto packs = documentToArray(doc);
for (auto packRaw : packs) {
auto packObj = packRaw.toObject();
ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>();
try {
loadIndexedPack(*pack, packObj);
newList << pack;
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
continue;
}
}
callbacks.on_succeed(newList);
});
// Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
@@ -60,29 +72,7 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
return netJob;
}
Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const
{
auto response = std::make_shared<QByteArray>();
auto job = getProject(args.pack.addonId.toString(), response);
QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
callbacks.on_succeed(doc, args.pack);
});
QObject::connect(job.get(), &NetJob::failed, [callbacks](QString reason) { callbacks.on_fail(reason); });
QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });
return job;
}
Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, VersionSearchCallbacks&& callbacks) const
Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const
{
auto versions_url_optional = getVersionsURL(args);
if (!versions_url_optional.has_value())
@@ -95,7 +85,7 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
QObject::connect(netJob.get(), &NetJob::succeeded, [response, callbacks, args] {
QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@@ -105,7 +95,32 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
return;
}
callbacks.on_succeed(doc, args.pack);
QVector<ModPlatform::IndexedVersion> unsortedVersions;
try {
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj, args.resourceType);
if (!file.addonId.isValid())
file.addonId = args.pack.addonId;
if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) // Heuristic to check if the returned value is valid
unsortedVersions.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(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
}
callbacks.on_succeed(unsortedVersions);
});
// Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
@@ -120,11 +135,147 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
}
callbacks.on_fail(reason, network_error_code);
});
QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });
return netJob;
}
Task::Ptr NetworkResourceAPI::getProject(QString addonId, std::shared_ptr<QByteArray> response) const
Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack>&& callbacks) const
{
auto response = std::make_shared<QByteArray>();
auto job = getProject(args.pack.addonId.toString(), response);
QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] {
auto pack = args.pack;
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
try {
auto obj = Json::requireObject(doc);
if (obj.contains("data"))
obj = Json::requireObject(obj, "data");
loadIndexedPack(pack, obj);
loadExtraPackInfo(pack, obj);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
}
callbacks.on_succeed(pack);
});
// Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
// This prevents the lambda from extending the lifetime of the shared resource,
// as it only temporarily locks the resource when needed.
auto weak = job.toWeakRef();
QObject::connect(job.get(), &NetJob::failed, [weak, callbacks](const QString& reason) {
int network_error_code = -1;
if (auto job = weak.lock()) {
if (auto netJob = qSharedPointerDynamicCast<NetJob>(job)) {
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) {
network_error_code = failed_action->replyStatusCode();
}
}
}
callbacks.on_fail(reason, network_error_code);
});
QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });
return job;
}
Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args, Callback<ModPlatform::IndexedVersion>&& callbacks) const
{
auto versions_url_optional = getDependencyURL(args);
if (!versions_url_optional.has_value())
return nullptr;
auto versions_url = versions_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
QJsonArray arr;
if (args.dependency.version.length() != 0 && doc.isObject()) {
arr.append(doc.object());
} else {
arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
}
QVector<ModPlatform::IndexedVersion> versions;
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj, ModPlatform::ResourceType::Mod);
if (!file.addonId.isValid())
file.addonId = args.dependency.addonId;
if (file.fileId.isValid() &&
(!file.loaders || args.loader & file.loaders)) // 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);
auto bestMatch = versions.size() != 0 ? versions.front() : ModPlatform::IndexedVersion();
callbacks.on_succeed(bestMatch);
});
// Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
// This prevents the lambda from extending the lifetime of the shared resource,
// as it only temporarily locks the resource when needed.
auto weak = netJob.toWeakRef();
QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) {
int network_error_code = -1;
if (auto netJob = weak.lock()) {
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
network_error_code = failed_action->replyStatusCode();
}
callbacks.on_fail(reason, network_error_code);
});
return netJob;
}
QString ResourceAPI::getGameVersionsString(std::list<Version> mcVersions) const
{
QString s;
for (auto& ver : mcVersions) {
s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver));
}
s.remove(s.length() - 1, 1); // remove last comma
return s;
}
QString ResourceAPI::mapMCVersionToModrinth(Version v) const
{
static const QString preString = " Pre-Release ";
auto verStr = v.toString();
if (verStr.contains(preString)) {
verStr.replace(preString, "-pre");
}
verStr.replace(" ", "-");
return verStr;
}
Task::Ptr ResourceAPI::getProject(QString addonId, std::shared_ptr<QByteArray> response) const
{
auto project_url_optional = getInfoURL(addonId);
if (!project_url_optional.has_value())
@@ -138,44 +289,3 @@ Task::Ptr NetworkResourceAPI::getProject(QString addonId, std::shared_ptr<QByteA
return netJob;
}
Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, DependencySearchCallbacks&& callbacks) const
{
auto versions_url_optional = getDependencyURL(args);
if (!versions_url_optional.has_value())
return nullptr;
auto versions_url = versions_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
QObject::connect(netJob.get(), &NetJob::succeeded, [response, callbacks, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
callbacks.on_succeed(doc, args.dependency);
});
// Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
// This prevents the lambda from extending the lifetime of the shared resource,
// as it only temporarily locks the resource when needed.
auto weak = netJob.toWeakRef();
QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) {
int network_error_code = -1;
if (auto netJob = weak.lock()) {
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
network_error_code = failed_action->replyStatusCode();
}
callbacks.on_fail(reason, network_error_code);
});
return netJob;
}

View File

@@ -4,7 +4,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* Copyright (c) 2023-2025 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
@@ -67,6 +67,13 @@ class ResourceAPI {
QString readable_name;
};
template <typename T>
struct Callback {
std::function<void(T&)> on_succeed;
std::function<void(QString const& reason, int network_error_code)> on_fail;
std::function<void()> on_abort;
};
struct SearchArgs {
ModPlatform::ResourceType type{};
int offset = 0;
@@ -79,31 +86,18 @@ class ResourceAPI {
std::optional<QStringList> categoryIds;
bool openSource;
};
struct SearchCallbacks {
std::function<void(QJsonDocument&)> on_succeed;
std::function<void(QString const& reason, int network_error_code)> on_fail;
std::function<void()> on_abort;
};
struct VersionSearchArgs {
ModPlatform::IndexedPack pack;
std::optional<std::list<Version>> mcVersions;
std::optional<ModPlatform::ModLoaderTypes> loaders;
};
struct VersionSearchCallbacks {
std::function<void(QJsonDocument&, ModPlatform::IndexedPack)> on_succeed;
std::function<void(QString const& reason, int network_error_code)> on_fail;
ModPlatform::ResourceType resourceType;
};
struct ProjectInfoArgs {
ModPlatform::IndexedPack pack;
};
struct ProjectInfoCallbacks {
std::function<void(QJsonDocument&, const ModPlatform::IndexedPack&)> on_succeed;
std::function<void(QString const& reason)> on_fail;
std::function<void()> on_abort;
};
struct DependencySearchArgs {
ModPlatform::Dependency dependency;
@@ -111,73 +105,52 @@ class ResourceAPI {
ModPlatform::ModLoaderTypes loader;
};
struct DependencySearchCallbacks {
std::function<void(QJsonDocument&, const ModPlatform::Dependency&)> on_succeed;
std::function<void(QString const& reason, int network_error_code)> on_fail;
};
public:
/** Gets a list of available sorting methods for this API. */
virtual auto getSortingMethods() const -> QList<SortingMethod> = 0;
public slots:
[[nodiscard]] virtual Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const
{
qWarning() << "TODO: ResourceAPI::searchProjects";
return nullptr;
}
virtual Task::Ptr getProject([[maybe_unused]] QString addonId,
[[maybe_unused]] std::shared_ptr<QByteArray> response) const
{
qWarning() << "TODO: ResourceAPI::getProject";
return nullptr;
}
virtual Task::Ptr getProjects([[maybe_unused]] QStringList addonIds,
[[maybe_unused]] std::shared_ptr<QByteArray> response) const
{
qWarning() << "TODO: ResourceAPI::getProjects";
return nullptr;
}
virtual Task::Ptr searchProjects(SearchArgs&&, Callback<QList<ModPlatform::IndexedPack::Ptr>>&&) const;
virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const
{
qWarning() << "TODO: ResourceAPI::getProjectInfo";
return nullptr;
}
virtual Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const
{
qWarning() << "TODO: ResourceAPI::getProjectVersions";
return nullptr;
}
virtual Task::Ptr getProject(QString addonId, std::shared_ptr<QByteArray> response) const;
virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const = 0;
virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const
{
qWarning() << "TODO";
return nullptr;
}
virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback<ModPlatform::IndexedPack>&&) const;
Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const;
virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback<ModPlatform::IndexedVersion>&&) const;
protected:
inline QString debugName() const { return "External resource API"; }
inline QString mapMCVersionToModrinth(Version v) const
{
static const QString preString = " Pre-Release ";
auto verStr = v.toString();
QString mapMCVersionToModrinth(Version v) const;
if (verStr.contains(preString)) {
verStr.replace(preString, "-pre");
}
verStr.replace(" ", "-");
return verStr;
}
QString getGameVersionsString(std::list<Version> mcVersions) const;
inline QString getGameVersionsString(std::list<Version> mcVersions) const
{
QString s;
for (auto& ver : mcVersions) {
s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver));
}
s.remove(s.length() - 1, 1); // remove last comma
return s;
}
public:
virtual auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> = 0;
virtual auto getInfoURL(QString const& id) const -> std::optional<QString> = 0;
virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> = 0;
virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional<QString> = 0;
/** Functions to load data into a pack.
*
* Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way.
*/
virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) const = 0;
virtual ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const = 0;
/** Converts a JSON document to a common array format.
*
* This is needed so that different providers, with different JSON structures, can be parsed
* uniformally. You NEED to re-implement this if you intend on using the default callbacks.
*/
virtual QJsonArray documentToArray(QJsonDocument& obj) const = 0;
/** Functions to load data into a pack.
*
* Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way.
*/
virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) const = 0;
};

View File

@@ -7,11 +7,13 @@
#include <QList>
#include <memory>
#include "BuildConfig.h"
#include "Json.h"
#include "Version.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "modplatform/helpers/NetworkResourceAPI.h"
#include "modplatform/flame/FlameModIndex.h"
class FlameAPI : public NetworkResourceAPI {
class FlameAPI : public ResourceAPI {
public:
QString getModFileChangelog(int modId, int fileId);
QString getModDescription(int modId);
@@ -138,6 +140,25 @@ class FlameAPI : public NetworkResourceAPI {
return url;
}
QJsonArray documentToArray(QJsonDocument& obj) const override { return Json::ensureArray(obj.object(), "data"); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { FlameMod::loadIndexedPack(m, obj); }
ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType resourceType) const override
{
auto arr = FlameMod::loadIndexedPackVersion(obj);
if (resourceType != ModPlatform::ResourceType::TexturePack) {
return arr;
}
// FIXME: Client-side version filtering. This won't take into account any user-selected filtering.
auto const& mc_versions = arr.mcVersion;
if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(),
[](auto const& mc_version) { return Version(mc_version) <= Version("1.6"); })) {
return arr;
}
return {};
};
void loadExtraPackInfo(ModPlatform::IndexedPack& m, [[maybe_unused]] QJsonObject&) const override { FlameMod::loadBody(m); }
private:
std::optional<QString> getInfoURL(QString const& id) const override
{

View File

@@ -58,7 +58,7 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.extraDataLoaded = true;
}
void FlameMod::loadBody(ModPlatform::IndexedPack& pack, [[maybe_unused]] QJsonObject& obj)
void FlameMod::loadBody(ModPlatform::IndexedPack& pack)
{
pack.extraData.body = api.getModDescription(pack.addonId.toInt());
@@ -204,31 +204,3 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
return file;
}
ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst)
{
auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");
auto loaders = profile->getSupportedModLoaders();
QList<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() &&
(!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // 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);
if (versions.size() != 0)
return versions.front();
return {};
}

View File

@@ -12,8 +12,7 @@ namespace FlameMod {
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadBody(ModPlatform::IndexedPack& m);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr);
ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false);
ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst);
} // namespace FlameMod

View File

@@ -1,150 +0,0 @@
#include "FlamePackIndex.h"
#include <QFileInfo>
#include <QUrl>
#include "Json.h"
#include "modplatform/ModIndex.h"
void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
pack.name = Json::requireString(obj, "name");
pack.description = Json::ensureString(obj, "summary", "");
auto logo = Json::requireObject(obj, "logo");
pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
pack.logoName = Json::requireString(obj, "slug") + "." + QFileInfo(QUrl(pack.logoUrl).fileName()).suffix();
auto authors = Json::requireArray(obj, "authors");
for (auto authorIter : authors) {
auto author = Json::requireObject(authorIter);
Flame::ModpackAuthor packAuthor;
packAuthor.name = Json::requireString(author, "name");
packAuthor.url = Json::requireString(author, "url");
pack.authors.append(packAuthor);
}
int defaultFileId = Json::requireInteger(obj, "mainFileId");
bool found = false;
// check if there are some files before adding the pack
auto files = Json::requireArray(obj, "latestFiles");
for (auto fileIter : files) {
auto file = Json::requireObject(fileIter);
int id = Json::requireInteger(file, "id");
// NOTE: for now, ignore everything that's not the default...
if (id != defaultFileId) {
continue;
}
auto versionArray = Json::requireArray(file, "gameVersions");
if (versionArray.size() < 1) {
continue;
}
found = true;
break;
}
if (!found) {
throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name));
}
loadIndexedInfo(pack, obj);
}
void Flame::loadIndexedInfo(IndexedPack& pack, QJsonObject& obj)
{
auto links_obj = Json::ensureObject(obj, "links");
pack.extra.websiteUrl = Json::ensureString(links_obj, "websiteUrl");
if (pack.extra.websiteUrl.endsWith('/'))
pack.extra.websiteUrl.chop(1);
pack.extra.issuesUrl = Json::ensureString(links_obj, "issuesUrl");
if (pack.extra.issuesUrl.endsWith('/'))
pack.extra.issuesUrl.chop(1);
pack.extra.sourceUrl = Json::ensureString(links_obj, "sourceUrl");
if (pack.extra.sourceUrl.endsWith('/'))
pack.extra.sourceUrl.chop(1);
pack.extra.wikiUrl = Json::ensureString(links_obj, "wikiUrl");
if (pack.extra.wikiUrl.endsWith('/'))
pack.extra.wikiUrl.chop(1);
pack.extraInfoLoaded = true;
}
void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr)
{
QList<Flame::IndexedVersion> unsortedVersions;
for (auto versionIter : arr) {
auto version = Json::requireObject(versionIter);
Flame::IndexedVersion file;
file.addonId = pack.addonId;
file.fileId = Json::requireInteger(version, "id");
auto versionArray = Json::requireArray(version, "gameVersions");
if (versionArray.size() < 1) {
continue;
}
for (auto mcVer : versionArray) {
auto str = mcVer.toString();
if (str.contains('.'))
file.mcVersion.append(str);
if (auto loader = str.toLower(); loader == "neoforge")
file.loaders |= ModPlatform::NeoForge;
else if (loader == "forge")
file.loaders |= ModPlatform::Forge;
else if (loader == "cauldron")
file.loaders |= ModPlatform::Cauldron;
else if (loader == "liteloader")
file.loaders |= ModPlatform::LiteLoader;
else if (loader == "fabric")
file.loaders |= ModPlatform::Fabric;
else if (loader == "quilt")
file.loaders |= ModPlatform::Quilt;
}
// pick the latest version supported
file.version = Json::requireString(version, "displayName");
ModPlatform::IndexedVersionType::VersionType ver_type;
switch (Json::requireInteger(version, "releaseType")) {
case 1:
ver_type = ModPlatform::IndexedVersionType::VersionType::Release;
break;
case 2:
ver_type = ModPlatform::IndexedVersionType::VersionType::Beta;
break;
case 3:
ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha;
break;
default:
ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown;
}
file.version_type = ModPlatform::IndexedVersionType(ver_type);
file.downloadUrl = Json::ensureString(version, "downloadUrl");
// only add if we have a download URL (third party distribution is enabled)
if (!file.downloadUrl.isEmpty()) {
unsortedVersions.append(file);
}
}
auto orderSortPredicate = [](const IndexedVersion& a, const IndexedVersion& b) -> bool { return a.fileId > b.fileId; };
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
pack.versions = unsortedVersions;
pack.versionsLoaded = true;
}
auto Flame::getVersionDisplayString(const IndexedVersion& version) -> QString
{
auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : "";
auto mcVersion =
!version.mcVersion.isEmpty() && !version.version.contains(version.mcVersion) ? QObject::tr(" for %1").arg(version.mcVersion) : "";
return QString("%1%2%3").arg(version.version, mcVersion, release_type);
}

View File

@@ -1,55 +0,0 @@
#pragma once
#include <QList>
#include <QMetaType>
#include <QString>
#include "modplatform/ModIndex.h"
namespace Flame {
struct ModpackAuthor {
QString name;
QString url;
};
struct IndexedVersion {
int addonId;
int fileId;
QString version;
ModPlatform::IndexedVersionType version_type;
ModPlatform::ModLoaderTypes loaders = {};
QString mcVersion;
QString downloadUrl;
};
struct ModpackExtra {
QString websiteUrl;
QString wikiUrl;
QString issuesUrl;
QString sourceUrl;
};
struct IndexedPack {
int addonId;
QString name;
QString description;
QList<ModpackAuthor> authors;
QString logoName;
QString logoUrl;
bool versionsLoaded = false;
QList<IndexedVersion> versions;
bool extraInfoLoaded = false;
ModpackExtra extra;
};
void loadIndexedPack(IndexedPack& m, QJsonObject& obj);
void loadIndexedInfo(IndexedPack&, QJsonObject&);
void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr);
auto getVersionDisplayString(const IndexedVersion&) -> QString;
} // namespace Flame
Q_DECLARE_METATYPE(Flame::IndexedPack)
Q_DECLARE_METATYPE(QList<Flame::IndexedVersion>)

View File

@@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <memory>
#include "modplatform/ResourceAPI.h"
class NetworkResourceAPI : public ResourceAPI {
public:
Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override;
Task::Ptr getProject(QString addonId, std::shared_ptr<QByteArray> response) const override;
Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override;
Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override;
Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const override;
protected:
virtual auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> = 0;
virtual auto getInfoURL(QString const& id) const -> std::optional<QString> = 0;
virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> = 0;
virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional<QString> = 0;
};

View File

@@ -5,12 +5,14 @@
#pragma once
#include "BuildConfig.h"
#include "Json.h"
#include "modplatform/ModIndex.h"
#include "modplatform/helpers/NetworkResourceAPI.h"
#include "modplatform/ResourceAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
#include <QDebug>
class ModrinthAPI : public NetworkResourceAPI {
class ModrinthAPI : public ResourceAPI {
public:
Task::Ptr currentVersion(QString hash, QString hash_format, std::shared_ptr<QByteArray> response);
@@ -214,4 +216,12 @@ class ModrinthAPI : public NetworkResourceAPI {
.arg(mapMCVersionToModrinth(args.mcVersion))
.arg(getModLoaderStrings(args.loader).join("\",\""));
};
QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object().value("hits").toArray(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadIndexedPack(m, obj); }
ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const override
{
return Modrinth::loadIndexedPackVersion(obj);
};
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadExtraPackData(m, obj); }
};

View File

@@ -13,7 +13,6 @@
#include "modplatform/EnsureMetadataTask.h"
#include "modplatform/helpers/OverrideUtils.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "net/ChecksumValidator.h"
#include "net/ApiDownload.h"
@@ -85,7 +84,7 @@ bool ModrinthCreationTask::updateInstance()
QString old_index_path(FS::PathCombine(old_index_folder, "modrinth.index.json"));
QFileInfo old_index_file(old_index_path);
if (old_index_file.exists()) {
std::vector<Modrinth::File> old_files;
std::vector<File> old_files;
parseManifest(old_index_path, old_files, false, false);
// Let's remove all duplicated, identical resources!
@@ -356,7 +355,7 @@ bool ModrinthCreationTask::createInstance()
}
bool ModrinthCreationTask::parseManifest(const QString& index_path,
std::vector<Modrinth::File>& files,
std::vector<File>& files,
bool set_internal_data,
bool show_optional_dialog)
{
@@ -377,9 +376,9 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
}
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
std::vector<Modrinth::File> optionalFiles;
std::vector<File> optionalFiles;
for (const auto& modInfo : jsonFiles) {
Modrinth::File file;
File file;
file.path = Json::requireString(modInfo, "path").replace("\\", "/");
auto env = Json::ensureObject(modInfo, "env");

View File

@@ -1,13 +1,27 @@
#pragma once
#include <optional>
#include <QByteArray>
#include <QCryptographicHash>
#include <QQueue>
#include <QString>
#include <QUrl>
#include <QVector>
#include "BaseInstance.h"
#include "InstanceCreationTask.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
class ModrinthCreationTask final : public InstanceCreationTask {
Q_OBJECT
struct File {
QString path;
QCryptographicHash::Algorithm hashAlgorithm;
QByteArray hash;
QQueue<QUrl> downloads;
bool required = true;
};
public:
ModrinthCreationTask(QString staging_path,
@@ -30,7 +44,7 @@ class ModrinthCreationTask final : public InstanceCreationTask {
bool createInstance() override;
private:
bool parseManifest(const QString&, std::vector<Modrinth::File>&, bool set_internal_data = true, bool show_optional_dialog = true);
bool parseManifest(const QString&, std::vector<File>&, bool set_internal_data = true, bool show_optional_dialog = true);
private:
QWidget* m_parent = nullptr;
@@ -38,7 +52,7 @@ class ModrinthCreationTask final : public InstanceCreationTask {
QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version, m_neoForge_version;
QString m_managed_id, m_managed_version_id, m_managed_name;
std::vector<Modrinth::File> m_files;
std::vector<File> m_files;
Task::Ptr m_task;
std::optional<InstancePtr> m_instance;

View File

@@ -112,25 +112,6 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
pack.extraDataLoaded = true;
}
void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr)
{
QList<ModPlatform::IndexedVersion> unsortedVersions;
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj);
if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
unsortedVersions.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(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
pack.versions = unsortedVersions;
pack.versionsLoaded = true;
}
ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name)
{
ModPlatform::IndexedVersion file;
@@ -244,28 +225,3 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q
return {};
}
ModPlatform::IndexedVersion Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m,
QJsonArray& arr,
const BaseInstance* inst)
{
auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");
auto loaders = profile->getSupportedModLoaders();
QList<ModPlatform::IndexedVersion> versions;
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj);
if (file.fileId.isValid() &&
(!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // 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.length() != 0 ? versions.front() : ModPlatform::IndexedVersion();
}

View File

@@ -25,8 +25,6 @@ namespace Modrinth {
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr);
auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion;
} // namespace Modrinth

View File

@@ -1,196 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@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/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
* Copyright 2022 kb1000
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ModrinthPackManifest.h"
#include <QFileInfo>
#include "Json.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include <QSet>
static ModrinthAPI api;
namespace Modrinth {
void loadIndexedPack(Modpack& pack, QJsonObject& obj)
{
pack.id = Json::ensureString(obj, "project_id");
pack.name = Json::ensureString(obj, "title");
pack.description = Json::ensureString(obj, "description");
auto temp_author_name = Json::ensureString(obj, "author");
pack.author = std::make_tuple(temp_author_name, api.getAuthorURL(temp_author_name));
pack.iconUrl = Json::ensureString(obj, "icon_url");
pack.iconName = QString("modrinth_%1.%2").arg(Json::ensureString(obj, "slug"), QFileInfo(pack.iconUrl.fileName()).suffix());
}
void loadIndexedInfo(Modpack& pack, QJsonObject& obj)
{
pack.extra.body = Json::ensureString(obj, "body");
pack.extra.projectUrl = QString("https://modrinth.com/modpack/%1").arg(Json::ensureString(obj, "slug"));
pack.extra.issuesUrl = Json::ensureString(obj, "issues_url");
if (pack.extra.issuesUrl.endsWith('/'))
pack.extra.issuesUrl.chop(1);
pack.extra.sourceUrl = Json::ensureString(obj, "source_url");
if (pack.extra.sourceUrl.endsWith('/'))
pack.extra.sourceUrl.chop(1);
pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url");
if (pack.extra.wikiUrl.endsWith('/'))
pack.extra.wikiUrl.chop(1);
pack.extra.discordUrl = Json::ensureString(obj, "discord_url");
if (pack.extra.discordUrl.endsWith('/'))
pack.extra.discordUrl.chop(1);
auto donate_arr = Json::ensureArray(obj, "donation_urls");
for (auto d : donate_arr) {
auto d_obj = Json::requireObject(d);
DonationData donate;
donate.id = Json::ensureString(d_obj, "id");
donate.platform = Json::ensureString(d_obj, "platform");
donate.url = Json::ensureString(d_obj, "url");
pack.extra.donate.append(donate);
}
pack.extra.status = Json::ensureString(obj, "status");
pack.extraInfoLoaded = true;
}
void loadIndexedVersions(Modpack& pack, QJsonDocument& doc)
{
QList<ModpackVersion> unsortedVersions;
auto arr = Json::requireArray(doc);
for (auto versionIter : arr) {
auto obj = Json::requireObject(versionIter);
auto file = loadIndexedVersion(obj);
if (!file.id.isEmpty()) // Heuristic to check if the returned value is valid
unsortedVersions.append(file);
}
auto orderSortPredicate = [](const ModpackVersion& a, const ModpackVersion& b) -> bool {
// dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
pack.versions.swap(unsortedVersions);
pack.versionsLoaded = true;
}
auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion
{
ModpackVersion file;
file.name = Json::requireString(obj, "name");
file.version = Json::requireString(obj, "version_number");
auto gameVersions = Json::ensureArray(obj, "game_versions");
if (!gameVersions.isEmpty()) {
file.gameVersion = Json::ensureString(gameVersions[0]);
file.gameVersion = ModrinthAPI::mapMCVersionFromModrinth(file.gameVersion);
}
auto loaders = Json::requireArray(obj, "loaders");
for (auto loader : loaders) {
if (loader == "neoforge")
file.loaders |= ModPlatform::NeoForge;
else if (loader == "forge")
file.loaders |= ModPlatform::Forge;
else if (loader == "cauldron")
file.loaders |= ModPlatform::Cauldron;
else if (loader == "liteloader")
file.loaders |= ModPlatform::LiteLoader;
else if (loader == "fabric")
file.loaders |= ModPlatform::Fabric;
else if (loader == "quilt")
file.loaders |= ModPlatform::Quilt;
}
file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type"));
file.changelog = Json::ensureString(obj, "changelog");
file.id = Json::requireString(obj, "id");
file.project_id = Json::requireString(obj, "project_id");
file.date = Json::requireString(obj, "date_published");
auto files = Json::requireArray(obj, "files");
for (auto file_iter : files) {
File indexed_file;
auto parent = Json::requireObject(file_iter);
auto is_primary = Json::ensureBoolean(parent, (const QString)QStringLiteral("primary"), false);
if (!is_primary) {
auto filename = Json::ensureString(parent, "filename");
// Checking suffix here is fine because it's the response from Modrinth,
// so one would assume it will always be in English.
if (!filename.endsWith("mrpack") && !filename.endsWith("zip"))
continue;
}
auto url = Json::requireString(parent, "url");
file.download_url = url;
if (is_primary)
break;
}
if (file.download_url.isEmpty())
return {};
return file;
}
auto getVersionDisplayString(const ModpackVersion& version) -> QString
{
auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : "";
auto mcVersion = !version.gameVersion.isEmpty() && !version.name.contains(version.gameVersion)
? QObject::tr(" for %1").arg(version.gameVersion)
: "";
auto versionStr = !version.name.contains(version.version) ? version.version : "";
return QString("%1%2 — %3%4").arg(version.name, mcVersion, versionStr, release_type);
}
} // namespace Modrinth

View File

@@ -1,129 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@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/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
* Copyright 2022 kb1000
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QMetaType>
#include <QByteArray>
#include <QCryptographicHash>
#include <QList>
#include <QQueue>
#include <QString>
#include <QUrl>
#include "modplatform/ModIndex.h"
class MinecraftInstance;
namespace Modrinth {
struct File {
QString path;
QCryptographicHash::Algorithm hashAlgorithm;
QByteArray hash;
QQueue<QUrl> downloads;
bool required = true;
};
struct DonationData {
QString id;
QString platform;
QString url;
};
struct ModpackExtra {
QString body;
QString projectUrl;
QString issuesUrl;
QString sourceUrl;
QString wikiUrl;
QString discordUrl;
QList<DonationData> donate;
QString status;
};
struct ModpackVersion {
QString name;
QString version;
QString gameVersion;
ModPlatform::IndexedVersionType version_type;
QString changelog;
ModPlatform::ModLoaderTypes loaders = {};
QString id;
QString project_id;
QString date;
QString download_url;
};
struct Modpack {
QString id;
QString name;
QString description;
std::tuple<QString, QUrl> author;
QString iconName;
QUrl iconUrl;
bool versionsLoaded = false;
bool extraInfoLoaded = false;
ModpackExtra extra;
QList<ModpackVersion> versions;
};
void loadIndexedPack(Modpack&, QJsonObject&);
void loadIndexedInfo(Modpack&, QJsonObject&);
void loadIndexedVersions(Modpack&, QJsonDocument&);
auto loadIndexedVersion(QJsonObject&) -> ModpackVersion;
auto validateDownloadUrl(QUrl) -> bool;
auto getVersionDisplayString(const ModpackVersion&) -> QString;
} // namespace Modrinth
Q_DECLARE_METATYPE(Modrinth::Modpack)
Q_DECLARE_METATYPE(Modrinth::ModpackVersion)
Q_DECLARE_METATYPE(QList<Modrinth::ModpackVersion>)