PrismLauncher/launcher/modplatform/packwiz/Packwiz.cpp
flow 6a18079953
refactor: generalize mod models and APIs to resources
Firstly, this abstract away behavior in the mod download models that can
also be applied to other types of resources into a superclass, allowing
other resource types to be implemented without so much code duplication.

For that, this also generalizes the APIs used (currently, ModrinthAPI
and FlameAPI) to be able to make requests to other types of resources.

It also does a general cleanup of both of those. In particular, this
makes use of std::optional instead of invalid values for errors and,
well, optional values :p

This is a squash of some commits that were becoming too interlaced
together to be cleanly separated.

Signed-off-by: flow <flowlnlnln@gmail.com>
2023-01-13 16:23:00 -03:00

315 lines
9.7 KiB
C++

// 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 "FileSystem.h"
#include "StringUtils.h"
#include "minecraft/mod/Mod.h"
#include "modplatform/ModIndex.h"
#include <toml++/toml.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_slug) -> QString
{
if (mod_slug.endsWith(".pw.toml"))
return mod_slug;
return QString("%1.pw.toml").arg(mod_slug);
}
static ModPlatform::ProviderCapabilities ProviderCaps;
// Helper functions for extracting data from the TOML file
auto stringEntry(toml::table table, QString entry_name) -> QString
{
auto node = table[StringUtils::toStdString(entry_name)];
if (!node) {
qCritical() << "Failed to read str property '" + entry_name + "' in mod metadata.";
return {};
}
return node.value_or("");
}
auto intEntry(toml::table table, QString entry_name) -> int
{
auto node = table[StringUtils::toStdString(entry_name)];
if (!node) {
qCritical() << "Failed to read int property '" + entry_name + "' in mod metadata.";
return {};
}
return node.value_or(0);
}
auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod
{
Mod mod;
mod.slug = mod_pack.slug;
mod.name = mod_pack.name;
mod.filename = mod_version.fileName;
if (mod_pack.provider == ModPlatform::ResourceProvider::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, QString slug) -> Mod
{
// Try getting metadata if it exists
Mod mod{ getIndexForMod(index_dir, slug) };
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.slug);
auto real_fname = getRealIndexName(index_dir, normalized_fname);
QFile index_file(index_dir.absoluteFilePath(real_fname));
if (real_fname != normalized_fname)
index_file.rename(normalized_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();
} else {
FS::ensureFilePathExists(index_file.fileName());
}
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::ResourceProvider::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::ResourceProvider::MODRINTH):
addToStream("mod-id", mod.mod_id().toString());
addToStream("version", mod.version().toString());
break;
}
}
index_file.flush();
index_file.close();
}
void V1::deleteModIndex(QDir& index_dir, QString& mod_slug)
{
auto normalized_fname = indexFileName(mod_slug);
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_slug);
return;
}
if (!index_file.remove()) {
qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_slug);
}
}
void V1::deleteModIndex(QDir& index_dir, QVariant& mod_id)
{
for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) {
auto mod = getIndexForMod(index_dir, file_name);
if (mod.mod_id() == mod_id) {
deleteModIndex(index_dir, mod.name);
break;
}
}
}
auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
{
Mod mod;
auto normalized_fname = indexFileName(slug);
auto real_fname = getRealIndexName(index_dir, normalized_fname, true);
if (real_fname.isEmpty())
return {};
toml::table table;
#if TOML_EXCEPTIONS
try {
table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
} catch (const toml::parse_error& err) {
qWarning() << QString("Could not open file %1!").arg(normalized_fname);
qWarning() << "Reason: " << QString(err.what());
return {};
}
#else
table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
if (!table) {
qWarning() << QString("Could not open file %1!").arg(normalized_fname);
qWarning() << "Reason: " << QString(table.error().what());
return {};
}
#endif
// index_file.close();
mod.slug = slug;
{ // Basic info
mod.name = stringEntry(table, "name");
mod.filename = stringEntry(table, "filename");
mod.side = stringEntry(table, "side");
}
{ // [download] info
auto download_table = table["download"].as_table();
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::ResourceProvider;
auto update_table = table["update"];
if (!update_table || !update_table.is_table()) {
qCritical() << QString("No [update] section found on mod metadata!");
return {};
}
toml::table* mod_provider_table = nullptr;
if ((mod_provider_table = update_table[ProviderCaps.name(Provider::FLAME)].as_table())) {
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 = update_table[ProviderCaps.name(Provider::MODRINTH)].as_table())) {
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 {};
}
}
return mod;
}
auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod
{
for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) {
auto mod = getIndexForMod(index_dir, file_name);
if (mod.mod_id() == mod_id)
return mod;
}
return {};
}
} // namespace Packwiz