Merge remote-tracking branch 'upstream/develop' into resource-meta

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad
2024-10-08 17:15:42 +01:00
534 changed files with 14803 additions and 8430 deletions

View File

@@ -15,9 +15,13 @@ class CheckUpdateTask : public Task {
public:
CheckUpdateTask(QList<Resource*>& resources,
std::list<Version>& mcVersions,
std::optional<ModPlatform::ModLoaderTypes> loaders,
std::shared_ptr<ResourceFolderModel> resource_model)
: Task(nullptr), m_resources(resources), m_game_versions(mcVersions), m_loaders(loaders), m_resource_model(resource_model){};
QList<ModPlatform::ModLoaderType> loadersList,
std::shared_ptr<ResourceFolderModel> resourceModel)
: Task(nullptr)
, m_resources(resources)
, m_game_versions(mcVersions)
, m_loaders_list(std::move(loadersList))
, m_resource_model(resourceModel){};
struct Update {
QString name;
@@ -67,7 +71,7 @@ class CheckUpdateTask : public Task {
protected:
QList<Resource*>& m_resources;
std::list<Version>& m_game_versions;
std::optional<ModPlatform::ModLoaderTypes> m_loaders;
QList<ModPlatform::ModLoaderType> m_loaders_list;
std::shared_ptr<ResourceFolderModel> m_resource_model;
std::vector<Update> m_updates;

View File

@@ -43,6 +43,10 @@ EnsureMetadataTask::EnsureMetadataTask(QList<Resource*>& resources, QDir dir, Mo
}
}
EnsureMetadataTask::EnsureMetadataTask(QHash<QString, Resource*>& resources, QDir dir, ModPlatform::ResourceProvider prov)
: Task(nullptr), m_resources(resources), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
{}
Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Resource* resource)
{
if (!resource || !resource->valid() || resource->type() == ResourceType::FOLDER)

View File

@@ -17,6 +17,7 @@ class EnsureMetadataTask : public Task {
public:
EnsureMetadataTask(Resource*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
EnsureMetadataTask(QList<Resource*>&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
EnsureMetadataTask(QHash<QString, Resource*>&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
~EnsureMetadataTask() = default;

View File

@@ -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
@@ -58,7 +59,7 @@ IndexedVersionType::VersionType IndexedVersionType::enumFromString(const QString
return s_indexed_version_type_names.value(type, IndexedVersionType::VersionType::Unknown);
}
auto ProviderCapabilities::name(ResourceProvider p) -> const char*
const char* ProviderCapabilities::name(ResourceProvider p)
{
switch (p) {
case ResourceProvider::MODRINTH:
@@ -68,7 +69,8 @@ auto ProviderCapabilities::name(ResourceProvider p) -> const char*
}
return {};
}
auto ProviderCapabilities::readableName(ResourceProvider p) -> QString
QString ProviderCapabilities::readableName(ResourceProvider p)
{
switch (p) {
case ResourceProvider::MODRINTH:
@@ -78,7 +80,8 @@ auto ProviderCapabilities::readableName(ResourceProvider p) -> QString
}
return {};
}
auto ProviderCapabilities::hashType(ResourceProvider p) -> QStringList
QStringList ProviderCapabilities::hashType(ResourceProvider p)
{
switch (p) {
case ResourceProvider::MODRINTH:
@@ -90,34 +93,13 @@ auto ProviderCapabilities::hashType(ResourceProvider p) -> QStringList
return {};
}
auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString type) -> QString
{
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1;
switch (p) {
case ResourceProvider::MODRINTH: {
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512;
break;
}
case ResourceProvider::FLAME:
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5;
break;
}
QCryptographicHash hash(algo);
if (!hash.addData(device))
qCritical() << "Failed to read JAR to create hash!";
Q_ASSERT(hash.result().length() == hash.hashLength(algo));
return { hash.result().toHex() };
}
QString getMetaURL(ResourceProvider provider, QVariant projectID)
{
return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") +
projectID.toString();
}
auto getModLoaderString(ModLoaderType type) -> const QString
auto getModLoaderAsString(ModLoaderType type) -> const QString
{
switch (type) {
case NeoForge:
@@ -138,4 +120,21 @@ auto getModLoaderString(ModLoaderType type) -> const QString
return "";
}
auto getModLoaderFromString(QString type) -> ModLoaderType
{
if (type == "neoforge")
return NeoForge;
if (type == "forge")
return Forge;
if (type == "cauldron")
return Cauldron;
if (type == "liteloader")
return LiteLoader;
if (type == "fabric")
return Fabric;
if (type == "quilt")
return Quilt;
return {};
}
} // namespace ModPlatform

View File

@@ -41,11 +41,10 @@ enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK };
enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN };
namespace ProviderCapabilities {
auto name(ResourceProvider) -> const char*;
auto readableName(ResourceProvider) -> QString;
auto hashType(ResourceProvider) -> QStringList;
auto hash(ResourceProvider, QIODevice*, QString type = "") -> QString;
};
const char* name(ResourceProvider);
QString readableName(ResourceProvider);
QStringList hashType(ResourceProvider);
} // namespace ProviderCapabilities
struct ModpackAuthor {
QString name;
@@ -108,6 +107,7 @@ struct IndexedVersion {
bool is_preferred = true;
QString changelog;
QList<Dependency> dependencies;
QString side; // this is for flame API
// For internal use, not provided by APIs
bool is_currently_selected = false;
@@ -182,7 +182,8 @@ inline auto getOverrideDeps() -> QList<OverrideDep>
QString getMetaURL(ResourceProvider provider, QVariant projectID);
auto getModLoaderString(ModLoaderType type) -> const QString;
auto getModLoaderAsString(ModLoaderType type) -> const QString;
auto getModLoaderFromString(QString type) -> ModLoaderType;
constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept
{
@@ -190,6 +191,11 @@ constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept
return x && !(x & (x - 1));
}
struct Category {
QString name;
QString id;
};
} // namespace ModPlatform
Q_DECLARE_METATYPE(ModPlatform::IndexedPack)

View File

@@ -4,6 +4,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* 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
@@ -73,6 +74,8 @@ class ResourceAPI {
std::optional<SortingMethod> sorting;
std::optional<ModPlatform::ModLoaderTypes> loaders;
std::optional<std::list<Version> > versions;
std::optional<QString> side;
std::optional<QStringList> categoryIds;
};
struct SearchCallbacks {
std::function<void(QJsonDocument&)> on_succeed;

View File

@@ -282,7 +282,7 @@ void PackInstallTask::deleteExistingFiles()
// Delete the files
for (const auto& item : filesToDelete) {
QFile::remove(item);
FS::deletePath(item);
}
}
@@ -343,9 +343,7 @@ QString PackInstallTask::getVersionForLoader(QString uid)
return Q_NULLPTR;
}
if (!vlist->isLoaded()) {
vlist->load(Net::Mode::Online);
}
vlist->waitToLoad();
if (m_version.loader.recommended || m_version.loader.latest) {
for (int i = 0; i < vlist->versions().size(); i++) {
@@ -638,8 +636,7 @@ void PackInstallTask::installConfigs()
auto dl = Net::ApiDownload::makeCached(url, entry);
if (!m_version.configs.sha1.isEmpty()) {
auto rawSha1 = QByteArray::fromHex(m_version.configs.sha1.toLatin1());
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, m_version.configs.sha1));
}
jobPtr->addNetAction(dl);
archivePath = entry->getFullPath();
@@ -758,8 +755,7 @@ void PackInstallTask::downloadMods()
auto dl = Net::ApiDownload::makeCached(url, entry);
if (!mod.md5.isEmpty()) {
auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5));
}
jobPtr->addNetAction(dl);
} else if (mod.type == ModType::Decomp) {
@@ -769,8 +765,7 @@ void PackInstallTask::downloadMods()
auto dl = Net::ApiDownload::makeCached(url, entry);
if (!mod.md5.isEmpty()) {
auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5));
}
jobPtr->addNetAction(dl);
} else {
@@ -783,8 +778,7 @@ void PackInstallTask::downloadMods()
auto dl = Net::ApiDownload::makeCached(url, entry);
if (!mod.md5.isEmpty()) {
auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5));
}
jobPtr->addNetAction(dl);
@@ -987,7 +981,7 @@ bool PackInstallTask::extractMods(const QMap<QString, VersionMod>& toExtract,
// the copy from the Configs.zip
QFileInfo fileInfo(to);
if (fileInfo.exists()) {
if (!QFile::remove(to)) {
if (!FS::deletePath(to)) {
qWarning() << "Failed to delete" << to;
return false;
}
@@ -1031,6 +1025,12 @@ void PackInstallTask::install()
return;
components->setComponentVersion("net.minecraftforge", version);
} else if (m_version.loader.type == QString("neoforge")) {
auto version = getVersionForLoader("net.neoforged");
if (version == Q_NULLPTR)
return;
components->setComponentVersion("net.neoforged", version);
} else if (m_version.loader.type == QString("fabric")) {
auto version = getVersionForLoader("net.fabricmc.fabric-loader");
if (version == Q_NULLPTR)
@@ -1069,36 +1069,7 @@ void PackInstallTask::install()
static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version)
{
auto vlist = APPLICATION->metadataIndex()->get(uid);
if (!vlist)
return {};
if (!vlist->isLoaded()) {
QEventLoop loadVersionLoop;
auto task = vlist->getLoadTask();
QObject::connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit);
if (!task->isRunning())
task->start();
loadVersionLoop.exec();
}
auto ver = vlist->getVersion(version);
if (!ver)
return {};
if (!ver->isLoaded()) {
QEventLoop loadVersionLoop;
ver->load(Net::Mode::Online);
auto task = ver->getCurrentTask();
QObject::connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit);
if (!task->isRunning())
task->start();
loadVersionLoop.exec();
}
return ver;
return APPLICATION->metadataIndex()->getLoadedVersion(uid, version);
}
} // namespace ATLauncher

