diff options
Diffstat (limited to 'launcher/modplatform')
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; } } |