diff options
Diffstat (limited to 'launcher/modplatform')
-rw-r--r-- | launcher/modplatform/ModIndex.cpp | 86 | ||||
-rw-r--r-- | launcher/modplatform/ModIndex.h | 35 | ||||
-rw-r--r-- | launcher/modplatform/flame/FlameAPI.h | 1 | ||||
-rw-r--r-- | launcher/modplatform/flame/FlameModIndex.cpp | 77 | ||||
-rw-r--r-- | launcher/modplatform/flame/FlameModIndex.h | 1 | ||||
-rw-r--r-- | launcher/modplatform/modrinth/ModrinthAPI.h | 1 | ||||
-rw-r--r-- | launcher/modplatform/modrinth/ModrinthPackIndex.cpp | 129 | ||||
-rw-r--r-- | launcher/modplatform/modrinth/ModrinthPackIndex.h | 1 | ||||
-rw-r--r-- | launcher/modplatform/packwiz/Packwiz.cpp | 289 | ||||
-rw-r--r-- | launcher/modplatform/packwiz/Packwiz.h | 93 | ||||
-rw-r--r-- | launcher/modplatform/packwiz/Packwiz_test.cpp | 86 | ||||
-rw-r--r-- | launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml | 13 | ||||
-rw-r--r-- | launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml | 13 |
13 files changed, 752 insertions, 73 deletions
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 7e1cf254..04dd2dac 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; @@ -22,10 +53,13 @@ struct IndexedVersion { QString date; QString fileName; QVector<QString> loaders = {}; + QString hash_type; + QString hash; }; struct IndexedPack { QVariant addonId; + Provider provider; QString name; QString description; QList<ModpackAuthor> authors; @@ -40,3 +74,4 @@ struct IndexedPack { } // namespace ModPlatform Q_DECLARE_METATYPE(ModPlatform::IndexedPack) +Q_DECLARE_METATYPE(ModPlatform::Provider) diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 8bb33d47..e31cf0a1 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 ba0824cf..ed6d64c3 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", ""); @@ -27,6 +30,17 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) } } +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, @@ -38,28 +52,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 { @@ -70,3 +69,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 d3171d94..2e0f2e86 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -16,5 +16,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/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 6119a4df..60ecbd32 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> diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index f7fa9864..fdce71c3 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", ""); @@ -57,46 +60,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 @@ -106,3 +73,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 7f306f25..df70278f 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -29,5 +29,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/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..023b990e --- /dev/null +++ b/launcher/modplatform/packwiz/Packwiz_test.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 <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(5); + + 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 |