View File

@@ -1,86 +1,101 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 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
* 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 "FileResolvingTask.h"
#include <algorithm>
#include "Json.h"
#include "QObjectPtr.h"
#include "modplatform/ModIndex.h"
#include "net/ApiDownload.h"
#include "net/ApiUpload.h"
#include "net/Upload.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
static const FlameAPI flameAPI;
static ModrinthAPI modrinthAPI;
Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess)
: m_network(network), m_toProcess(toProcess)
: m_network(network), m_manifest(toProcess)
{}
bool Flame::FileResolvingTask::abort()
{
bool aborted = true;
if (m_dljob)
aborted &= m_dljob->abort();
if (m_checkJob)
aborted &= m_checkJob->abort();
if (m_task) {
aborted = m_task->abort();
}
return aborted ? Task::abort() : false;
}
void Flame::FileResolvingTask::executeTask()
{
if (m_toProcess.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately
if (m_manifest.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately
emitSucceeded();
return;
}
setStatus(tr("Resolving mod IDs..."));
setProgress(0, 3);
m_dljob.reset(new NetJob("Mod id resolver", m_network));
result.reset(new QByteArray());
// build json data to send
QJsonObject object;
m_result.reset(new QByteArray());
object["fileIds"] = QJsonArray::fromVariantList(
std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
l.push_back(s.fileId);
return l;
}));
QByteArray data = Json::toText(object);
auto dl = Net::ApiUpload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data);
m_dljob->addNetAction(dl);
QStringList fileIds;
for (auto file : m_manifest.files) {
fileIds.push_back(QString::number(file.fileId));
}
m_task = flameAPI.getFiles(fileIds, m_result);
auto step_progress = std::make_shared<TaskStepProgress>();
connect(m_dljob.get(), &NetJob::finished, this, [this, step_progress]() {
connect(m_task.get(), &Task::finished, this, [this, step_progress]() {
step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress);
netJobFinished();
});
connect(m_dljob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) {
step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress);
emitFailed(reason);
});
connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_dljob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
stepProgress(*step_progress);
});
connect(m_dljob.get(), &NetJob::status, this, [this, step_progress](QString status) {
connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_dljob->start();
m_task->start();
}
void Flame::FileResolvingTask::netJobFinished()
{
setProgress(1, 3);
// job to check modrinth for blocked projects
m_checkJob.reset(new NetJob("Modrinth check", m_network));
blockedProjects = QMap<File*, std::shared_ptr<QByteArray>>();
QJsonDocument doc;
QJsonArray array;
try {
doc = Json::requireDocument(*result);
doc = Json::requireDocument(*m_result);
array = Json::requireArray(doc.object()["data"]);
} catch (Json::JsonException& e) {
qCritical() << "Non-JSON data returned from the CF API";
@@ -91,125 +106,157 @@ void Flame::FileResolvingTask::netJobFinished()
return;
}
QStringList hashes;
for (QJsonValueRef file : array) {
auto fileid = Json::requireInteger(Json::requireObject(file)["id"]);
auto& out = m_toProcess.files[fileid];
try {
out.parseFromObject(Json::requireObject(file));
} catch ([[maybe_unused]] const JSONValidationError& e) {
qDebug() << "Blocked mod on curseforge" << out.fileName;
auto hash = out.hash;
if (!hash.isEmpty()) {
auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash);
auto output = std::make_shared<QByteArray>();
auto dl = Net::ApiDownload::makeByteArray(QUrl(url), output);
QObject::connect(dl.get(), &Net::ApiDownload::succeeded, [&out]() { out.resolved = true; });
m_checkJob->addNetAction(dl);
blockedProjects.insert(&out, output);
auto obj = Json::requireObject(file);
auto version = FlameMod::loadIndexedPackVersion(obj);
auto fileid = version.fileId.toInt();
m_manifest.files[fileid].version = version;
auto url = QUrl(version.downloadUrl, QUrl::TolerantMode);
if (!url.isValid() && "sha1" == version.hash_type && !version.hash.isEmpty()) {
hashes.push_back(version.hash);
}
} catch (Json::JsonException& e) {
qCritical() << "Non-JSON data returned from the CF API";
qCritical() << e.cause();
emitFailed(tr("Invalid data returned from the API."));
return;
}
}
if (hashes.isEmpty()) {
getFlameProjects();
return;
}
m_result.reset(new QByteArray());
m_task = modrinthAPI.currentVersions(hashes, "sha1", m_result);
(dynamic_cast<NetJob*>(m_task.get()))->setAskRetry(false);
auto step_progress = std::make_shared<TaskStepProgress>();
connect(m_checkJob.get(), &NetJob::finished, this, [this, step_progress]() {
connect(m_task.get(), &Task::finished, this, [this, step_progress]() {
step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress);
modrinthCheckFinished();
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*m_result, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *m_result;
failed(parse_error.errorString());
return;
}
try {
auto entries = Json::requireObject(doc);
for (auto& out : m_manifest.files) {
auto url = QUrl(out.version.downloadUrl, QUrl::TolerantMode);
if (!url.isValid() && "sha1" == out.version.hash_type && !out.version.hash.isEmpty()) {
try {
auto entry = Json::requireObject(entries, out.version.hash);
auto file = Modrinth::loadIndexedPackVersion(entry);
// If there's more than one mod loader for this version, we can't know for sure
// which file is relative to each loader, so it's best to not use any one and
// let the user download it manually.
if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) {
out.version.downloadUrl = file.downloadUrl;
qDebug() << "Found alternative on modrinth " << out.version.fileName;
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << entries;
}
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
getFlameProjects();
});
connect(m_checkJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) {
step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress);
});
connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_checkJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
stepProgress(*step_progress);
});
connect(m_checkJob.get(), &NetJob::status, this, [this, step_progress](QString status) {
connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_checkJob->start();
m_task->start();
}
void Flame::FileResolvingTask::modrinthCheckFinished()
void Flame::FileResolvingTask::getFlameProjects()
{
setProgress(2, 3);
qDebug() << "Finished with blocked mods : " << blockedProjects.size();
for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) {
auto& out = *it;
auto bytes = blockedProjects[out];
if (!out->resolved) {
continue;
}
QJsonDocument doc = QJsonDocument::fromJson(*bytes);
auto obj = doc.object();
auto file = Modrinth::loadIndexedPackVersion(obj);
// If there's more than one mod loader for this version, we can't know for sure
// which file is relative to each loader, so it's best to not use any one and
// let the user download it manually.
if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) {
out->url = file.downloadUrl;
qDebug() << "Found alternative on modrinth " << out->fileName;
} else {
out->resolved = false;
}
m_result.reset(new QByteArray());
QStringList addonIds;
for (auto file : m_manifest.files) {
addonIds.push_back(QString::number(file.projectId));
}
// copy to an output list and filter out projects found on modrinth
auto block = std::make_shared<QList<File*>>();
auto it = blockedProjects.keys();
std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File* f) { return !f->resolved; });
// Display not found mods early
if (!block->empty()) {
// blocked mods found, we need the slug for displaying.... we need another job :D !
m_slugJob.reset(new NetJob("Slug Job", m_network));
int index = 0;
for (auto mod : *block) {
auto projectId = mod->projectId;
auto output = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId);
auto dl = Net::ApiDownload::makeByteArray(url, output);
qDebug() << "Fetching url slug for file:" << mod->fileName;
QObject::connect(dl.get(), &Net::ApiDownload::succeeded, [block, index, output]() {
auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done
auto json = QJsonDocument::fromJson(*output);
auto base =
Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json), "data"), "links"), "websiteUrl");
auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId));
mod->websiteUrl = link;
});
m_slugJob->addNetAction(dl);
index++;
}
auto step_progress = std::make_shared<TaskStepProgress>();
connect(m_slugJob.get(), &NetJob::succeeded, this, [this, step_progress]() {
step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress);
emitSucceeded();
});
connect(m_slugJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress);
emitFailed(reason);
});
connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_slugJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
stepProgress(*step_progress);
});
connect(m_slugJob.get(), &NetJob::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_slugJob->start();
} else {
m_task = flameAPI.getProjects(addonIds, m_result);
auto step_progress = std::make_shared<TaskStepProgress>();
connect(m_task.get(), &Task::succeeded, this, [this, step_progress] {
QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*m_result, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *m_result;
return;
}
try {
QJsonArray entries;
entries = Json::requireArray(Json::requireObject(doc), "data");
for (auto entry : entries) {
auto entry_obj = Json::requireObject(entry);
auto id = Json::requireInteger(entry_obj, "id");
auto file = std::find_if(m_manifest.files.begin(), m_manifest.files.end(),
[id](const Flame::File& file) { return file.projectId == id; });
if (file == m_manifest.files.end()) {
continue;
}
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName));
FlameMod::loadIndexedPack(file->pack, entry_obj);
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress);
emitSucceeded();
}
});
connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) {
step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress);
emitFailed(reason);
});
connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total);
stepProgress(*step_progress);
});
connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_task->start();
}

