aboutsummaryrefslogtreecommitdiff
path: root/launcher/modplatform
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/modplatform')
-rw-r--r--launcher/modplatform/ModAPI.h2
-rw-r--r--launcher/modplatform/ModIndex.cpp86
-rw-r--r--launcher/modplatform/ModIndex.h35
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp43
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.cpp16
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.h16
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp124
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.h8
-rw-r--r--launcher/modplatform/flame/FlameAPI.h1
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp77
-rw-r--r--launcher/modplatform/flame/FlameModIndex.h1
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.cpp12
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.h1
-rw-r--r--launcher/modplatform/flame/PackManifest.cpp35
-rw-r--r--launcher/modplatform/flame/PackManifest.h10
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h9
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp129
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.h1
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackManifest.cpp22
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackManifest.h7
-rw-r--r--launcher/modplatform/packwiz/Packwiz.cpp289
-rw-r--r--launcher/modplatform/packwiz/Packwiz.h93
-rw-r--r--launcher/modplatform/packwiz/Packwiz_test.cpp87
-rw-r--r--launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml13
-rw-r--r--launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml13
-rw-r--r--launcher/modplatform/technic/TechnicPackProcessor.cpp21
26 files changed, 985 insertions, 166 deletions
diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h
index 24d80385..91b760df 100644
--- a/launcher/modplatform/ModAPI.h
+++ b/launcher/modplatform/ModAPI.h
@@ -70,7 +70,7 @@ class ModAPI {
{
QString s;
for(auto& ver : mcVersions){
- s += QString("%1,").arg(ver.toString());
+ s += QString("\"%1\",").arg(ver.toString());
}
s.remove(s.length() - 1, 1); //remove last comma
return s;
diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp
new file mode 100644
index 00000000..3c4b7887
--- /dev/null
+++ b/launcher/modplatform/ModIndex.cpp
@@ -0,0 +1,86 @@
+// 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/>.
+*/
+
+#include "modplatform/ModIndex.h"
+
+#include <QCryptographicHash>
+
+namespace ModPlatform {
+
+auto ProviderCapabilities::name(Provider p) -> const char*
+{
+ switch (p) {
+ case Provider::MODRINTH:
+ return "modrinth";
+ case Provider::FLAME:
+ return "curseforge";
+ }
+ return {};
+}
+auto ProviderCapabilities::readableName(Provider p) -> QString
+{
+ switch (p) {
+ case Provider::MODRINTH:
+ return "Modrinth";
+ case Provider::FLAME:
+ return "CurseForge";
+ }
+ return {};
+}
+auto ProviderCapabilities::hashType(Provider p) -> QStringList
+{
+ switch (p) {
+ case Provider::MODRINTH:
+ return { "sha512", "sha1" };
+ case Provider::FLAME:
+ // Try newer formats first, fall back to old format
+ return { "sha1", "md5", "murmur2" };
+ }
+ return {};
+}
+auto ProviderCapabilities::hash(Provider p, QByteArray& data, QString type) -> QByteArray
+{
+ switch (p) {
+ case Provider::MODRINTH: {
+ // NOTE: Data is the result of reading the entire JAR file!
+
+ // If 'type' was specified, we use that
+ if (!type.isEmpty() && hashType(p).contains(type)) {
+ if (type == "sha512")
+ return QCryptographicHash::hash(data, QCryptographicHash::Sha512);
+ else if (type == "sha1")
+ return QCryptographicHash::hash(data, QCryptographicHash::Sha1);
+ }
+
+ return QCryptographicHash::hash(data, QCryptographicHash::Sha512);
+ }
+ case Provider::FLAME:
+ // If 'type' was specified, we use that
+ if (!type.isEmpty() && hashType(p).contains(type)) {
+ if(type == "sha1")
+ return QCryptographicHash::hash(data, QCryptographicHash::Sha1);
+ else if (type == "md5")
+ return QCryptographicHash::hash(data, QCryptographicHash::Md5);
+ }
+
+ break;
+ }
+ return {};
+}
+
+} // namespace ModPlatform
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index 4d1d02a5..c27643af 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -1,3 +1,21 @@
+// 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 <QList>
@@ -8,6 +26,19 @@
namespace ModPlatform {
+enum class Provider {
+ MODRINTH,
+ FLAME
+};
+
+class ProviderCapabilities {
+ public:
+ auto name(Provider) -> const char*;
+ auto readableName(Provider) -> QString;
+ auto hashType(Provider) -> QStringList;
+ auto hash(Provider, QByteArray&, QString type = "") -> QByteArray;
+};
+
struct ModpackAuthor {
QString name;
QString url;
@@ -28,6 +59,8 @@ struct IndexedVersion {
QString date;
QString fileName;
QVector<QString> loaders = {};
+ QString hash_type;
+ QString hash;
};
struct ExtraPackData {
@@ -41,6 +74,7 @@ struct ExtraPackData {
struct IndexedPack {
QVariant addonId;
+ Provider provider;
QString name;
QString description;
QList<ModpackAuthor> authors;
@@ -59,3 +93,4 @@ struct IndexedPack {
} // namespace ModPlatform
Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
+Q_DECLARE_METATYPE(ModPlatform::Provider)
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index 9b14f355..62c7bf6d 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -414,7 +414,31 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared
bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile)
{
- if(m_version.mainClass == QString() && m_version.extraArguments == QString()) {
+ if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) {
+ return true;
+ }
+
+ auto mainClass = m_version.mainClass.mainClass;
+ auto extraArguments = m_version.extraArguments.arguments;
+
+ auto hasMainClassDepends = !m_version.mainClass.depends.isEmpty();
+ auto hasExtraArgumentsDepends = !m_version.extraArguments.depends.isEmpty();
+ if (hasMainClassDepends || hasExtraArgumentsDepends) {
+ QSet<QString> mods;
+ for (const auto& item : m_version.mods) {
+ mods.insert(item.name);
+ }
+
+ if (hasMainClassDepends && !mods.contains(m_version.mainClass.depends)) {
+ mainClass = "";
+ }
+
+ if (hasExtraArgumentsDepends && !mods.contains(m_version.extraArguments.depends)) {
+ extraArguments = "";
+ }
+ }
+
+ if (mainClass.isEmpty() && extraArguments.isEmpty()) {
return true;
}
@@ -442,12 +466,12 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr<
auto f = std::make_shared<VersionFile>();
f->name = m_pack + " " + m_version_name;
- if(m_version.mainClass != QString() && !mainClasses.contains(m_version.mainClass)) {
- f->mainClass = m_version.mainClass;
+ if (!mainClass.isEmpty() && !mainClasses.contains(mainClass)) {
+ f->mainClass = mainClass;
}
// Parse out tweakers
- auto args = m_version.extraArguments.split(" ");
+ auto args = extraArguments.split(" ");
QString previous;
for(auto arg : args) {
if(arg.startsWith("--tweakClass=") || previous == "--tweakClass") {
@@ -757,6 +781,17 @@ bool PackInstallTask::extractMods(
for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) {
auto &from = iter.key();
auto &to = iter.value();
+
+ // If the file already exists, assume the mod is the correct copy - and remove
+ // the copy from the Configs.zip
+ QFileInfo fileInfo(to);
+ if (fileInfo.exists()) {
+ if (!QFile::remove(to)) {
+ qWarning() << "Failed to delete" << to;
+ return false;
+ }
+ }
+
FS::copy fileCopyOperation(from, to);
if(!fileCopyOperation()) {
qWarning() << "Failed to copy" << from << "to" << to;
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
index d01ec32c..3af02a09 100644
--- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
@@ -212,6 +212,18 @@ static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj
m.update = Json::ensureString(obj, "update", "");
}
+static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj)
+{
+ m.mainClass = Json::ensureString(obj, "mainClass", "");
+ m.depends = Json::ensureString(obj, "depends", "");
+}
+
+static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj)
+{
+ a.arguments = Json::ensureString(obj, "arguments", "");
+ a.depends = Json::ensureString(obj, "depends", "");
+}
+
void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
{
v.version = Json::requireString(obj, "version");
@@ -220,12 +232,12 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
if(obj.contains("mainClass")) {
auto main = Json::requireObject(obj, "mainClass");
- v.mainClass = Json::ensureString(main, "mainClass", "");
+ loadVersionMainClass(v.mainClass, main);
}
if(obj.contains("extraArguments")) {
auto arguments = Json::requireObject(obj, "extraArguments");
- v.extraArguments = Json::ensureString(arguments, "arguments", "");
+ loadVersionExtraArguments(v.extraArguments, arguments);
}
if(obj.contains("loader")) {
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h
index 23e162e3..43510c50 100644
--- a/launcher/modplatform/atlauncher/ATLPackManifest.h
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.h
@@ -150,13 +150,25 @@ struct VersionMessages
QString update;
};
+struct PackVersionMainClass
+{
+ QString mainClass;
+ QString depends;
+};
+
+struct PackVersionExtraArguments
+{
+ QString arguments;
+ QString depends;
+};
+
struct PackVersion
{
QString version;
QString minecraft;
bool noConfigs;
- QString mainClass;
- QString extraArguments;
+ PackVersionMainClass mainClass;
+ PackVersionExtraArguments extraArguments;
VersionLoader loader;
QVector<VersionLibrary> libraries;
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
index 95924a68..a790ab9c 100644
--- a/launcher/modplatform/flame/FileResolvingTask.cpp
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -1,7 +1,9 @@
#include "FileResolvingTask.h"
+
#include "Json.h"
+#include "net/Upload.h"
-Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess)
+Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess)
: m_network(network), m_toProcess(toProcess)
{}
@@ -10,40 +12,116 @@ void Flame::FileResolvingTask::executeTask()
setStatus(tr("Resolving mod IDs..."));
setProgress(0, m_toProcess.files.size());
m_dljob = new NetJob("Mod id resolver", m_network);
- results.resize(m_toProcess.files.size());
- int index = 0;
- for (auto& file : m_toProcess.files) {
- auto projectIdStr = QString::number(file.projectId);
- auto fileIdStr = QString::number(file.fileId);
- 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++;
- }
+ result.reset(new QByteArray());
+ //build json data to send
+ QJsonObject object;
+
+ object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
+ l.push_back(s.fileId);
+ return l;
+ }));
+ QByteArray data = Json::toText(object);
+ auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result.get(), data);
+ m_dljob->addNetAction(dl);
connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
m_dljob->start();
}
void Flame::FileResolvingTask::netJobFinished()
{
- bool failed = false;
int index = 0;
- for (auto& bytes : results) {
- auto& out = m_toProcess.files[index];
+ // job to check modrinth for blocked projects
+ auto job = new NetJob("Modrinth check", m_network);
+ blockedProjects = QMap<File *,QByteArray *>();
+ auto doc = Json::requireDocument(*result);
+ auto array = Json::requireArray(doc.object()["data"]);
+ for (QJsonValueRef file : array) {
+ auto fileid = Json::requireInteger(Json::requireObject(file)["id"]);
+ auto& out = m_toProcess.files[fileid];
try {
- failed &= (!out.parseFromBytes(bytes));
+ out.parseFromObject(Json::requireObject(file));
} catch (const JSONValidationError& e) {
- qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
- qCritical() << e.cause();
- qCritical() << "JSON:";
- qCritical() << bytes;
- failed = true;
+ qDebug() << "Blocked mod on curseforge" << out.fileName;
+ auto hash = out.hash;
+ if(!hash.isEmpty()) {
+ auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash);
+ auto output = new QByteArray();
+ auto dl = Net::Download::makeByteArray(QUrl(url), output);
+ QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() {
+ out.resolved = true;
+ });
+
+ job->addNetAction(dl);
+ blockedProjects.insert(&out, output);
+ }
}
index++;
}
- if (!failed) {
- emitSucceeded();
+ connect(job, &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished);
+
+ job->start();
+}
+
+void Flame::FileResolvingTask::modrinthCheckFinished() {
+ qDebug() << "Finished with blocked mods : " << blockedProjects.size();
+
+ for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) {
+ auto &out = *it;
+ auto bytes = blockedProjects[out];
+ if (!out->resolved) {
+ delete bytes;
+ continue;
+ }
+ QJsonDocument doc = QJsonDocument::fromJson(*bytes);
+ auto obj = doc.object();
+ auto array = Json::requireArray(obj,"files");
+ for (auto file: array) {
+ auto fileObj = Json::requireObject(file);
+ auto primary = Json::requireBoolean(fileObj,"primary");
+ if (primary) {
+ out->url = Json::requireUrl(fileObj,"url");
+ qDebug() << "Found alternative on modrinth " << out->fileName;
+ break;
+ }
+ }
+ delete bytes;
+ }
+ //copy to an output list and filter out projects found on modrinth
+ auto block = new QList<File *>();
+ auto it = blockedProjects.keys();
+ std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) {
+ return !f->resolved;
+ });
+ //Display not found mods early
+ if (!block->empty()) {
+ //blocked mods found, we need the slug for displaying.... we need another job :D !
+ auto slugJob = new NetJob("Slug Job", m_network);
+ auto slugs = QVector<QByteArray>(block->size());
+ auto index = 0;
+ for (auto fileInfo: *block) {
+ auto projectId = fileInfo->projectId;
+ slugs[index] = QByteArray();
+ auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId);
+ auto dl = Net::Download::makeByteArray(url, &slugs[index]);
+ slugJob->addNetAction(dl);
+ index++;
+ }
+ connect(slugJob, &NetJob::succeeded, this, [slugs, this, slugJob, block]() {
+ slugJob->deleteLater();
+ auto index = 0;
+ for (const auto &slugResult: slugs) {
+ auto json = QJsonDocument::fromJson(slugResult);
+ auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"),
+ "websiteUrl");
+ auto mod = block->at(index);
+ auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId));
+ mod->websiteUrl = link;
+ index++;
+ }
+ emitSucceeded();
+ });
+ slugJob->start();
} else {
- emitFailed(tr("Some mod ID resolving tasks failed."));
+ emitSucceeded();
}
}
diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h
index 5e5adcd7..87981f0a 100644
--- a/launcher/modplatform/flame/FileResolvingTask.h
+++ b/launcher/modplatform/flame/FileResolvingTask.h
@@ -10,7 +10,7 @@ class FileResolvingTask : public Task
{
Q_OBJECT
public:
- explicit FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest &toProcess);
+ explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess);
virtual ~FileResolvingTask() {};
const Flame::Manifest &getResults() const
@@ -27,7 +27,11 @@ protected slots:
private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
Flame::Manifest m_toProcess;
- QVector<QByteArray> results;
+ std::shared_ptr<QByteArray> result;
NetJob::Ptr m_dljob;
+
+ void modrinthCheckFinished();
+
+ QMap<File *, QByteArray *> blockedProjects;
};
}
diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h
index 3b5c5782..424153d2 100644
--- a/launcher/modplatform/flame/FlameAPI.h
+++ b/launcher/modplatform/flame/FlameAPI.h
@@ -1,5 +1,6 @@
#pragma once
+#include "modplatform/ModIndex.h"
#include "modplatform/helpers/NetworkModAPI.h"
class FlameAPI : public NetworkModAPI {
diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp
index 1a2f2bd4..b99bfb26 100644
--- a/launcher/modplatform/flame/FlameModIndex.cpp
+++ b/launcher/modplatform/flame/FlameModIndex.cpp
@@ -6,9 +6,12 @@
#include "modplatform/flame/FlameAPI.h"
#include "net/NetJob.h"
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
+ pack.provider = ModPlatform::Provider::FLAME;
pack.name = Json::requireString(obj, "name");
pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
pack.description = Json::ensureString(obj, "summary", "");
@@ -48,6 +51,17 @@ void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
pack.extraDataLoaded = true;
}
+static QString enumToString(int hash_algorithm)
+{
+ switch(hash_algorithm){
+ default:
+ case 1:
+ return "sha1";
+ case 2:
+ return "md5";
+ }
+}
+
void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
@@ -59,28 +73,13 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
+
+ auto file = loadIndexedPackVersion(obj);
+ if(!file.addonId.isValid())
+ file.addonId = pack.addonId;
- auto versionArray = Json::requireArray(obj, "gameVersions");
- if (versionArray.isEmpty()) {
- continue;
- }
-
- ModPlatform::IndexedVersion file;
- for (auto mcVer : versionArray) {
- 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");
-
- unsortedVersions.append(file);
+ 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 {
@@ -91,3 +90,39 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
pack.versions = unsortedVersions;
pack.versionsLoaded = true;
}
+
+auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion
+{
+ auto versionArray = Json::requireArray(obj, "gameVersions");
+ if (versionArray.isEmpty()) {
+ return {};
+ }
+
+ ModPlatform::IndexedVersion file;
+ for (auto mcVer : versionArray) {
+ auto str = mcVer.toString();
+
+ if (str.contains('.'))
+ file.mcVersion.append(str);
+ }
+
+ file.addonId = Json::requireInteger(obj, "modId");
+ 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 hash_list = Json::ensureArray(obj, "hashes");
+ for (auto h : hash_list) {
+ auto hash_entry = Json::ensureObject(h);
+ auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::FLAME);
+ auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm"));
+ if (hash_types.contains(hash_algo)) {
+ file.hash = Json::requireString(hash_entry, "value");
+ file.hash_type = hash_algo;
+ break;
+ }
+ }
+ return file;
+}
diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h
index c631a6f3..9c6c1c6c 100644
--- a/launcher/modplatform/flame/FlameModIndex.h
+++ b/launcher/modplatform/flame/FlameModIndex.h
@@ -17,5 +17,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
BaseInstance* inst);
+auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion;
} // namespace FlameMod
diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp
index 43aae02e..ba1622d1 100644
--- a/launcher/modplatform/flame/FlamePackIndex.cpp
+++ b/launcher/modplatform/flame/FlamePackIndex.cpp
@@ -90,16 +90,12 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr)
// pick the latest version supported
file.mcVersion = versionArray[0].toString();
file.version = Json::requireString(version, "displayName");
- file.fileName = Json::requireString(version, "fileName");
file.downloadUrl = Json::ensureString(version, "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).leftRef(4).toInt())
- ,QString::number(QString::number(file.fileId).rightRef(3).toInt())
- ,QUrl::toPercentEncoding(file.fileName));
+
+ // only add if we have a download URL (third party distribution is enabled)
+ if (!file.downloadUrl.isEmpty()) {
+ unsortedVersions.append(file);
}
- unsortedVersions.append(file);
}
auto orderSortPredicate = [](const IndexedVersion& a, const IndexedVersion& b) -> bool { return a.fileId > b.fileId; };
diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h
index c0781d62..1ca0fc0e 100644
--- a/launcher/modplatform/flame/FlamePackIndex.h
+++ b/launcher/modplatform/flame/FlamePackIndex.h
@@ -18,7 +18,6 @@ struct IndexedVersion {
QString version;
QString mcVersion;
QString downloadUrl;
- QString fileName;
};
struct ModpackExtra {
diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp
index e4f90c1a..12a4b990 100644
--- a/launcher/modplatform/flame/PackManifest.cpp
+++ b/launcher/modplatform/flame/PackManifest.cpp
@@ -41,7 +41,7 @@ static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest)
auto obj = Json::requireObject(item);
Flame::File file;
loadFileV1(file, obj);
- m.files.append(file);
+ m.files.insert(file.fileId,file);
}
m.overrides = Json::ensureString(manifest, "overrides", "overrides");
}
@@ -61,21 +61,9 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
loadManifestV1(m, obj);
}
-bool Flame::File::parseFromBytes(const QByteArray& bytes)
+bool Flame::File::parseFromObject(const QJsonObject& obj)
{
- 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");
-
fileName = Json::requireString(obj, "fileName");
-
- QString rawUrl = Json::requireString(obj, "downloadUrl");
- url = QUrl(rawUrl, QUrl::TolerantMode);
- 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
type = File::Type::SingleFile;
@@ -87,6 +75,25 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes)
// this is probably a mod, dunno what else could modpacks download
targetFolder = "mods";
}
+ // get the hash
+ hash = QString();
+ auto hashes = Json::ensureArray(obj, "hashes");
+ for(QJsonValueRef item : hashes) {
+ auto hobj = Json::requireObject(item);
+ auto algo = Json::requireInteger(hobj, "algo");
+ auto value = Json::requireString(hobj, "value");
+ if (algo == 1) {
+ hash = value;
+ }
+ }
+
+
+ // may throw, if the project is blocked
+ QString rawUrl = Json::ensureString(obj, "downloadUrl");
+ url = QUrl(rawUrl, QUrl::TolerantMode);
+ if (!url.isValid()) {
+ throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
+ }
resolved = true;
return true;
diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h
index 02f39f0e..26a48d1c 100644
--- a/launcher/modplatform/flame/PackManifest.h
+++ b/launcher/modplatform/flame/PackManifest.h
@@ -2,19 +2,24 @@
#include <QString>
#include <QVector>
+#include <QMap>
#include <QUrl>
+#include <QJsonObject>
namespace Flame
{
struct File
{
// NOTE: throws JSONValidationError
- bool parseFromBytes(const QByteArray &bytes);
+ bool parseFromObject(const QJsonObject& object);
int projectId = 0;
int fileId = 0;
// NOTE: the opposite to 'optional'. This is at the time of writing unused.
bool required = true;
+ QString hash;
+ // NOTE: only set on blocked files ! Empty otherwise.
+ QString websiteUrl;
// our
bool resolved = false;
@@ -54,7 +59,8 @@ struct Manifest
QString name;
QString version;
QString author;
- QVector<Flame::File> files;
+ //File id -> File
+ QMap<int,Flame::File> files;
QString overrides;
};
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
index 13b62f0c..89e52d6c 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.h
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -20,6 +20,7 @@
#include "BuildConfig.h"
#include "modplatform/ModAPI.h"
+#include "modplatform/ModIndex.h"
#include "modplatform/helpers/NetworkModAPI.h"
#include <QDebug>
@@ -84,11 +85,11 @@ class ModrinthAPI : public NetworkModAPI {
{
return QString(BuildConfig.MODRINTH_PROD_URL +
"/project/%1/version?"
- "game_versions=[%2]"
+ "game_versions=[%2]&"
"loaders=[\"%3\"]")
- .arg(args.addonId)
- .arg(getGameVersionsString(args.mcVersions))
- .arg(getModLoaderStrings(args.loaders).join("\",\""));
+ .arg(args.addonId,
+ getGameVersionsString(args.mcVersions),
+ getModLoaderStrings(args.loaders).join("\",\""));
};
auto getGameVersionsArray(std::list<Version> mcVersions) const -> QString
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
index a9aa3a9d..b6f5490a 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -1,19 +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/>.
- */
+* 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/>.
+*/
#include "ModrinthPackIndex.h"
#include "ModrinthAPI.h"
@@ -24,10 +25,12 @@
#include "net/NetJob.h"
static ModrinthAPI api;
+static ModPlatform::ProviderCapabilities ProviderCaps;
void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireString(obj, "project_id");
+ pack.provider = ModPlatform::Provider::MODRINTH;
pack.name = Json::requireString(obj, "title");
QString slug = Json::ensureString(obj, "slug", "");
@@ -94,46 +97,10 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
- ModPlatform::IndexedVersion file;
- file.addonId = Json::requireString(obj, "project_id");
- file.fileId = Json::requireString(obj, "id");
- file.date = Json::requireString(obj, "date_published");
- auto versionArray = Json::requireArray(obj, "game_versions");
- if (versionArray.empty()) { continue; }
- for (auto mcVer : versionArray) {
- file.mcVersion.append(mcVer.toString());
- }
- auto loaders = Json::requireArray(obj, "loaders");
- for (auto loader : loaders) {
- file.loaders.append(loader.toString());
- }
- file.version = Json::requireString(obj, "name");
-
- auto files = Json::requireArray(obj, "files");
- int i = 0;
-
- // Find correct file (needed in cases where one version may have multiple files)
- // Will default to the last one if there's no primary (though I think Modrinth requires that
- // at least one file is primary, idk)
- // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed
- while (i < files.count() - 1){
- auto parent = files[i].toObject();
- auto fileName = Json::requireString(parent, "filename");
-
- // Grab the primary file, if available
- if(Json::requireBoolean(parent, "primary"))
- break;
-
- i++;
- }
-
- auto parent = files[i].toObject();
- if (parent.contains("url")) {
- file.downloadUrl = Json::requireString(parent, "url");
- file.fileName = Json::requireString(parent, "filename");
+ 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
@@ -143,3 +110,61 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
pack.versions = unsortedVersions;
pack.versionsLoaded = true;
}
+
+auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedVersion
+{
+ ModPlatform::IndexedVersion file;
+
+ file.addonId = Json::requireString(obj, "project_id");
+ file.fileId = Json::requireString(obj, "id");
+ file.date = Json::requireString(obj, "date_published");
+ auto versionArray = Json::requireArray(obj, "game_versions");
+ if (versionArray.empty()) {
+ return {};
+ }
+ for (auto mcVer : versionArray) {
+ file.mcVersion.append(mcVer.toString());
+ }
+ auto loaders = Json::requireArray(obj, "loaders");
+ for (auto loader : loaders) {
+ file.loaders.append(loader.toString());
+ }
+ file.version = Json::requireString(obj, "name");
+
+ auto files = Json::requireArray(obj, "files");
+ int i = 0;
+
+ // Find correct file (needed in cases where one version may have multiple files)
+ // Will default to the last one if there's no primary (though I think Modrinth requires that
+ // at least one file is primary, idk)
+ // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed
+ while (i < files.count() - 1) {
+ auto parent = files[i].toObject();
+ auto fileName = Json::requireString(parent, "filename");
+
+ // Grab the primary file, if available
+ if (Json::requireBoolean(parent, "primary"))
+ break;
+
+ i++;
+ }
+
+ auto parent = files[i].toObject();
+ if (parent.contains("url")) {
+ file.downloadUrl = Json::requireString(parent, "url");
+ file.fileName = Json::requireString(parent, "filename");
+ 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;
+ }
+ }
+
+ return file;
+ }
+
+ return {};
+}
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h
index b0e3736f..b7936204 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.h
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -30,5 +30,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
BaseInstance* inst);
+auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion;
} // namespace Modrinth
diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
index 73c8ef84..a4620df9 100644
--- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
@@ -42,6 +42,8 @@
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
+#include <QSet>
+
static ModrinthAPI api;
namespace Modrinth {
@@ -120,23 +122,6 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc)
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;
@@ -166,9 +151,6 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion
auto url = Json::requireString(parent, "url");
- if(!validateDownloadUrl(url))
- continue;
-
file.download_url = url;
if(is_primary)
break;
diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h
index e95cb589..035dc62e 100644
--- a/launcher/modplatform/modrinth/ModrinthPackManifest.h
+++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h
@@ -40,6 +40,7 @@
#include <QByteArray>
#include <QCryptographicHash>
+#include <QQueue>
#include <QString>
#include <QUrl>
#include <QVector>
@@ -48,14 +49,12 @@ class MinecraftInstance;
namespace Modrinth {
-struct File
-{
+struct File {
QString path;
QCryptographicHash::Algorithm hashAlgorithm;
QByteArray hash;
- // TODO: should this support multiple download URLs, like the JSON does?
- QUrl download;
+ QQueue<QUrl> downloads;
};
struct DonationData {
diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp
new file mode 100644
index 00000000..0782b9f4
--- /dev/null
+++ b/launcher/modplatform/packwiz/Packwiz.cpp
@@ -0,0 +1,289 @@
+// 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/>.
+*/
+
+#include "Packwiz.h"
+
+#include <QDebug>
+#include <QDir>
+#include <QObject>
+
+#include "toml.h"
+#include "FileSystem.h"
+
+#include "minecraft/mod/Mod.h"
+#include "modplatform/ModIndex.h"
+
+namespace Packwiz {
+
+auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString
+{
+ QFile index_file(index_dir.absoluteFilePath(normalized_fname));
+
+ QString real_fname = normalized_fname;
+ if (!index_file.exists()) {
+ // Tries to get similar entries
+ for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) {
+ if (!QString::compare(normalized_fname, file_name, Qt::CaseInsensitive)) {
+ real_fname = file_name;
+ break;
+ }
+ }
+
+ if(should_find_match && !QString::compare(normalized_fname, real_fname, Qt::CaseSensitive)){
+ qCritical() << "Could not find a match for a valid metadata file!";
+ qCritical() << "File: " << normalized_fname;
+ return {};
+ }
+ }
+
+ return real_fname;
+}
+
+// Helpers
+static inline auto indexFileName(QString const& mod_name) -> QString
+{
+ if(mod_name.endsWith(".pw.toml"))
+ return mod_name;
+ return QString("%1.pw.toml").arg(mod_name);
+}
+
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+// Helper functions for extracting data from the TOML file
+auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString
+{
+ toml_datum_t var = toml_string_in(parent, entry_name);
+ if (!var.ok) {
+ qCritical() << QString("Failed to read str property '%1' in mod metadata.").arg(entry_name);
+ return {};
+ }
+
+ QString tmp = var.u.s;
+ free(var.u.s);
+
+ return tmp;
+}
+
+auto intEntry(toml_table_t* parent, const char* entry_name) -> int
+{
+ toml_datum_t var = toml_int_in(parent, entry_name);
+ if (!var.ok) {
+ qCritical() << QString("Failed to read int property '%1' in mod metadata.").arg(entry_name);
+ return {};
+ }
+
+ return var.u.i;
+}
+
+
+auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod
+{
+ Mod mod;
+
+ mod.name = mod_pack.name;
+ mod.filename = mod_version.fileName;
+
+ if(mod_pack.provider == ModPlatform::Provider::FLAME){
+ mod.mode = "metadata:curseforge";
+ }
+ else {
+ mod.mode = "url";
+ mod.url = mod_version.downloadUrl;
+ }
+
+ mod.hash_format = mod_version.hash_type;
+ mod.hash = mod_version.hash;
+
+ mod.provider = mod_pack.provider;
+ mod.file_id = mod_version.fileId;
+ mod.project_id = mod_pack.addonId;
+
+ return mod;
+}
+
+auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod
+{
+ auto mod_name = internal_mod.name();
+
+ // Try getting metadata if it exists
+ Mod mod { getIndexForMod(index_dir, mod_name) };
+ if(mod.isValid())
+ return mod;
+
+ qWarning() << QString("Tried to create mod metadata with a Mod without metadata!");
+
+ return {};
+}
+
+void V1::updateModIndex(QDir& index_dir, Mod& mod)
+{
+ if(!mod.isValid()){
+ qCritical() << QString("Tried to update metadata of an invalid mod!");
+ return;
+ }
+
+ // Ensure the corresponding mod's info exists, and create it if not
+
+ auto normalized_fname = indexFileName(mod.name);
+ auto real_fname = getRealIndexName(index_dir, normalized_fname);
+
+ QFile index_file(index_dir.absoluteFilePath(real_fname));
+
+ // There's already data on there!
+ // TODO: We should do more stuff here, as the user is likely trying to
+ // override a file. In this case, check versions and ask the user what
+ // they want to do!
+ if (index_file.exists()) { index_file.remove(); }
+
+ if (!index_file.open(QIODevice::ReadWrite)) {
+ qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name));
+ return;
+ }
+
+ // Put TOML data into the file
+ QTextStream in_stream(&index_file);
+ auto addToStream = [&in_stream](QString&& key, QString value) { in_stream << QString("%1 = \"%2\"\n").arg(key, value); };
+
+ {
+ addToStream("name", mod.name);
+ addToStream("filename", mod.filename);
+ addToStream("side", mod.side);
+
+ in_stream << QString("\n[download]\n");
+ addToStream("mode", mod.mode);
+ addToStream("url", mod.url.toString());
+ addToStream("hash-format", mod.hash_format);
+ addToStream("hash", mod.hash);
+
+ in_stream << QString("\n[update]\n");
+ in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider));
+ switch(mod.provider){
+ case(ModPlatform::Provider::FLAME):
+ in_stream << QString("file-id = %1\n").arg(mod.file_id.toString());
+ in_stream << QString("project-id = %1\n").arg(mod.project_id.toString());
+ break;
+ case(ModPlatform::Provider::MODRINTH):
+ addToStream("mod-id", mod.mod_id().toString());
+ addToStream("version", mod.version().toString());
+ break;
+ }
+ }
+
+ index_file.close();
+}
+
+void V1::deleteModIndex(QDir& index_dir, QString& mod_name)
+{
+ auto normalized_fname = indexFileName(mod_name);
+ auto real_fname = getRealIndexName(index_dir, normalized_fname);
+ if (real_fname.isEmpty())
+ return;
+
+ QFile index_file(index_dir.absoluteFilePath(real_fname));
+
+ if(!index_file.exists()){
+ qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_name);
+ return;
+ }
+
+ if(!index_file.remove()){
+ qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_name);
+ }
+}
+
+auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
+{
+ Mod mod;
+
+ auto normalized_fname = indexFileName(index_file_name);
+ auto real_fname = getRealIndexName(index_dir, normalized_fname, true);
+ if (real_fname.isEmpty())
+ return {};
+
+ QFile index_file(index_dir.absoluteFilePath(real_fname));
+
+ if (!index_file.open(QIODevice::ReadOnly)) {
+ qWarning() << QString("Failed to open mod metadata for %1").arg(index_file_name);
+ return {};
+ }
+
+ toml_table_t* table = nullptr;
+
+ // NOLINTNEXTLINE(modernize-avoid-c-arrays)
+ char errbuf[200];
+ auto file_bytearray = index_file.readAll();
+ table = toml_parse(file_bytearray.data(), errbuf, sizeof(errbuf));
+
+ index_file.close();
+
+ if (!table) {
+ qWarning() << QString("Could not open file %1!").arg(indexFileName(index_file_name));
+ qWarning() << "Reason: " << QString(errbuf);
+ return {};
+ }
+
+ { // Basic info
+ mod.name = stringEntry(table, "name");
+ mod.filename = stringEntry(table, "filename");
+ mod.side = stringEntry(table, "side");
+ }
+
+ { // [download] info
+ toml_table_t* download_table = toml_table_in(table, "download");
+ if (!download_table) {
+ qCritical() << QString("No [download] section found on mod metadata!");
+ return {};
+ }
+
+ mod.mode = stringEntry(download_table, "mode");
+ mod.url = stringEntry(download_table, "url");
+ mod.hash_format = stringEntry(download_table, "hash-format");
+ mod.hash = stringEntry(download_table, "hash");
+ }
+
+ { // [update] info
+ using Provider = ModPlatform::Provider;
+
+ toml_table_t* update_table = toml_table_in(table, "update");
+ if (!update_table) {
+ qCritical() << QString("No [update] section found on mod metadata!");
+ return {};
+ }
+
+ toml_table_t* mod_provider_table = nullptr;
+ if ((mod_provider_table = toml_table_in(update_table, ProviderCaps.name(Provider::FLAME)))) {
+ mod.provider = Provider::FLAME;
+ mod.file_id = intEntry(mod_provider_table, "file-id");
+ mod.project_id = intEntry(mod_provider_table, "project-id");
+ } else if ((mod_provider_table = toml_table_in(update_table, ProviderCaps.name(Provider::MODRINTH)))) {
+ mod.provider = Provider::MODRINTH;
+ mod.mod_id() = stringEntry(mod_provider_table, "mod-id");
+ mod.version() = stringEntry(mod_provider_table, "version");
+ } else {
+ qCritical() << QString("No mod provider on mod metadata!");
+ return {};
+ }
+
+ }
+
+ toml_free(table);
+
+ return mod;
+}
+
+} // namespace Packwiz
diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h
new file mode 100644
index 00000000..3c99769c
--- /dev/null
+++ b/launcher/modplatform/packwiz/Packwiz.h
@@ -0,0 +1,93 @@
+// 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 "modplatform/ModIndex.h"
+
+#include <QString>
+#include <QUrl>
+#include <QVariant>
+
+struct toml_table_t;
+class QDir;
+
+// Mod from launcher/minecraft/mod/Mod.h
+class Mod;
+
+namespace Packwiz {
+
+auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString;
+
+auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString;
+auto intEntry(toml_table_t* parent, const char* entry_name) -> int;
+
+class V1 {
+ public:
+ struct Mod {
+ QString name {};
+ QString filename {};
+ // FIXME: make side an enum
+ QString side {"both"};
+
+ // [download]
+ QString mode {};
+ QUrl url {};
+ QString hash_format {};
+ QString hash {};
+
+ // [update]
+ ModPlatform::Provider provider {};
+ QVariant file_id {};
+ QVariant project_id {};
+
+ public:
+ // This is a totally heuristic, but should work for now.
+ auto isValid() const -> bool { return !name.isEmpty() && !project_id.isNull(); }
+
+ // Different providers can use different names for the same thing
+ // Modrinth-specific
+ auto mod_id() -> QVariant& { return project_id; }
+ auto version() -> QVariant& { return file_id; }
+ };
+
+ /* Generates the object representing the information in a mod.pw.toml file via
+ * its common representation in the launcher, when downloading mods.
+ * */
+ static auto createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod;
+ /* Generates the object representing the information in a mod.pw.toml file via
+ * its common representation in the launcher.
+ * */
+ static auto createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod;
+
+ /* Updates the mod index for the provided mod.
+ * This creates a new index if one does not exist already
+ * TODO: Ask the user if they want to override, and delete the old mod's files, or keep the old one.
+ * */
+ static void updateModIndex(QDir& index_dir, Mod& mod);
+
+ /* Deletes the metadata for the mod with the given name. If the metadata doesn't exist, it does nothing. */
+ static void deleteModIndex(QDir& index_dir, QString& mod_name);
+
+ /* Gets the metadata for a mod with a particular name.
+ * If the mod doesn't have a metadata, it simply returns an empty Mod object.
+ * */
+ static auto getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod;
+};
+
+} // namespace Packwiz
diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp
new file mode 100644
index 00000000..3d47f9f7
--- /dev/null
+++ b/launcher/modplatform/packwiz/Packwiz_test.cpp
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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 <QTemporaryDir>
+#include <QTest>
+
+#include "Packwiz.h"
+#include "TestUtil.h"
+
+class PackwizTest : public QObject {
+ Q_OBJECT
+
+ private slots:
+ // Files taken from https://github.com/packwiz/packwiz-example-pack
+ void loadFromFile_Modrinth()
+ {
+ QString source = QFINDTESTDATA("testdata");
+
+ QDir index_dir(source);
+ QString name_mod("borderless-mining.pw.toml");
+ QVERIFY(index_dir.entryList().contains(name_mod));
+
+ auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod);
+
+ QVERIFY(metadata.isValid());
+
+ QCOMPARE(metadata.name, "Borderless Mining");
+ QCOMPARE(metadata.filename, "borderless-mining-1.1.1+1.18.jar");
+ QCOMPARE(metadata.side, "client");
+
+ QCOMPARE(metadata.url, QUrl("https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar"));
+ QCOMPARE(metadata.hash_format, "sha512");
+ QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d");
+
+ QCOMPARE(metadata.provider, ModPlatform::Provider::MODRINTH);
+ QCOMPARE(metadata.version(), "ug2qKTPR");
+ QCOMPARE(metadata.mod_id(), "kYq5qkSL");
+ }
+
+ void loadFromFile_Curseforge()
+ {
+ QString source = QFINDTESTDATA("testdata");
+
+ QDir index_dir(source);
+ QString name_mod("screenshot-to-clipboard-fabric.pw.toml");
+ QVERIFY(index_dir.entryList().contains(name_mod));
+
+ // Try without the .pw.toml at the end
+ name_mod.chop(8);
+
+ auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod);
+
+ QVERIFY(metadata.isValid());
+
+ QCOMPARE(metadata.name, "Screenshot to Clipboard (Fabric)");
+ QCOMPARE(metadata.filename, "screenshot-to-clipboard-1.0.7-fabric.jar");
+ QCOMPARE(metadata.side, "both");
+
+ QCOMPARE(metadata.url, QUrl("https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar"));
+ QCOMPARE(metadata.hash_format, "murmur2");
+ QCOMPARE(metadata.hash, "1781245820");
+
+ QCOMPARE(metadata.provider, ModPlatform::Provider::FLAME);
+ QCOMPARE(metadata.file_id, 3509043);
+ QCOMPARE(metadata.project_id, 327154);
+ }
+};
+
+QTEST_GUILESS_MAIN(PackwizTest)
+
+#include "Packwiz_test.moc"
diff --git a/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml b/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml
new file mode 100644
index 00000000..16545fd4
--- /dev/null
+++ b/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml
@@ -0,0 +1,13 @@
+name = "Borderless Mining"
+filename = "borderless-mining-1.1.1+1.18.jar"
+side = "client"
+
+[download]
+url = "https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar"
+hash-format = "sha512"
+hash = "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d"
+
+[update]
+[update.modrinth]
+mod-id = "kYq5qkSL"
+version = "ug2qKTPR"
diff --git a/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml b/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml
new file mode 100644
index 00000000..87d70ada
--- /dev/null
+++ b/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml
@@ -0,0 +1,13 @@
+name = "Screenshot to Clipboard (Fabric)"
+filename = "screenshot-to-clipboard-1.0.7-fabric.jar"
+side = "both"
+
+[download]
+url = "https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar"
+hash-format = "murmur2"
+hash = "1781245820"
+
+[update]
+[update.curseforge]
+file-id = 3509043
+project-id = 327154
diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp
index 782fb9b2..471b4a2f 100644
--- a/launcher/modplatform/technic/TechnicPackProcessor.cpp
+++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp
@@ -185,13 +185,22 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const
components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1));
}
}
- else if (libraryName.startsWith("net.minecraftforge:minecraftforge:"))
+ else
{
- components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2));
- }
- else if (libraryName.startsWith("net.fabricmc:fabric-loader:"))
- {
- components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2));
+ static QStringList possibleLoaders{
+ "net.minecraftforge:minecraftforge:",
+ "net.fabricmc:fabric-loader:",
+ "org.quiltmc:quilt-loader:"
+ };
+ for (const auto& loader : possibleLoaders)
+ {
+ if (libraryName.startsWith(loader))
+ {
+ auto loaderComponent = loader.chopped(1).replace(":", ".");
+ components->setComponentVersion(loaderComponent, libraryName.section(':', 2));
+ break;
+ }
+ }
}
}
}