aboutsummaryrefslogtreecommitdiff
path: root/launcher/modplatform
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/modplatform')
-rw-r--r--launcher/modplatform/CheckUpdateTask.h51
-rw-r--r--launcher/modplatform/EnsureMetadataTask.cpp534
-rw-r--r--launcher/modplatform/EnsureMetadataTask.h54
-rw-r--r--launcher/modplatform/ModAPI.h40
-rw-r--r--launcher/modplatform/ModIndex.h8
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp160
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.h16
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.cpp64
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.h23
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp11
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.h3
-rw-r--r--launcher/modplatform/flame/FlameAPI.cpp148
-rw-r--r--launcher/modplatform/flame/FlameAPI.h10
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.cpp179
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.h25
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp18
-rw-r--r--launcher/modplatform/flame/FlameModIndex.h2
-rw-r--r--launcher/modplatform/flame/PackManifest.h37
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.cpp29
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.h2
-rw-r--r--launcher/modplatform/legacy_ftb/PackFetchTask.cpp37
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.cpp39
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.h6
-rw-r--r--launcher/modplatform/legacy_ftb/PrivatePackManager.cpp43
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.cpp286
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.h80
-rw-r--r--launcher/modplatform/modpacksch/FTBPackManifest.cpp46
-rw-r--r--launcher/modplatform/modpacksch/FTBPackManifest.h48
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.cpp108
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h28
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp174
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.h23
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp41
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.h2
-rw-r--r--launcher/modplatform/packwiz/Packwiz.cpp69
-rw-r--r--launcher/modplatform/packwiz/Packwiz.h23
-rw-r--r--launcher/modplatform/packwiz/Packwiz_test.cpp7
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.h6
-rw-r--r--launcher/modplatform/technic/TechnicPackProcessor.cpp14
39 files changed, 2272 insertions, 222 deletions
diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h
new file mode 100644
index 00000000..91922034
--- /dev/null
+++ b/launcher/modplatform/CheckUpdateTask.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "minecraft/mod/Mod.h"
+#include "modplatform/ModAPI.h"
+#include "modplatform/ModIndex.h"
+#include "tasks/Task.h"
+
+class ModDownloadTask;
+class ModFolderModel;
+
+class CheckUpdateTask : public Task {
+ Q_OBJECT
+
+ public:
+ CheckUpdateTask(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {};
+
+ struct UpdatableMod {
+ QString name;
+ QString old_hash;
+ QString old_version;
+ QString new_version;
+ QString changelog;
+ ModPlatform::Provider provider;
+ ModDownloadTask* download;
+
+ public:
+ UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::Provider p, ModDownloadTask* t)
+ : name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t)
+ {}
+ };
+
+ auto getUpdatable() -> std::vector<UpdatableMod>&& { return std::move(m_updatable); }
+
+ public slots:
+ bool abort() override = 0;
+
+ protected slots:
+ void executeTask() override = 0;
+
+ signals:
+ void checkFailed(Mod* failed, QString reason, QUrl recover_url = {});
+
+ protected:
+ QList<Mod*>& m_mods;
+ std::list<Version>& m_game_versions;
+ ModAPI::ModLoaderTypes m_loaders;
+ std::shared_ptr<ModFolderModel> m_mods_folder;
+
+ std::vector<UpdatableMod> m_updatable;
+};
diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp
new file mode 100644
index 00000000..60c54c4e
--- /dev/null
+++ b/launcher/modplatform/EnsureMetadataTask.cpp
@@ -0,0 +1,534 @@
+#include "EnsureMetadataTask.h"
+
+#include <MurmurHash2.h>
+#include <QDebug>
+
+#include "FileSystem.h"
+#include "Json.h"
+#include "minecraft/mod/Mod.h"
+#include "minecraft/mod/tasks/LocalModUpdateTask.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/MultipleOptionsTask.h"
+
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+static ModrinthAPI modrinth_api;
+static FlameAPI flame_api;
+
+EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov)
+{
+ auto hash = getHash(mod);
+ if (hash.isEmpty())
+ emitFail(mod);
+ else
+ m_mods.insert(hash, mod);
+}
+
+EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov)
+ : Task(nullptr), m_index_dir(dir), m_provider(prov)
+{
+ for (auto* mod : mods) {
+ if (!mod->valid()) {
+ emitFail(mod);
+ continue;
+ }
+
+ auto hash = getHash(mod);
+ if (hash.isEmpty()) {
+ emitFail(mod);
+ continue;
+ }
+
+ m_mods.insert(hash, mod);
+ }
+}
+
+QString EnsureMetadataTask::getHash(Mod* mod)
+{
+ /* Here we create a mapping hash -> mod, because we need that relationship to parse the API routes */
+ QByteArray jar_data;
+ try {
+ jar_data = FS::read(mod->fileinfo().absoluteFilePath());
+ } catch (FS::FileSystemException& e) {
+ qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name());
+ qCritical() << QString("Reason: ") << e.cause();
+
+ return {};
+ }
+
+ switch (m_provider) {
+ case ModPlatform::Provider::MODRINTH: {
+ auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+
+ return QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex());
+ }
+ case ModPlatform::Provider::FLAME: {
+ QByteArray jar_data_treated;
+ for (char c : jar_data) {
+ // CF-specific
+ if (!(c == 9 || c == 10 || c == 13 || c == 32))
+ jar_data_treated.push_back(c);
+ }
+
+ return QString::number(MurmurHash2(jar_data_treated, jar_data_treated.length()));
+ }
+ }
+
+ return {};
+}
+
+bool EnsureMetadataTask::abort()
+{
+ // Prevent sending signals to a dead object
+ disconnect(this, 0, 0, 0);
+
+ if (m_current_task)
+ return m_current_task->abort();
+ return true;
+}
+
+void EnsureMetadataTask::executeTask()
+{
+ setStatus(tr("Checking if mods have metadata..."));
+
+ for (auto* mod : m_mods) {
+ if (!mod->valid()) {
+ qDebug() << "Mod" << mod->name() << "is invalid!";
+ emitFail(mod);
+ continue;
+ }
+
+ // They already have the right metadata :o
+ if (mod->status() != ModStatus::NoMetadata && mod->metadata() && mod->metadata()->provider == m_provider) {
+ qDebug() << "Mod" << mod->name() << "already has metadata!";
+ emitReady(mod);
+ continue;
+ }
+
+ // Folders don't have metadata
+ if (mod->type() == Mod::MOD_FOLDER) {
+ emitReady(mod);
+ }
+ }
+
+ NetJob::Ptr version_task;
+
+ switch (m_provider) {
+ case (ModPlatform::Provider::MODRINTH):
+ version_task = modrinthVersionsTask();
+ break;
+ case (ModPlatform::Provider::FLAME):
+ version_task = flameVersionsTask();
+ break;
+ }
+
+ auto invalidade_leftover = [this] {
+ QMutableHashIterator<QString, Mod*> mods_iter(m_mods);
+ while (mods_iter.hasNext()) {
+ auto mod = mods_iter.next();
+ emitFail(mod.value());
+ }
+
+ emitSucceeded();
+ };
+
+ connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] {
+ NetJob::Ptr project_task;
+
+ switch (m_provider) {
+ case (ModPlatform::Provider::MODRINTH):
+ project_task = modrinthProjectsTask();
+ break;
+ case (ModPlatform::Provider::FLAME):
+ project_task = flameProjectsTask();
+ break;
+ }
+
+ if (!project_task) {
+ invalidade_leftover();
+ return;
+ }
+
+ connect(project_task.get(), &Task::finished, this, [=] {
+ invalidade_leftover();
+ project_task->deleteLater();
+ m_current_task = nullptr;
+ });
+
+ m_current_task = project_task.get();
+ project_task->start();
+ });
+
+ connect(version_task.get(), &Task::finished, [=] {
+ version_task->deleteLater();
+ m_current_task = nullptr;
+ });
+
+ if (m_mods.size() > 1)
+ setStatus(tr("Requesting metadata information from %1...").arg(ProviderCaps.readableName(m_provider)));
+ else if (!m_mods.empty())
+ setStatus(tr("Requesting metadata information from %1 for '%2'...")
+ .arg(ProviderCaps.readableName(m_provider), m_mods.begin().value()->name()));
+
+ m_current_task = version_task.get();
+ version_task->start();
+}
+
+void EnsureMetadataTask::emitReady(Mod* m)
+{
+ qDebug() << QString("Generated metadata for %1").arg(m->name());
+ emit metadataReady(m);
+
+ m_mods.remove(getHash(m));
+}
+
+void EnsureMetadataTask::emitFail(Mod* m)
+{
+ qDebug() << QString("Failed to generate metadata for %1").arg(m->name());
+ emit metadataFailed(m);
+
+ m_mods.remove(getHash(m));
+}
+
+// Modrinth
+
+NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
+{
+ auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+
+ auto* response = new QByteArray();
+ auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);
+
+ // Prevents unfortunate timings when aborting the task
+ if (!ver_task)
+ return {};
+
+ connect(ver_task.get(), &NetJob::succeeded, this, [this, response] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &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() << *response;
+
+ failed(parse_error.errorString());
+ return;
+ }
+
+ try {
+ auto entries = Json::requireObject(doc);
+ for (auto& hash : m_mods.keys()) {
+ auto mod = m_mods.find(hash).value();
+ try {
+ auto entry = Json::requireObject(entries, hash);
+
+ setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
+ qDebug() << "Getting version for" << mod->name() << "from Modrinth";
+
+ m_temp_versions.insert(hash, Modrinth::loadIndexedPackVersion(entry));
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(mod);
+ }
+ }
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ return ver_task;
+}
+
+NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
+{
+ QHash<QString, QString> addonIds;
+ for (auto const& data : m_temp_versions)
+ addonIds.insert(data.addonId.toString(), data.hash);
+
+ auto response = new QByteArray();
+ NetJob::Ptr proj_task;
+
+ if (addonIds.isEmpty()) {
+ qWarning() << "No addonId found!";
+ } else if (addonIds.size() == 1) {
+ proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response);
+ } else {
+ proj_task = modrinth_api.getProjects(addonIds.keys(), response);
+ }
+
+ // Prevents unfortunate timings when aborting the task
+ if (!proj_task)
+ return {};
+
+ connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*response, &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() << *response;
+ return;
+ }
+
+ try {
+ QJsonArray entries;
+ if (addonIds.size() == 1)
+ entries = { doc.object() };
+ else
+ entries = Json::requireArray(doc);
+
+ for (auto entry : entries) {
+ auto entry_obj = Json::requireObject(entry);
+
+ ModPlatform::IndexedPack pack;
+ Modrinth::loadIndexedPack(pack, entry_obj);
+
+ auto hash = addonIds.find(pack.addonId.toString()).value();
+
+ auto mod_iter = m_mods.find(hash);
+ if (mod_iter == m_mods.end()) {
+ qWarning() << "Invalid project id from the API response.";
+ continue;
+ }
+
+ auto* mod = mod_iter.value();
+
+ try {
+ setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
+
+ modrinthCallback(pack, m_temp_versions.find(hash).value(), mod);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(mod);
+ }
+ }
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ return proj_task;
+}
+
+// Flame
+NetJob::Ptr EnsureMetadataTask::flameVersionsTask()
+{
+ auto* response = new QByteArray();
+
+ QList<uint> fingerprints;
+ for (auto& murmur : m_mods.keys()) {
+ fingerprints.push_back(murmur.toUInt());
+ }
+
+ auto ver_task = flame_api.matchFingerprints(fingerprints, response);
+
+ connect(ver_task.get(), &Task::succeeded, this, [this, response] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &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() << *response;
+
+ failed(parse_error.errorString());
+ return;
+ }
+
+ try {
+ auto doc_obj = Json::requireObject(doc);
+ auto data_obj = Json::requireObject(doc_obj, "data");
+ auto data_arr = Json::requireArray(data_obj, "exactMatches");
+
+ if (data_arr.isEmpty()) {
+ qWarning() << "No matches found for fingerprint search!";
+
+ return;
+ }
+
+ for (auto match : data_arr) {
+ auto match_obj = Json::ensureObject(match, {});
+ auto file_obj = Json::ensureObject(match_obj, "file", {});
+
+ if (match_obj.isEmpty() || file_obj.isEmpty()) {
+ qWarning() << "Fingerprint match is empty!";
+
+ return;
+ }
+
+ auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt());
+ auto mod = m_mods.find(fingerprint);
+ if (mod == m_mods.end()) {
+ qWarning() << "Invalid fingerprint from the API response.";
+ continue;
+ }
+
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*mod)->name()));
+
+ m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj));
+ }
+
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ return ver_task;
+}
+
+NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
+{
+ QHash<QString, QString> addonIds;
+ for (auto const& hash : m_mods.keys()) {
+ if (m_temp_versions.contains(hash)) {
+ auto const& data = m_temp_versions.find(hash).value();
+
+ auto id_str = data.addonId.toString();
+ if (!id_str.isEmpty())
+ addonIds.insert(data.addonId.toString(), hash);
+ }
+ }
+
+ auto response = new QByteArray();
+ NetJob::Ptr proj_task;
+
+ if (addonIds.isEmpty()) {
+ qWarning() << "No addonId found!";
+ } else if (addonIds.size() == 1) {
+ proj_task = flame_api.getProject(*addonIds.keyBegin(), response);
+ } else {
+ proj_task = flame_api.getProjects(addonIds.keys(), response);
+ }
+
+ // Prevents unfortunate timings when aborting the task
+ if (!proj_task)
+ return {};
+
+ connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*response, &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() << *response;
+ return;
+ }
+
+ try {
+ QJsonArray entries;
+ if (addonIds.size() == 1)
+ entries = { Json::requireObject(Json::requireObject(doc), "data") };
+ else
+ entries = Json::requireArray(Json::requireObject(doc), "data");
+
+ for (auto entry : entries) {
+ auto entry_obj = Json::requireObject(entry);
+
+ auto id = QString::number(Json::requireInteger(entry_obj, "id"));
+ auto hash = addonIds.find(id).value();
+ auto mod = m_mods.find(hash).value();
+
+ try {
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name()));
+
+ ModPlatform::IndexedPack pack;
+ FlameMod::loadIndexedPack(pack, entry_obj);
+
+ flameCallback(pack, m_temp_versions.find(hash).value(), mod);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(mod);
+ }
+ }
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ return proj_task;
+}
+
+void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod)
+{
+ // Prevent file name mismatch
+ ver.fileName = mod->fileinfo().fileName();
+ if (ver.fileName.endsWith(".disabled"))
+ ver.fileName.chop(9);
+
+ QDir tmp_index_dir(m_index_dir);
+
+ {
+ LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
+ QEventLoop loop;
+
+ QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
+
+ update_metadata.start();
+
+ if (!update_metadata.isFinished())
+ loop.exec();
+ }
+
+ auto metadata = Metadata::get(tmp_index_dir, pack.slug);
+ if (!metadata.isValid()) {
+ qCritical() << "Failed to generate metadata at last step!";
+ emitFail(mod);
+ return;
+ }
+
+ mod->setMetadata(metadata);
+
+ emitReady(mod);
+}
+
+void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod)
+{
+ try {
+ // Prevent file name mismatch
+ ver.fileName = mod->fileinfo().fileName();
+ if (ver.fileName.endsWith(".disabled"))
+ ver.fileName.chop(9);
+
+ QDir tmp_index_dir(m_index_dir);
+
+ {
+ LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
+ QEventLoop loop;
+
+ QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
+
+ update_metadata.start();
+
+ if (!update_metadata.isFinished())
+ loop.exec();
+ }
+
+ auto metadata = Metadata::get(tmp_index_dir, pack.slug);
+ if (!metadata.isValid()) {
+ qCritical() << "Failed to generate metadata at last step!";
+ emitFail(mod);
+ return;
+ }
+
+ mod->setMetadata(metadata);
+
+ emitReady(mod);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+
+ emitFail(mod);
+ }
+}
diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h
new file mode 100644
index 00000000..79db6976
--- /dev/null
+++ b/launcher/modplatform/EnsureMetadataTask.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "ModIndex.h"
+#include "tasks/SequentialTask.h"
+#include "net/NetJob.h"
+
+class Mod;
+class QDir;
+class MultipleOptionsTask;
+
+class EnsureMetadataTask : public Task {
+ Q_OBJECT
+
+ public:
+ EnsureMetadataTask(Mod*, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
+ EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
+
+ ~EnsureMetadataTask() = default;
+
+ public slots:
+ bool abort() override;
+ protected slots:
+ void executeTask() override;
+
+ private:
+ // FIXME: Move to their own namespace
+ auto modrinthVersionsTask() -> NetJob::Ptr;
+ auto modrinthProjectsTask() -> NetJob::Ptr;
+
+ auto flameVersionsTask() -> NetJob::Ptr;
+ auto flameProjectsTask() -> NetJob::Ptr;
+
+ // Helpers
+ void emitReady(Mod*);
+ void emitFail(Mod*);
+
+ auto getHash(Mod*) -> QString;
+
+ private slots:
+ void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*);
+ void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*);
+
+ signals:
+ void metadataReady(Mod*);
+ void metadataFailed(Mod*);
+
+ private:
+ QHash<QString, Mod*> m_mods;
+ QDir m_index_dir;
+ ModPlatform::Provider m_provider;
+
+ QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
+ NetJob* m_current_task;
+};
diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h
index 91b760df..4114d83c 100644
--- a/launcher/modplatform/ModAPI.h
+++ b/launcher/modplatform/ModAPI.h
@@ -1,9 +1,46 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
#pragma once
#include <QString>
#include <QList>
+#include <list>
#include "Version.h"
+#include "net/NetJob.h"
namespace ModPlatform {
class ListModel;
@@ -38,6 +75,9 @@ class ModAPI {
virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0;
virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0;
+ virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0;
+ virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0;
+
struct VersionSearchArgs {
QString addonId;
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index c27643af..dc297d03 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -54,13 +54,16 @@ struct IndexedVersion {
QVariant addonId;
QVariant fileId;
QString version;
- QVector<QString> mcVersion;
+ QString version_number = {};
+ QStringList mcVersion;
QString downloadUrl;
QString date;
QString fileName;
- QVector<QString> loaders = {};
+ QStringList loaders = {};
QString hash_type;
QString hash;
+ bool is_preferred = true;
+ QString changelog;
};
struct ExtraPackData {
@@ -76,6 +79,7 @@ struct IndexedPack {
QVariant addonId;
Provider provider;
QString name;
+ QString slug;
QString description;
QList<ModpackAuthor> authors;
QString logoName;
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index b4936bd8..5ed13470 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -36,7 +36,7 @@
#include "ATLPackInstallTask.h"
-#include <QtConcurrent/QtConcurrent>
+#include <QtConcurrent>
#include <quazip/quazip.h>
@@ -60,12 +60,13 @@ namespace ATLauncher {
static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version);
-PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version)
+PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version, InstallMode installMode)
{
m_support = support;
m_pack_name = packName;
m_pack_safe_name = packName.replace(QRegularExpression("[^A-Za-z0-9]"), "");
m_version_name = version;
+ m_install_mode = installMode;
}
bool PackInstallTask::abort()
@@ -117,9 +118,30 @@ void PackInstallTask::onDownloadSucceeded()
}
m_version = version;
- // Display install message if one exists
- if (!m_version.messages.install.isEmpty())
- m_support->displayMessage(m_version.messages.install);
+ // Derived from the installation mode
+ QString message;
+ bool resetDirectory;
+
+ switch (m_install_mode) {
+ case InstallMode::Reinstall:
+ case InstallMode::Update:
+ message = m_version.messages.update;
+ resetDirectory = true;
+ break;
+
+ case InstallMode::Install:
+ message = m_version.messages.install;
+ resetDirectory = false;
+ break;
+
+ default:
+ emitFailed(tr("Unsupported installation mode"));
+ break;
+ }
+
+ // Display message if one exists
+ if (!message.isEmpty())
+ m_support->displayMessage(message);
auto ver = getComponentVersion("net.minecraft", m_version.minecraft);
if (!ver) {
@@ -128,6 +150,10 @@ void PackInstallTask::onDownloadSucceeded()
}
minecraftVersion = ver;
+ if (resetDirectory) {
+ deleteExistingFiles();
+ }
+
if(m_version.noConfigs) {
downloadMods();
}
@@ -143,6 +169,116 @@ void PackInstallTask::onDownloadFailed(QString reason)
emitFailed(reason);
}
+void PackInstallTask::deleteExistingFiles()
+{
+ setStatus(tr("Deleting existing files..."));
+
+ // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/delete
+ VersionDeletes deletes;
+ deletes.folders.append(VersionDelete{ "root", "mods%s%" });
+ deletes.folders.append(VersionDelete{ "root", "configs%s%" });
+ deletes.folders.append(VersionDelete{ "root", "bin%s%" });
+
+ // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/keep
+ VersionKeeps keeps;
+ keeps.files.append(VersionKeep{ "root", "mods%s%PortalGunSounds.pak" });
+ keeps.folders.append(VersionKeep{ "root", "mods%s%rei_minimap%s%" });
+ keeps.folders.append(VersionKeep{ "root", "mods%s%VoxelMods%s%" });
+ keeps.files.append(VersionKeep{ "root", "config%s%NEI.cfg" });
+ keeps.files.append(VersionKeep{ "root", "options.txt" });
+ keeps.files.append(VersionKeep{ "root", "servers.dat" });
+
+ // Merge with version deletes and keeps
+ for (const auto& item : m_version.deletes.files)
+ deletes.files.append(item);
+ for (const auto& item : m_version.deletes.folders)
+ deletes.folders.append(item);
+ for (const auto& item : m_version.keeps.files)
+ keeps.files.append(item);
+ for (const auto& item : m_version.keeps.folders)
+ keeps.folders.append(item);
+
+ auto getPathForBase = [this](const QString& base) {
+ auto minecraftPath = FS::PathCombine(m_stagingPath, "minecraft");
+
+ if (base == "root") {
+ return minecraftPath;
+ }
+ else if (base == "config") {
+ return FS::PathCombine(minecraftPath, "config");
+ }
+ else {
+ qWarning() << "Unrecognised base path" << base;
+ return minecraftPath;
+ }
+ };
+
+ auto convertToSystemPath = [](const QString& path) {
+ auto t = path;
+ t.replace("%s%", QDir::separator());
+ return t;
+ };
+
+ auto shouldKeep = [keeps, getPathForBase, convertToSystemPath](const QString& fullPath) {
+ for (const auto& item : keeps.files) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto path = FS::PathCombine(basePath, targetPath);
+
+ if (fullPath == path) {
+ return true;
+ }
+ }
+
+ for (const auto& item : keeps.folders) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto path = FS::PathCombine(basePath, targetPath);
+
+ if (fullPath.startsWith(path)) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ // Keep track of files to delete
+ QSet<QString> filesToDelete;
+
+ for (const auto& item : deletes.files) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto fullPath = FS::PathCombine(basePath, targetPath);
+
+ if (shouldKeep(fullPath))
+ continue;
+
+ filesToDelete.insert(fullPath);
+ }
+
+ for (const auto& item : deletes.folders) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto fullPath = FS::PathCombine(basePath, targetPath);
+
+ QDirIterator it(fullPath, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ auto path = it.next();
+
+ if (shouldKeep(path))
+ continue;
+
+ filesToDelete.insert(path);
+ }
+ }
+
+ // Delete the files
+ for (const auto& item : filesToDelete) {
+ QFile::remove(item);
+ }
+}
+
QString PackInstallTask::getDirForModType(ModType type, QString raw)
{
switch (type) {
@@ -557,7 +693,11 @@ void PackInstallTask::extractConfigs()
return;
}
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/minecraft");
+#else
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/minecraft");
+#endif
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, [&]()
{
downloadMods();
@@ -702,7 +842,11 @@ void PackInstallTask::onModsDownloaded() {
jobPtr.reset();
if(!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) {
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), &PackInstallTask::extractMods, this, modsToExtract, modsToDecomp, modsToCopy);
+#else
m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy);
+#endif
connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onModsExtracted);
connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, [&]()
{
@@ -754,7 +898,7 @@ bool PackInstallTask::extractMods(
QString folderToExtract = "";
if(mod.type == ModType::Extract) {
folderToExtract = mod.extractFolder;
- folderToExtract.remove(QRegExp("^/"));
+ folderToExtract.remove(QRegularExpression("^/"));
}
qDebug() << "Extracting " + mod.file + " to " + extractToDir;
@@ -830,14 +974,14 @@ void PackInstallTask::install()
auto version = getVersionForLoader("net.minecraftforge");
if(version == Q_NULLPTR) return;
- components->setComponentVersion("net.minecraftforge", version, true);
+ components->setComponentVersion("net.minecraftforge", version);
}
else if(m_version.loader.type == QString("fabric"))
{
auto version = getVersionForLoader("net.fabricmc.fabric-loader");
if(version == Q_NULLPTR) return;
- components->setComponentVersion("net.fabricmc.fabric-loader", version, true);
+ components->setComponentVersion("net.fabricmc.fabric-loader", version);
}
else if(m_version.loader.type != QString())
{
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
index f55873e9..a7124d59 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
@@ -46,10 +46,16 @@
#include "minecraft/PackProfile.h"
#include "meta/Version.h"
-#include <nonstd/optional>
+#include <optional>
namespace ATLauncher {
+enum class InstallMode {
+ Install,
+ Reinstall,
+ Update,
+};
+
class UserInteractionSupport {
public:
@@ -75,7 +81,7 @@ class PackInstallTask : public InstanceTask
Q_OBJECT
public:
- explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version);
+ explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version, InstallMode installMode = InstallMode::Install);
virtual ~PackInstallTask(){}
bool canAbort() const override { return true; }
@@ -99,6 +105,7 @@ private:
bool createLibrariesComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
bool createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
+ void deleteExistingFiles();
void installConfigs();
void extractConfigs();
void downloadMods();
@@ -117,6 +124,7 @@ private:
NetJob::Ptr jobPtr;
QByteArray response;
+ InstallMode m_install_mode;
QString m_pack_name;
QString m_pack_safe_name;
QString m_version_name;
@@ -131,8 +139,8 @@ private:
Meta::VersionPtr minecraftVersion;
QMap<QString, Meta::VersionPtr> componentsToInstall;
- QFuture<nonstd::optional<QStringList>> m_extractFuture;
- QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ QFuture<std::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
QFuture<bool> m_modExtractFuture;
QFutureWatcher<bool> m_modExtractFutureWatcher;
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
index 3af02a09..5a458f4e 100644
--- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
@@ -224,6 +224,64 @@ static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a,
a.depends = Json::ensureString(obj, "depends", "");
}
+static void loadVersionKeep(ATLauncher::VersionKeep& k, QJsonObject& obj)
+{
+ k.base = Json::requireString(obj, "base");
+ k.target = Json::requireString(obj, "target");
+}
+
+static void loadVersionKeeps(ATLauncher::VersionKeeps& k, QJsonObject& obj)
+{
+ if (obj.contains("files")) {
+ auto files = Json::requireArray(obj, "files");
+ for (const auto keepRaw : files) {
+ auto keepObj = Json::requireObject(keepRaw);
+ ATLauncher::VersionKeep keep;
+ loadVersionKeep(keep, keepObj);
+ k.files.append(keep);
+ }
+ }
+
+ if (obj.contains("folders")) {
+ auto folders = Json::requireArray(obj, "folders");
+ for (const auto keepRaw : folders) {
+ auto keepObj = Json::requireObject(keepRaw);
+ ATLauncher::VersionKeep keep;
+ loadVersionKeep(keep, keepObj);
+ k.folders.append(keep);
+ }
+ }
+}
+
+static void loadVersionDelete(ATLauncher::VersionDelete& d, QJsonObject& obj)
+{
+ d.base = Json::requireString(obj, "base");
+ d.target = Json::requireString(obj, "target");
+}
+
+static void loadVersionDeletes(ATLauncher::VersionDeletes& d, QJsonObject& obj)
+{
+ if (obj.contains("files")) {
+ auto files = Json::requireArray(obj, "files");
+ for (const auto deleteRaw : files) {
+ auto deleteObj = Json::requireObject(deleteRaw);
+ ATLauncher::VersionDelete versionDelete;
+ loadVersionDelete(versionDelete, deleteObj);
+ d.files.append(versionDelete);
+ }
+ }
+
+ if (obj.contains("folders")) {
+ auto folders = Json::requireArray(obj, "folders");
+ for (const auto deleteRaw : folders) {
+ auto deleteObj = Json::requireObject(deleteRaw);
+ ATLauncher::VersionDelete versionDelete;
+ loadVersionDelete(versionDelete, deleteObj);
+ d.folders.append(versionDelete);
+ }
+ }
+}
+
void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
{
v.version = Json::requireString(obj, "version");
@@ -284,4 +342,10 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
auto messages = Json::ensureObject(obj, "messages");
loadVersionMessages(v.messages, messages);
+
+ auto keeps = Json::ensureObject(obj, "keeps");
+ loadVersionKeeps(v.keeps, keeps);
+
+ auto deletes = Json::ensureObject(obj, "deletes");
+ loadVersionDeletes(v.deletes, deletes);
}
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h
index 43510c50..571c976d 100644
--- a/launcher/modplatform/atlauncher/ATLPackManifest.h
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.h
@@ -150,6 +150,26 @@ struct VersionMessages
QString update;
};
+struct VersionKeep {
+ QString base;
+ QString target;
+};
+
+struct VersionKeeps {
+ QVector<VersionKeep> files;
+ QVector<VersionKeep> folders;
+};
+
+struct VersionDelete {
+ QString base;
+ QString target;
+};
+
+struct VersionDeletes {
+ QVector<VersionDelete> files;
+ QVector<VersionDelete> folders;
+};
+
struct PackVersionMainClass
{
QString mainClass;
@@ -178,6 +198,9 @@ struct PackVersion
QMap<QString, QString> colours;
QMap<QString, QString> warnings;
VersionMessages messages;
+
+ VersionKeeps keeps;
+ VersionDeletes deletes;
};
void loadVersion(PackVersion & v, QJsonObject & obj);
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
index a790ab9c..058d2471 100644
--- a/launcher/modplatform/flame/FileResolvingTask.cpp
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -7,10 +7,17 @@ Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAcc
: m_network(network), m_toProcess(toProcess)
{}
+bool Flame::FileResolvingTask::abort()
+{
+ if (m_dljob)
+ return m_dljob->abort();
+ return true;
+}
+
void Flame::FileResolvingTask::executeTask()
{
setStatus(tr("Resolving mod IDs..."));
- setProgress(0, m_toProcess.files.size());
+ setProgress(0, 3);
m_dljob = new NetJob("Mod id resolver", m_network);
result.reset(new QByteArray());
//build json data to send
@@ -29,6 +36,7 @@ void Flame::FileResolvingTask::executeTask()
void Flame::FileResolvingTask::netJobFinished()
{
+ setProgress(1, 3);
int index = 0;
// job to check modrinth for blocked projects
auto job = new NetJob("Modrinth check", m_network);
@@ -63,6 +71,7 @@ void Flame::FileResolvingTask::netJobFinished()
}
void Flame::FileResolvingTask::modrinthCheckFinished() {
+ setProgress(2, 3);
qDebug() << "Finished with blocked mods : " << blockedProjects.size();
for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) {
diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h
index 87981f0a..f71b87ce 100644
--- a/launcher/modplatform/flame/FileResolvingTask.h
+++ b/launcher/modplatform/flame/FileResolvingTask.h
@@ -13,6 +13,9 @@ public:
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess);
virtual ~FileResolvingTask() {};
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
const Flame::Manifest &getResults() const
{
return m_toProcess;
diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp
new file mode 100644
index 00000000..0ff04f72
--- /dev/null
+++ b/launcher/modplatform/flame/FlameAPI.cpp
@@ -0,0 +1,148 @@
+#include "FlameAPI.h"
+#include "FlameModIndex.h"
+
+#include "Application.h"
+#include "BuildConfig.h"
+#include "Json.h"
+
+#include "net/Upload.h"
+
+auto FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Flame::MatchFingerprints"), APPLICATION->network());
+
+ QJsonObject body_obj;
+ QJsonArray fingerprints_arr;
+ for (auto& fp : fingerprints) {
+ fingerprints_arr.append(QString("%1").arg(fp));
+ }
+
+ body_obj["fingerprints"] = fingerprints_arr;
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
+{
+ QEventLoop lock;
+ QString changelog;
+
+ auto* netJob = new NetJob(QString("Flame::FileChangelog"), APPLICATION->network());
+ auto* response = new QByteArray();
+ netJob->addNetAction(Net::Download::makeByteArray(
+ QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog")
+ .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))),
+ response));
+
+ QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &changelog] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from Flame::FileChangelog at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ netJob->failed(parse_error.errorString());
+ return;
+ }
+
+ changelog = Json::ensureString(doc.object(), "data");
+ });
+
+ QObject::connect(netJob, &NetJob::finished, [response, &lock] {
+ delete response;
+ lock.quit();
+ });
+
+ netJob->start();
+ lock.exec();
+
+ return changelog;
+}
+
+auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion
+{
+ QEventLoop loop;
+
+ auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network());
+ auto response = new QByteArray();
+ ModPlatform::IndexedVersion ver;
+
+ netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response));
+
+ QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ try {
+ auto obj = Json::requireObject(doc);
+ auto arr = Json::requireArray(obj, "data");
+
+ QJsonObject latest_file_obj;
+ ModPlatform::IndexedVersion ver_tmp;
+
+ for (auto file : arr) {
+ auto file_obj = Json::requireObject(file);
+ auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj);
+ if(file_tmp.date > ver_tmp.date) {
+ ver_tmp = file_tmp;
+ latest_file_obj = file_obj;
+ }
+ }
+
+ ver = FlameMod::loadIndexedPackVersion(latest_file_obj);
+ } catch (Json::JsonException& e) {
+ qCritical() << "Failed to parse response from a version request.";
+ qCritical() << e.what();
+ qDebug() << doc;
+ }
+ });
+
+ QObject::connect(netJob, &NetJob::finished, [response, netJob, &loop] {
+ netJob->deleteLater();
+ delete response;
+ loop.quit();
+ });
+
+ netJob->start();
+
+ loop.exec();
+
+ return ver;
+}
+
+auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob*
+{
+ auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network());
+
+ QJsonObject body_obj;
+ QJsonArray addons_arr;
+ for (auto& addonId : addonIds) {
+ addons_arr.append(addonId);
+ }
+
+ body_obj["modIds"] = addons_arr;
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
+ QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; });
+
+ return netJob;
+}
diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h
index 424153d2..336df387 100644
--- a/launcher/modplatform/flame/FlameAPI.h
+++ b/launcher/modplatform/flame/FlameAPI.h
@@ -4,6 +4,14 @@
#include "modplatform/helpers/NetworkModAPI.h"
class FlameAPI : public NetworkModAPI {
+ public:
+ auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr;
+ auto getModFileChangelog(int modId, int fileId) -> QString;
+
+ auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
+
+ auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
+
private:
inline auto getSortFieldInt(QString sortString) const -> int
{
@@ -59,7 +67,7 @@ class FlameAPI : public NetworkModAPI {
};
public:
- static auto getMappedModLoader(const ModLoaderTypes loaders) -> const int
+ static auto getMappedModLoader(const ModLoaderTypes loaders) -> int
{
// https://docs.curseforge.com/?http#tocS_ModLoaderType
if (loaders & Forge)
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp
new file mode 100644
index 00000000..8dd3a846
--- /dev/null
+++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp
@@ -0,0 +1,179 @@
+#include "FlameCheckUpdate.h"
+#include "FlameAPI.h"
+#include "FlameModIndex.h"
+
+#include <MurmurHash2.h>
+
+#include "FileSystem.h"
+#include "Json.h"
+
+#include "ModDownloadTask.h"
+
+static FlameAPI api;
+
+bool FlameCheckUpdate::abort()
+{
+ m_was_aborted = true;
+ if (m_net_job)
+ return m_net_job->abort();
+ return true;
+}
+
+ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info)
+{
+ ModPlatform::IndexedPack pack;
+
+ QEventLoop loop;
+
+ auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network());
+
+ auto response = new QByteArray();
+ auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString());
+ auto dl = Net::Download::makeByteArray(url, response);
+ get_project_job->addNetAction(dl);
+
+ QObject::connect(get_project_job, &NetJob::succeeded, [response, &pack]() {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ try {
+ auto doc_obj = Json::requireObject(doc);
+ auto data_obj = Json::requireObject(doc_obj, "data");
+ FlameMod::loadIndexedPack(pack, data_obj);
+ } catch (Json::JsonException& e) {
+ qWarning() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ QObject::connect(get_project_job, &NetJob::finished, [&loop, get_project_job] {
+ get_project_job->deleteLater();
+ loop.quit();
+ });
+
+ get_project_job->start();
+ loop.exec();
+
+ return pack;
+}
+
+ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId)
+{
+ ModPlatform::IndexedVersion ver;
+
+ QEventLoop loop;
+
+ auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network());
+
+ auto response = new QByteArray();
+ auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId));
+ auto dl = Net::Download::makeByteArray(url, response);
+ get_file_info_job->addNetAction(dl);
+
+ QObject::connect(get_file_info_job, &NetJob::succeeded, [response, &ver]() {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ try {
+ auto doc_obj = Json::requireObject(doc);
+ auto data_obj = Json::requireObject(doc_obj, "data");
+ ver = FlameMod::loadIndexedPackVersion(data_obj);
+ } catch (Json::JsonException& e) {
+ qWarning() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ QObject::connect(get_file_info_job, &NetJob::finished, [&loop, get_file_info_job] {
+ get_file_info_job->deleteLater();
+ loop.quit();
+ });
+
+ get_file_info_job->start();
+ loop.exec();
+
+ return ver;
+}
+
+/* Check for update:
+ * - Get latest version available
+ * - Compare hash of the latest version with the current hash
+ * - If equal, no updates, else, there's updates, so add to the list
+ * */
+void FlameCheckUpdate::executeTask()
+{
+ setStatus(tr("Preparing mods for CurseForge..."));
+
+ int i = 0;
+ for (auto* mod : m_mods) {
+ if (!mod->enabled()) {
+ emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
+ continue;
+ }
+
+ setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name()));
+ setProgress(i++, m_mods.size());
+
+ auto latest_ver = api.getLatestVersion({ mod->metadata()->project_id.toString(), m_game_versions, m_loaders });
+
+ // Check if we were aborted while getting the latest version
+ if (m_was_aborted) {
+ aborted();
+ return;
+ }
+
+ setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod->name()));
+
+ if (!latest_ver.addonId.isValid()) {
+ emit checkFailed(mod, tr("No valid version found for this mod. It's probably unavailable for the current game "
+ "version / mod loader."));
+ continue;
+ }
+
+ if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != mod->metadata()->file_id) {
+ auto pack = getProjectInfo(latest_ver);
+ auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString());
+ emit checkFailed(mod, tr("Mod has a new update available, but is not downloadable using CurseForge."), recover_url);
+
+ continue;
+ }
+
+ if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) {
+ // Fake pack with the necessary info to pass to the download task :)
+ ModPlatform::IndexedPack pack;
+ pack.name = mod->name();
+ pack.slug = mod->metadata()->slug;
+ pack.addonId = mod->metadata()->project_id;
+ pack.websiteUrl = mod->homeurl();
+ for (auto& author : mod->authors())
+ pack.authors.append({ author });
+ pack.description = mod->description();
+ pack.provider = ModPlatform::Provider::FLAME;
+
+ auto old_version = mod->version();
+ if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
+ auto current_ver = getFileInfo(latest_ver.addonId.toInt(), mod->metadata()->file_id.toInt());
+ old_version = current_ver.version;
+ }
+
+ auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder);
+ m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version,
+ api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
+ ModPlatform::Provider::FLAME, download_task);
+ }
+ }
+
+ emitSucceeded();
+}
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h
new file mode 100644
index 00000000..163c706c
--- /dev/null
+++ b/launcher/modplatform/flame/FlameCheckUpdate.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "Application.h"
+#include "modplatform/CheckUpdateTask.h"
+#include "net/NetJob.h"
+
+class FlameCheckUpdate : public CheckUpdateTask {
+ Q_OBJECT
+
+ public:
+ FlameCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ : CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
+ {}
+
+ public slots:
+ bool abort() override;
+
+ protected slots:
+ void executeTask() override;
+
+ private:
+ NetJob* m_net_job = nullptr;
+
+ bool m_was_aborted = false;
+};
diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp
index b99bfb26..746018e2 100644
--- a/launcher/modplatform/flame/FlameModIndex.cpp
+++ b/launcher/modplatform/flame/FlameModIndex.cpp
@@ -7,20 +7,22 @@
#include "net/NetJob.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
+static FlameAPI api;
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
pack.provider = ModPlatform::Provider::FLAME;
pack.name = Json::requireString(obj, "name");
+ pack.slug = Json::requireString(obj, "slug");
pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
pack.description = Json::ensureString(obj, "summary", "");
- QJsonObject logo = Json::requireObject(obj, "logo");
- pack.logoName = Json::requireString(logo, "title");
- pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
+ QJsonObject logo = Json::ensureObject(obj, "logo");
+ pack.logoName = Json::ensureString(logo, "title");
+ pack.logoUrl = Json::ensureString(logo, "thumbnailUrl");
- auto authors = Json::requireArray(obj, "authors");
+ auto authors = Json::ensureArray(obj, "authors");
for (auto authorIter : authors) {
auto author = Json::requireObject(authorIter);
ModPlatform::ModpackAuthor packAuthor;
@@ -91,7 +93,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
pack.versionsLoaded = true;
}
-auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion
+auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion
{
auto versionArray = Json::requireArray(obj, "gameVersions");
if (versionArray.isEmpty()) {
@@ -110,7 +112,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV
file.fileId = Json::requireInteger(obj, "id");
file.date = Json::requireString(obj, "fileDate");
file.version = Json::requireString(obj, "displayName");
- file.downloadUrl = Json::requireString(obj, "downloadUrl");
+ file.downloadUrl = Json::ensureString(obj, "downloadUrl");
file.fileName = Json::requireString(obj, "fileName");
auto hash_list = Json::ensureArray(obj, "hashes");
@@ -124,5 +126,9 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV
break;
}
}
+
+ if(load_changelog)
+ file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt());
+
return file;
}
diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h
index 9c6c1c6c..a839dd83 100644
--- a/launcher/modplatform/flame/FlameModIndex.h
+++ b/launcher/modplatform/flame/FlameModIndex.h
@@ -17,6 +17,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
BaseInstance* inst);
-auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion;
+auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion;
} // namespace FlameMod
diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h
index 26a48d1c..677db1c3 100644
--- a/launcher/modplatform/flame/PackManifest.h
+++ b/launcher/modplatform/flame/PackManifest.h
@@ -1,3 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
#pragma once
#include <QString>
@@ -25,7 +60,7 @@ struct File
bool resolved = false;
QString fileName;
QUrl url;
- QString targetFolder = QLatin1Literal("mods");
+ QString targetFolder = QStringLiteral("mods");
enum class Type
{
Unknown,
diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp
index d7abd10f..90edfe31 100644
--- a/launcher/modplatform/helpers/NetworkModAPI.cpp
+++ b/launcher/modplatform/helpers/NetworkModAPI.cpp
@@ -33,18 +33,14 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const
void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack)
{
- auto id_str = pack.addonId.toString();
- auto netJob = new NetJob(QString("%1::ModInfo").arg(id_str), APPLICATION->network());
- auto searchUrl = getModInfoURL(id_str);
-
auto response = new QByteArray();
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
+ auto job = getProject(pack.addonId.toString(), response);
- QObject::connect(netJob, &NetJob::succeeded, [response, &pack, caller] {
+ QObject::connect(job, &NetJob::succeeded, caller, [caller, &pack, response] {
QJsonParseError parse_error{};
- auto doc = QJsonDocument::fromJson(*response, &parse_error);
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response for " << pack.name << " at " << parse_error.offset
+ qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
@@ -53,7 +49,7 @@ void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pac
caller->infoRequestFinished(doc, pack);
});
- netJob->start();
+ job->start();
}
void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const
@@ -83,3 +79,18 @@ void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) co
netJob->start();
}
+
+auto NetworkModAPI::getProject(QString addonId, QByteArray* response) const -> NetJob*
+{
+ auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network());
+ auto searchUrl = getModInfoURL(addonId);
+
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
+
+ QObject::connect(netJob, &NetJob::finished, [response, netJob] {
+ netJob->deleteLater();
+ delete response;
+ });
+
+ return netJob;
+}
diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h
index 87d77ad1..989bcec4 100644
--- a/launcher/modplatform/helpers/NetworkModAPI.h
+++ b/launcher/modplatform/helpers/NetworkModAPI.h
@@ -8,6 +8,8 @@ class NetworkModAPI : public ModAPI {
void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override;
void getVersions(CallerType* caller, VersionSearchArgs&& args) const override;
+ auto getProject(QString addonId, QByteArray* response) const -> NetJob* override;
+
protected:
virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0;
virtual auto getModInfoURL(QString& id) const -> QString = 0;
diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
index 961fe868..4da6a866 100644
--- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
+++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
@@ -1,3 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
#include "PackFetchTask.h"
#include "PrivatePackManager.h"
@@ -103,7 +138,7 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac
if(!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol))
{
- auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:3d!").arg(errorMsg, errorLine, errorCol);
+ auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:%3!").arg(errorMsg).arg(errorLine).arg(errorCol);
qWarning() << fullErrMsg;
data.clear();
return false;
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
index c63a9f1e..83e14969 100644
--- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
@@ -1,3 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
#include "PackInstallTask.h"
#include <QtConcurrent>
@@ -88,7 +123,11 @@ void PackInstallTask::unzip()
return;
}
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip");
+#else
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
+#endif
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled);
m_extractFutureWatcher.setFuture(m_extractFuture);
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h
index a7395220..da4c0da5 100644
--- a/launcher/modplatform/legacy_ftb/PackInstallTask.h
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h
@@ -10,7 +10,7 @@
#include "net/NetJob.h"
-#include <nonstd/optional>
+#include <optional>
namespace LegacyFTB {
@@ -46,8 +46,8 @@ private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
bool abortable = false;
std::unique_ptr<QuaZip> m_packZip;
- QFuture<nonstd::optional<QStringList>> m_extractFuture;
- QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ QFuture<std::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
NetJob::Ptr netJobContainer;
QString archivePath;
diff --git a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp
index 501e6003..1a81f026 100644
--- a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp
+++ b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp
@@ -1,3 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
#include "PrivatePackManager.h"
#include <QDebug>
@@ -10,7 +45,13 @@ void PrivatePackManager::load()
{
try
{
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ auto foo = QString::fromUtf8(FS::read(m_filename)).split('\n', Qt::SkipEmptyParts);
+ currentPacks = QSet<QString>(foo.begin(), foo.end());
+#else
currentPacks = QString::fromUtf8(FS::read(m_filename)).split('\n', QString::SkipEmptyParts).toSet();
+#endif
+
dirty = false;
}
catch(...)
@@ -28,7 +69,7 @@ void PrivatePackManager::save() const
}
try
{
- QStringList list = currentPacks.toList();
+ QStringList list = currentPacks.values();
FS::write(m_filename, list.join('\n').toUtf8());
dirty = false;
}
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
index c324ffda..3c15667c 100644
--- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
+++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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
@@ -40,103 +42,193 @@
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
+#include "modplatform/flame/PackManifest.h"
#include "net/ChecksumValidator.h"
#include "settings/INISettingsObject.h"
-#include "BuildConfig.h"
#include "Application.h"
+#include "BuildConfig.h"
+#include "ui/dialogs/BlockedModsDialog.h"
namespace ModpacksCH {
-PackInstallTask::PackInstallTask(Modpack pack, QString version)
-{
- m_pack = pack;
- m_version_name = version;
-}
+PackInstallTask::PackInstallTask(Modpack pack, QString version, QWidget* parent)
+ : m_pack(std::move(pack)), m_version_name(std::move(version)), m_parent(parent)
+{}
bool PackInstallTask::abort()
{
- if(abortable)
- {
- return jobPtr->abort();
- }
- return false;
+ bool aborted = true;
+
+ if (m_net_job)
+ aborted &= m_net_job->abort();
+ if (m_mod_id_resolver_task)
+ aborted &= m_mod_id_resolver_task->abort();
+
+ // FIXME: This should be 'emitAborted()', but InstanceStaging doesn't connect to the abort signal yet...
+ if (aborted)
+ emitFailed(tr("Aborted"));
+
+ return aborted;
}
void PackInstallTask::executeTask()
{
- // Find pack version
- bool found = false;
- VersionInfo version;
+ setStatus(tr("Getting the manifest..."));
- for(auto vInfo : m_pack.versions) {
- if (vInfo.name == m_version_name) {
- found = true;
- version = vInfo;
- break;
- }
- }
+ // Find pack version
+ auto version_it = std::find_if(m_pack.versions.constBegin(), m_pack.versions.constEnd(),
+ [this](ModpacksCH::VersionInfo const& a) { return a.name == m_version_name; });
- if(!found) {
+ if (version_it == m_pack.versions.constEnd()) {
emitFailed(tr("Failed to find pack version %1").arg(m_version_name));
return;
}
- auto *netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network());
+ auto version = *version_it;
+
+ auto* netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network());
+
auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2").arg(m_pack.id).arg(version.id);
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
- jobPtr = netJob;
- jobPtr->start();
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &m_response));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onManifestDownloadSucceeded);
+ QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onManifestDownloadFailed);
+ QObject::connect(netJob, &NetJob::progress, this, &PackInstallTask::setProgress);
- QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
- QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
+ m_net_job = netJob;
+
+ netJob->start();
}
-void PackInstallTask::onDownloadSucceeded()
+void PackInstallTask::onManifestDownloadSucceeded()
{
- jobPtr.reset();
-
- QJsonParseError parse_error;
- QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
- if(parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << response;
+ m_net_job.reset();
+
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << m_response;
return;
}
- auto obj = doc.object();
-
ModpacksCH::Version version;
- try
- {
+ try {
+ auto obj = Json::requireObject(doc);
ModpacksCH::loadVersion(version, obj);
- }
- catch (const JSONValidationError &e)
- {
+ } catch (const JSONValidationError& e) {
emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
return;
}
+
m_version = version;
- downloadPack();
+ resolveMods();
}
-void PackInstallTask::onDownloadFailed(QString reason)
+void PackInstallTask::resolveMods()
{
- jobPtr.reset();
- emitFailed(reason);
+ setStatus(tr("Resolving mods..."));
+ setProgress(0, 100);
+
+ m_file_id_map.clear();
+
+ Flame::Manifest manifest;
+ int index = 0;
+
+ for (auto const& file : m_version.files) {
+ if (!file.serverOnly && file.url.isEmpty()) {
+ if (file.curseforge.file_id <= 0) {
+ emitFailed(tr("Invalid manifest: There's no information available to download the file '%1'!").arg(file.name));
+ return;
+ }
+
+ Flame::File flame_file;
+ flame_file.projectId = file.curseforge.project_id;
+ flame_file.fileId = file.curseforge.file_id;
+ flame_file.hash = file.sha1;
+
+ manifest.files.insert(flame_file.fileId, flame_file);
+ m_file_id_map.append(flame_file.fileId);
+ } else {
+ m_file_id_map.append(-1);
+ }
+
+ index++;
+ }
+
+ m_mod_id_resolver_task = new Flame::FileResolvingTask(APPLICATION->network(), manifest);
+
+ connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::succeeded, this, &PackInstallTask::onResolveModsSucceeded);
+ connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::failed, this, &PackInstallTask::onResolveModsFailed);
+ connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::progress, this, &PackInstallTask::setProgress);
+
+ m_mod_id_resolver_task->start();
+}
+
+void PackInstallTask::onResolveModsSucceeded()
+{
+ m_abortable = false;
+
+ QString text;
+ QList<QUrl> urls;
+ auto anyBlocked = false;
+
+ Flame::Manifest results = m_mod_id_resolver_task->getResults();
+ for (int index = 0; index < m_file_id_map.size(); index++) {
+ auto const file_id = m_file_id_map.at(index);
+ if (file_id < 0)
+ continue;
+
+ Flame::File results_file = results.files[file_id];
+ VersionFile& local_file = m_version.files[index];
+
+ // First check for blocked mods
+ if (!results_file.resolved || results_file.url.isEmpty()) {
+ QString type(local_file.type);
+
+ type[0] = type[0].toUpper();
+ text += QString("%1: %2 - <a href='%3'>%3</a><br/>").arg(type, local_file.name, results_file.websiteUrl);
+ urls.append(QUrl(results_file.websiteUrl));
+ anyBlocked = true;
+ } else {
+ local_file.url = results_file.url.toString();
+ }
+ }
+
+ m_mod_id_resolver_task.reset();
+
+ if (anyBlocked) {
+ qDebug() << "Blocked files found, displaying file list";
+
+ auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked files found"),
+ tr("The following files are not available for download in third party launchers.<br/>"
+ "You will need to manually download them and add them to the instance."),
+ text,
+ urls);
+
+ if (message_dialog->exec() == QDialog::Accepted)
+ downloadPack();
+ else
+ abort();
+ } else {
+ downloadPack();
+ }
}
void PackInstallTask::downloadPack()
{
setStatus(tr("Downloading mods..."));
- jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
- for(auto file : m_version.files) {
- if(file.serverOnly) continue;
+ auto* jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
+ for (auto const& file : m_version.files) {
+ if (file.serverOnly || file.url.isEmpty())
+ continue;
- QFileInfo fileName(file.name);
- auto cacheName = fileName.completeBaseName() + "-" + file.sha1 + "." + fileName.suffix();
+ QFileInfo file_info(file.name);
+ auto cacheName = file_info.completeBaseName() + "-" + file.sha1 + "." + file_info.suffix();
auto entry = APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", cacheName);
entry->setStale(true);
@@ -144,58 +236,64 @@ void PackInstallTask::downloadPack()
auto relpath = FS::PathCombine("minecraft", file.path, file.name);
auto path = FS::PathCombine(m_stagingPath, relpath);
- if (filesToCopy.contains(path)) {
+ if (m_files_to_copy.contains(path)) {
qWarning() << "Ignoring" << file.url << "as a file of that path is already downloading.";
continue;
}
+
qDebug() << "Will download" << file.url << "to" << path;
- filesToCopy[path] = entry->getFullPath();
+ m_files_to_copy[path] = entry->getFullPath();
auto dl = Net::Download::makeCached(file.url, entry);
if (!file.sha1.isEmpty()) {
auto rawSha1 = QByteArray::fromHex(file.sha1.toLatin1());
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
}
+
jobPtr->addNetAction(dl);
}
- connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
- {
- abortable = false;
- jobPtr.reset();
- install();
- });
- connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
- {
- abortable = false;
- jobPtr.reset();
- emitFailed(reason);
- });
- connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
- {
- abortable = true;
- setProgress(current, total);
- });
+ connect(jobPtr, &NetJob::succeeded, this, &PackInstallTask::onModDownloadSucceeded);
+ connect(jobPtr, &NetJob::failed, this, &PackInstallTask::onModDownloadFailed);
+ connect(jobPtr, &NetJob::progress, this, &PackInstallTask::setProgress);
+ m_net_job = jobPtr;
jobPtr->start();
+
+ m_abortable = true;
+}
+
+void PackInstallTask::onModDownloadSucceeded()
+{
+ m_net_job.reset();
+ install();
}
void PackInstallTask::install()
{
- setStatus(tr("Copying modpack files"));
+ setStatus(tr("Copying modpack files..."));
+ setProgress(0, m_files_to_copy.size());
+ QCoreApplication::processEvents();
+
+ m_abortable = false;
- for (auto iter = filesToCopy.begin(); iter != filesToCopy.end(); iter++) {
- auto &to = iter.key();
- auto &from = iter.value();
+ int i = 0;
+ for (auto iter = m_files_to_copy.constBegin(); iter != m_files_to_copy.constEnd(); iter++) {
+ auto& to = iter.key();
+ auto& from = iter.value();
FS::copy fileCopyOperation(from, to);
- if(!fileCopyOperation()) {
+ if (!fileCopyOperation()) {
qWarning() << "Failed to copy" << from << "to" << to;
emitFailed(tr("Failed to copy files"));
return;
}
+
+ setProgress(i++, m_files_to_copy.size());
+ QCoreApplication::processEvents();
}
- setStatus(tr("Installing modpack"));
+ setStatus(tr("Installing modpack..."));
+ QCoreApplication::processEvents();
auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
@@ -205,21 +303,21 @@ void PackInstallTask::install()
auto components = instance.getPackProfile();
components->buildingFromScratch();
- for(auto target : m_version.targets) {
- if(target.type == "game" && target.name == "minecraft") {
+ for (auto target : m_version.targets) {
+ if (target.type == "game" && target.name == "minecraft") {
components->setComponentVersion("net.minecraft", target.version, true);
break;
}
}
- for(auto target : m_version.targets) {
- if(target.type != "modloader") continue;
+ for (auto target : m_version.targets) {
+ if (target.type != "modloader")
+ continue;
- if(target.name == "forge") {
- components->setComponentVersion("net.minecraftforge", target.version, true);
- }
- else if(target.name == "fabric") {
- components->setComponentVersion("net.fabricmc.fabric-loader", target.version, true);
+ if (target.name == "forge") {
+ components->setComponentVersion("net.minecraftforge", target.version);
+ } else if (target.name == "fabric") {
+ components->setComponentVersion("net.fabricmc.fabric-loader", target.version);
}
}
@@ -245,4 +343,20 @@ void PackInstallTask::install()
emitSucceeded();
}
+void PackInstallTask::onManifestDownloadFailed(QString reason)
+{
+ m_net_job.reset();
+ emitFailed(reason);
+}
+void PackInstallTask::onResolveModsFailed(QString reason)
+{
+ m_net_job.reset();
+ emitFailed(reason);
}
+void PackInstallTask::onModDownloadFailed(QString reason)
+{
+ m_net_job.reset();
+ emitFailed(reason);
+}
+
+} // namespace ModpacksCH
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h
index ff59b695..e63ca0df 100644
--- a/launcher/modplatform/modpacksch/FTBPackInstallTask.h
+++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.h
@@ -1,18 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-only
/*
- * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
#pragma once
@@ -20,44 +40,60 @@
#include "FTBPackManifest.h"
#include "InstanceTask.h"
+#include "QObjectPtr.h"
+#include "modplatform/flame/FileResolvingTask.h"
#include "net/NetJob.h"
+#include <QWidget>
+
namespace ModpacksCH {
-class PackInstallTask : public InstanceTask
+class PackInstallTask final : public InstanceTask
{
Q_OBJECT
public:
- explicit PackInstallTask(Modpack pack, QString version);
- virtual ~PackInstallTask(){}
+ explicit PackInstallTask(Modpack pack, QString version, QWidget* parent = nullptr);
+ ~PackInstallTask() override = default;
- bool canAbort() const override { return true; }
+ bool canAbort() const override { return m_abortable; }
bool abort() override;
protected:
- virtual void executeTask() override;
+ void executeTask() override;
private slots:
- void onDownloadSucceeded();
- void onDownloadFailed(QString reason);
+ void onManifestDownloadSucceeded();
+ void onResolveModsSucceeded();
+ void onModDownloadSucceeded();
+
+ void onManifestDownloadFailed(QString reason);
+ void onResolveModsFailed(QString reason);
+ void onModDownloadFailed(QString reason);
private:
+ void resolveMods();
void downloadPack();
void install();
private:
- bool abortable = false;
+ bool m_abortable = true;
+
+ NetJob::Ptr m_net_job = nullptr;
+ shared_qobject_ptr<Flame::FileResolvingTask> m_mod_id_resolver_task = nullptr;
+
+ QList<int> m_file_id_map;
- NetJob::Ptr jobPtr;
- QByteArray response;
+ QByteArray m_response;
Modpack m_pack;
QString m_version_name;
Version m_version;
- QMap<QString, QString> filesToCopy;
+ QMap<QString, QString> m_files_to_copy;
+ //FIXME: nuke
+ QWidget* m_parent;
};
}
diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.cpp b/launcher/modplatform/modpacksch/FTBPackManifest.cpp
index e2d47a5b..421527ae 100644
--- a/launcher/modplatform/modpacksch/FTBPackManifest.cpp
+++ b/launcher/modplatform/modpacksch/FTBPackManifest.cpp
@@ -1,18 +1,37 @@
+// SPDX-License-Identifier: GPL-3.0-only
/*
- * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
#include "FTBPackManifest.h"
@@ -127,13 +146,16 @@ static void loadVersionFile(ModpacksCH::VersionFile & a, QJsonObject & obj)
a.path = Json::requireString(obj, "path");
a.name = Json::requireString(obj, "name");
a.version = Json::requireString(obj, "version");
- a.url = Json::requireString(obj, "url");
+ a.url = Json::ensureString(obj, "url"); // optional
a.sha1 = Json::requireString(obj, "sha1");
a.size = Json::requireInteger(obj, "size");
a.clientOnly = Json::requireBoolean(obj, "clientonly");
a.serverOnly = Json::requireBoolean(obj, "serveronly");
a.optional = Json::requireBoolean(obj, "optional");
a.updated = Json::requireInteger(obj, "updated");
+ auto curseforgeObj = Json::ensureObject(obj, "curseforge"); // optional
+ a.curseforge.project_id = Json::ensureInteger(curseforgeObj, "project");
+ a.curseforge.file_id = Json::ensureInteger(curseforgeObj, "file");
}
void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj)
diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.h b/launcher/modplatform/modpacksch/FTBPackManifest.h
index da45d8ac..a8b6f35e 100644
--- a/launcher/modplatform/modpacksch/FTBPackManifest.h
+++ b/launcher/modplatform/modpacksch/FTBPackManifest.h
@@ -1,18 +1,37 @@
+// SPDX-License-Identifier: GPL-3.0-only
/*
- * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright 2020 Petr Mrazek <peterix@gmail.com>
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * 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.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2020 Petr Mrazek <peterix@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
#pragma once
@@ -97,6 +116,12 @@ struct VersionTarget
int64_t updated;
};
+struct VersionFileCurseForge
+{
+ int project_id;
+ int file_id;
+};
+
struct VersionFile
{
int id;
@@ -111,6 +136,7 @@ struct VersionFile
bool serverOnly;
bool optional;
int64_t updated;
+ VersionFileCurseForge curseforge;
};
struct Version
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp
new file mode 100644
index 00000000..747cf4c3
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp
@@ -0,0 +1,108 @@
+#include "ModrinthAPI.h"
+
+#include "Application.h"
+#include "Json.h"
+#include "net/Upload.h"
+
+auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersion"), APPLICATION->network());
+
+ netJob->addNetAction(Net::Download::makeByteArray(
+ QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersions"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ Json::writeStringList(body_obj, "hashes", hashes);
+ Json::writeString(body_obj, "algorithm", hash_format);
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::latestVersion(QString hash,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
+
+ QStringList game_versions;
+ for (auto& ver : mcVersions) {
+ game_versions.append(ver.toString());
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(
+ QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::latestVersions(const QStringList& hashes,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ Json::writeStringList(body_obj, "hashes", hashes);
+ Json::writeString(body_obj, "algorithm", hash_format);
+
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
+
+ QStringList game_versions;
+ for (auto& ver : mcVersions) {
+ game_versions.append(ver.toString());
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob*
+{
+ auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network());
+ auto searchUrl = getMultipleModInfoURL(addonIds);
+
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
+
+ QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
+
+ return netJob;
+}
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
index 89e52d6c..e1a18681 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.h
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -27,6 +27,29 @@
class ModrinthAPI : public NetworkModAPI {
public:
+ auto currentVersion(QString hash,
+ QString hash_format,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto currentVersions(const QStringList& hashes,
+ QString hash_format,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto latestVersion(QString hash,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto latestVersions(const QStringList& hashes,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
+
+ public:
inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; };
static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList
@@ -81,6 +104,11 @@ class ModrinthAPI : public NetworkModAPI {
return BuildConfig.MODRINTH_PROD_URL + "/project/" + id;
};
+ inline auto getMultipleModInfoURL(QStringList ids) const -> QString
+ {
+ return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\""));
+ };
+
inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
{
return QString(BuildConfig.MODRINTH_PROD_URL +
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
new file mode 100644
index 00000000..79d8edf7
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -0,0 +1,174 @@
+#include "ModrinthCheckUpdate.h"
+#include "ModrinthAPI.h"
+#include "ModrinthPackIndex.h"
+
+#include "FileSystem.h"
+#include "Json.h"
+
+#include "ModDownloadTask.h"
+
+static ModrinthAPI api;
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+bool ModrinthCheckUpdate::abort()
+{
+ if (m_net_job)
+ return m_net_job->abort();
+ return true;
+}
+
+/* Check for update:
+ * - Get latest version available
+ * - Compare hash of the latest version with the current hash
+ * - If equal, no updates, else, there's updates, so add to the list
+ * */
+void ModrinthCheckUpdate::executeTask()
+{
+ setStatus(tr("Preparing mods for Modrinth..."));
+ setProgress(0, 3);
+
+ QHash<QString, Mod*> mappings;
+
+ // Create all hashes
+ QStringList hashes;
+ auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+ for (auto* mod : m_mods) {
+ if (!mod->enabled()) {
+ emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
+ continue;
+ }
+
+ auto hash = mod->metadata()->hash;
+
+ // Sadly the API can only handle one hash type per call, se we
+ // need to generate a new hash if the current one is innadequate
+ // (though it will rarely happen, if at all)
+ if (mod->metadata()->hash_format != best_hash_type) {
+ QByteArray jar_data;
+
+ try {
+ jar_data = FS::read(mod->fileinfo().absoluteFilePath());
+ } catch (FS::FileSystemException& e) {
+ qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name());
+ qCritical() << QString("Reason: ") << e.cause();
+
+ failed(e.what());
+ return;
+ }
+
+ hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, best_hash_type).toHex());
+ }
+
+ hashes.append(hash);
+ mappings.insert(hash, mod);
+ }
+
+ auto* response = new QByteArray();
+ auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
+
+ QEventLoop lock;
+
+ 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;
+
+ failed(parse_error.errorString());
+ return;
+ }
+
+ setStatus(tr("Parsing the API response from Modrinth..."));
+ setProgress(2, 3);
+
+ try {
+ for (auto hash : mappings.keys()) {
+ auto project_obj = doc[hash].toObject();
+
+ // If the returned project is empty, but we have Modrinth metadata,
+ // it means this specific version is not available
+ if (project_obj.isEmpty()) {
+ qDebug() << "Mod " << mappings.find(hash).value()->name() << " got an empty response.";
+ qDebug() << "Hash: " << hash;
+
+ emit checkFailed(
+ mappings.find(hash).value(),
+ tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader."));
+
+ continue;
+ }
+
+ // Sometimes a version may have multiple files, one with "forge" and one with "fabric",
+ // so we may want to filter it
+ QString loader_filter;
+ static auto flags = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt };
+ for (auto flag : flags) {
+ if (m_loaders.testFlag(flag)) {
+ loader_filter = api.getModLoaderString(flag);
+ break;
+ }
+ }
+
+ // Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
+ // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter
+ // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
+ // Such is the pain of having arbitrary files for a given version .-.
+
+ auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, best_hash_type, loader_filter);
+ if (project_ver.downloadUrl.isEmpty()) {
+ qCritical() << "Modrinth mod without download url!";
+ qCritical() << project_ver.fileName;
+
+ emit checkFailed(mappings.find(hash).value(), tr("Mod has an empty download URL"));
+
+ continue;
+ }
+
+ auto mod_iter = mappings.find(hash);
+ if (mod_iter == mappings.end()) {
+ qCritical() << "Failed to remap mod from Modrinth!";
+ continue;
+ }
+ auto mod = *mod_iter;
+
+ auto key = project_ver.hash;
+ if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) {
+ if (mod->version() == project_ver.version_number)
+ continue;
+
+ // Fake pack with the necessary info to pass to the download task :)
+ ModPlatform::IndexedPack pack;
+ pack.name = mod->name();
+ pack.slug = mod->metadata()->slug;
+ pack.addonId = mod->metadata()->project_id;
+ pack.websiteUrl = mod->homeurl();
+ for (auto& author : mod->authors())
+ pack.authors.append({ author });
+ pack.description = mod->description();
+ pack.provider = ModPlatform::Provider::MODRINTH;
+
+ auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder);
+
+ m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
+ ModPlatform::Provider::MODRINTH, download_task);
+ }
+ }
+ } catch (Json::JsonException& e) {
+ failed(e.cause() + " : " + e.what());
+ }
+ });
+
+ connect(job.get(), &Task::finished, &lock, &QEventLoop::quit);
+
+ setStatus(tr("Waiting for the API response from Modrinth..."));
+ setProgress(1, 3);
+
+ m_net_job = job.get();
+ job->start();
+
+ lock.exec();
+
+ emitSucceeded();
+}
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
new file mode 100644
index 00000000..abf8ada1
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "Application.h"
+#include "modplatform/CheckUpdateTask.h"
+#include "net/NetJob.h"
+
+class ModrinthCheckUpdate : public CheckUpdateTask {
+ Q_OBJECT
+
+ public:
+ ModrinthCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ : CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
+ {}
+
+ public slots:
+ bool abort() override;
+
+ protected slots:
+ void executeTask() override;
+
+ private:
+ NetJob* m_net_job = nullptr;
+};
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
index b6f5490a..e50dd96d 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -29,13 +29,16 @@ static ModPlatform::ProviderCapabilities ProviderCaps;
void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
- pack.addonId = Json::requireString(obj, "project_id");
+ pack.addonId = Json::ensureString(obj, "project_id");
+ if (pack.addonId.toString().isEmpty())
+ pack.addonId = Json::requireString(obj, "id");
+
pack.provider = ModPlatform::Provider::MODRINTH;
pack.name = Json::requireString(obj, "title");
- QString slug = Json::ensureString(obj, "slug", "");
- if (!slug.isEmpty())
- pack.websiteUrl = "https://modrinth.com/mod/" + Json::ensureString(obj, "slug", "");
+ pack.slug = Json::ensureString(obj, "slug", "");
+ if (!pack.slug.isEmpty())
+ pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug;
else
pack.websiteUrl = "";
@@ -45,7 +48,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.logoName = pack.addonId.toString();
ModPlatform::ModpackAuthor modAuthor;
- modAuthor.name = Json::requireString(obj, "author");
+ modAuthor.name = Json::ensureString(obj, "author", QObject::tr("No author(s)"));
modAuthor.url = api.getAuthorURL(modAuthor.name);
pack.authors.append(modAuthor);
@@ -111,7 +114,7 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
pack.versionsLoaded = true;
}
-auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedVersion
+auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_type, QString preferred_file_name) -> ModPlatform::IndexedVersion
{
ModPlatform::IndexedVersion file;
@@ -130,6 +133,8 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
file.loaders.append(loader.toString());
}
file.version = Json::requireString(obj, "name");
+ file.version_number = Json::requireString(obj, "version_number");
+ file.changelog = Json::requireString(obj, "changelog");
auto files = Json::requireArray(obj, "files");
int i = 0;
@@ -142,6 +147,11 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
auto parent = files[i].toObject();
auto fileName = Json::requireString(parent, "filename");
+ if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name)) {
+ file.is_preferred = true;
+ break;
+ }
+
// Grab the primary file, if available
if (Json::requireBoolean(parent, "primary"))
break;
@@ -153,13 +163,20 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
if (parent.contains("url")) {
file.downloadUrl = Json::requireString(parent, "url");
file.fileName = Json::requireString(parent, "filename");
+ file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
auto hash_list = Json::requireObject(parent, "hashes");
- auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH);
- for (auto& hash_type : hash_types) {
- if (hash_list.contains(hash_type)) {
- file.hash = Json::requireString(hash_list, hash_type);
- file.hash_type = hash_type;
- break;
+
+ if (hash_list.contains(preferred_hash_type)) {
+ file.hash = Json::requireString(hash_list, preferred_hash_type);
+ file.hash_type = preferred_hash_type;
+ } else {
+ auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH);
+ for (auto& hash_type : hash_types) {
+ if (hash_list.contains(hash_type)) {
+ file.hash = Json::requireString(hash_list, hash_type);
+ file.hash_type = hash_type;
+ break;
+ }
}
}
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h
index b7936204..31881414 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.h
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -30,6 +30,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
BaseInstance* inst);
-auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion;
+auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
} // namespace Modrinth
diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp
index 0782b9f4..c3561093 100644
--- a/launcher/modplatform/packwiz/Packwiz.cpp
+++ b/launcher/modplatform/packwiz/Packwiz.cpp
@@ -55,11 +55,11 @@ auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_fin
}
// Helpers
-static inline auto indexFileName(QString const& mod_name) -> QString
+static inline auto indexFileName(QString const& mod_slug) -> QString
{
- if(mod_name.endsWith(".pw.toml"))
- return mod_name;
- return QString("%1.pw.toml").arg(mod_name);
+ if(mod_slug.endsWith(".pw.toml"))
+ return mod_slug;
+ return QString("%1.pw.toml").arg(mod_slug);
}
static ModPlatform::ProviderCapabilities ProviderCaps;
@@ -95,6 +95,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo
{
Mod mod;
+ mod.slug = mod_pack.slug;
mod.name = mod_pack.name;
mod.filename = mod_version.fileName;
@@ -116,12 +117,10 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo
return mod;
}
-auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod
+auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod
{
- auto mod_name = internal_mod.name();
-
// Try getting metadata if it exists
- Mod mod { getIndexForMod(index_dir, mod_name) };
+ Mod mod { getIndexForMod(index_dir, slug) };
if(mod.isValid())
return mod;
@@ -139,11 +138,14 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
// Ensure the corresponding mod's info exists, and create it if not
- auto normalized_fname = indexFileName(mod.name);
+ auto normalized_fname = indexFileName(mod.slug);
auto real_fname = getRealIndexName(index_dir, normalized_fname);
QFile index_file(index_dir.absoluteFilePath(real_fname));
+ if (real_fname != normalized_fname)
+ index_file.rename(normalized_fname);
+
// There's already data on there!
// TODO: We should do more stuff here, as the user is likely trying to
// override a file. In this case, check versions and ask the user what
@@ -184,33 +186,46 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
}
}
+ index_file.flush();
index_file.close();
}
-void V1::deleteModIndex(QDir& index_dir, QString& mod_name)
+void V1::deleteModIndex(QDir& index_dir, QString& mod_slug)
{
- auto normalized_fname = indexFileName(mod_name);
+ auto normalized_fname = indexFileName(mod_slug);
auto real_fname = getRealIndexName(index_dir, normalized_fname);
if (real_fname.isEmpty())
return;
QFile index_file(index_dir.absoluteFilePath(real_fname));
- if(!index_file.exists()){
- qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_name);
+ if (!index_file.exists()) {
+ qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_slug);
return;
}
- if(!index_file.remove()){
- qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_name);
+ if (!index_file.remove()) {
+ qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_slug);
+ }
+}
+
+void V1::deleteModIndex(QDir& index_dir, QVariant& mod_id)
+{
+ for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) {
+ auto mod = getIndexForMod(index_dir, file_name);
+
+ if (mod.mod_id() == mod_id) {
+ deleteModIndex(index_dir, mod.name);
+ break;
+ }
}
}
-auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
+auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
{
Mod mod;
- auto normalized_fname = indexFileName(index_file_name);
+ auto normalized_fname = indexFileName(slug);
auto real_fname = getRealIndexName(index_dir, normalized_fname, true);
if (real_fname.isEmpty())
return {};
@@ -218,7 +233,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
QFile index_file(index_dir.absoluteFilePath(real_fname));
if (!index_file.open(QIODevice::ReadOnly)) {
- qWarning() << QString("Failed to open mod metadata for %1").arg(index_file_name);
+ qWarning() << QString("Failed to open mod metadata for %1").arg(slug);
return {};
}
@@ -232,11 +247,13 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
index_file.close();
if (!table) {
- qWarning() << QString("Could not open file %1!").arg(indexFileName(index_file_name));
+ qWarning() << QString("Could not open file %1!").arg(normalized_fname);
qWarning() << "Reason: " << QString(errbuf);
return {};
}
+ mod.slug = slug;
+
{ // Basic info
mod.name = stringEntry(table, "name");
mod.filename = stringEntry(table, "filename");
@@ -286,4 +303,16 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
return mod;
}
-} // namespace Packwiz
+auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod
+{
+ for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) {
+ auto mod = getIndexForMod(index_dir, file_name);
+
+ if (mod.mod_id() == mod_id)
+ return mod;
+ }
+
+ return {};
+}
+
+} // namespace Packwiz
diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h
index 3c99769c..3ec80377 100644
--- a/launcher/modplatform/packwiz/Packwiz.h
+++ b/launcher/modplatform/packwiz/Packwiz.h
@@ -40,6 +40,7 @@ auto intEntry(toml_table_t* parent, const char* entry_name) -> int;
class V1 {
public:
struct Mod {
+ QString slug {};
QString name {};
QString filename {};
// FIXME: make side an enum
@@ -58,7 +59,7 @@ class V1 {
public:
// This is a totally heuristic, but should work for now.
- auto isValid() const -> bool { return !name.isEmpty() && !project_id.isNull(); }
+ auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); }
// Different providers can use different names for the same thing
// Modrinth-specific
@@ -71,9 +72,9 @@ class V1 {
* */
static auto createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod;
/* Generates the object representing the information in a mod.pw.toml file via
- * its common representation in the launcher.
+ * its common representation in the launcher, plus a necessary slug.
* */
- static auto createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod;
+ static auto createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod;
/* Updates the mod index for the provided mod.
* This creates a new index if one does not exist already
@@ -81,13 +82,21 @@ class V1 {
* */
static void updateModIndex(QDir& index_dir, Mod& mod);
- /* Deletes the metadata for the mod with the given name. If the metadata doesn't exist, it does nothing. */
- static void deleteModIndex(QDir& index_dir, QString& mod_name);
+ /* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */
+ static void deleteModIndex(QDir& index_dir, QString& mod_slug);
- /* Gets the metadata for a mod with a particular name.
+ /* Deletes the metadata for the mod with the given id. If the metadata doesn't exist, it does nothing. */
+ static void deleteModIndex(QDir& index_dir, QVariant& mod_id);
+
+ /* Gets the metadata for a mod with a particular file name.
+ * If the mod doesn't have a metadata, it simply returns an empty Mod object.
+ * */
+ static auto getIndexForMod(QDir& index_dir, QString slug) -> Mod;
+
+ /* Gets the metadata for a mod with a particular id.
* If the mod doesn't have a metadata, it simply returns an empty Mod object.
* */
- static auto getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod;
+ static auto getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod;
};
} // namespace Packwiz
diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp
index d6251148..aa0c35df 100644
--- a/launcher/modplatform/packwiz/Packwiz_test.cpp
+++ b/launcher/modplatform/packwiz/Packwiz_test.cpp
@@ -32,10 +32,11 @@ class PackwizTest : public QObject {
QString source = QFINDTESTDATA("testdata");
QDir index_dir(source);
- QString name_mod("borderless-mining.pw.toml");
- QVERIFY(index_dir.entryList().contains(name_mod));
+ QString slug_mod("borderless-mining");
+ QString file_name = slug_mod + ".pw.toml";
+ QVERIFY(index_dir.entryList().contains(file_name));
- auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod);
+ auto metadata = Packwiz::V1::getIndexForMod(index_dir, slug_mod);
QVERIFY(metadata.isValid());
diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h
index 4d1fcbff..981ccf8a 100644
--- a/launcher/modplatform/technic/SingleZipPackInstallTask.h
+++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h
@@ -24,7 +24,7 @@
#include <QStringList>
#include <QUrl>
-#include <nonstd/optional>
+#include <optional>
namespace Technic {
@@ -57,8 +57,8 @@ private:
QString m_archivePath;
NetJob::Ptr m_filesNetJob;
std::unique_ptr<QuaZip> m_packZip;
- QFuture<nonstd::optional<QStringList>> m_extractFuture;
- QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ QFuture<std::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
};
} // namespace Technic
diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp
index 471b4a2f..95feb4b2 100644
--- a/launcher/modplatform/technic/TechnicPackProcessor.cpp
+++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp
@@ -187,17 +187,17 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const
}
else
{
- static QStringList possibleLoaders{
- "net.minecraftforge:minecraftforge:",
- "net.fabricmc:fabric-loader:",
- "org.quiltmc:quilt-loader:"
+ // <Technic library name prefix> -> <our component name>
+ static QMap<QString, QString> loaderMap {
+ {"net.minecraftforge:minecraftforge:", "net.minecraftforge"},
+ {"net.fabricmc:fabric-loader:", "net.fabricmc.fabric-loader"},
+ {"org.quiltmc:quilt-loader:", "org.quiltmc.quilt-loader"}
};
- for (const auto& loader : possibleLoaders)
+ for (const auto& loader : loaderMap.keys())
{
if (libraryName.startsWith(loader))
{
- auto loaderComponent = loader.chopped(1).replace(":", ".");
- components->setComponentVersion(loaderComponent, libraryName.section(':', 2));
+ components->setComponentVersion(loaderMap.value(loader), libraryName.section(':', 2));
break;
}
}