View File

@@ -1,7 +1,25 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 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
* 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 <QNetworkAccessManager>
#include "PackManifest.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
namespace Flame {
@@ -9,12 +27,12 @@ class FileResolvingTask : public Task {
Q_OBJECT
public:
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess);
virtual ~FileResolvingTask(){};
virtual ~FileResolvingTask() = default;
bool canAbort() const override { return true; }
bool abort() override;
const Flame::Manifest& getResults() const { return m_toProcess; }
const Flame::Manifest& getResults() const { return m_manifest; }
protected:
virtual void executeTask() override;
@@ -22,16 +40,13 @@ class FileResolvingTask : public Task {
protected slots:
void netJobFinished();
private:
void getFlameProjects();
private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
Flame::Manifest m_toProcess;
std::shared_ptr<QByteArray> result;
NetJob::Ptr m_dljob;
NetJob::Ptr m_checkJob;
NetJob::Ptr m_slugJob;
void modrinthCheckFinished();
QMap<File*, std::shared_ptr<QByteArray>> blockedProjects;
Flame::Manifest m_manifest;
std::shared_ptr<QByteArray> m_result;
Task::Ptr m_task;
};
} // namespace Flame

View File

@@ -3,14 +3,16 @@
// SPDX-License-Identifier: GPL-3.0-only
#include "FlameAPI.h"
#include <memory>
#include <optional>
#include "FlameModIndex.h"
#include "Application.h"
#include "Json.h"
#include "modplatform/ModIndex.h"
#include "net/ApiDownload.h"
#include "net/ApiUpload.h"
#include "net/NetJob.h"
#include "net/Upload.h"
Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response)
{
@@ -32,7 +34,7 @@ Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, std::shar
return netJob;
}
auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
QString FlameAPI::getModFileChangelog(int modId, int fileId)
{
QEventLoop lock;
QString changelog;
@@ -67,7 +69,7 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
return changelog;
}
auto FlameAPI::getModDescription(int modId) -> QString
QString FlameAPI::getModDescription(int modId)
{
QEventLoop lock;
QString description;
@@ -100,7 +102,7 @@ auto FlameAPI::getModDescription(int modId) -> QString
return description;
}
auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion
QList<ModPlatform::IndexedVersion> FlameAPI::getLatestVersions(VersionSearchArgs&& args)
{
auto versions_url_optional = getVersionsURL(args);
if (!versions_url_optional.has_value())
@@ -112,7 +114,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
auto netJob = makeShared<NetJob>(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
ModPlatform::IndexedVersion ver;
QList<ModPlatform::IndexedVersion> ver;
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
@@ -132,9 +134,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
for (auto file : arr) {
auto file_obj = Json::requireObject(file);
auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj);
if (file_tmp.date > ver.date && (!args.loaders.has_value() || !file_tmp.loaders || args.loaders.value() & file_tmp.loaders))
ver = file_tmp;
ver.append(FlameMod::loadIndexedPackVersion(file_obj));
}
} catch (Json::JsonException& e) {
@@ -144,7 +144,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
}
});
QObject::connect(netJob.get(), &NetJob::finished, [&loop] { loop.quit(); });
QObject::connect(netJob.get(), &NetJob::finished, &loop, &QEventLoop::quit);
netJob->start();
@@ -220,3 +220,65 @@ QList<ResourceAPI::SortingMethod> FlameAPI::getSortingMethods() const
{ 7, "Category", QObject::tr("Sort by Category") },
{ 8, "GameVersion", QObject::tr("Sort by Game Version") } };
}
Task::Ptr FlameAPI::getModCategories(std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Flame::GetCategories"), APPLICATION->network());
netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl("https://api.curseforge.com/v1/categories?gameId=432&classId=6"), response));
QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; });
return netJob;
}
QList<ModPlatform::Category> FlameAPI::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 obj = Json::requireObject(doc);
auto arr = Json::requireArray(obj, "data");
for (auto val : arr) {
auto cat = Json::requireObject(val);
auto id = Json::requireInteger(cat, "id");
auto name = Json::requireString(cat, "name");
categories.push_back({ name, QString::number(id) });
}
} catch (Json::JsonException& e) {
qCritical() << "Failed to parse response from a version request.";
qCritical() << e.what();
qDebug() << doc;
}
return categories;
};
std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QList<ModPlatform::IndexedVersion> versions,
QList<ModPlatform::ModLoaderType> instanceLoaders,
ModPlatform::ModLoaderTypes modLoaders)
{
// edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update
auto bestVersion = [&versions](ModPlatform::ModLoaderTypes loader) {
std::optional<ModPlatform::IndexedVersion> ver;
for (auto file_tmp : versions) {
if (file_tmp.loaders & loader && (!ver.has_value() || file_tmp.date > ver->date)) {
ver = file_tmp;
}
}
return ver;
};
for (auto l : instanceLoaders) {
auto ver = bestVersion(l);
if (ver.has_value()) {
return ver;
}
}
return bestVersion(modLoaders);
}

View File

@@ -4,7 +4,7 @@
#pragma once
#include <algorithm>
#include <QList>
#include <memory>
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
@@ -12,19 +12,25 @@
class FlameAPI : public NetworkResourceAPI {
public:
auto getModFileChangelog(int modId, int fileId) -> QString;
auto getModDescription(int modId) -> QString;
QString getModFileChangelog(int modId, int fileId);
QString getModDescription(int modId);
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
QList<ModPlatform::IndexedVersion> getLatestVersions(VersionSearchArgs&& args);
std::optional<ModPlatform::IndexedVersion> getLatestVersion(QList<ModPlatform::IndexedVersion> versions,
QList<ModPlatform::ModLoaderType> instanceLoaders,
ModPlatform::ModLoaderTypes fallback);
Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
Task::Ptr matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response);
Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const;
Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr<QByteArray> response) const;
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
static Task::Ptr getModCategories(std::shared_ptr<QByteArray> response);
static QList<ModPlatform::Category> loadModCategories(std::shared_ptr<QByteArray> response);
static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool
[[nodiscard]] QList<ResourceAPI::SortingMethod> getSortingMethods() const override;
static inline bool validateModLoaders(ModPlatform::ModLoaderTypes loaders)
{
return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt);
}
@@ -63,7 +69,7 @@ class FlameAPI : public NetworkResourceAPI {
return 0;
}
static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList
static const QStringList getModLoaderStrings(const ModPlatform::ModLoaderTypes types)
{
QStringList l;
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt }) {
@@ -74,10 +80,7 @@ class FlameAPI : public NetworkResourceAPI {
return l;
}
static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> const QString
{
return "[" + getModLoaderStrings(types).join(',') + "]";
}
static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types) { return "[" + getModLoaderStrings(types).join(',') + "]"; }
private:
[[nodiscard]] std::optional<QString> getSearchURL(SearchArgs const& args) const override
@@ -96,6 +99,9 @@ class FlameAPI : public NetworkResourceAPI {
get_arguments.append("sortOrder=desc");
if (args.loaders.has_value())
get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value())));
if (args.categoryIds.has_value() && !args.categoryIds->empty())
get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(",")));
get_arguments.append(gameVersionStr);
return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&');

View File

