Merge pull request #426 from flowln/mod_perma
Add on-disk mod metadata information
This commit is contained in:
86
launcher/modplatform/ModIndex.cpp
Normal file
86
launcher/modplatform/ModIndex.cpp
Normal file
@ -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
|
@ -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)
|
||||
|
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "modplatform/helpers/NetworkModAPI.h"
|
||||
|
||||
class FlameAPI : public NetworkModAPI {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
#include "BuildConfig.h"
|
||||
#include "modplatform/ModAPI.h"
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "modplatform/helpers/NetworkModAPI.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
@ -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 {};
|
||||
}
|
||||
|
@ -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
|
||||
|
289
launcher/modplatform/packwiz/Packwiz.cpp
Normal file
289
launcher/modplatform/packwiz/Packwiz.cpp
Normal file
@ -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
|
93
launcher/modplatform/packwiz/Packwiz.h
Normal file
93
launcher/modplatform/packwiz/Packwiz.h
Normal file
@ -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
|
86
launcher/modplatform/packwiz/Packwiz_test.cpp
Normal file
86
launcher/modplatform/packwiz/Packwiz_test.cpp
Normal file
@ -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"
|
13
launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml
vendored
Normal file
13
launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml
vendored
Normal file
@ -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"
|
13
launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml
vendored
Normal file
13
launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml
vendored
Normal file
@ -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
|
Reference in New Issue
Block a user