aboutsummaryrefslogtreecommitdiff
path: root/launcher/modplatform/modrinth
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/modplatform/modrinth')
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.cpp108
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h28
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp174
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.h23
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp41
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.h2
6 files changed, 363 insertions, 13 deletions
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp
new file mode 100644
index 00000000..747cf4c3
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp
@@ -0,0 +1,108 @@
+#include "ModrinthAPI.h"
+
+#include "Application.h"
+#include "Json.h"
+#include "net/Upload.h"
+
+auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersion"), APPLICATION->network());
+
+ netJob->addNetAction(Net::Download::makeByteArray(
+ QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersions"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ Json::writeStringList(body_obj, "hashes", hashes);
+ Json::writeString(body_obj, "algorithm", hash_format);
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::latestVersion(QString hash,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
+
+ QStringList game_versions;
+ for (auto& ver : mcVersions) {
+ game_versions.append(ver.toString());
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(
+ QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::latestVersions(const QStringList& hashes,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ Json::writeStringList(body_obj, "hashes", hashes);
+ Json::writeString(body_obj, "algorithm", hash_format);
+
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
+
+ QStringList game_versions;
+ for (auto& ver : mcVersions) {
+ game_versions.append(ver.toString());
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob*
+{
+ auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network());
+ auto searchUrl = getMultipleModInfoURL(addonIds);
+
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
+
+ QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
+
+ return netJob;
+}
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
index 89e52d6c..e1a18681 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.h
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -27,6 +27,29 @@
class ModrinthAPI : public NetworkModAPI {
public:
+ auto currentVersion(QString hash,
+ QString hash_format,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto currentVersions(const QStringList& hashes,
+ QString hash_format,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto latestVersion(QString hash,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto latestVersions(const QStringList& hashes,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
+
+ public:
inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; };
static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList
@@ -81,6 +104,11 @@ class ModrinthAPI : public NetworkModAPI {
return BuildConfig.MODRINTH_PROD_URL + "/project/" + id;
};
+ inline auto getMultipleModInfoURL(QStringList ids) const -> QString
+ {
+ return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\""));
+ };
+
inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
{
return QString(BuildConfig.MODRINTH_PROD_URL +
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
new file mode 100644
index 00000000..79d8edf7
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -0,0 +1,174 @@
+#include "ModrinthCheckUpdate.h"
+#include "ModrinthAPI.h"
+#include "ModrinthPackIndex.h"
+
+#include "FileSystem.h"
+#include "Json.h"
+
+#include "ModDownloadTask.h"
+
+static ModrinthAPI api;
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+bool ModrinthCheckUpdate::abort()
+{
+ if (m_net_job)
+ return m_net_job->abort();
+ return true;
+}
+
+/* Check for update:
+ * - Get latest version available
+ * - Compare hash of the latest version with the current hash
+ * - If equal, no updates, else, there's updates, so add to the list
+ * */
+void ModrinthCheckUpdate::executeTask()
+{
+ setStatus(tr("Preparing mods for Modrinth..."));
+ setProgress(0, 3);
+
+ QHash<QString, Mod*> mappings;
+
+ // Create all hashes
+ QStringList hashes;
+ auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+ for (auto* mod : m_mods) {
+ if (!mod->enabled()) {
+ emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
+ continue;
+ }
+
+ auto hash = mod->metadata()->hash;
+
+ // Sadly the API can only handle one hash type per call, se we
+ // need to generate a new hash if the current one is innadequate
+ // (though it will rarely happen, if at all)
+ if (mod->metadata()->hash_format != best_hash_type) {
+ QByteArray jar_data;
+
+ try {
+ jar_data = FS::read(mod->fileinfo().absoluteFilePath());
+ } catch (FS::FileSystemException& e) {
+ qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name());
+ qCritical() << QString("Reason: ") << e.cause();
+
+ failed(e.what());
+ return;
+ }
+
+ hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, best_hash_type).toHex());
+ }
+
+ hashes.append(hash);
+ mappings.insert(hash, mod);
+ }
+
+ auto* response = new QByteArray();
+ auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
+
+ QEventLoop lock;
+
+ connect(job.get(), &Task::succeeded, this, [this, response, &mappings, best_hash_type, job] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ failed(parse_error.errorString());
+ return;
+ }
+
+ setStatus(tr("Parsing the API response from Modrinth..."));
+ setProgress(2, 3);
+
+ try {
+ for (auto hash : mappings.keys()) {
+ auto project_obj = doc[hash].toObject();
+
+ // If the returned project is empty, but we have Modrinth metadata,
+ // it means this specific version is not available
+ if (project_obj.isEmpty()) {
+ qDebug() << "Mod " << mappings.find(hash).value()->name() << " got an empty response.";
+ qDebug() << "Hash: " << hash;
+
+ emit checkFailed(
+ mappings.find(hash).value(),
+ tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader."));
+
+ continue;
+ }
+
+ // Sometimes a version may have multiple files, one with "forge" and one with "fabric",
+ // so we may want to filter it
+ QString loader_filter;
+ static auto flags = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt };
+ for (auto flag : flags) {
+ if (m_loaders.testFlag(flag)) {
+ loader_filter = api.getModLoaderString(flag);
+ break;
+ }
+ }
+
+ // Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
+ // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter
+ // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
+ // Such is the pain of having arbitrary files for a given version .-.
+
+ auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, best_hash_type, loader_filter);
+ if (project_ver.downloadUrl.isEmpty()) {
+ qCritical() << "Modrinth mod without download url!";
+ qCritical() << project_ver.fileName;
+
+ emit checkFailed(mappings.find(hash).value(), tr("Mod has an empty download URL"));
+
+ continue;
+ }
+
+ auto mod_iter = mappings.find(hash);
+ if (mod_iter == mappings.end()) {
+ qCritical() << "Failed to remap mod from Modrinth!";
+ continue;
+ }
+ auto mod = *mod_iter;
+
+ auto key = project_ver.hash;
+ if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) {
+ if (mod->version() == project_ver.version_number)
+ continue;
+
+ // Fake pack with the necessary info to pass to the download task :)
+ ModPlatform::IndexedPack pack;
+ pack.name = mod->name();
+ pack.slug = mod->metadata()->slug;
+ pack.addonId = mod->metadata()->project_id;
+ pack.websiteUrl = mod->homeurl();
+ for (auto& author : mod->authors())
+ pack.authors.append({ author });
+ pack.description = mod->description();
+ pack.provider = ModPlatform::Provider::MODRINTH;
+
+ auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder);
+
+ m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
+ ModPlatform::Provider::MODRINTH, download_task);
+ }
+ }
+ } catch (Json::JsonException& e) {
+ failed(e.cause() + " : " + e.what());
+ }
+ });
+
+ connect(job.get(), &Task::finished, &lock, &QEventLoop::quit);
+
+ setStatus(tr("Waiting for the API response from Modrinth..."));
+ setProgress(1, 3);
+
+ m_net_job = job.get();
+ job->start();
+
+ lock.exec();
+
+ emitSucceeded();
+}
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
new file mode 100644
index 00000000..abf8ada1
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "Application.h"
+#include "modplatform/CheckUpdateTask.h"
+#include "net/NetJob.h"
+
+class ModrinthCheckUpdate : public CheckUpdateTask {
+ Q_OBJECT
+
+ public:
+ ModrinthCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ : CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
+ {}
+
+ public slots:
+ bool abort() override;
+
+ protected slots:
+ void executeTask() override;
+
+ private:
+ NetJob* m_net_job = nullptr;
+};
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
index b6f5490a..e50dd96d 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -29,13 +29,16 @@ static ModPlatform::ProviderCapabilities ProviderCaps;
void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
- pack.addonId = Json::requireString(obj, "project_id");
+ pack.addonId = Json::ensureString(obj, "project_id");
+ if (pack.addonId.toString().isEmpty())
+ pack.addonId = Json::requireString(obj, "id");
+
pack.provider = ModPlatform::Provider::MODRINTH;
pack.name = Json::requireString(obj, "title");
- QString slug = Json::ensureString(obj, "slug", "");
- if (!slug.isEmpty())
- pack.websiteUrl = "https://modrinth.com/mod/" + Json::ensureString(obj, "slug", "");
+ pack.slug = Json::ensureString(obj, "slug", "");
+ if (!pack.slug.isEmpty())
+ pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug;
else
pack.websiteUrl = "";
@@ -45,7 +48,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.logoName = pack.addonId.toString();
ModPlatform::ModpackAuthor modAuthor;
- modAuthor.name = Json::requireString(obj, "author");
+ modAuthor.name = Json::ensureString(obj, "author", QObject::tr("No author(s)"));
modAuthor.url = api.getAuthorURL(modAuthor.name);
pack.authors.append(modAuthor);
@@ -111,7 +114,7 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
pack.versionsLoaded = true;
}
-auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedVersion
+auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_type, QString preferred_file_name) -> ModPlatform::IndexedVersion
{
ModPlatform::IndexedVersion file;
@@ -130,6 +133,8 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
file.loaders.append(loader.toString());
}
file.version = Json::requireString(obj, "name");
+ file.version_number = Json::requireString(obj, "version_number");
+ file.changelog = Json::requireString(obj, "changelog");
auto files = Json::requireArray(obj, "files");
int i = 0;
@@ -142,6 +147,11 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
auto parent = files[i].toObject();
auto fileName = Json::requireString(parent, "filename");
+ if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name)) {
+ file.is_preferred = true;
+ break;
+ }
+
// Grab the primary file, if available
if (Json::requireBoolean(parent, "primary"))
break;
@@ -153,13 +163,20 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
if (parent.contains("url")) {
file.downloadUrl = Json::requireString(parent, "url");
file.fileName = Json::requireString(parent, "filename");
+ file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
auto hash_list = Json::requireObject(parent, "hashes");
- auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH);
- for (auto& hash_type : hash_types) {
- if (hash_list.contains(hash_type)) {
- file.hash = Json::requireString(hash_list, hash_type);
- file.hash_type = hash_type;
- break;
+
+ if (hash_list.contains(preferred_hash_type)) {
+ file.hash = Json::requireString(hash_list, preferred_hash_type);
+ file.hash_type = preferred_hash_type;
+ } else {
+ auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH);
+ for (auto& hash_type : hash_types) {
+ if (hash_list.contains(hash_type)) {
+ file.hash = Json::requireString(hash_list, hash_type);
+ file.hash_type = hash_type;
+ break;
+ }
}
}
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h
index b7936204..31881414 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.h
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -30,6 +30,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
BaseInstance* inst);
-auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion;
+auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
} // namespace Modrinth