@@ -1,4 +1,5 @@
#include "FlameCheckUpdate.h"
#include "Application.h"
#include "FlameAPI.h"
#include "FlameModIndex.h"
@@ -124,25 +125,21 @@ void FlameCheckUpdate::executeTask()
int i = 0;
for (auto* resource : m_resources) {
if (!resource->enabled()) {
emit checkFailed(resource, tr("Disabled resources won't be updated, to prevent resource duplication issues!"));
continue;
}
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(resource->name()));
setProgress(i++, m_resources.size());
auto latest_ver = api.getLatestVersion({ { resource->metadata()->project_id.toString() }, m_game_versions, m_loaders });
auto latest_vers = api.getLatestVersions({ { resource->metadata()->project_id.toString() }, m_game_versions });
// Check if we were aborted while getting the latest version
if (m_was_aborted) {
aborted();
return;
}
auto latest_ver = api.getLatestVersion(latest_vers, m_loaders_list, resource->metadata()->loaders);
setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name()));
if (!latest_ver.addonId.isValid()) {
if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) {
QString reason;
if (dynamic_cast<Mod*>(resource) != nullptr)
reason =
@@ -155,9 +152,9 @@ void FlameCheckUpdate::executeTask()
continue;
}
if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != resource->metadata()->file_id) {
auto pack = getProjectInfo(latest_ver);
auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString());
if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) {
auto pack = getProjectInfo(latest_ver.value());
auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver->fileId.toString());
emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."), recover_url);
continue;
@@ -169,11 +166,9 @@ void FlameCheckUpdate::executeTask()
pack->slug = resource->metadata()->slug;
pack->addonId = resource->metadata()->project_id;
pack->provider = ModPlatform::ResourceProvider::FLAME;
if (!latest_ver.hash.isEmpty() &&
(resource->metadata()->hash != latest_ver.hash || resource->status() == ResourceStatus::NOT_INSTALLED)) {
auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver, m_resource_model);
QString old_version = resource->metadata()->version_number;
if (!latest_ver->hash.isEmpty() &&
(resource->metadata()->hash != latest_ver->hash || resource->status() == ResourceStatus::NOT_INSTALLED)) {
auto old_version = resource->metadata()->version_number;
if (old_version.isEmpty()) {
if (resource->status() == ResourceStatus::NOT_INSTALLED)
old_version = tr("Not installed");
@@ -181,11 +176,12 @@ void FlameCheckUpdate::executeTask()
old_version = tr("Unknown");
}
m_updates.emplace_back(pack->name, resource->metadata()->hash, old_version, latest_ver.version, latest_ver.version_type,
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
ModPlatform::ResourceProvider::FLAME, download_task);
auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver.value(), m_resource_model);
m_updates.emplace_back(pack->name, resource->metadata()->hash, old_version, latest_ver->version, latest_ver->version_type,
api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()),
ModPlatform::ResourceProvider::FLAME, download_task, resource->enabled());
}
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, latest_ver));
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, latest_ver.value()));
}
emitSucceeded();

View File

@@ -10,9 +10,9 @@ class FlameCheckUpdate : public CheckUpdateTask {
public:
FlameCheckUpdate(QList<Resource*>& resources,
std::list<Version>& mcVersions,
std::optional<ModPlatform::ModLoaderTypes> loaders,
std::shared_ptr<ResourceFolderModel> resource_model)
: CheckUpdateTask(resources, mcVersions, loaders, resource_model)
QList<ModPlatform::ModLoaderType> loadersList,
std::shared_ptr<ResourceFolderModel> resourceModel)
: CheckUpdateTask(resources, mcVersions, loadersList, resourceModel)
{}
public slots:

View File

@@ -35,8 +35,11 @@
#include "FlameInstanceCreationTask.h"
#include "QObjectPtr.h"
#include "minecraft/mod/tasks/LocalResourceUpdateTask.h"
#include "modplatform/flame/FileResolvingTask.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/flame/PackManifest.h"
#include "Application.h"
@@ -51,6 +54,7 @@
#include "settings/INISettingsObject.h"
#include "tasks/ConcurrentTask.h"
#include "ui/dialogs/BlockedModsDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
@@ -58,7 +62,6 @@
#include <QFileInfo>
#include "meta/Index.h"
#include "meta/VersionList.h"
#include "minecraft/World.h"
#include "minecraft/mod/tasks/LocalResourceParse.h"
#include "net/ApiDownload.h"
@@ -208,8 +211,7 @@ bool FlameCreationTask::updateInstance()
Flame::File file;
// We don't care about blocked mods, we just need local data to delete the file
file.parseFromObject(entry_obj, false);
file.version = FlameMod::loadIndexedPackVersion(entry_obj);
auto id = Json::requireInteger(entry_obj, "id");
old_files.insert(id, file);
}
@@ -219,10 +221,10 @@ bool FlameCreationTask::updateInstance()
// Delete the files
for (auto& file : old_files) {
if (file.fileName.isEmpty() || file.targetFolder.isEmpty())
if (file.version.fileName.isEmpty() || file.targetFolder.isEmpty())
continue;
QString relative_path(FS::PathCombine(file.targetFolder, file.fileName));
QString relative_path(FS::PathCombine(file.targetFolder, file.version.fileName));
qDebug() << "Scheduling" << relative_path << "for removal";
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path));
}
@@ -322,7 +324,7 @@ bool FlameCreationTask::createInstance()
// Keep index file in case we need it some other time (like when changing versions)
QString new_index_place(FS::PathCombine(parent_folder, "manifest.json"));
FS::ensureFilePathExists(new_index_place);
QFile::rename(index_path, new_index_place);
FS::move(index_path, new_index_place);
} catch (const JSONValidationError& e) {
setError(tr("Could not understand pack manifest:\n") + e.cause());
@@ -336,7 +338,7 @@ bool FlameCreationTask::createInstance()
Override::createOverrides("overrides", parent_folder, overridePath);
QString mcPath = FS::PathCombine(m_stagingPath, "minecraft");
if (!QFile::rename(overridePath, mcPath)) {
if (!FS::move(overridePath, mcPath)) {
setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides);
return false;
}
@@ -471,15 +473,15 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
QList<BlockedMod> blocked_mods;
auto anyBlocked = false;
for (const auto& result : results.files.values()) {
if (result.fileName.endsWith(".zip")) {
m_ZIP_resources.append(std::make_pair(result.fileName, result.targetFolder));
if (result.version.fileName.endsWith(".zip")) {
m_ZIP_resources.append(std::make_pair(result.version.fileName, result.targetFolder));
}
if (!result.resolved || result.url.isEmpty()) {
if (result.version.downloadUrl.isEmpty()) {
BlockedMod blocked_mod;
blocked_mod.name = result.fileName;
blocked_mod.websiteUrl = result.websiteUrl;
blocked_mod.hash = result.hash;
blocked_mod.name = result.version.fileName;
blocked_mod.websiteUrl = QString("%1/download/%2").arg(result.pack.websiteUrl, QString::number(result.fileId));
blocked_mod.hash = result.version.hash;
blocked_mod.matched = false;
blocked_mod.localPath = "";
blocked_mod.targetFolder = result.targetFolder;
@@ -521,7 +523,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
QStringList optionalFiles;
for (auto& result : results) {
if (!result.required) {
optionalFiles << FS::PathCombine(result.targetFolder, result.fileName);
optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName);
}
}
@@ -537,7 +539,10 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
selectedOptionalMods = optionalModDialog.getResult();
}
for (const auto& result : results) {
auto relpath = FS::PathCombine(result.targetFolder, result.fileName);
auto fileName = result.version.fileName;
fileName = FS::RemoveInvalidPathChars(fileName);
auto relpath = FS::PathCombine(result.targetFolder, fileName);
if (!result.required && !selectedOptionalMods.contains(relpath)) {
relpath += ".disabled";
}
@@ -545,36 +550,16 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
relpath = FS::PathCombine("minecraft", relpath);
auto path = FS::PathCombine(m_stagingPath, relpath);
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fallthrough intentional, we treat these as plain old mods and dump them wherever.
}
/* fallthrough */
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {
qDebug() << "Will download" << result.url << "to" << path;
auto dl = Net::ApiDownload::makeFile(result.url, path);
m_files_job->addNetAction(dl);
}
break;
}
case Flame::File::Type::Modpack:
logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath));
break;
case Flame::File::Type::Cmod2:
case Flame::File::Type::Ctoc:
case Flame::File::Type::Unknown:
logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
break;
if (!result.version.downloadUrl.isEmpty()) {
qDebug() << "Will download" << result.version.downloadUrl << "to" << path;
auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path);
m_files_job->addNetAction(dl);
}
}
m_mod_id_resolver.reset();
connect(m_files_job.get(), &NetJob::succeeded, this, [&]() {
connect(m_files_job.get(), &NetJob::finished, this, [this, &loop]() {
m_files_job.reset();
validateZIPResources();
validateZIPResources(loop);
});
connect(m_files_job.get(), &NetJob::failed, [&](QString reason) {
m_files_job.reset();
@@ -585,7 +570,6 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
setProgress(current, total);
});
connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
setStatus(tr("Downloading mods..."));
m_files_job->start();
@@ -623,9 +607,10 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
setAbortable(true);
}
void FlameCreationTask::validateZIPResources()
void FlameCreationTask::validateZIPResources(QEventLoop& loop)
{
qDebug() << "Validating whether resources stored as .zip are in the right place";
QStringList zipMods;
for (auto [fileName, targetFolder] : m_ZIP_resources) {
qDebug() << "Checking" << fileName << "...";
auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName);
@@ -665,6 +650,7 @@ void FlameCreationTask::validateZIPResources()
switch (type) {
case PackedResourceType::Mod:
validatePath(fileName, targetFolder, "mods");
zipMods.push_back(fileName);
break;
case PackedResourceType::ResourcePack:
validatePath(fileName, targetFolder, "resourcepacks");
@@ -690,4 +676,17 @@ void FlameCreationTask::validateZIPResources()
break;
}
}
// TODO make this work with other sorts of resource
auto task = makeShared<ConcurrentTask>(this, "CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
auto results = m_mod_id_resolver->getResults().files;
auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index");
for (auto file : results) {
if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) {
continue;
}
task->addTask(makeShared<LocalResourceUpdateTask>(folder, file.pack, file.version));
}
connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
m_process_update_file_info_job = task;
task->start();
}

View File

