Merge remote-tracking branch 'upstream/develop' into resource-meta
Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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('&');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -120,3 +120,41 @@ QList<ResourceAPI::SortingMethod> ModrinthAPI::getSortingMethods() const
|
||||
{ 4, "newest", QObject::tr("Sort by Newest") },
|
||||
{ 5, "updated", QObject::tr("Sort by Last Updated") } };
|
||||
}
|
||||
|
||||
Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr<QByteArray> response)
|
||||
{
|
||||
auto netJob = makeShared<NetJob>(QString("Modrinth::GetCategories"), APPLICATION->network());
|
||||
netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category"), response));
|
||||
QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Modrinth failed to get categories:" << msg; });
|
||||
return netJob;
|
||||
}
|
||||
|
||||
QList<ModPlatform::Category> ModrinthAPI::loadModCategories(std::shared_ptr<QByteArray> response)
|
||||
{
|
||||
QList<ModPlatform::Category> categories;
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
return categories;
|
||||
}
|
||||
|
||||
try {
|
||||
auto arr = Json::requireArray(doc);
|
||||
|
||||
for (auto val : arr) {
|
||||
auto cat = Json::requireObject(val);
|
||||
auto name = Json::requireString(cat, "name");
|
||||
if (Json::ensureString(cat, "project_type", "") == "mod")
|
||||
categories.push_back({ name, name });
|
||||
}
|
||||
|
||||
} catch (Json::JsonException& e) {
|
||||
qCritical() << "Failed to parse response from a version request.";
|
||||
qCritical() << e.what();
|
||||
qDebug() << doc;
|
||||
}
|
||||
return categories;
|
||||
};
|
||||
@@ -30,6 +30,9 @@ class ModrinthAPI : public NetworkResourceAPI {
|
||||
|
||||
Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
|
||||
|
||||
static Task::Ptr getModCategories(std::shared_ptr<QByteArray> response);
|
||||
static QList<ModPlatform::Category> loadModCategories(std::shared_ptr<QByteArray> response);
|
||||
|
||||
public:
|
||||
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
|
||||
|
||||
@@ -41,7 +44,7 @@ class ModrinthAPI : public NetworkResourceAPI {
|
||||
for (auto loader :
|
||||
{ ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader }) {
|
||||
if (types & loader) {
|
||||
l << getModLoaderString(loader);
|
||||
l << getModLoaderAsString(loader);
|
||||
}
|
||||
}
|
||||
return l;
|
||||
@@ -56,6 +59,27 @@ class ModrinthAPI : public NetworkResourceAPI {
|
||||
return l.join(',');
|
||||
}
|
||||
|
||||
static auto getCategoriesFilters(QStringList categories) -> const QString
|
||||
{
|
||||
QStringList l;
|
||||
for (auto cat : categories) {
|
||||
l << QString("\"categories:%1\"").arg(cat);
|
||||
}
|
||||
return l.join(',');
|
||||
}
|
||||
|
||||
static auto getSideFilters(QString side) -> const QString
|
||||
{
|
||||
if (side.isEmpty() || side == "both") {
|
||||
return {};
|
||||
}
|
||||
if (side == "client")
|
||||
return QString("\"client_side:required\",\"client_side:optional\"");
|
||||
if (side == "server")
|
||||
return QString("\"server_side:required\",\"server_side:optional\"");
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type)
|
||||
{
|
||||
@@ -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(','));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ struct ModpackExtra {
|
||||
struct ModpackVersion {
|
||||
QString name;
|
||||
QString version;
|
||||
QString gameVersion;
|
||||
ModPlatform::IndexedVersionType version_type;
|
||||
QString changelog;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user