Merge branch 'develop' into data-packs
Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
@@ -120,3 +120,41 @@ QList<ResourceAPI::SortingMethod> ModrinthAPI::getSortingMethods() const
|
||||
{ 4, "newest", QObject::tr("Sort by Newest") },
|
||||
{ 5, "updated", QObject::tr("Sort by Last Updated") } };
|
||||
}
|
||||
|
||||
Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr<QByteArray> response)
|
||||
{
|
||||
auto netJob = makeShared<NetJob>(QString("Modrinth::GetCategories"), APPLICATION->network());
|
||||
netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category"), response));
|
||||
QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Modrinth failed to get categories:" << msg; });
|
||||
return netJob;
|
||||
}
|
||||
|
||||
QList<ModPlatform::Category> ModrinthAPI::loadModCategories(std::shared_ptr<QByteArray> response)
|
||||
{
|
||||
QList<ModPlatform::Category> categories;
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
return categories;
|
||||
}
|
||||
|
||||
try {
|
||||
auto arr = Json::requireArray(doc);
|
||||
|
||||
for (auto val : arr) {
|
||||
auto cat = Json::requireObject(val);
|
||||
auto name = Json::requireString(cat, "name");
|
||||
if (Json::ensureString(cat, "project_type", "") == "mod")
|
||||
categories.push_back({ name, name });
|
||||
}
|
||||
|
||||
} catch (Json::JsonException& e) {
|
||||
qCritical() << "Failed to parse response from a version request.";
|
||||
qCritical() << e.what();
|
||||
qDebug() << doc;
|
||||
}
|
||||
return categories;
|
||||
};
|
||||
@@ -30,6 +30,9 @@ class ModrinthAPI : public NetworkResourceAPI {
|
||||
|
||||
Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
|
||||
|
||||
static Task::Ptr getModCategories(std::shared_ptr<QByteArray> response);
|
||||
static QList<ModPlatform::Category> loadModCategories(std::shared_ptr<QByteArray> response);
|
||||
|
||||
public:
|
||||
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
|
||||
|
||||
@@ -41,7 +44,7 @@ class ModrinthAPI : public NetworkResourceAPI {
|
||||
for (auto loader :
|
||||
{ ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader }) {
|
||||
if (types & loader) {
|
||||
l << getModLoaderString(loader);
|
||||
l << getModLoaderAsString(loader);
|
||||
}
|
||||
}
|
||||
return l;
|
||||
@@ -56,6 +59,27 @@ class ModrinthAPI : public NetworkResourceAPI {
|
||||
return l.join(',');
|
||||
}
|
||||
|
||||
static auto getCategoriesFilters(QStringList categories) -> const QString
|
||||
{
|
||||
QStringList l;
|
||||
for (auto cat : categories) {
|
||||
l << QString("\"categories:%1\"").arg(cat);
|
||||
}
|
||||
return l.join(',');
|
||||
}
|
||||
|
||||
static auto getSideFilters(QString side) -> const QString
|
||||
{
|
||||
if (side.isEmpty() || side == "both") {
|
||||
return {};
|
||||
}
|
||||
if (side == "client")
|
||||
return QString("\"client_side:required\",\"client_side:optional\"");
|
||||
if (side == "server")
|
||||
return QString("\"server_side:required\",\"server_side:optional\"");
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type)
|
||||
{
|
||||
@@ -74,6 +98,7 @@ class ModrinthAPI : public NetworkResourceAPI {
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
[[nodiscard]] QString createFacets(SearchArgs const& args) const
|
||||
{
|
||||
QStringList facets_list;
|
||||
@@ -84,6 +109,14 @@ class ModrinthAPI : public NetworkResourceAPI {
|
||||
facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value())));
|
||||
if (args.type == ModPlatform::ResourceType::DATA_PACK)
|
||||
facets_list.append("[\"categories:datapack\"]");
|
||||
if (args.side.has_value()) {
|
||||
auto side = getSideFilters(args.side.value());
|
||||
if (!side.isEmpty())
|
||||
facets_list.append(QString("[%1]").arg(side));
|
||||
}
|
||||
if (args.categoryIds.has_value() && !args.categoryIds->empty())
|
||||
facets_list.append(QString("[%1]").arg(getCategoriesFilters(args.categoryIds.value())));
|
||||
|
||||
facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type)));
|
||||
|
||||
return QString("[%1]").arg(facets_list.join(','));
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
#include "ModrinthCheckUpdate.h"
|
||||
#include "Application.h"
|
||||
#include "ModrinthAPI.h"
|
||||
#include "ModrinthPackIndex.h"
|
||||
|
||||
#include "Json.h"
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "ResourceDownloadTask.h"
|
||||
|
||||
#include "modplatform/helpers/HashUtils.h"
|
||||
|
||||
#include "tasks/ConcurrentTask.h"
|
||||
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
|
||||
static ModrinthAPI api;
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
|
||||
ModrinthCheckUpdate::ModrinthCheckUpdate(QList<Mod*>& mods,
|
||||
std::list<Version>& mcVersions,
|
||||
QList<ModPlatform::ModLoaderType> loadersList,
|
||||
std::shared_ptr<ModFolderModel> mods_folder)
|
||||
: CheckUpdateTask(mods, mcVersions, loadersList, mods_folder)
|
||||
, m_hash_type(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first())
|
||||
{}
|
||||
|
||||
bool ModrinthCheckUpdate::abort()
|
||||
{
|
||||
if (m_net_job)
|
||||
return m_net_job->abort();
|
||||
if (m_job)
|
||||
return m_job->abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -30,156 +37,185 @@ bool ModrinthCheckUpdate::abort()
|
||||
void ModrinthCheckUpdate::executeTask()
|
||||
{
|
||||
setStatus(tr("Preparing mods for Modrinth..."));
|
||||
setProgress(0, 3);
|
||||
setProgress(0, 9);
|
||||
|
||||
QHash<QString, Mod*> mappings;
|
||||
|
||||
// Create all hashes
|
||||
QStringList hashes;
|
||||
auto best_hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
|
||||
|
||||
ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
|
||||
auto hashing_task =
|
||||
makeShared<ConcurrentTask>(this, "MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
|
||||
for (auto* mod : m_mods) {
|
||||
if (!mod->enabled()) {
|
||||
emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
|
||||
continue;
|
||||
}
|
||||
|
||||
auto hash = mod->metadata()->hash;
|
||||
|
||||
// Sadly the API can only handle one hash type per call, se we
|
||||
// need to generate a new hash if the current one is innadequate
|
||||
// (though it will rarely happen, if at all)
|
||||
if (mod->metadata()->hash_format != best_hash_type) {
|
||||
auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath());
|
||||
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [&hashes, &mappings, mod](QString hash) {
|
||||
hashes.append(hash);
|
||||
mappings.insert(hash, mod);
|
||||
});
|
||||
if (mod->metadata()->hash_format != m_hash_type) {
|
||||
auto hash_task = Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH);
|
||||
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mappings.insert(hash, mod); });
|
||||
connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); });
|
||||
hashing_task.addTask(hash_task);
|
||||
hashing_task->addTask(hash_task);
|
||||
} else {
|
||||
hashes.append(hash);
|
||||
mappings.insert(hash, mod);
|
||||
m_mappings.insert(hash, mod);
|
||||
}
|
||||
}
|
||||
|
||||
QEventLoop loop;
|
||||
connect(&hashing_task, &Task::finished, [&loop] { loop.quit(); });
|
||||
hashing_task.start();
|
||||
loop.exec();
|
||||
connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader);
|
||||
m_job = hashing_task;
|
||||
hashing_task->start();
|
||||
}
|
||||
|
||||
auto response = std::make_shared<QByteArray>();
|
||||
auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
|
||||
void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr<QByteArray> response,
|
||||
ModPlatform::ModLoaderTypes loader,
|
||||
bool forceModLoaderCheck)
|
||||
{
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
|
||||
QEventLoop lock;
|
||||
emitFailed(parse_error.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
connect(job.get(), &Task::succeeded, this, [this, response, &mappings, best_hash_type, job] {
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
setStatus(tr("Parsing the API response from Modrinth..."));
|
||||
setProgress(m_next_loader_idx * 2, 9);
|
||||
|
||||
failed(parse_error.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(tr("Parsing the API response from Modrinth..."));
|
||||
setProgress(2, 3);
|
||||
|
||||
try {
|
||||
for (auto hash : mappings.keys()) {
|
||||
auto project_obj = doc[hash].toObject();
|
||||
|
||||
// If the returned project is empty, but we have Modrinth metadata,
|
||||
// it means this specific version is not available
|
||||
if (project_obj.isEmpty()) {
|
||||
qDebug() << "Mod " << mappings.find(hash).value()->name() << " got an empty response.";
|
||||
qDebug() << "Hash: " << hash;
|
||||
|
||||
emit checkFailed(
|
||||
mappings.find(hash).value(),
|
||||
tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader."));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sometimes a version may have multiple files, one with "forge" and one with "fabric",
|
||||
// so we may want to filter it
|
||||
QString loader_filter;
|
||||
if (m_loaders.has_value()) {
|
||||
static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge,
|
||||
ModPlatform::ModLoaderType::Fabric, ModPlatform::ModLoaderType::Quilt };
|
||||
for (auto flag : flags) {
|
||||
if (m_loaders.value().testFlag(flag)) {
|
||||
loader_filter = ModPlatform::getModLoaderString(flag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
|
||||
// - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the
|
||||
// loader_filter
|
||||
// - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
|
||||
// Such is the pain of having arbitrary files for a given version .-.
|
||||
|
||||
auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, best_hash_type, loader_filter);
|
||||
if (project_ver.downloadUrl.isEmpty()) {
|
||||
qCritical() << "Modrinth mod without download url!";
|
||||
qCritical() << project_ver.fileName;
|
||||
|
||||
emit checkFailed(mappings.find(hash).value(), tr("Mod has an empty download URL"));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
auto mod_iter = mappings.find(hash);
|
||||
if (mod_iter == mappings.end()) {
|
||||
qCritical() << "Failed to remap mod from Modrinth!";
|
||||
continue;
|
||||
}
|
||||
auto mod = *mod_iter;
|
||||
|
||||
auto key = project_ver.hash;
|
||||
|
||||
// Fake pack with the necessary info to pass to the download task :)
|
||||
auto pack = std::make_shared<ModPlatform::IndexedPack>();
|
||||
pack->name = mod->name();
|
||||
pack->slug = mod->metadata()->slug;
|
||||
pack->addonId = mod->metadata()->project_id;
|
||||
pack->websiteUrl = mod->homeurl();
|
||||
for (auto& author : mod->authors())
|
||||
pack->authors.append({ author });
|
||||
pack->description = mod->description();
|
||||
pack->provider = ModPlatform::ResourceProvider::MODRINTH;
|
||||
if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) {
|
||||
if (mod->version() == project_ver.version_number)
|
||||
continue;
|
||||
|
||||
auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_mods_folder);
|
||||
|
||||
m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.version_type,
|
||||
project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task);
|
||||
}
|
||||
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, project_ver));
|
||||
try {
|
||||
for (auto hash : m_mappings.keys()) {
|
||||
if (forceModLoaderCheck && !(m_mappings[hash]->loaders() & loader)) {
|
||||
continue;
|
||||
}
|
||||
} catch (Json::JsonException& e) {
|
||||
failed(e.cause() + " : " + e.what());
|
||||
}
|
||||
});
|
||||
auto project_obj = doc[hash].toObject();
|
||||
|
||||
connect(job.get(), &Task::finished, &lock, &QEventLoop::quit);
|
||||
// If the returned project is empty, but we have Modrinth metadata,
|
||||
// it means this specific version is not available
|
||||
if (project_obj.isEmpty()) {
|
||||
qDebug() << "Mod " << m_mappings.find(hash).value()->name() << " got an empty response." << "Hash: " << hash;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sometimes a version may have multiple files, one with "forge" and one with "fabric",
|
||||
// so we may want to filter it
|
||||
QString loader_filter;
|
||||
static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge,
|
||||
ModPlatform::ModLoaderType::Quilt, ModPlatform::ModLoaderType::Fabric };
|
||||
for (auto flag : flags) {
|
||||
if (loader.testFlag(flag)) {
|
||||
loader_filter = ModPlatform::getModLoaderAsString(flag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
|
||||
// - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the
|
||||
// loader_filter
|
||||
// - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
|
||||
// Such is the pain of having arbitrary files for a given version .-.
|
||||
|
||||
auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, m_hash_type, loader_filter);
|
||||
if (project_ver.downloadUrl.isEmpty()) {
|
||||
qCritical() << "Modrinth mod without download url!" << project_ver.fileName;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
auto mod_iter = m_mappings.find(hash);
|
||||
if (mod_iter == m_mappings.end()) {
|
||||
qCritical() << "Failed to remap mod from Modrinth!";
|
||||
continue;
|
||||
}
|
||||
auto mod = *mod_iter;
|
||||
m_mappings.remove(hash);
|
||||
|
||||
auto key = project_ver.hash;
|
||||
|
||||
// Fake pack with the necessary info to pass to the download task :)
|
||||
auto pack = std::make_shared<ModPlatform::IndexedPack>();
|
||||
pack->name = mod->name();
|
||||
pack->slug = mod->metadata()->slug;
|
||||
pack->addonId = mod->metadata()->project_id;
|
||||
pack->websiteUrl = mod->homeurl();
|
||||
for (auto& author : mod->authors())
|
||||
pack->authors.append({ author });
|
||||
pack->description = mod->description();
|
||||
pack->provider = ModPlatform::ResourceProvider::MODRINTH;
|
||||
if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) {
|
||||
if (mod->version() == project_ver.version_number)
|
||||
continue;
|
||||
|
||||
auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_mods_folder);
|
||||
|
||||
m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.version_type,
|
||||
project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task, mod->enabled());
|
||||
}
|
||||
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, project_ver));
|
||||
}
|
||||
} catch (Json::JsonException& e) {
|
||||
emitFailed(e.cause() + " : " + e.what());
|
||||
return;
|
||||
}
|
||||
checkNextLoader();
|
||||
}
|
||||
|
||||
void ModrinthCheckUpdate::getUpdateModsForLoader(ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck)
|
||||
{
|
||||
auto response = std::make_shared<QByteArray>();
|
||||
QStringList hashes;
|
||||
if (forceModLoaderCheck) {
|
||||
for (auto hash : m_mappings.keys()) {
|
||||
if (m_mappings[hash]->loaders() & loader) {
|
||||
hashes.append(hash);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hashes = m_mappings.keys();
|
||||
}
|
||||
auto job = api.latestVersions(hashes, m_hash_type, m_game_versions, loader, response);
|
||||
|
||||
connect(job.get(), &Task::succeeded, this,
|
||||
[this, response, loader, forceModLoaderCheck] { checkVersionsResponse(response, loader, forceModLoaderCheck); });
|
||||
|
||||
connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader);
|
||||
|
||||
setStatus(tr("Waiting for the API response from Modrinth..."));
|
||||
setProgress(1, 3);
|
||||
setProgress(m_next_loader_idx * 2 - 1, 9);
|
||||
|
||||
m_net_job = qSharedPointerObjectCast<NetJob, Task>(job);
|
||||
m_job = job;
|
||||
job->start();
|
||||
|
||||
lock.exec();
|
||||
|
||||
emitSucceeded();
|
||||
}
|
||||
|
||||
void ModrinthCheckUpdate::checkNextLoader()
|
||||
{
|
||||
if (m_mappings.isEmpty()) {
|
||||
emitSucceeded();
|
||||
return;
|
||||
}
|
||||
if (m_next_loader_idx < m_loaders_list.size()) {
|
||||
getUpdateModsForLoader(m_loaders_list.at(m_next_loader_idx));
|
||||
m_next_loader_idx++;
|
||||
return;
|
||||
}
|
||||
static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge, ModPlatform::ModLoaderType::Quilt,
|
||||
ModPlatform::ModLoaderType::Fabric };
|
||||
for (auto flag : flags) {
|
||||
if (!m_loaders_list.contains(flag)) {
|
||||
m_loaders_list.append(flag);
|
||||
m_next_loader_idx++;
|
||||
setProgress(m_next_loader_idx * 2 - 1, 9);
|
||||
for (auto m : m_mappings) {
|
||||
if (m->loaders() & flag) {
|
||||
getUpdateModsForLoader(flag, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setProgress(m_next_loader_idx * 2, 9);
|
||||
}
|
||||
}
|
||||
for (auto m : m_mappings) {
|
||||
emit checkFailed(m,
|
||||
tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader."));
|
||||
}
|
||||
emitSucceeded();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "Application.h"
|
||||
#include "modplatform/CheckUpdateTask.h"
|
||||
#include "net/NetJob.h"
|
||||
|
||||
class ModrinthCheckUpdate : public CheckUpdateTask {
|
||||
Q_OBJECT
|
||||
@@ -10,17 +8,21 @@ class ModrinthCheckUpdate : public CheckUpdateTask {
|
||||
public:
|
||||
ModrinthCheckUpdate(QList<Mod*>& mods,
|
||||
std::list<Version>& mcVersions,
|
||||
std::optional<ModPlatform::ModLoaderTypes> loaders,
|
||||
std::shared_ptr<ModFolderModel> mods_folder)
|
||||
: CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
|
||||
{}
|
||||
QList<ModPlatform::ModLoaderType> loadersList,
|
||||
std::shared_ptr<ModFolderModel> mods_folder);
|
||||
|
||||
public slots:
|
||||
bool abort() override;
|
||||
|
||||
protected slots:
|
||||
void executeTask() override;
|
||||
void getUpdateModsForLoader(ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck = false);
|
||||
void checkVersionsResponse(std::shared_ptr<QByteArray> response, ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck = false);
|
||||
void checkNextLoader();
|
||||
|
||||
private:
|
||||
NetJob::Ptr m_net_job = nullptr;
|
||||
Task::Ptr m_job = nullptr;
|
||||
QHash<QString, Mod*> m_mappings;
|
||||
QString m_hash_type;
|
||||
int m_next_loader_idx = 0;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "ui/pages/modplatform/OptionalModDialog.h"
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QFileInfo>
|
||||
#include <vector>
|
||||
|
||||
bool ModrinthCreationTask::abort()
|
||||
@@ -58,6 +59,7 @@ bool ModrinthCreationTask::updateInstance()
|
||||
return false;
|
||||
|
||||
auto version_name = inst->getManagedPackVersionName();
|
||||
m_root_path = QFileInfo(inst->gameRoot()).fileName();
|
||||
auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : "";
|
||||
|
||||
if (shouldConfirmUpdate()) {
|
||||
@@ -171,9 +173,9 @@ bool ModrinthCreationTask::createInstance()
|
||||
// Keep index file in case we need it some other time (like when changing versions)
|
||||
QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json"));
|
||||
FS::ensureFilePathExists(new_index_place);
|
||||
QFile::rename(index_path, new_index_place);
|
||||
FS::move(index_path, new_index_place);
|
||||
|
||||
auto mcPath = FS::PathCombine(m_stagingPath, ".minecraft");
|
||||
auto mcPath = FS::PathCombine(m_stagingPath, m_root_path);
|
||||
|
||||
auto override_path = FS::PathCombine(m_stagingPath, "overrides");
|
||||
if (QFile::exists(override_path)) {
|
||||
@@ -181,7 +183,7 @@ bool ModrinthCreationTask::createInstance()
|
||||
Override::createOverrides("overrides", parent_folder, override_path);
|
||||
|
||||
// Apply the overrides
|
||||
if (!QFile::rename(override_path, mcPath)) {
|
||||
if (!FS::move(override_path, mcPath)) {
|
||||
setError(tr("Could not rename the overrides folder:\n") + "overrides");
|
||||
return false;
|
||||
}
|
||||
@@ -226,20 +228,25 @@ bool ModrinthCreationTask::createInstance()
|
||||
// Don't add managed info to packs without an ID (most likely imported from ZIP)
|
||||
if (!m_managed_id.isEmpty())
|
||||
instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version());
|
||||
else
|
||||
instance.setManagedPack("modrinth", "", name(), "", "");
|
||||
|
||||
instance.setName(name());
|
||||
instance.saveNow();
|
||||
|
||||
m_files_job.reset(new NetJob(tr("Mod Download Modrinth"), APPLICATION->network()));
|
||||
|
||||
auto root_modpack_path = FS::PathCombine(m_stagingPath, ".minecraft");
|
||||
auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path);
|
||||
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
|
||||
|
||||
for (auto file : m_files) {
|
||||
auto file_path = FS::PathCombine(root_modpack_path, file.path);
|
||||
auto fileName = file.path;
|
||||
fileName = FS::RemoveInvalidPathChars(fileName);
|
||||
auto file_path = FS::PathCombine(root_modpack_path, fileName);
|
||||
if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) {
|
||||
// This means we somehow got out of the root folder, so abort here to prevent exploits
|
||||
setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk and isn't allowed.")
|
||||
.arg(file.path));
|
||||
.arg(fileName));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -252,7 +259,7 @@ bool ModrinthCreationTask::createInstance()
|
||||
// FIXME: This really needs to be put into a ConcurrentTask of
|
||||
// MultipleOptionsTask's , once those exist :)
|
||||
auto param = dl.toWeakRef();
|
||||
connect(dl.get(), &NetAction::failed, [this, &file, file_path, param] {
|
||||
connect(dl.get(), &Task::failed, [this, &file, file_path, param] {
|
||||
auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
|
||||
ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
|
||||
m_files_job->addNetAction(ndl);
|
||||
@@ -289,7 +296,7 @@ bool ModrinthCreationTask::createInstance()
|
||||
// Only change the name if it didn't use a custom name, so that the previous custom name
|
||||
// is preserved, but if we're using the original one, we update the version string.
|
||||
// NOTE: This needs to come before the copyManagedPack call!
|
||||
if (inst->name().contains(inst->getManagedPackVersionName())) {
|
||||
if (inst->name().contains(inst->getManagedPackVersionName()) && inst->name() != instance.name()) {
|
||||
if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange)
|
||||
inst->setName(instance.name());
|
||||
}
|
||||
|
||||
@@ -46,4 +46,6 @@ class ModrinthCreationTask final : public InstanceCreationTask {
|
||||
NetJob::Ptr m_files_job;
|
||||
|
||||
std::optional<InstancePtr> m_instance;
|
||||
|
||||
QString m_root_path = "minecraft";
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include "ModrinthPackExportTask.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
@@ -27,6 +28,8 @@
|
||||
#include "minecraft/PackProfile.h"
|
||||
#include "minecraft/mod/MetadataHandler.h"
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
#include "modplatform/helpers/HashUtils.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" });
|
||||
const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" });
|
||||
@@ -102,8 +105,6 @@ void ModrinthPackExportTask::collectHashes()
|
||||
}))
|
||||
continue;
|
||||
|
||||
QCryptographicHash sha512(QCryptographicHash::Algorithm::Sha512);
|
||||
|
||||
QFile openFile(file.absoluteFilePath());
|
||||
if (!openFile.open(QFile::ReadOnly)) {
|
||||
qWarning() << "Could not open" << file << "for hashing";
|
||||
@@ -115,7 +116,7 @@ void ModrinthPackExportTask::collectHashes()
|
||||
qWarning() << "Could not read" << file;
|
||||
continue;
|
||||
}
|
||||
sha512.addData(data);
|
||||
auto sha512 = Hashing::hash(data, Hashing::Algorithm::Sha512);
|
||||
|
||||
auto allMods = mcInstance->loaderModList()->allMods();
|
||||
if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
|
||||
@@ -127,11 +128,9 @@ void ModrinthPackExportTask::collectHashes()
|
||||
if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) {
|
||||
qDebug() << "Resolving" << relative << "from index";
|
||||
|
||||
QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1);
|
||||
sha1.addData(data);
|
||||
auto sha1 = Hashing::hash(data, Hashing::Algorithm::Sha1);
|
||||
|
||||
ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size(),
|
||||
mod->metadata()->side };
|
||||
ResolvedFile resolvedFile{ sha1, sha512, url.toEncoded(), openFile.size(), mod->metadata()->side };
|
||||
resolvedFiles[relative] = resolvedFile;
|
||||
|
||||
// nice! we've managed to resolve based on local metadata!
|
||||
@@ -142,7 +141,7 @@ void ModrinthPackExportTask::collectHashes()
|
||||
}
|
||||
|
||||
qDebug() << "Enqueueing" << relative << "for Modrinth query";
|
||||
pendingHashes[relative] = sha512.result().toHex();
|
||||
pendingHashes[relative] = sha512;
|
||||
}
|
||||
|
||||
setAbortable(true);
|
||||
@@ -157,8 +156,8 @@ void ModrinthPackExportTask::makeApiRequest()
|
||||
setStatus(tr("Finding versions for hashes..."));
|
||||
auto response = std::make_shared<QByteArray>();
|
||||
task = api.currentVersions(pendingHashes.values(), "sha512", response);
|
||||
connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); });
|
||||
connect(task.get(), &NetJob::failed, this, &ModrinthPackExportTask::emitFailed);
|
||||
connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); });
|
||||
connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed);
|
||||
task->start();
|
||||
}
|
||||
}
|
||||
@@ -200,7 +199,7 @@ void ModrinthPackExportTask::buildZip()
|
||||
{
|
||||
setStatus(tr("Adding files..."));
|
||||
|
||||
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true);
|
||||
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true, true);
|
||||
zipTask->addExtraFile("modrinth.index.json", generateIndex());
|
||||
|
||||
zipTask->setExcludeFiles(resolvedFiles.keys());
|
||||
@@ -287,16 +286,12 @@ QByteArray ModrinthPackExportTask::generateIndex()
|
||||
env["client"] = "required";
|
||||
env["server"] = "required";
|
||||
}
|
||||
switch (iterator->side) {
|
||||
case Metadata::ModSide::ClientSide:
|
||||
env["server"] = "unsupported";
|
||||
break;
|
||||
case Metadata::ModSide::ServerSide:
|
||||
env["client"] = "unsupported";
|
||||
break;
|
||||
case Metadata::ModSide::UniversalSide:
|
||||
break;
|
||||
}
|
||||
|
||||
// a server side mod does not imply that the mod does not work on the client
|
||||
// however, if a mrpack mod is marked as server-only it will not install on the client
|
||||
if (iterator->side == Metadata::ModSide::ClientSide)
|
||||
env["server"] = "unsupported";
|
||||
|
||||
fileOut["env"] = env;
|
||||
|
||||
fileOut["path"] = path;
|
||||
|
||||
@@ -68,7 +68,7 @@ class ModrinthPackExportTask : public Task {
|
||||
void collectFiles();
|
||||
void collectHashes();
|
||||
void makeApiRequest();
|
||||
void parseApiResponse(const std::shared_ptr<QByteArray> response);
|
||||
void parseApiResponse(std::shared_ptr<QByteArray> response);
|
||||
void buildZip();
|
||||
|
||||
QByteArray generateIndex();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@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
|
||||
@@ -17,6 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "ModrinthPackIndex.h"
|
||||
#include "FileSystem.h"
|
||||
#include "ModrinthAPI.h"
|
||||
|
||||
#include "Json.h"
|
||||
@@ -25,7 +27,6 @@
|
||||
#include "modplatform/ModIndex.h"
|
||||
|
||||
static ModrinthAPI api;
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
|
||||
bool shouldDownloadOnSide(QString side)
|
||||
{
|
||||
@@ -104,6 +105,8 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
|
||||
pack.extraData.donate.append(donate);
|
||||
}
|
||||
|
||||
pack.extraData.status = Json::ensureString(obj, "status");
|
||||
|
||||
pack.extraData.body = Json::ensureString(obj, "body").remove("<br>");
|
||||
|
||||
pack.extraDataLoaded = true;
|
||||
@@ -112,16 +115,11 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
|
||||
void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst)
|
||||
{
|
||||
QVector<ModPlatform::IndexedVersion> unsortedVersions;
|
||||
auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
|
||||
QString mcVersion = profile->getComponentVersion("net.minecraft");
|
||||
auto loaders = profile->getSupportedModLoaders();
|
||||
|
||||
for (auto versionIter : arr) {
|
||||
auto obj = versionIter.toObject();
|
||||
auto file = loadIndexedPackVersion(obj);
|
||||
|
||||
if (file.fileId.isValid() &&
|
||||
(!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid
|
||||
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 {
|
||||
@@ -133,8 +131,9 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArra
|
||||
pack.versionsLoaded = true;
|
||||
}
|
||||
|
||||
auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name)
|
||||
-> ModPlatform::IndexedVersion
|
||||
auto Modrinth::loadIndexedPackVersion(QJsonObject& obj,
|
||||
QString preferred_hash_type,
|
||||
QString preferred_file_name) -> ModPlatform::IndexedVersion
|
||||
{
|
||||
ModPlatform::IndexedVersion file;
|
||||
|
||||
@@ -152,15 +151,15 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
|
||||
for (auto loader : loaders) {
|
||||
if (loader == "neoforge")
|
||||
file.loaders |= ModPlatform::NeoForge;
|
||||
if (loader == "forge")
|
||||
else if (loader == "forge")
|
||||
file.loaders |= ModPlatform::Forge;
|
||||
if (loader == "cauldron")
|
||||
else if (loader == "cauldron")
|
||||
file.loaders |= ModPlatform::Cauldron;
|
||||
if (loader == "liteloader")
|
||||
else if (loader == "liteloader")
|
||||
file.loaders |= ModPlatform::LiteLoader;
|
||||
if (loader == "fabric")
|
||||
else if (loader == "fabric")
|
||||
file.loaders |= ModPlatform::Fabric;
|
||||
if (loader == "quilt")
|
||||
else if (loader == "quilt")
|
||||
file.loaders |= ModPlatform::Quilt;
|
||||
}
|
||||
file.version = Json::requireString(obj, "name");
|
||||
@@ -224,6 +223,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
|
||||
if (parent.contains("url")) {
|
||||
file.downloadUrl = Json::requireString(parent, "url");
|
||||
file.fileName = Json::requireString(parent, "filename");
|
||||
file.fileName = FS::RemoveInvalidPathChars(file.fileName);
|
||||
file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
|
||||
auto hash_list = Json::requireObject(parent, "hashes");
|
||||
|
||||
@@ -231,7 +231,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
|
||||
file.hash = Json::requireString(hash_list, preferred_hash_type);
|
||||
file.hash_type = preferred_hash_type;
|
||||
} else {
|
||||
auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH);
|
||||
auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH);
|
||||
for (auto& hash_type : hash_types) {
|
||||
if (hash_list.contains(hash_type)) {
|
||||
file.hash = Json::requireString(hash_list, hash_type);
|
||||
@@ -247,8 +247,9 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
|
||||
return {};
|
||||
}
|
||||
|
||||
auto Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst)
|
||||
-> ModPlatform::IndexedVersion
|
||||
auto Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m,
|
||||
QJsonArray& arr,
|
||||
const BaseInstance* inst) -> ModPlatform::IndexedVersion
|
||||
{
|
||||
auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
|
||||
QString mcVersion = profile->getComponentVersion("net.minecraft");
|
||||
|
||||
@@ -95,6 +95,8 @@ void loadIndexedInfo(Modpack& pack, QJsonObject& obj)
|
||||
pack.extra.donate.append(donate);
|
||||
}
|
||||
|
||||
pack.extra.status = Json::ensureString(obj, "status");
|
||||
|
||||
pack.extraInfoLoaded = true;
|
||||
}
|
||||
|
||||
@@ -129,6 +131,10 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion
|
||||
|
||||
file.name = Json::requireString(obj, "name");
|
||||
file.version = Json::requireString(obj, "version_number");
|
||||
auto gameVersions = Json::ensureArray(obj, "game_versions");
|
||||
if (!gameVersions.isEmpty()) {
|
||||
file.gameVersion = Json::ensureString(gameVersions[0]);
|
||||
}
|
||||
file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type"));
|
||||
file.changelog = Json::ensureString(obj, "changelog");
|
||||
|
||||
|
||||
@@ -77,11 +77,14 @@ struct ModpackExtra {
|
||||
QString discordUrl;
|
||||
|
||||
QList<DonationData> donate;
|
||||
|
||||
QString status;
|
||||
};
|
||||
|
||||
struct ModpackVersion {
|
||||
QString name;
|
||||
QString version;
|
||||
QString gameVersion;
|
||||
ModPlatform::IndexedVersionType version_type;
|
||||
QString changelog;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user