@@ -74,7 +74,7 @@ class FlameCreationTask final : public InstanceCreationTask {
void idResolverSucceeded(QEventLoop&);
void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
void validateZIPResources();
void validateZIPResources(QEventLoop& loop);
QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
private:

View File

@@ -1,5 +1,6 @@
#include "FlameModIndex.h"
#include "FileSystem.h"
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
@@ -19,6 +20,9 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
QJsonObject logo = Json::ensureObject(obj, "logo");
pack.logoName = Json::ensureString(logo, "title");
pack.logoUrl = Json::ensureString(logo, "thumbnailUrl");
if (pack.logoUrl.isEmpty()) {
pack.logoUrl = Json::ensureString(logo, "url");
}
auto authors = Json::ensureArray(obj, "authors");
for (auto authorIter : authors) {
@@ -78,10 +82,6 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
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();
@@ -89,8 +89,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
if (!file.addonId.isValid())
file.addonId = pack.addonId;
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);
}
@@ -116,19 +115,25 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
if (str.contains('.'))
file.mcVersion.append(str);
auto loader = str.toLower();
if (loader == "neoforge")
if (auto loader = str.toLower(); 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;
else if (loader == "server" || loader == "client") {
if (file.side.isEmpty())
file.side = loader;
else if (file.side != loader)
file.side = "both";
}
}
file.addonId = Json::requireInteger(obj, "modId");
@@ -137,6 +142,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
file.version = Json::requireString(obj, "displayName");
file.downloadUrl = Json::ensureString(obj, "downloadUrl");
file.fileName = Json::requireString(obj, "fileName");
file.fileName = FS::RemoveInvalidPathChars(file.fileName);
ModPlatform::IndexedVersionType::VersionType ver_type;
switch (Json::requireInteger(obj, "releaseType")) {

View File

@@ -116,7 +116,7 @@ void FlamePackExportTask::collectHashes()
if (relative.startsWith("resourcepacks/") &&
(relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack
auto hashTask = Hashing::createFlameHasher(file.absoluteFilePath());
auto hashTask = Hashing::createHasher(file.absoluteFilePath(), ModPlatform::ResourceProvider::FLAME);
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") });
@@ -140,7 +140,7 @@ void FlamePackExportTask::collectHashes()
continue;
}
auto hashTask = Hashing::createFlameHasher(mod->fileinfo().absoluteFilePath());
auto hashTask = Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::FLAME);
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true });
@@ -201,7 +201,7 @@ void FlamePackExportTask::makeApiRequest()
<< " reason: " << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
emitFailed(parseError.errorString());
return;
}
@@ -213,6 +213,7 @@ void FlamePackExportTask::makeApiRequest()
if (dataArr.isEmpty()) {
qWarning() << "No matches found for fingerprint search!";
getProjectsInfo();
return;
}
for (auto match : dataArr) {
@@ -243,9 +244,9 @@ void FlamePackExportTask::makeApiRequest()
qDebug() << doc;
}
pendingHashes.clear();
getProjectsInfo();
});
connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo);
connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed);
connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo);
task->start();
}
@@ -279,7 +280,7 @@ void FlamePackExportTask::getProjectsInfo()
qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset
<< " reason: " << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
emitFailed(parseError.errorString());
return;
}
@@ -333,7 +334,7 @@ void FlamePackExportTask::buildZip()
setStatus(tr("Adding files..."));
setProgress(4, 5);
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true);
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true, false);
zipTask->addExtraFile("manifest.json", generateIndex());
zipTask->addExtraFile("modlist.html", generateHTML());

View File

@@ -68,35 +68,3 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
}
loadManifestV1(m, obj);
}
bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked)
{
fileName = Json::requireString(obj, "fileName");
// This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
// It is also optional
type = File::Type::SingleFile;
targetFolder = "mods";
// get the hash
hash = QString();
auto hashes = Json::ensureArray(obj, "hashes");
for (QJsonValueRef item : hashes) {
auto hobj = Json::requireObject(item);
auto algo = Json::requireInteger(hobj, "algo");
auto value = Json::requireString(hobj, "value");
if (algo == 1) {
hash = value;
}
}
// may throw, if the project is blocked
QString rawUrl = Json::ensureString(obj, "downloadUrl");
url = QUrl(rawUrl, QUrl::TolerantMode);
if (!url.isValid() && throw_on_blocked) {
throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
}
resolved = true;
return true;
}

View File

@@ -40,26 +40,20 @@
#include <QString>
#include <QUrl>
#include <QVector>
#include "modplatform/ModIndex.h"
namespace Flame {
struct File {
// NOTE: throws JSONValidationError
bool parseFromObject(const QJsonObject& object, bool throw_on_blocked = true);
int projectId = 0;
int fileId = 0;
// NOTE: the opposite to 'optional'
bool required = true;
QString hash;
// NOTE: only set on blocked files ! Empty otherwise.
QString websiteUrl;
ModPlatform::IndexedPack pack;
ModPlatform::IndexedVersion version;
// our
bool resolved = false;
QString fileName;
QUrl url;
QString targetFolder = QStringLiteral("mods");
enum class Type { Unknown, Folder, Ctoc, SingleFile, Cmod2, Modpack, Mod } type = Type::Mod;
};
struct Modloader {

View File

@@ -42,17 +42,28 @@ QString toHTML(QList<Mod*> mods, OptionalData extraData)
}
if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", ").toHtmlEscaped();
if (extraData & FileName)
line += QString(" (%1)").arg(mod->fileinfo().fileName().toHtmlEscaped());
lines.append(QString("<li>%1</li>").arg(line));
}
return QString("<html><body><ul>\n\t%1\n</ul></body></html>").arg(lines.join("\n\t"));
}
QString toMarkdownEscaped(QString src)
{
for (auto ch : "\\`*_{}[]<>()#+-.!|")
src.replace(ch, QString("\\%1").arg(ch));
return src;
}
QString toMarkdown(QList<Mod*> mods, OptionalData extraData)
{
QStringList lines;
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name();
auto modName = toMarkdownEscaped(mod->name());
if (extraData & Url) {
auto url = mod->metaurl();
if (!url.isEmpty())
@@ -60,14 +71,16 @@ QString toMarkdown(QList<Mod*> mods, OptionalData extraData)
}
auto line = modName;
if (extraData & Version) {
auto ver = mod->version();
auto ver = toMarkdownEscaped(mod->version());
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
ver = toMarkdownEscaped(meta->version().toString());
if (!ver.isEmpty())
line += QString(" [%1]").arg(ver);
}
if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", ");
line += " by " + toMarkdownEscaped(mod->authors().join(", "));
if (extraData & FileName)
line += QString(" (%1)").arg(toMarkdownEscaped(mod->fileinfo().fileName()));
lines << "- " + line;
}
return lines.join("\n");
@@ -95,6 +108,8 @@ QString toPlainTXT(QList<Mod*> mods, OptionalData extraData)
}
if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", ");
if (extraData & FileName)
line += QString(" (%1)").arg(mod->fileinfo().fileName());
lines << line;
}
return lines.join("\n");
@@ -122,6 +137,8 @@ QString toJSON(QList<Mod*> mods, OptionalData extraData)
}
if (extraData & Authors && !mod->authors().isEmpty())
line["authors"] = QJsonArray::fromStringList(mod->authors());
if (extraData & FileName)
line["filename"] = mod->fileinfo().fileName();
lines << line;
}
QJsonDocument doc;
@@ -154,6 +171,8 @@ QString toCSV(QList<Mod*> mods, OptionalData extraData)
authors = QString("\"%1\"").arg(mod->authors().join(","));
data << authors;
}
if (extraData & FileName)
data << mod->fileinfo().fileName();
lines << data.join(",");
}
return lines.join("\n");
@@ -189,11 +208,13 @@ QString exportToModList(QList<Mod*> mods, QString lineTemplate)
if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString();
auto authors = mod->authors().join(", ");
auto filename = mod->fileinfo().fileName();
lines << QString(lineTemplate)
.replace("{name}", modName)
.replace("{url}", url)
.replace("{version}", ver)
.replace("{authors}", authors);
.replace("{authors}", authors)
.replace("{filename}", filename);
}
return lines.join("\n");
}

View File

@@ -23,11 +23,7 @@
namespace ExportToModList {
enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM };
enum OptionalData {
Authors = 1 << 0,
Url = 1 << 1,
Version = 1 << 2,
};
enum OptionalData { Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 };
QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData);
QString exportToModList(QList<Mod*> mods, QString lineTemplate);
} // namespace ExportToModList

View File

