aboutsummaryrefslogtreecommitdiff
path: root/launcher/modplatform
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/modplatform')
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp50
-rw-r--r--launcher/modplatform/flame/FlameAPI.h32
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp59
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.cpp52
-rw-r--r--launcher/modplatform/flame/PackManifest.cpp96
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h40
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp17
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.h17
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackManifest.cpp156
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackManifest.h107
-rw-r--r--launcher/modplatform/technic/SolderPackManifest.cpp2
11 files changed, 451 insertions, 177 deletions
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
index 3889a935..0deb99c4 100644
--- a/launcher/modplatform/flame/FileResolvingTask.cpp
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -1,14 +1,9 @@
#include "FileResolvingTask.h"
#include "Json.h"
-namespace {
- const char * metabase = "https://cursemeta.dries007.net";
-}
-
Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess)
: m_network(network), m_toProcess(toProcess)
-{
-}
+{}
void Flame::FileResolvingTask::executeTask()
{
@@ -17,14 +12,13 @@ void Flame::FileResolvingTask::executeTask()
m_dljob = new NetJob("Mod id resolver", m_network);
results.resize(m_toProcess.files.size());
int index = 0;
- for(auto & file: m_toProcess.files)
- {
+ for (auto& file : m_toProcess.files) {
auto projectIdStr = QString::number(file.projectId);
auto fileIdStr = QString::number(file.fileId);
- QString metaurl = QString("%1/%2/%3.json").arg(metabase, projectIdStr, fileIdStr);
+ QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr);
auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]);
m_dljob->addNetAction(dl);
- index ++;
+ index++;
}
connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
m_dljob->start();
@@ -34,16 +28,25 @@ void Flame::FileResolvingTask::netJobFinished()
{
bool failed = false;
int index = 0;
- for(auto & bytes: results)
- {
- auto & out = m_toProcess.files[index];
- try
- {
- failed &= (!out.parseFromBytes(bytes));
- }
- catch (const JSONValidationError &e)
- {
-
+ for (auto& bytes : results) {
+ auto& out = m_toProcess.files[index];
+ try {
+ bool fail = (!out.parseFromBytes(bytes));
+ if(fail){
+ //failed :( probably disabled mod, try to add to the list
+ auto doc = Json::requireDocument(bytes);
+ if (!doc.isObject()) {
+ throw JSONValidationError(QString("data is not an object? that's not supposed to happen"));
+ }
+ auto obj = Json::ensureObject(doc.object(), "data");
+ //FIXME : HACK, MAY NOT WORK FOR LONG
+ out.url = QUrl(QString("https://media.forgecdn.net/files/%1/%2/%3")
+ .arg(QString::number(QString::number(out.fileId).leftRef(4).toInt())
+ ,QString::number(QString::number(out.fileId).rightRef(3).toInt())
+ ,QUrl::toPercentEncoding(out.fileName)), QUrl::TolerantMode);
+ }
+ failed &= fail;
+ } catch (const JSONValidationError& e) {
qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
qCritical() << e.cause();
qCritical() << "JSON:";
@@ -52,12 +55,9 @@ void Flame::FileResolvingTask::netJobFinished()
}
index++;
}
- if(!failed)
- {
+ if (!failed) {
emitSucceeded();
- }
- else
- {
+ } else {
emitFailed(tr("Some mod ID resolving tasks failed."));
}
}
diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h
index ce02df65..61628e60 100644
--- a/launcher/modplatform/flame/FlameAPI.h
+++ b/launcher/modplatform/flame/FlameAPI.h
@@ -4,32 +4,52 @@
class FlameAPI : public NetworkModAPI {
private:
+ inline auto getSortFieldInt(QString sortString) const -> int
+ {
+ return sortString == "Featured" ? 1
+ : sortString == "Popularity" ? 2
+ : sortString == "LastUpdated" ? 3
+ : sortString == "Name" ? 4
+ : sortString == "Author" ? 5
+ : sortString == "TotalDownloads" ? 6
+ : sortString == "Category" ? 7
+ : sortString == "GameVersion" ? 8
+ : 1;
+ }
+
+ private:
inline auto getModSearchURL(SearchArgs& args) const -> QString override
{
auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString();
return QString(
- "https://addons-ecs.forgesvc.net/api/v2/addon/search?"
+ "https://api.curseforge.com/v1/mods/search?"
"gameId=432&"
- "categoryId=0&"
- "sectionId=6&"
+ "classId=6&"
"index=%1&"
"pageSize=25&"
"searchFilter=%2&"
- "sort=%3&"
+ "sortField=%3&"
+ "sortOrder=desc&"
"modLoaderType=%4&"
"%5")
.arg(args.offset)
.arg(args.search)
- .arg(args.sorting)
+ .arg(getSortFieldInt(args.sorting))
.arg(getMappedModLoader(args.mod_loader))
.arg(gameVersionStr);
};
inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
{
- return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(args.addonId);
+ QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : "";
+ QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loader));
+
+ return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3")
+ .arg(args.addonId)
+ .arg(gameVersionQuery)
+ .arg(modLoaderQuery);
};
public:
diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp
index c7b86b5c..9846b156 100644
--- a/launcher/modplatform/flame/FlameModIndex.cpp
+++ b/launcher/modplatform/flame/FlameModIndex.cpp
@@ -10,23 +10,12 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
pack.name = Json::requireString(obj, "name");
- pack.websiteUrl = Json::ensureString(obj, "websiteUrl", "");
+ pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
pack.description = Json::ensureString(obj, "summary", "");
- bool thumbnailFound = false;
- auto attachments = Json::requireArray(obj, "attachments");
- for (auto attachmentRaw : attachments) {
- auto attachmentObj = Json::requireObject(attachmentRaw);
- bool isDefault = attachmentObj.value("isDefault").toBool(false);
- if (isDefault) {
- thumbnailFound = true;
- pack.logoName = Json::requireString(attachmentObj, "title");
- pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
- break;
- }
- }
-
- if (!thumbnailFound) { throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); }
+ QJsonObject logo = Json::requireObject(obj, "logo");
+ pack.logoName = Json::requireString(logo, "title");
+ pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
auto authors = Json::requireArray(obj, "authors");
for (auto authorIter : authors) {
@@ -45,49 +34,41 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
{
QVector<ModPlatform::IndexedVersion> unsortedVersions;
auto profile = (dynamic_cast<MinecraftInstance*>(inst))->getPackProfile();
- bool hasFabric = FlameAPI::getMappedModLoader(profile->getModLoader()) == ModAPI::Fabric;
QString mcVersion = profile->getComponentVersion("net.minecraft");
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
- auto versionArray = Json::requireArray(obj, "gameVersion");
- if (versionArray.isEmpty()) { continue; }
+ auto versionArray = Json::requireArray(obj, "gameVersions");
+ if (versionArray.isEmpty()) {
+ continue;
+ }
ModPlatform::IndexedVersion file;
for (auto mcVer : versionArray) {
- file.mcVersion.append(mcVer.toString());
+ auto str = mcVer.toString();
+
+ if (str.contains('.'))
+ file.mcVersion.append(str);
}
file.addonId = pack.addonId;
file.fileId = Json::requireInteger(obj, "id");
file.date = Json::requireString(obj, "fileDate");
file.version = Json::requireString(obj, "displayName");
- file.downloadUrl = Json::requireString(obj, "downloadUrl");
file.fileName = Json::requireString(obj, "fileName");
-
- auto modules = Json::requireArray(obj, "modules");
- bool is_valid_fabric_version = false;
- for (auto m : modules) {
- auto fname = Json::requireString(m.toObject(), "foldername");
- // FIXME: This does not work properly when a mod supports more than one mod loader, since
- // FIXME: This also doesn't deal with Quilt mods at the moment
- // they bundle the meta files for all of them in the same arquive, even when that version
- // doesn't support the given mod loader.
- if (hasFabric) {
- if (fname == "fabric.mod.json") {
- is_valid_fabric_version = true;
- break;
- }
- } else
- break;
- // NOTE: Since we're not validating forge versions, we can just skip this loop.
+ file.downloadUrl = Json::ensureString(obj, "downloadUrl", "");
+ if(file.downloadUrl.isEmpty()){
+ //FIXME : HACK, MAY NOT WORK FOR LONG
+ file.downloadUrl = QString("https://media.forgecdn.net/files/%1/%2/%3")
+ .arg(QString::number(QString::number(file.fileId.toInt()).leftRef(4).toInt())
+ ,QString::number(QString::number(file.fileId.toInt()).rightRef(3).toInt())
+ ,QUrl::toPercentEncoding(file.fileName));
}
- if (hasFabric && !is_valid_fabric_version) continue;
-
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;
diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp
index 3d8ea22a..ac24c647 100644
--- a/launcher/modplatform/flame/FlamePackIndex.cpp
+++ b/launcher/modplatform/flame/FlamePackIndex.cpp
@@ -2,76 +2,63 @@
#include "Json.h"
-void Flame::loadIndexedPack(Flame::IndexedPack & pack, QJsonObject & obj)
+void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
pack.name = Json::requireString(obj, "name");
- pack.websiteUrl = Json::ensureString(obj, "websiteUrl", "");
+ pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
pack.description = Json::ensureString(obj, "summary", "");
- bool thumbnailFound = false;
- auto attachments = Json::requireArray(obj, "attachments");
- for(auto attachmentRaw: attachments) {
- auto attachmentObj = Json::requireObject(attachmentRaw);
- bool isDefault = attachmentObj.value("isDefault").toBool(false);
- if(isDefault) {
- thumbnailFound = true;
- pack.logoName = Json::requireString(attachmentObj, "title");
- pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
- break;
- }
- }
-
- if(!thumbnailFound) {
- throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name));
- }
+ auto logo = Json::requireObject(obj, "logo");
+ pack.logoName = Json::requireString(logo, "title");
+ pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
auto authors = Json::requireArray(obj, "authors");
- for(auto authorIter: 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, "defaultFileId");
+ 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) {
+ 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) {
+ if (id != defaultFileId) {
continue;
}
- auto versionArray = Json::requireArray(file, "gameVersion");
- if(versionArray.size() < 1) {
+ auto versionArray = Json::requireArray(file, "gameVersions");
+ if (versionArray.size() < 1) {
continue;
}
found = true;
break;
}
- if(!found) {
+ if (!found) {
throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name));
}
}
-void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr)
+void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr)
{
QVector<Flame::IndexedVersion> unsortedVersions;
- for(auto versionIter: arr) {
+ for (auto versionIter : arr) {
auto version = Json::requireObject(versionIter);
- Flame::IndexedVersion file;
+ Flame::IndexedVersion file;
file.addonId = pack.addonId;
file.fileId = Json::requireInteger(version, "id");
- auto versionArray = Json::requireArray(version, "gameVersion");
- if(versionArray.size() < 1) {
+ auto versionArray = Json::requireArray(version, "gameVersions");
+ if (versionArray.size() < 1) {
continue;
}
@@ -82,10 +69,7 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr)
unsortedVersions.append(file);
}
- auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool
- {
- return a.fileId > b.fileId;
- };
+ 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;
diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp
index b928fd16..3217a756 100644
--- a/launcher/modplatform/flame/PackManifest.cpp
+++ b/launcher/modplatform/flame/PackManifest.cpp
@@ -1,28 +1,27 @@
#include "PackManifest.h"
#include "Json.h"
-static void loadFileV1(Flame::File & f, QJsonObject & file)
+static void loadFileV1(Flame::File& f, QJsonObject& file)
{
f.projectId = Json::requireInteger(file, "projectID");
f.fileId = Json::requireInteger(file, "fileID");
f.required = Json::ensureBoolean(file, QString("required"), true);
}
-static void loadModloaderV1(Flame::Modloader & m, QJsonObject & modLoader)
+static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader)
{
m.id = Json::requireString(modLoader, "id");
m.primary = Json::ensureBoolean(modLoader, QString("primary"), false);
}
-static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft)
+static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft)
{
m.version = Json::requireString(minecraft, "version");
// extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack
// intended use is likely hardcoded in the 'Flame' client, the manifest says nothing
m.libraries = Json::ensureString(minecraft, QString("libraries"), QString());
auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray());
- for (QJsonValueRef item : arr)
- {
+ for (QJsonValueRef item : arr) {
auto obj = Json::requireObject(item);
Flame::Modloader loader;
loadModloaderV1(loader, obj);
@@ -30,16 +29,15 @@ static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft)
}
}
-static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest)
+static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest)
{
auto mc = Json::requireObject(manifest, "minecraft");
loadMinecraftV1(m.minecraft, mc);
m.name = Json::ensureString(manifest, QString("name"), "Unnamed");
m.version = Json::ensureString(manifest, QString("version"), QString());
- m.author = Json::ensureString(manifest, QString("author"), "Anonymous Coward");
+ m.author = Json::ensureString(manifest, QString("author"), "Anonymous");
auto arr = Json::ensureArray(manifest, "files", QJsonArray());
- for (QJsonValueRef item : arr)
- {
+ for (QJsonValueRef item : arr) {
auto obj = Json::requireObject(item);
Flame::File file;
loadFileV1(file, obj);
@@ -48,18 +46,16 @@ static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest)
m.overrides = Json::ensureString(manifest, "overrides", "overrides");
}
-void Flame::loadManifest(Flame::Manifest & m, const QString &filepath)
+void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
{
auto doc = Json::requireDocument(filepath);
auto obj = Json::requireObject(doc);
m.manifestType = Json::requireString(obj, "manifestType");
- if(m.manifestType != "minecraftModpack")
- {
+ if (m.manifestType != "minecraftModpack") {
throw JSONValidationError("Not a modpack manifest!");
}
m.manifestVersion = Json::requireInteger(obj, "manifestVersion");
- if(m.manifestVersion != 1)
- {
+ if (m.manifestVersion != 1) {
throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion));
}
loadManifestV1(m, obj);
@@ -68,59 +64,35 @@ void Flame::loadManifest(Flame::Manifest & m, const QString &filepath)
bool Flame::File::parseFromBytes(const QByteArray& bytes)
{
auto doc = Json::requireDocument(bytes);
- auto obj = Json::requireObject(doc);
- // result code signifies true failure.
- if(obj.contains("code"))
- {
- qCritical() << "Resolving of" << projectId << fileId << "failed because of a negative result:";
- qCritical() << bytes;
+ if (!doc.isObject()) {
+ throw JSONValidationError(QString("data is not an object? that's not supposed to happen"));
+ }
+ auto obj = Json::ensureObject(doc.object(), "data");
+
+ fileName = Json::requireString(obj, "fileName");
+
+ // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
+ // It is also optional
+ type = File::Type::SingleFile;
+
+ if (fileName.endsWith(".zip")) {
+ // this is probably a resource pack
+ targetFolder = "resourcepacks";
+ } else {
+ // this is probably a mod, dunno what else could modpacks download
+ targetFolder = "mods";
+ }
+ QString rawUrl = Json::ensureString(obj, "downloadUrl");
+
+ if(rawUrl.isEmpty()){
+ //either there somehow is an emtpy string as a link, or it's null either way it's invalid
+ //soft failing
return false;
}
- fileName = Json::requireString(obj, "FileNameOnDisk");
- QString rawUrl = Json::requireString(obj, "DownloadURL");
url = QUrl(rawUrl, QUrl::TolerantMode);
- if(!url.isValid())
- {
+ if (!url.isValid()) {
throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
}
- // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
- // It is also optional
- QJsonObject projObj = Json::ensureObject(obj, "_Project", {});
- if(!projObj.isEmpty())
- {
- QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower();
- if(strType == "singlefile")
- {
- type = File::Type::SingleFile;
- }
- else if(strType == "ctoc")
- {
- type = File::Type::Ctoc;
- }
- else if(strType == "cmod2")
- {
- type = File::Type::Cmod2;
- }
- else if(strType == "mod")
- {
- type = File::Type::Mod;
- }
- else if(strType == "folder")
- {
- type = File::Type::Folder;
- }
- else if(strType == "modpack")
- {
- type = File::Type::Modpack;
- }
- else
- {
- qCritical() << "Resolving of" << projectId << fileId << "failed because of unknown file type:" << strType;
- type = File::Type::Unknown;
- return false;
- }
- targetFolder = Json::ensureString(projObj, "Path", "mods");
- }
resolved = true;
return true;
}
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
index 86852c94..6d642b5e 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.h
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -1,5 +1,24 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - 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/>.
+ */
+
#pragma once
+#include "BuildConfig.h"
#include "modplatform/ModAPI.h"
#include "modplatform/helpers/NetworkModAPI.h"
@@ -47,13 +66,13 @@ class ModrinthAPI : public NetworkModAPI {
return "";
}
- return QString(
- "https://api.modrinth.com/v2/search?"
- "offset=%1&"
- "limit=25&"
- "query=%2&"
- "index=%3&"
- "facets=[[%4],%5[\"project_type:mod\"]]")
+ return QString(BuildConfig.MODRINTH_PROD_URL +
+ "/search?"
+ "offset=%1&"
+ "limit=25&"
+ "query=%2&"
+ "index=%3&"
+ "facets=[[%4],%5[\"project_type:mod\"]]")
.arg(args.offset)
.arg(args.search)
.arg(args.sorting)
@@ -63,9 +82,10 @@ class ModrinthAPI : public NetworkModAPI {
inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
{
- return QString("https://api.modrinth.com/v2/project/%1/version?"
- "game_versions=[%2]"
- "loaders=[\"%3\"]")
+ return QString(BuildConfig.MODRINTH_PROD_URL +
+ "/project/%1/version?"
+ "game_versions=[%2]"
+ "loaders=[\"%3\"]")
.arg(args.addonId)
.arg(getGameVersionsString(args.mcVersions))
.arg(getModLoaderStrings(args.loader).join("\",\""));
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
index a3c2f166..f7fa9864 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -1,3 +1,20 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ *
+ * 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 "ModrinthPackIndex.h"
#include "ModrinthAPI.h"
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h
index fd17847a..7f306f25 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.h
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -1,3 +1,20 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ *
+ * 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 "modplatform/ModIndex.h"
diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
new file mode 100644
index 00000000..f1ad39ce
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - 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 "Json.h"
+
+#include "modplatform/modrinth/ModrinthAPI.h"
+
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+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.iconName = QString("modrinth_%1").arg(Json::ensureString(obj, "slug"));
+ pack.iconUrl = Json::ensureString(obj, "icon_url");
+}
+
+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.sourceUrl = Json::ensureString(obj, "source_url");
+ pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url");
+
+ pack.extraInfoLoaded = true;
+}
+
+void loadIndexedVersions(Modpack& pack, QJsonDocument& doc)
+{
+ QVector<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 validateDownloadUrl(QUrl url) -> bool
+{
+ auto domain = url.host();
+ if(domain == "cdn.modrinth.com")
+ return true;
+ if(domain == "edge.forgecdn.net")
+ return true;
+ if(domain == "media.forgecdn.net")
+ return true;
+ if(domain == "github.com")
+ return true;
+ if(domain == "raw.githubusercontent.com")
+ return true;
+
+ return false;
+}
+
+auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion
+{
+ ModpackVersion file;
+
+ file.name = Json::requireString(obj, "name");
+ file.version = Json::requireString(obj, "version_number");
+
+ 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, "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");
+
+ if(!validateDownloadUrl(url))
+ continue;
+
+ file.download_url = url;
+ if(is_primary)
+ break;
+ }
+
+ if(file.download_url.isEmpty())
+ return {};
+
+ return file;
+}
+
+} // namespace Modrinth
diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h
new file mode 100644
index 00000000..e5fc9a70
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - 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 <QString>
+#include <QUrl>
+#include <QVector>
+
+class MinecraftInstance;
+
+namespace Modrinth {
+
+struct File
+{
+ QString path;
+
+ QCryptographicHash::Algorithm hashAlgorithm;
+ QByteArray hash;
+ // TODO: should this support multiple download URLs, like the JSON does?
+ QUrl download;
+};
+
+struct ModpackExtra {
+ QString body;
+
+ QString projectUrl;
+ QString sourceUrl;
+ QString wikiUrl;
+};
+
+struct ModpackVersion {
+ QString name;
+ QString version;
+
+ 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;
+ QVector<ModpackVersion> versions;
+};
+
+void loadIndexedPack(Modpack&, QJsonObject&);
+void loadIndexedInfo(Modpack&, QJsonObject&);
+void loadIndexedVersions(Modpack&, QJsonDocument&);
+auto loadIndexedVersion(QJsonObject&) -> ModpackVersion;
+
+auto validateDownloadUrl(QUrl) -> bool;
+
+}
+
+Q_DECLARE_METATYPE(Modrinth::Modpack)
+Q_DECLARE_METATYPE(Modrinth::ModpackVersion)
diff --git a/launcher/modplatform/technic/SolderPackManifest.cpp b/launcher/modplatform/technic/SolderPackManifest.cpp
index 16fe0b0e..e52a7ec0 100644
--- a/launcher/modplatform/technic/SolderPackManifest.cpp
+++ b/launcher/modplatform/technic/SolderPackManifest.cpp
@@ -37,7 +37,7 @@ void loadPack(Pack& v, QJsonObject& obj)
static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj)
{
b.name = Json::requireString(obj, "name");
- b.version = Json::requireString(obj, "version");
+ b.version = Json::ensureString(obj, "version", "");
b.md5 = Json::requireString(obj, "md5");
b.url = Json::requireString(obj, "url");
}