@@ -1,10 +1,9 @@
#include "HashUtils.h"
#include <QBuffer>
#include <QDebug>
#include <QFile>
#include "FileSystem.h"
#include "StringUtils.h"
#include <QtConcurrentRun>
#include <MurmurHash2.h>
@@ -14,129 +13,153 @@ Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provid
{
switch (provider) {
case ModPlatform::ResourceProvider::MODRINTH:
return createModrinthHasher(file_path);
return makeShared<Hasher>(file_path,
ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first());
case ModPlatform::ResourceProvider::FLAME:
return createFlameHasher(file_path);
return makeShared<Hasher>(file_path, Algorithm::Murmur2);
default:
qCritical() << "[Hashing]"
<< "Unrecognized mod platform!";
qCritical() << "[Hashing]" << "Unrecognized mod platform!";
return nullptr;
}
}
Hasher::Ptr createModrinthHasher(QString file_path)
Hasher::Ptr createHasher(QString file_path, QString type)
{
return makeShared<ModrinthHasher>(file_path);
return makeShared<Hasher>(file_path, type);
}
Hasher::Ptr createFlameHasher(QString file_path)
class QIODeviceReader : public Murmur2::Reader {
public:
QIODeviceReader(QIODevice* device) : m_device(device) {}
virtual ~QIODeviceReader() = default;
virtual int read(char* s, int n) { return m_device->read(s, n); }
virtual bool eof() { return m_device->atEnd(); }
virtual void goToBeginning() { m_device->seek(0); }
virtual void close() { m_device->close(); }
private:
QIODevice* m_device;
};
QString algorithmToString(Algorithm type)
{
return makeShared<FlameHasher>(file_path);
switch (type) {
case Algorithm::Md4:
return "md4";
case Algorithm::Md5:
return "md5";
case Algorithm::Sha1:
return "sha1";
case Algorithm::Sha256:
return "sha256";
case Algorithm::Sha512:
return "sha512";
case Algorithm::Murmur2:
return "murmur2";
// case Algorithm::Unknown:
default:
break;
}
return "unknown";
}
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider)
Algorithm algorithmFromString(QString type)
{
return makeShared<BlockedModHasher>(file_path, provider);
if (type == "md4")
return Algorithm::Md4;
if (type == "md5")
return Algorithm::Md5;
if (type == "sha1")
return Algorithm::Sha1;
if (type == "sha256")
return Algorithm::Sha256;
if (type == "sha512")
return Algorithm::Sha512;
if (type == "murmur2")
return Algorithm::Murmur2;
return Algorithm::Unknown;
}
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type)
QString hash(QIODevice* device, Algorithm type)
{
auto hasher = makeShared<BlockedModHasher>(file_path, provider);
hasher->useHashType(type);
return hasher;
}
void ModrinthHasher::executeTask()
{
QFile file(m_path);
try {
file.open(QFile::ReadOnly);
} catch (FS::FileSystemException& e) {
qCritical() << QString("Failed to open JAR file in %1").arg(m_path);
qCritical() << QString("Reason: ") << e.cause();
emitFailed("Failed to open file for hashing.");
return;
if (!device->isOpen() && !device->open(QFile::ReadOnly))
return "";
QCryptographicHash::Algorithm alg = QCryptographicHash::Sha1;
switch (type) {
case Algorithm::Md4:
alg = QCryptographicHash::Algorithm::Md4;
break;
case Algorithm::Md5:
alg = QCryptographicHash::Algorithm::Md5;
break;
case Algorithm::Sha1:
alg = QCryptographicHash::Algorithm::Sha1;
break;
case Algorithm::Sha256:
alg = QCryptographicHash::Algorithm::Sha256;
break;
case Algorithm::Sha512:
alg = QCryptographicHash::Algorithm::Sha512;
break;
case Algorithm::Murmur2: { // CF-specific
auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); };
auto reader = std::make_unique<QIODeviceReader>(device);
auto result = QString::number(Murmur2::hash(reader.get(), 4 * MiB, should_filter_out));
device->close();
return result;
}
case Algorithm::Unknown:
device->close();
return "";
}
auto hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first();
m_hash = ModPlatform::ProviderCapabilities::hash(ModPlatform::ResourceProvider::MODRINTH, &file, hash_type);
QCryptographicHash hash(alg);
if (!hash.addData(device))
qCritical() << "Failed to read JAR to create hash!";
file.close();
if (m_hash.isEmpty()) {
emitFailed("Empty hash!");
} else {
emitSucceeded();
emit resultsReady(m_hash);
}
Q_ASSERT(hash.result().length() == hash.hashLength(alg));
auto result = hash.result().toHex();
device->close();
return result;
}
void FlameHasher::executeTask()
QString hash(QString fileName, Algorithm type)
{
// CF-specific
auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); };
std::ifstream file_stream(StringUtils::toStdString(m_path).c_str(), std::ifstream::binary);
// TODO: This is very heavy work, but apparently QtConcurrent can't use move semantics, so we can't boop this to another thread.
// How do we make this non-blocking then?
m_hash = QString::number(MurmurHash2(std::move(file_stream), 4 * MiB, should_filter_out));
if (m_hash.isEmpty()) {
emitFailed("Empty hash!");
} else {
emitSucceeded();
emit resultsReady(m_hash);
}
QFile file(fileName);
return hash(&file, type);
}
BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) : Hasher(file_path), provider(provider)
QString hash(QByteArray data, Algorithm type)
{
setObjectName(QString("BlockedModHasher: %1").arg(file_path));
hash_type = ModPlatform::ProviderCapabilities::hashType(provider).first();
QBuffer buff(&data);
return hash(&buff, type);
}
void BlockedModHasher::executeTask()
void Hasher::executeTask()
{
QFile file(m_path);
try {
file.open(QFile::ReadOnly);
} catch (FS::FileSystemException& e) {
qCritical() << QString("Failed to open JAR file in %1").arg(m_path);
qCritical() << QString("Reason: ") << e.cause();
emitFailed("Failed to open file for hashing.");
return;
}
m_hash = ModPlatform::ProviderCapabilities::hash(provider, &file, hash_type);
file.close();
if (m_hash.isEmpty()) {
emitFailed("Empty hash!");
} else {
emitSucceeded();
emit resultsReady(m_hash);
}
m_future = QtConcurrent::run(
QThreadPool::globalInstance(), [](QString fileName, Algorithm type) { return hash(fileName, type); }, m_path, m_alg);
connect(&m_watcher, &QFutureWatcher<QString>::finished, this, [this] {
if (m_future.isCanceled()) {
emitAborted();
} else if (m_result = m_future.result(); m_result.isEmpty()) {
emitFailed("Empty hash!");
} else {
emitSucceeded();
emit resultsReady(m_result);
}
});
m_watcher.setFuture(m_future);
}
QStringList BlockedModHasher::getHashTypes()
bool Hasher::abort()
{
return ModPlatform::ProviderCapabilities::hashType(provider);
}
bool BlockedModHasher::useHashType(QString type)
{
auto types = ModPlatform::ProviderCapabilities::hashType(provider);
if (types.contains(type)) {
hash_type = type;
if (m_future.isRunning()) {
m_future.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not
// occur immediately.
return true;
}
qDebug() << "Bad hash type " << type << " for provider";
return false;
}
} // namespace Hashing

View File

@@ -1,5 +1,8 @@
#pragma once
#include <QCryptographicHash>
#include <QFuture>
#include <QFutureWatcher>
#include <QString>
#include "modplatform/ModIndex.h"
@@ -7,61 +10,42 @@
namespace Hashing {
enum class Algorithm { Md4, Md5, Sha1, Sha256, Sha512, Murmur2, Unknown };
QString algorithmToString(Algorithm type);
Algorithm algorithmFromString(QString type);
QString hash(QIODevice* device, Algorithm type);
QString hash(QString fileName, Algorithm type);
QString hash(QByteArray data, Algorithm type);
class Hasher : public Task {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Hasher>;
Hasher(QString file_path) : m_path(std::move(file_path)) {}
Hasher(QString file_path, Algorithm alg) : m_path(file_path), m_alg(alg) {}
Hasher(QString file_path, QString alg) : Hasher(file_path, algorithmFromString(alg)) {}
/* We can't really abort this task, but we can say we aborted and finish our thing quickly :) */
bool abort() override { return true; }
bool abort() override;
void executeTask() override = 0;
void executeTask() override;
QString getResult() const { return m_hash; };
QString getResult() const { return m_result; };
QString getPath() const { return m_path; };
signals:
void resultsReady(QString hash);
protected:
QString m_hash;
QString m_path;
};
class FlameHasher : public Hasher {
public:
FlameHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("FlameHasher: %1").arg(file_path)); }
void executeTask() override;
};
class ModrinthHasher : public Hasher {
public:
ModrinthHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("ModrinthHasher: %1").arg(file_path)); }
void executeTask() override;
};
class BlockedModHasher : public Hasher {
public:
BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
void executeTask() override;
QStringList getHashTypes();
bool useHashType(QString type);
private:
ModPlatform::ResourceProvider provider;
QString hash_type;
QString m_result;
QString m_path;
Algorithm m_alg;
QFuture<QString> m_future;
QFutureWatcher<QString> m_watcher;
};
Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider);
Hasher::Ptr createFlameHasher(QString file_path);
Hasher::Ptr createModrinthHasher(QString file_path);
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type);
Hasher::Ptr createHasher(QString file_path, QString type);
} // namespace Hashing

View File

@@ -45,8 +45,8 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) {
int network_error_code = -1;
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply)
network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
network_error_code = failed_action->replyStatusCode();
callbacks.on_fail(reason, network_error_code);
});
@@ -104,8 +104,8 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
});
QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) {
int network_error_code = -1;
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply)
network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
network_error_code = failed_action->replyStatusCode();
callbacks.on_fail(reason, network_error_code);
});
@@ -155,8 +155,8 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args,
});
QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) {
int network_error_code = -1;
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply)
network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
network_error_code = failed_action->replyStatusCode();
callbacks.on_fail(reason, network_error_code);
});

View File

@@ -10,7 +10,7 @@ void createOverrides(const QString& name, const QString& parent_folder, const QS
{
QString file_path(FS::PathCombine(parent_folder, name + ".txt"));
if (QFile::exists(file_path))
QFile::remove(file_path);
FS::deletePath(file_path);
FS::ensureFilePathExists(file_path);

View File

@@ -13,7 +13,7 @@ class PackFetchTask : public QObject {
Q_OBJECT
public:
PackFetchTask(shared_qobject_ptr<QNetworkAccessManager> network) : QObject(nullptr), m_network(network){};
PackFetchTask(shared_qobject_ptr<QNetworkAccessManager> network) : QObject(nullptr), m_network(network) {};
virtual ~PackFetchTask() = default;
void fetch();

View File

@@ -137,7 +137,7 @@ void PackInstallTask::install()
QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
if (unzipMcDir.exists()) {
// ok, found minecraft dir, move contents to instance dir
if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) {
if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) {
emitFailed(tr("Failed to move unzipped Minecraft!"));
return;
}

View File

@@ -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;
};

View File

@@ -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)
{
@@ -73,6 +97,7 @@ class ModrinthAPI : public NetworkResourceAPI {
return "";
}
[[nodiscard]] QString createFacets(SearchArgs const& args) const
{
QStringList facets_list;
@@ -81,6 +106,14 @@ class ModrinthAPI : public NetworkResourceAPI {
facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value())));
if (args.versions.has_value())
facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value())));
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(','));

View File

@@ -1,23 +1,23 @@
#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;
bool ModrinthCheckUpdate::abort()
{
if (m_net_job)
return m_net_job->abort();
if (m_job)
return m_job->abort();
return true;
}
@@ -29,158 +29,187 @@ bool ModrinthCheckUpdate::abort()
void ModrinthCheckUpdate::executeTask()
{
setStatus(tr("Preparing resources for Modrinth..."));
setProgress(0, 3);
setProgress(0, 9);
QHash<QString, Resource*> mappings;
// Create all hashes
QStringList hashes;
auto best_hash_type = ModPlatform::ProviderCapabilities::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* resource : m_resources) {
if (!resource->enabled()) {
emit checkFailed(resource, tr("Disabled resources won't be updated, to prevent resource duplication issues!"));
continue;
}
auto hash = resource->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 (resource->metadata()->hash_format != best_hash_type) {
auto hash_task = Hashing::createModrinthHasher(resource->fileinfo().absoluteFilePath());
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [&hashes, &mappings, resource](QString hash) {
hashes.append(hash);
mappings.insert(hash, resource);
});
if (resource->metadata()->hash_format != m_hash_type) {
auto hash_task = Hashing::createHasher(resource->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH);
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_mappings.insert(hash, resource); });
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, resource);
m_mappings.insert(hash, resource);
}
}
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;
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;
emitFailed(parse_error.errorString());
return;
}
emitFailed(parse_error.errorString());
return;
}
setStatus(tr("Parsing the API response from Modrinth..."));
setProgress(m_next_loader_idx * 2, 9);
setStatus(tr("Parsing the API response from Modrinth..."));
setProgress(2, 3);
try {
for (auto iter = m_mappings.begin(); iter != m_mappings.end(); iter++) {
const QString& hash = iter.key();
Resource* resource = iter.value();
try {
for (auto iter = mappings.begin(); iter != mappings.end(); iter++) {
const QString& hash = iter.key();
Resource* resource = iter.value();
auto project_obj = doc[hash].toObject();
if (forceModLoaderCheck && !(resource->metadata()->loaders & loader))
continue;
// If the returned project is empty, but we have Modrinth metadata,
// it means this specific version is not available
if (project_obj.isEmpty()) {
qDebug() << "Resource " << resource->name() << " got an empty response.";
qDebug() << "Hash: " << hash;
auto project_obj = doc[hash].toObject();
QString reason;
if (dynamic_cast<Mod*>(resource) != nullptr)
reason =
tr("No valid version found for this resource. It's probably unavailable for the current game "
"version / mod loader.");
else
reason = tr("No valid version found for this resource. It's probably unavailable for the current game version.");
// 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;
emit checkFailed(resource, reason);
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,
ModPlatform::ModLoaderType::LiteLoader };
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 resource without download url!";
qCritical() << project_ver.fileName;
emit checkFailed(mappings.find(hash).value(), tr("Resource has an empty download URL"));
continue;
}
auto resource_iter = mappings.find(hash);
if (resource_iter == mappings.end()) {
qCritical() << "Failed to remap resource from Modrinth!";
continue;
}
// Fake pack with the necessary info to pass to the download task :)
auto pack = std::make_shared<ModPlatform::IndexedPack>();
pack->name = resource->name();
pack->slug = resource->metadata()->slug;
pack->addonId = resource->metadata()->project_id;
pack->provider = ModPlatform::ResourceProvider::MODRINTH;
if ((project_ver.hash != hash && project_ver.is_preferred) || (resource->status() == ResourceStatus::NOT_INSTALLED)) {
auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_resource_model);
QString old_version = resource->metadata()->version_number;
if (old_version.isEmpty()) {
if (resource->status() == ResourceStatus::NOT_INSTALLED)
old_version = tr("Not installed");
else
old_version = tr("Unknown");
}
m_updates.emplace_back(pack->name, hash, old_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));
continue;
}
} catch (Json::JsonException& e) {
emitFailed(e.cause() + ": " + e.what());
return;
}
emitSucceeded();
});
connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::emitFailed);
// 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);
// 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->provider = ModPlatform::ResourceProvider::MODRINTH;
if ((project_ver.hash != hash && project_ver.is_preferred) || (resource->status() == ResourceStatus::NOT_INSTALLED)) {
auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_resource_model);
QString old_version = resource->metadata()->version_number;
if (old_version.isEmpty()) {
if (resource->status() == ResourceStatus::NOT_INSTALLED)
old_version = tr("Not installed");
else
old_version = tr("Unknown");
}
m_updates.emplace_back(pack->name, hash, old_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));
}
} 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]->metadata()->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();
}
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 resource : m_mappings) {
if (resource->metadata()->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();
}

View File

@@ -1,18 +1,17 @@
#pragma once
#include "Application.h"
#include "modplatform/CheckUpdateTask.h"
#include "net/NetJob.h"
class ModrinthCheckUpdate : public CheckUpdateTask {
Q_OBJECT
public:
ModrinthCheckUpdate(QList<Resource*>& resources,
std::list<Version>& mcVersions,
std::optional<ModPlatform::ModLoaderTypes> loaders,
std::shared_ptr<ResourceFolderModel> resource_model)
: CheckUpdateTask(resources, mcVersions, loaders, resource_model)
std::list<Version>& mcVersions,
QList<ModPlatform::ModLoaderType> loadersList,
std::shared_ptr<ResourceFolderModel> resourceModel)
: CheckUpdateTask(resources, mcVersions, loadersList, resourceModel)
, m_hash_type(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first())
{}
public slots:
@@ -20,7 +19,13 @@ class ModrinthCheckUpdate : public CheckUpdateTask {
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, Resource*> m_mappings;
QString m_hash_type;
int m_next_loader_idx = 0;
};

View File

@@ -5,8 +5,12 @@
#include "InstanceList.h"
#include "Json.h"
#include "QObjectPtr.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/Mod.h"
#include "modplatform/EnsureMetadataTask.h"
#include "modplatform/helpers/OverrideUtils.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
@@ -21,6 +25,7 @@
#include <QAbstractButton>
#include <QFileInfo>
#include <QHash>
#include <vector>
bool ModrinthCreationTask::abort()
@@ -29,8 +34,8 @@ bool ModrinthCreationTask::abort()
return false;
m_abort = true;
if (m_files_job)
m_files_job->abort();
if (m_task)
m_task->abort();
return Task::abort();
}
@@ -173,7 +178,7 @@ 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, m_root_path);
@@ -183,7 +188,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;
}
@@ -234,33 +239,43 @@ bool ModrinthCreationTask::createInstance()
instance.setName(name());
instance.saveNow();
m_files_job.reset(new NetJob(tr("Mod Download Modrinth"), APPLICATION->network()));
auto downloadMods = makeShared<NetJob>(tr("Mod Download Modrinth"), APPLICATION->network());
auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path);
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
// TODO make this work with other sorts of resource
QHash<QString, Resource*> resources;
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;
}
if (fileName.startsWith("mods/")) {
auto mod = new Mod(file_path);
ModDetails d;
d.mod_id = file_path;
mod->setDetails(d);
resources[file.hash.toHex()] = mod;
}
qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path;
auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_files_job->addNetAction(dl);
downloadMods->addNetAction(dl);
if (!file.downloads.empty()) {
// 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, [&file, file_path, param, downloadMods] {
auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_files_job->addNetAction(ndl);
downloadMods->addNetAction(ndl);
if (auto shared = param.lock())
shared->succeeded();
});
@@ -269,23 +284,44 @@ bool ModrinthCreationTask::createInstance()
bool ended_well = false;
connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { ended_well = true; });
connect(m_files_job.get(), &NetJob::failed, [&](const QString& reason) {
connect(downloadMods.get(), &NetJob::succeeded, this, [&]() { ended_well = true; });
connect(downloadMods.get(), &NetJob::failed, [&](const QString& reason) {
ended_well = false;
setError(reason);
});
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit);
connect(downloadMods.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, total);
});
connect(m_files_job.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress);
connect(downloadMods.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress);
setStatus(tr("Downloading mods..."));
m_files_job->start();
downloadMods->start();
m_task = downloadMods;
loop.exec();
QEventLoop ensureMetaLoop;
QDir folder = FS::PathCombine(instance.modsRoot(), ".index");
auto ensureMetadataTask = makeShared<EnsureMetadataTask>(resources, folder, ModPlatform::ResourceProvider::MODRINTH);
connect(ensureMetadataTask.get(), &Task::succeeded, this, [&]() { ended_well = true; });
connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit);
connect(ensureMetadataTask.get(), &Task::progress, [&](qint64 current, qint64 total) {
setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, total);
});
connect(ensureMetadataTask.get(), &Task::stepProgress, this, &ModrinthCreationTask::propagateStepProgress);
ensureMetadataTask->start();
m_task = ensureMetadataTask;
ensureMetaLoop.exec();
for (auto m : resources) {
delete m;
}
resources.clear();
// Update information of the already installed instance, if any.
if (m_instance && ended_well) {
setAbortable(false);
@@ -344,23 +380,8 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
}
QJsonObject hashes = Json::requireObject(modInfo, "hashes");
QString hash;
QCryptographicHash::Algorithm hashAlgorithm;
hash = Json::ensureString(hashes, "sha1");
hashAlgorithm = QCryptographicHash::Sha1;
if (hash.isEmpty()) {
hash = Json::ensureString(hashes, "sha512");
hashAlgorithm = QCryptographicHash::Sha512;
if (hash.isEmpty()) {
hash = Json::ensureString(hashes, "sha256");
hashAlgorithm = QCryptographicHash::Sha256;
if (hash.isEmpty()) {
throw JSONValidationError("No hash found for: " + file.path);
}
}
}
file.hash = QByteArray::fromHex(hash.toLatin1());
file.hashAlgorithm = hashAlgorithm;
file.hash = QByteArray::fromHex(Json::requireString(hashes, "sha512").toLatin1());
file.hashAlgorithm = QCryptographicHash::Sha512;
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
// (as Modrinth seems to incorrectly handle spaces)

View File

@@ -1,15 +1,11 @@
#pragma once
#include <optional>
#include "BaseInstance.h"
#include "InstanceCreationTask.h"
#include <optional>
#include "minecraft/MinecraftInstance.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "net/NetJob.h"
class ModrinthCreationTask final : public InstanceCreationTask {
Q_OBJECT
@@ -43,7 +39,7 @@ class ModrinthCreationTask final : public InstanceCreationTask {
QString m_managed_id, m_managed_version_id, m_managed_name;
std::vector<Modrinth::File> m_files;
NetJob::Ptr m_files_job;
Task::Ptr m_task;
std::optional<InstancePtr> m_instance;

View File

@@ -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());

View File

@@ -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"
@@ -113,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 {
@@ -134,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;
@@ -153,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");
@@ -225,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");
@@ -248,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");

View File

@@ -131,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");

View File

@@ -84,6 +84,7 @@ struct ModpackExtra {
struct ModpackVersion {
QString name;
QString version;
QString gameVersion;
ModPlatform::IndexedVersionType version_type;
QString changelog;

View File

@@ -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
@@ -112,10 +113,14 @@ auto V1::createModFormat([[maybe_unused]] const QDir& index_dir,
mod.provider = mod_pack.provider;
mod.file_id = mod_version.fileId;
mod.project_id = mod_pack.addonId;
mod.side = stringToSide(mod_pack.side);
mod.side = stringToSide(mod_version.side.isEmpty() ? mod_pack.side : mod_version.side);
mod.loaders = mod_version.loaders;
mod.mcVersions = mod_version.mcVersion;
mod.mcVersions.sort();
mod.releaseType = mod_version.version_type;
mod.version_number = mod_version.version_number;
if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a versio
if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a version number
mod.version_number = mod_version.version;
return mod;
@@ -184,6 +189,18 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod)
break;
}
toml::array loaders;
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric,
ModPlatform::Quilt }) {
if (mod.loaders & loader) {
loaders.push_back(getModLoaderAsString(loader).toStdString());
}
}
toml::array mcVersions;
for (auto version : mod.mcVersions) {
mcVersions.push_back(version.toStdString());
}
if (!index_file.open(QIODevice::ReadWrite)) {
qCritical() << QString("Could not open file %1!").arg(normalized_fname);
return;
@@ -195,6 +212,9 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod)
auto tbl = toml::table{ { "name", mod.name.toStdString() },
{ "filename", mod.filename.toStdString() },
{ "side", sideToString(mod.side).toStdString() },
{ "loaders", loaders },
{ "mcVersions", mcVersions },
{ "releaseType", mod.releaseType.toString().toStdString() },
{ "download",
toml::table{
{ "mode", mod.mode.toStdString() },
@@ -279,6 +299,25 @@ auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod
mod.name = stringEntry(table, "name");
mod.filename = stringEntry(table, "filename");
mod.side = stringToSide(stringEntry(table, "side"));
mod.releaseType = ModPlatform::IndexedVersionType(stringEntry(table, "releaseType"));
if (auto loaders = table["loaders"]; loaders && loaders.is_array()) {
for (auto&& loader : *loaders.as_array()) {
if (loader.is_string()) {
mod.loaders |= ModPlatform::getModLoaderFromString(QString::fromStdString(loader.as_string()->value_or("")));
}
}
}
if (auto versions = table["mcVersions"]; versions && versions.is_array()) {
for (auto&& version : *versions.as_array()) {
if (version.is_string()) {
auto ver = QString::fromStdString(version.as_string()->value_or(""));
if (!ver.isEmpty()) {
mod.mcVersions << ver;
}
}
}
mod.mcVersions.sort();
}
}
{ // [download] info

View File

@@ -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
@@ -43,6 +44,9 @@ class V1 {
QString name{};
QString filename{};
Side side{ Side::UniversalSide };
ModPlatform::ModLoaderTypes loaders;
QStringList mcVersions;
ModPlatform::IndexedVersionType releaseType;
// [download]
QString mode{};

View File

@@ -114,8 +114,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded()
auto dl = Net::ApiDownload::makeFile(mod.url, path);
if (!mod.md5.isEmpty()) {
auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5));
}
m_filesNetJob->addNetAction(dl);

View File

@@ -83,8 +83,10 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
data = file.readAll();
file.close();
} else {
if (minecraftVersion.isEmpty())
if (minecraftVersion.isEmpty()) {
emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown"));
return;
}
components->setComponentVersion("net.minecraft", minecraftVersion, true);
components->installJarMods({ modpackJar });
@@ -131,7 +133,9 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
file.close();
} else {
// This is the "Vanilla" modpack, excluded by the search code
emit failed(tr("Unable to find a \"version.json\"!"));
components->setComponentVersion("net.minecraft", minecraftVersion, true);
components->saveNow();
emit succeeded();
return;
}
@@ -155,8 +159,26 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
auto libraryObject = Json::ensureObject(library, {}, "");
auto libraryName = Json::ensureString(libraryObject, "name", "", "");
if ((libraryName.startsWith("net.minecraftforge:forge:") || libraryName.startsWith("net.minecraftforge:fmlloader:")) &&
libraryName.contains('-')) {
if (libraryName.startsWith("net.neoforged.fancymodloader:")) { // it is neoforge
// no easy way to get the version from the libs so use the arguments
auto arguments = Json::ensureObject(root, "arguments", {});
bool isVersionArg = false;
QString neoforgeVersion;
for (auto arg : Json::ensureArray(arguments, "game", {})) {
auto argument = Json::ensureString(arg, "");
if (isVersionArg) {
neoforgeVersion = argument;
break;
} else {
isVersionArg = "--fml.neoForgeVersion" == argument || "--fml.forgeVersion" == argument;
}
}
if (!neoforgeVersion.isEmpty()) {
components->setComponentVersion("net.neoforged", neoforgeVersion);
}
break;
} else if ((libraryName.startsWith("net.minecraftforge:forge:") || libraryName.startsWith("net.minecraftforge:fmlloader:")) &&
libraryName.contains('-')) {
QString libraryVersion = libraryName.section(':', 2);
if (!libraryVersion.startsWith("1.7.10-")) {
components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1));
@@ -164,6 +186,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
// 1.7.10 versions sometimes look like 1.7.10-10.13.4.1614-1.7.10, this filters out the 10.13.4.1614 part
components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1));
}
break;
} else {
// <Technic library name prefix> -> <our component name>
static QMap<QString, QString> loaderMap{ { "net.minecraftforge:minecraftforge:", "net.minecraftforge" },