From dd6aabf9ab2c974816aef4e889e059fa0cdad53b Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 30 May 2022 13:30:39 -0300 Subject: feat: add ChooseProviderDialog Allows you to prompt the user for choosing a (mod) provider. This should be fairly independent of the mod updater logic, so it can be used for other ends later down the road :^) Signed-off-by: flow --- launcher/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) (limited to 'launcher/CMakeLists.txt') diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index ecdeaac0..7b8dd9c5 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -836,6 +836,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ModDownloadDialog.h ui/dialogs/ScrollMessageBox.cpp ui/dialogs/ScrollMessageBox.h + ui/dialogs/ChooseProviderDialog.h + ui/dialogs/ChooseProviderDialog.cpp # GUI - widgets ui/widgets/Common.cpp @@ -941,6 +943,7 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/EditAccountDialog.ui ui/dialogs/ReviewMessageBox.ui ui/dialogs/ScrollMessageBox.ui + ui/dialogs/ChooseProviderDialog.ui ) qt_add_resources(LAUNCHER_RESOURCES -- cgit From 9a44c9221139428fa4e3bdf560f6bfdc6fcbe75d Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 2 Jun 2022 19:34:08 -0300 Subject: feat: add MultipleOptionsTask This is a variation of a Sequential Task, in which a subtask failing will prompt the next one to execute, and a subtask being successful will stop the task. This way, this can be used for easily managing fallbacks with tasks. :D Signed-off-by: flow --- launcher/CMakeLists.txt | 2 ++ launcher/tasks/MultipleOptionsTask.cpp | 48 ++++++++++++++++++++++++++++++++++ launcher/tasks/MultipleOptionsTask.h | 19 ++++++++++++++ launcher/tasks/SequentialTask.h | 14 +++++----- 4 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 launcher/tasks/MultipleOptionsTask.cpp create mode 100644 launcher/tasks/MultipleOptionsTask.h (limited to 'launcher/CMakeLists.txt') diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 7b8dd9c5..3be161be 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -397,6 +397,8 @@ set(TASKS_SOURCES tasks/ConcurrentTask.cpp tasks/SequentialTask.h tasks/SequentialTask.cpp + tasks/MultipleOptionsTask.h + tasks/MultipleOptionsTask.cpp ) ecm_add_test(tasks/Task_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp new file mode 100644 index 00000000..6e853568 --- /dev/null +++ b/launcher/tasks/MultipleOptionsTask.cpp @@ -0,0 +1,48 @@ +#include "MultipleOptionsTask.h" + +#include + +MultipleOptionsTask::MultipleOptionsTask(QObject* parent, const QString& task_name) : SequentialTask(parent, task_name) {} + +void MultipleOptionsTask::startNext() +{ + Task* previous = nullptr; + if (m_currentIndex != -1) { + previous = m_queue[m_currentIndex].get(); + disconnect(previous, 0, this, 0); + } + + m_currentIndex++; + if ((previous && previous->wasSuccessful())) { + emitSucceeded(); + return; + } + + Task::Ptr next = m_queue[m_currentIndex]; + + connect(next.get(), &Task::failed, this, &MultipleOptionsTask::subTaskFailed); + connect(next.get(), &Task::succeeded, this, &MultipleOptionsTask::startNext); + + connect(next.get(), &Task::status, this, &MultipleOptionsTask::subTaskStatus); + connect(next.get(), &Task::stepStatus, this, &MultipleOptionsTask::subTaskStatus); + + connect(next.get(), &Task::progress, this, &MultipleOptionsTask::subTaskProgress); + + qDebug() << QString("Making attemp %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size()); + setStatus(tr("Making attempt #%1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size())); + setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); + + next->start(); +} + +void MultipleOptionsTask::subTaskFailed(QString const& reason) +{ + qDebug() << QString("Failed attempt #%1 of %2. Reason: %3").arg(m_currentIndex + 1).arg(m_queue.size()).arg(reason); + if(m_currentIndex < m_queue.size() - 1) { + startNext(); + return; + } + + qWarning() << QString("All attempts have failed!"); + emitFailed(); +} diff --git a/launcher/tasks/MultipleOptionsTask.h b/launcher/tasks/MultipleOptionsTask.h new file mode 100644 index 00000000..7c508b00 --- /dev/null +++ b/launcher/tasks/MultipleOptionsTask.h @@ -0,0 +1,19 @@ +#pragma once + +#include "SequentialTask.h" + +/* This task type will attempt to do run each of it's subtasks in sequence, + * until one of them succeeds. When that happens, the remaining tasks will not run. + * */ +class MultipleOptionsTask : public SequentialTask +{ + Q_OBJECT +public: + explicit MultipleOptionsTask(QObject *parent = nullptr, const QString& task_name = ""); + virtual ~MultipleOptionsTask() = default; + +private +slots: + void startNext() override; + void subTaskFailed(const QString &msg) override; +}; diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h index 942ebec2..f5a58b1b 100644 --- a/launcher/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -20,17 +20,17 @@ public: void addTask(Task::Ptr task); -protected slots: - void executeTask() override; public slots: bool abort() override; -private +protected slots: - void startNext(); - void subTaskFailed(const QString &msg); - void subTaskStatus(const QString &msg); - void subTaskProgress(qint64 current, qint64 total); + void executeTask() override; + + virtual void startNext(); + virtual void subTaskFailed(const QString &msg); + virtual void subTaskStatus(const QString &msg); + virtual void subTaskProgress(qint64 current, qint64 total); protected: void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); }; -- cgit From 32a9545360b10058cf84b951ee88959adf3bf374 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:02:11 -0300 Subject: libs: add murmur2 library Signed-off-by: flow --- CMakeLists.txt | 1 + launcher/CMakeLists.txt | 1 + libraries/README.md | 6 +++ libraries/murmur2/CMakeLists.txt | 12 +++++ libraries/murmur2/src/MurmurHash2.cpp | 86 +++++++++++++++++++++++++++++++++++ libraries/murmur2/src/MurmurHash2.h | 30 ++++++++++++ 6 files changed, 136 insertions(+) create mode 100644 libraries/murmur2/CMakeLists.txt create mode 100644 libraries/murmur2/src/MurmurHash2.cpp create mode 100644 libraries/murmur2/src/MurmurHash2.h (limited to 'launcher/CMakeLists.txt') diff --git a/CMakeLists.txt b/CMakeLists.txt index c88a32ba..33c53b82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -323,6 +323,7 @@ add_subdirectory(libraries/optional-bare) add_subdirectory(libraries/tomlc99) # toml parser add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much add_subdirectory(libraries/gamemode) +add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API ############################### Built Artifacts ############################### diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 3be161be..c02480dd 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -972,6 +972,7 @@ add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHE target_link_libraries(Launcher_logic systeminfo Launcher_classparser + Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} optional-bare diff --git a/libraries/README.md b/libraries/README.md index 360c34b1..946e34d8 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -149,6 +149,12 @@ BSD licensed, derived from [QtSingleApplication](https://github.com/qtproject/qt Changes are made to make the code more generic and useful in less usual conditions. +## murmur2 + +Canonical implementation of the murmur2 hash, taken from [SMHasher](https://github.com/aappleby/smhasher). + +Public domain (the author disclaimed the copyright). + ## optional-bare A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later. diff --git a/libraries/murmur2/CMakeLists.txt b/libraries/murmur2/CMakeLists.txt new file mode 100644 index 00000000..f3068201 --- /dev/null +++ b/libraries/murmur2/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.9.4) +project(murmur2) + +set(MURMUR_SOURCES + src/MurmurHash2.h + src/MurmurHash2.cpp +) + +add_library(Launcher_murmur2 STATIC ${MURMUR_SOURCES}) +target_include_directories(Launcher_murmur2 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "src" ) + +generate_export_header(Launcher_murmur2) diff --git a/libraries/murmur2/src/MurmurHash2.cpp b/libraries/murmur2/src/MurmurHash2.cpp new file mode 100644 index 00000000..3e52e6d1 --- /dev/null +++ b/libraries/murmur2/src/MurmurHash2.cpp @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------------- +// MurmurHash2 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. + +// Note - This code makes a few assumptions about how your machine behaves - + +// 1. We can read a 4-byte value from any address without crashing +// 2. sizeof(int) == 4 + +// And it has a few limitations - + +// 1. It will not work incrementally. +// 2. It will not produce the same results on little-endian and big-endian +// machines. + +#include "MurmurHash2.h" + +//----------------------------------------------------------------------------- +// Platform-specific functions and macros + +// Microsoft Visual Studio + +#if defined(_MSC_VER) + +#define BIG_CONSTANT(x) (x) + +// Other compilers + +#else // defined(_MSC_VER) + +#define BIG_CONSTANT(x) (x##LLU) + +#endif // !defined(_MSC_VER) + +//----------------------------------------------------------------------------- + +uint64_t MurmurHash2 ( const void* key, int len, uint32_t seed ) +{ + // 'm' and 'r' are mixing constants generated offline. + // They're not really 'magic', they just happen to work well. + + const uint32_t m = 0x5bd1e995; + const int r = 24; + + // Initialize the hash to a 'random' value + + uint32_t h = seed ^ len; + + // Mix 4 bytes at a time into the hash + const auto* data = (const unsigned char*) key; + while(len >= 4) + { + uint32_t k = *(uint32_t*)data; + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + data += 4*sizeof(char); + len -= 4; + } + + // Handle the last few bytes of the input array + + switch(len) + { + case 3: h ^= data[2] << 16; + case 2: h ^= data[1] << 8; + case 1: h ^= data[0]; + h *= m; + }; + + // Do a few final mixes of the hash to ensure the last few + // bytes are well-incorporated. + + h ^= h >> 13; + h *= m; + h ^= h >> 15; + + return h; +} + +//----------------------------------------------------------------------------- diff --git a/libraries/murmur2/src/MurmurHash2.h b/libraries/murmur2/src/MurmurHash2.h new file mode 100644 index 00000000..c7b83bca --- /dev/null +++ b/libraries/murmur2/src/MurmurHash2.h @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// MurmurHash2 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. + +#pragma once + +//----------------------------------------------------------------------------- +// Platform-specific functions and macros + +// Microsoft Visual Studio + +#if defined(_MSC_VER) && (_MSC_VER < 1600) + +typedef unsigned char uint8_t; +typedef unsigned int uint32_t; +typedef unsigned __int64 uint64_t; + +// Other compilers + +#else // defined(_MSC_VER) + +#include + +#endif // !defined(_MSC_VER) + +//----------------------------------------------------------------------------- + +uint64_t MurmurHash2 ( const void* key, int len, uint32_t seed = 1 ); + +//----------------------------------------------------------------------------- -- cgit From 0e52112016fe9942f7448cd83914f6266904c311 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:04:49 -0300 Subject: feat: add some api calls to modrinth Calls added: - Get version from hash - Get versions from hashes - Latest version of a project from a hash, loader(s), and game version(s) - Latest versions of multiple project from hashes, loader(s), and game version(s) Some of those are not used yet, but may be of use later on, so we have it if we need it :) Signed-off-by: flow --- launcher/CMakeLists.txt | 2 +- launcher/modplatform/ModIndex.h | 1 + launcher/modplatform/modrinth/ModrinthAPI.cpp | 97 ++++++++++++++++++++++ launcher/modplatform/modrinth/ModrinthAPI.h | 22 +++++ .../modplatform/modrinth/ModrinthPackIndex.cpp | 26 ++++-- launcher/modplatform/modrinth/ModrinthPackIndex.h | 2 +- 6 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 launcher/modplatform/modrinth/ModrinthAPI.cpp (limited to 'launcher/CMakeLists.txt') diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index c02480dd..085cc211 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -484,7 +484,7 @@ set(API_SOURCES modplatform/flame/FlameAPI.h modplatform/modrinth/ModrinthAPI.h - + modplatform/modrinth/ModrinthAPI.cpp modplatform/helpers/NetworkModAPI.h modplatform/helpers/NetworkModAPI.cpp ) diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index c27643af..459eb261 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -61,6 +61,7 @@ struct IndexedVersion { QVector loaders = {}; QString hash_type; QString hash; + bool is_preferred = true; }; struct ExtraPackData { diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp new file mode 100644 index 00000000..0d5d4bab --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -0,0 +1,97 @@ +#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 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 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; +} diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 89e52d6c..9694b85e 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -22,10 +22,32 @@ #include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" +#include "net/NetJob.h" #include 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 mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr; + + auto latestVersions(const QStringList& hashes, + QString hash_format, + std::list mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr; + public: inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index b6f5490a..4e738819 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -111,7 +111,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; @@ -142,6 +142,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 +158,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"); 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& network, BaseInstance* inst); -auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; +auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; } // namespace Modrinth -- cgit From 4bcf8e6975d4f314654062dde01d627ed7495866 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:06:51 -0300 Subject: feat: add api call to flame Call added: - Get Fingerprints Matches - Get Mod File Changelog Signed-off-by: flow --- launcher/CMakeLists.txt | 1 + launcher/modplatform/flame/FlameAPI.cpp | 125 +++++++++++++++++++++++++++ launcher/modplatform/flame/FlameAPI.h | 8 ++ launcher/modplatform/flame/FlameModIndex.cpp | 2 +- 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 launcher/modplatform/flame/FlameAPI.cpp (limited to 'launcher/CMakeLists.txt') diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 085cc211..6dcea8e2 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -483,6 +483,7 @@ set(API_SOURCES modplatform/ModAPI.h modplatform/flame/FlameAPI.h + modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h modplatform/modrinth/ModrinthAPI.cpp modplatform/helpers/NetworkModAPI.h diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp new file mode 100644 index 00000000..983e09fd --- /dev/null +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -0,0 +1,125 @@ +#include "FlameAPI.h" +#include "FlameModIndex.h" + +#include "Application.h" +#include "BuildConfig.h" +#include "Json.h" + +#include "net/Upload.h" + +auto FlameAPI::matchFingerprints(const std::list& 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; +} diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index aea76ff1..e45b5cb1 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -3,7 +3,15 @@ #include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" +#include "net/NetJob.h" + class FlameAPI : public NetworkModAPI { + public: + auto matchFingerprints(const std::list& fingerprints, QByteArray* response) -> NetJob::Ptr; + auto getModFileChangelog(int modId, int fileId) -> QString; + + auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; + private: inline auto getSortFieldInt(QString sortString) const -> int { diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index b99bfb26..0ad1d4ba 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -110,7 +110,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"); -- cgit From 844b2457769d61131f97b5e82bb134568dfd42ed Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:08:01 -0300 Subject: feat: add EnsureMetadataTask This task is responsible for checking if the mod has metadata for a specific provider, and create it if it doesn't. In the context of the mod updater, this is not the best architecture, since we do a single task for each mod. However, this way of structuring it allows us to use it later on in more diverse scenarios. This way we decouple this task from the mod updater, trading off some performance (though that will be mitigated when we have a way of running arbitrary tasks concurrently). Signed-off-by: flow --- launcher/CMakeLists.txt | 3 + launcher/modplatform/EnsureMetadataTask.cpp | 244 ++++++++++++++++++++++++++++ launcher/modplatform/EnsureMetadataTask.h | 41 +++++ 3 files changed, 288 insertions(+) create mode 100644 launcher/modplatform/EnsureMetadataTask.cpp create mode 100644 launcher/modplatform/EnsureMetadataTask.h (limited to 'launcher/CMakeLists.txt') diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6dcea8e2..25546c38 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -482,6 +482,9 @@ set(API_SOURCES modplatform/ModAPI.h + modplatform/EnsureMetadataTask.h + modplatform/EnsureMetadataTask.cpp + modplatform/flame/FlameAPI.h modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp new file mode 100644 index 00000000..dc92d8ab --- /dev/null +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -0,0 +1,244 @@ +#include "EnsureMetadataTask.h" + +#include +#include + +#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, bool try_all, ModPlatform::Provider prov) + : m_mod(mod), m_index_dir(dir), m_provider(prov), m_try_all(try_all) +{} + +bool EnsureMetadataTask::abort() +{ + return m_task_handler->abort(); +} + +void EnsureMetadataTask::executeTask() +{ + // They already have the right metadata :o + if (m_mod.status() != ModStatus::NoMetadata && m_mod.metadata() && m_mod.metadata()->provider == m_provider) { + emitReady(); + return; + } + + // Folders don't have metadata + if (m_mod.type() == Mod::MOD_FOLDER) { + emitReady(); + return; + } + + setStatus(tr("Generating %1's metadata...").arg(m_mod.name())); + qDebug() << QString("Generating %1's metadata...").arg(m_mod.name()); + + QByteArray jar_data; + + try { + jar_data = FS::read(m_mod.fileinfo().absoluteFilePath()); + } catch (FS::FileSystemException& e) { + qCritical() << QString("Failed to open / read JAR file of %1").arg(m_mod.name()); + qCritical() << QString("Reason: ") << e.cause(); + + emitFail(); + return; + } + + auto tsk = new MultipleOptionsTask(nullptr, "GetMetadataTask"); + + switch (m_provider) { + case (ModPlatform::Provider::MODRINTH): + modrinthEnsureMetadata(*tsk, jar_data); + if (m_try_all) + flameEnsureMetadata(*tsk, jar_data); + + break; + case (ModPlatform::Provider::FLAME): + flameEnsureMetadata(*tsk, jar_data); + if (m_try_all) + modrinthEnsureMetadata(*tsk, jar_data); + + break; + } + + connect(tsk, &MultipleOptionsTask::finished, this, [tsk] { tsk->deleteLater(); }); + connect(tsk, &MultipleOptionsTask::failed, [this] { + qCritical() << QString("Download of %1's metadata failed").arg(m_mod.name()); + + emitFail(); + }); + connect(tsk, &MultipleOptionsTask::succeeded, this, &EnsureMetadataTask::emitReady); + + m_task_handler = tsk; + + tsk->start(); +} + +void EnsureMetadataTask::emitReady() +{ + emit metadataReady(); + emitSucceeded(); +} + +void EnsureMetadataTask::emitFail() +{ + qDebug() << QString("Failed to generate metadata for %1").arg(m_mod.name()); + emit metadataFailed(); + //emitFailed(tr("Failed to generate metadata for %1").arg(m_mod.name())); + emitSucceeded(); +} + +void EnsureMetadataTask::modrinthEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data) +{ + // Modrinth currently garantees that some hash types will always be present. + // But let's be sure and cover all cases anyways :) + for (auto hash_type : ProviderCaps.hashType(ModPlatform::Provider::MODRINTH)) { + auto* response = new QByteArray(); + auto hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex()); + auto ver_task = modrinth_api.currentVersion(hash, hash_type, response); + + // Prevents unfortunate timings when aborting the task + if (!ver_task) + return; + + connect(ver_task.get(), &NetJob::succeeded, this, [this, ver_task, response] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << m_mod.name() << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + ver_task->failed(parse_error.errorString()); + return; + } + + auto doc_obj = Json::requireObject(doc); + auto ver = Modrinth::loadIndexedPackVersion(doc_obj, {}, m_mod.fileinfo().fileName()); + + // Minimal IndexedPack to create the metadata + ModPlatform::IndexedPack pack; + pack.name = m_mod.name(); + pack.provider = ModPlatform::Provider::MODRINTH; + pack.addonId = ver.addonId; + + // Prevent file name mismatch + ver.fileName = m_mod.fileinfo().fileName(); + + QDir tmp_index_dir(m_index_dir); + + { + LocalModUpdateTask update_metadata(m_index_dir, pack, ver); + QEventLoop loop; + QTimer timeout; + + QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + + update_metadata.start(); + timeout.start(100); + + loop.exec(); + } + + auto mod_name = m_mod.name(); + auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name)); + m_mod.setMetadata(meta); + }); + + tsk.addTask(ver_task); + } +} + +void EnsureMetadataTask::flameEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data) +{ + 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); + } + + auto* response = new QByteArray(); + + std::list fingerprints; + auto murmur = MurmurHash2(jar_data_treated, jar_data_treated.length()); + fingerprints.push_back(murmur); + + auto ver_task = flame_api.matchFingerprints(fingerprints, response); + + connect(ver_task.get(), &Task::succeeded, this, [this, ver_task, response] { + QDir tmp_index_dir(m_index_dir); + + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << m_mod.name() << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + ver_task->failed(parse_error.errorString()); + return; + } + + try { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::ensureObject(doc_obj, "data"); + auto match_obj = Json::ensureObject(Json::ensureArray(data_obj, "exactMatches")[0], {}); + if (match_obj.isEmpty()) { + qCritical() << "Fingerprint match is empty!"; + + ver_task->failed(parse_error.errorString()); + return; + } + + auto file_obj = Json::ensureObject(match_obj, "file"); + + ModPlatform::IndexedPack pack; + pack.name = m_mod.name(); + pack.provider = ModPlatform::Provider::FLAME; + pack.addonId = Json::requireInteger(file_obj, "modId"); + + ModPlatform::IndexedVersion ver = FlameMod::loadIndexedPackVersion(file_obj); + + // Prevent file name mismatch + ver.fileName = m_mod.fileinfo().fileName(); + + { + LocalModUpdateTask update_metadata(m_index_dir, pack, ver); + QEventLoop loop; + QTimer timeout; + + QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + + update_metadata.start(); + timeout.start(100); + + loop.exec(); + } + + auto mod_name = m_mod.name(); + auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name)); + m_mod.setMetadata(meta); + + } catch (Json::JsonException& e) { + emitFailed(e.cause() + " : " + e.what()); + } + }); + + tsk.addTask(ver_task); +} diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h new file mode 100644 index 00000000..624e253a --- /dev/null +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -0,0 +1,41 @@ +#pragma once + +#include "ModIndex.h" +#include "tasks/SequentialTask.h" + +class Mod; +class QDir; +class MultipleOptionsTask; + +class EnsureMetadataTask : public Task { + Q_OBJECT + + public: + EnsureMetadataTask(Mod&, QDir&, bool try_all, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + + public slots: + bool abort() override; + protected slots: + void executeTask() override; + + private: + // FIXME: Move to their own namespace + void modrinthEnsureMetadata(SequentialTask&, QByteArray&); + void flameEnsureMetadata(SequentialTask&, QByteArray&); + + // Helpers + void emitReady(); + void emitFail(); + + signals: + void metadataReady(); + void metadataFailed(); + + private: + Mod& m_mod; + QDir& m_index_dir; + ModPlatform::Provider m_provider; + bool m_try_all; + + MultipleOptionsTask* m_task_handler = nullptr; +}; -- cgit From b8b71c7dd29fbdc6c98d60ec54c57cff74f4cbfd Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 21:26:26 -0300 Subject: feat: add mod update check tasks Those tasks take a list of mods and check on the mod providers for updates. They assume that the mods have metadata already. Signed-off-by: flow --- launcher/CMakeLists.txt | 6 + launcher/modplatform/CheckUpdateTask.h | 51 +++++ launcher/modplatform/ModIndex.h | 1 + launcher/modplatform/flame/FlameCheckUpdate.cpp | 228 +++++++++++++++++++++ launcher/modplatform/flame/FlameCheckUpdate.h | 25 +++ .../modplatform/modrinth/ModrinthCheckUpdate.cpp | 163 +++++++++++++++ .../modplatform/modrinth/ModrinthCheckUpdate.h | 23 +++ .../modplatform/modrinth/ModrinthPackIndex.cpp | 3 +- 8 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 launcher/modplatform/CheckUpdateTask.h create mode 100644 launcher/modplatform/flame/FlameCheckUpdate.cpp create mode 100644 launcher/modplatform/flame/FlameCheckUpdate.h create mode 100644 launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp create mode 100644 launcher/modplatform/modrinth/ModrinthCheckUpdate.h (limited to 'launcher/CMakeLists.txt') diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 25546c38..2313f0e4 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -485,6 +485,8 @@ set(API_SOURCES modplatform/EnsureMetadataTask.h modplatform/EnsureMetadataTask.cpp + modplatform/CheckUpdateTask.h + modplatform/flame/FlameAPI.h modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h @@ -514,6 +516,8 @@ set(FLAME_SOURCES modplatform/flame/PackManifest.cpp modplatform/flame/FileResolvingTask.h modplatform/flame/FileResolvingTask.cpp + modplatform/flame/FlameCheckUpdate.cpp + modplatform/flame/FlameCheckUpdate.h ) set(MODRINTH_SOURCES @@ -521,6 +525,8 @@ set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackIndex.h modplatform/modrinth/ModrinthPackManifest.cpp modplatform/modrinth/ModrinthPackManifest.h + modplatform/modrinth/ModrinthCheckUpdate.cpp + modplatform/modrinth/ModrinthCheckUpdate.h ) set(MODPACKSCH_SOURCES diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h new file mode 100644 index 00000000..8c701e46 --- /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(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + : 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&& { 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: + std::list& m_mods; + std::list& m_game_versions; + ModAPI::ModLoaderTypes m_loaders; + std::shared_ptr m_mods_folder; + + std::vector m_updatable; +}; diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 966082ab..f8ef211e 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -54,6 +54,7 @@ struct IndexedVersion { QVariant addonId; QVariant fileId; QString version; + QString version_number = {}; QVector mcVersion; QString downloadUrl; QString date; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp new file mode 100644 index 00000000..f1983fa4 --- /dev/null +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -0,0 +1,228 @@ +#include "FlameCheckUpdate.h" +#include "FlameAPI.h" +#include "FlameModIndex.h" + +#include + +#include "FileSystem.h" +#include "Json.h" + +#include "ModDownloadTask.h" + +static FlameAPI api; +static ModPlatform::ProviderCapabilities ProviderCaps; + +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; +} + +/* 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...")); + setProgress(0, 5); + + QHash mappings; + + // Create all hashes + QStringList hashes; + std::list murmur_hashes; + + auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::FLAME).first(); + for (auto mod : m_mods) { + auto hash = mod.metadata()->hash; + + 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; + } + + 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); + } + + auto murmur_hash = MurmurHash2(jar_data_treated, jar_data_treated.length()); + murmur_hashes.emplace_back(murmur_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) + hash = QString(ProviderCaps.hash(ModPlatform::Provider::FLAME, jar_data, best_hash_type).toHex()); + + hashes.append(hash); + mappings.insert(hash, mod); + } + + auto* response = new QByteArray(); + auto job = api.matchFingerprints(murmur_hashes, response); + + QEventLoop lock; + + connect(job.get(), &Task::succeeded, this, [this, response, &mappings] { + 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; + + failed(parse_error.errorString()); + return; + } + + setStatus(tr("Parsing the first API response from CurseForge...")); + setProgress(2, 5); + + try { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::ensureObject(doc_obj, "data"); + auto match_arr = Json::ensureArray(data_obj, "exactMatches"); + for (auto match : match_arr) { + auto match_obj = Json::ensureObject(match); + + ModPlatform::IndexedVersion current_ver; + try { + auto file_obj = Json::requireObject(match_obj, "file"); + current_ver = FlameMod::loadIndexedPackVersion(file_obj); + } catch (Json::JsonException& e) { + qCritical() << "Error while parsing Flame indexed version"; + qCritical() << e.what(); + failed(tr("An error occured while parsing a CurseForge indexed version!")); + return; + } + + auto mod_iter = mappings.find(current_ver.hash); + if (mod_iter == mappings.end()) { + qCritical() << "Failed to remap mod from Flame!"; + continue; + } + + auto mod = mod_iter.value(); + + setStatus(tr("Waiting for the API response from CurseForge for '%1'...").arg(mod.name())); + setProgress(3, 5); + + auto latest_ver = api.getLatestVersion({ current_ver.addonId.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())); + setProgress(4, 5); + + 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 != current_ver.fileId) { + 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 opted-out on CurseForge"), recover_url); + + continue; + } + + if (!latest_ver.hash.isEmpty() && current_ver.hash != latest_ver.hash) { + // Fake pack with the necessary info to pass to the download task :) + ModPlatform::IndexedPack pack; + pack.name = mod.name(); + 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 download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder); + m_updatable.emplace_back(mod.name(), current_ver.hash, current_ver.version, latest_ver.version, + api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), + ModPlatform::Provider::FLAME, download_task); + } + } + + } catch (Json::JsonException& e) { + failed(e.cause() + " : " + e.what()); + } + }); + + connect(job.get(), &Task::finished, &lock, &QEventLoop::quit); + + setStatus(tr("Waiting for the first API response from CurseForge...")); + setProgress(1, 5); + + m_net_job = job.get(); + job->start(); + + lock.exec(); + + emitSucceeded(); +} diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h new file mode 100644 index 00000000..f068f08f --- /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(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr 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/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp new file mode 100644 index 00000000..81a2652a --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -0,0 +1,163 @@ +#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 mappings; + + // Create all hashes + QStringList hashes; + auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); + for (auto mod : m_mods) { + 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 (project_obj.isEmpty()) { + qDebug() << "Mod " << mappings.find(hash).value().name() << " got an empty response."; + qDebug() << "Hash: " << hash; + + emit checkFailed(mappings.find(hash).value(), tr("Couldn't find mod in Modrinth")); + + 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.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(mod.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..7e685a6d --- /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(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr 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 9736e861..1910c9be 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -130,6 +130,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_t 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"); @@ -159,7 +160,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_t if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); - file.is_preferred = Json::requireBoolean(parent, "primary"); + file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1); auto hash_list = Json::requireObject(parent, "hashes"); if (hash_list.contains(preferred_hash_type)) { -- cgit From 6e2869834f59ae4863e63a16de97aa3019723b26 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 4 Jun 2022 16:22:46 -0300 Subject: feat: add mod update dialog This subclasses the Review mods dialog to make a "Update review" one. Also, all the necessary components built until now are put together in a coherent unity that checks and generates metadata on-the-fly and checks for mod updates, while giving and receiving feedback to the user. Signed-off-by: flow --- launcher/CMakeLists.txt | 2 + launcher/modplatform/flame/FlameCheckUpdate.cpp | 19 +- .../modplatform/modrinth/ModrinthCheckUpdate.cpp | 5 +- launcher/ui/dialogs/ModUpdateDialog.cpp | 317 +++++++++++++++++++++ launcher/ui/dialogs/ModUpdateDialog.h | 61 ++++ launcher/ui/dialogs/ScrollMessageBox.ui | 2 +- 6 files changed, 390 insertions(+), 16 deletions(-) create mode 100644 launcher/ui/dialogs/ModUpdateDialog.cpp create mode 100644 launcher/ui/dialogs/ModUpdateDialog.h (limited to 'launcher/CMakeLists.txt') diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 2313f0e4..3c9aee6a 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -850,6 +850,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ScrollMessageBox.h ui/dialogs/ChooseProviderDialog.h ui/dialogs/ChooseProviderDialog.cpp + ui/dialogs/ModUpdateDialog.cpp + ui/dialogs/ModUpdateDialog.h # GUI - widgets ui/widgets/Common.cpp diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index f1983fa4..12d99029 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -74,16 +74,13 @@ void FlameCheckUpdate::executeTask() setStatus(tr("Preparing mods for CurseForge...")); setProgress(0, 5); - QHash mappings; + QHash mappings; // Create all hashes - QStringList hashes; std::list murmur_hashes; auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::FLAME).first(); for (auto mod : m_mods) { - auto hash = mod.metadata()->hash; - QByteArray jar_data; try { @@ -106,14 +103,7 @@ void FlameCheckUpdate::executeTask() auto murmur_hash = MurmurHash2(jar_data_treated, jar_data_treated.length()); murmur_hashes.emplace_back(murmur_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) - hash = QString(ProviderCaps.hash(ModPlatform::Provider::FLAME, jar_data, best_hash_type).toHex()); - - hashes.append(hash); - mappings.insert(hash, mod); + mappings.insert(mod.metadata()->mod_id().toInt(), mod); } auto* response = new QByteArray(); @@ -154,9 +144,10 @@ void FlameCheckUpdate::executeTask() return; } - auto mod_iter = mappings.find(current_ver.hash); + auto mod_iter = mappings.find(current_ver.addonId.toInt()); if (mod_iter == mappings.end()) { qCritical() << "Failed to remap mod from Flame!"; + qDebug() << match_obj; continue; } @@ -191,7 +182,7 @@ void FlameCheckUpdate::executeTask() continue; } - if (!latest_ver.hash.isEmpty() && current_ver.hash != latest_ver.hash) { + if (!latest_ver.hash.isEmpty() && (current_ver.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(); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 81a2652a..981c4216 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -81,11 +81,14 @@ void ModrinthCheckUpdate::executeTask() 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("Couldn't find mod in Modrinth")); + emit checkFailed(mappings.find(hash).value(), tr("Couldn't find the latest version of this mod with the correct mod loader and game version.")); continue; } diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp new file mode 100644 index 00000000..b60fd304 --- /dev/null +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -0,0 +1,317 @@ +#include "ModUpdateDialog.h" +#include "ChooseProviderDialog.h" +#include "CustomMessageBox.h" +#include "ProgressDialog.h" +#include "ScrollMessageBox.h" +#include "ui_ReviewMessageBox.h" + +#include "FileSystem.h" +#include "Json.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "modplatform/EnsureMetadataTask.h" +#include "modplatform/flame/FlameCheckUpdate.h" +#include "modplatform/modrinth/ModrinthCheckUpdate.h" + +#include + +#include +#include + +static ModPlatform::ProviderCapabilities ProviderCaps; + +static std::list mcVersions(BaseInstance* inst) +{ + return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; +} + +static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) +{ + return { static_cast(inst)->getPackProfile()->getModLoaders() }; +} + +ModUpdateDialog::ModUpdateDialog(QWidget* parent, + BaseInstance* instance, + const std::shared_ptr& mods, + std::list& search_for) + : ReviewMessageBox(parent, tr("Confirm mods to update"), "") + , m_parent(parent) + , m_mod_model(mods) + , m_candidates(search_for) + , m_instance(instance) +{ + ReviewMessageBox::setGeometry(0, 0, 800, 600); + + ui->explainLabel->setText(tr("You're about to update the following mods:")); + ui->onlyCheckedLabel->setText(tr("Only mods with a check will be updated!")); + + connect(&m_check_task, &Task::failed, this, + [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + + connect(&m_check_task, &Task::succeeded, this, [&]() { + QStringList warnings = m_check_task.warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); + } + }); +} + +void ModUpdateDialog::checkCandidates() +{ + // Ensure mods have valid metadata + auto went_well = ensureMetadata(); + if (!went_well) { + m_aborted = true; + return; + } + + // Report failed metadata generation + if (!m_failed_metadata.empty()) { + QString text; + for (const auto& mod : m_failed_metadata) { + text += tr("Mod name: %1
File name: %2

").arg(mod.name(), mod.fileinfo().fileName()); + } + + ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"), + tr("Could not generate metadata for the following mods:
" + "Do you wish to proceed without those mods?"), + text); + message_dialog.setModal(true); + if (message_dialog.exec() == QDialog::Rejected) { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + } + + auto versions = mcVersions(m_instance); + auto loaders = mcLoaders(m_instance); + + if (!m_modrinth_to_update.empty()) { + m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model); + connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this, + [this](Mod mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); + m_check_task.addTask(m_modrinth_check_task); + } + + if (!m_flame_to_update.empty()) { + m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model); + connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this, + [this](Mod mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); + m_check_task.addTask(m_flame_check_task); + } + + // Check for updates + ProgressDialog progress_dialog(m_parent); + progress_dialog.setSkipButton(true, tr("Abort")); + progress_dialog.setVisible(true); + progress_dialog.setWindowTitle(tr("Checking for updates...")); + auto ret = progress_dialog.execWithTask(&m_check_task); + + // If the dialog was skipped / some download error happened + if (ret == QDialog::DialogCode::Rejected) { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + + // Add found updates for Modrinth + if (m_modrinth_check_task) { + auto modrinth_updates = m_modrinth_check_task->getUpdatable(); + for (auto& updatable : modrinth_updates) { + qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); + + appendMod(updatable); + m_tasks.insert(updatable.name, updatable.download); + } + } + + // Add found updated for Flame + if (m_flame_check_task) { + auto flame_updates = m_flame_check_task->getUpdatable(); + for (auto& updatable : flame_updates) { + qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); + + appendMod(updatable); + m_tasks.insert(updatable.name, updatable.download); + } + } + + // Report failed update checking + if (!m_failed_check_update.empty()) { + QString text; + for (const auto& failed : m_failed_check_update) { + const auto& mod = std::get<0>(failed); + const auto& reason = std::get<1>(failed); + const auto& recover_url = std::get<2>(failed); + + qDebug() << mod.name() << " failed to check for updates!"; + + text += tr("Mod name: %1").arg(mod.name()) + "
"; + if (!reason.isEmpty()) + text += tr("Reason: %1").arg(reason) + "
"; + if (!recover_url.isEmpty()) + text += tr("Possible solution: ") + tr("Getting the latest version manually:") + "
" + + QString("").arg(recover_url.toString()) + recover_url.toString() + "
"; + text += "
"; + } + + ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"), + tr("Could not check or get the following mods for updates:
" + "Do you wish to proceed without those mods?"), + text); + message_dialog.setModal(true); + if (message_dialog.exec() == QDialog::Rejected) { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + } + + // If there's no mod to be updated + if (ui->modTreeWidget->topLevelItemCount() == 0) + m_no_updates = true; + + if (m_aborted || m_no_updates) + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); +} + +// Part 1: Ensure we have a valid metadata +auto ModUpdateDialog::ensureMetadata() -> bool +{ + auto index_dir = indexDir(); + + auto* seq = new SequentialTask(m_parent, tr("Looking for metadata")); + + bool confirm_rest = false; + bool try_others_rest = false; + bool skip_rest = false; + ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH; + + for (auto& candidate : m_candidates) { + if (candidate.status() != ModStatus::NoMetadata) { + onMetadataEnsured(candidate); + continue; + } + + if (skip_rest) + continue; + + if (confirm_rest) { + auto* task = new EnsureMetadataTask(candidate, index_dir, try_others_rest, provider_rest); + connect(task, &EnsureMetadataTask::metadataReady, [this, &candidate] { onMetadataEnsured(candidate); }); + connect(task, &EnsureMetadataTask::metadataFailed, [this, &candidate] { onMetadataFailed(candidate); }); + seq->addTask(task); + continue; + } + + ChooseProviderDialog chooser(this); + chooser.setDescription(tr("This mod (%1) does not have a metadata yet. We need to create one in order to keep relevant " + "information on how to update this " + "mod. To do this, please select a mod provider from which we can search for updates for %1.") + .arg(candidate.name())); + auto confirmed = chooser.exec() == QDialog::DialogCode::Accepted; + + auto response = chooser.getResponse(); + + if (response.skip_all) + skip_rest = true; + if (response.confirm_all) { + confirm_rest = true; + provider_rest = response.chosen; + try_others_rest = response.try_others; + } + + if (confirmed) { + auto* task = new EnsureMetadataTask(candidate, index_dir, response.try_others, response.chosen); + connect(task, &EnsureMetadataTask::metadataReady, [this, &candidate] { onMetadataEnsured(candidate); }); + connect(task, &EnsureMetadataTask::metadataFailed, [this, &candidate] { onMetadataFailed(candidate); }); + seq->addTask(task); + } + } + + ProgressDialog checking_dialog(m_parent); + checking_dialog.setSkipButton(true, tr("Abort")); + checking_dialog.setWindowTitle(tr("Generating metadata...")); + auto ret_metadata = checking_dialog.execWithTask(seq); + + return (ret_metadata != QDialog::DialogCode::Rejected); +} + +void ModUpdateDialog::onMetadataEnsured(Mod& mod) +{ + // When the mod is a folder, for instance + if (!mod.metadata()) + return; + + switch (mod.metadata()->provider) { + case ModPlatform::Provider::MODRINTH: + m_modrinth_to_update.push_back(mod); + break; + case ModPlatform::Provider::FLAME: + m_flame_to_update.push_back(mod); + break; + } +} + +void ModUpdateDialog::onMetadataFailed(Mod& mod) +{ + m_failed_metadata.push_back(mod); +} + +void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) +{ + auto item_top = new QTreeWidgetItem(ui->modTreeWidget); + item_top->setCheckState(0, Qt::CheckState::Checked); + item_top->setText(0, info.name); + item_top->setExpanded(true); + + auto provider_item = new QTreeWidgetItem(item_top); + provider_item->setText(0, tr("Provider: %1").arg(ProviderCaps.readableName(info.provider))); + + auto old_version_item = new QTreeWidgetItem(item_top); + old_version_item->setText(0, tr("Old version: %1").arg(info.old_version.isEmpty() ? tr("Not installed") : info.old_version)); + + auto new_version_item = new QTreeWidgetItem(item_top); + new_version_item->setText(0, tr("New version: %1").arg(info.new_version)); + + auto changelog_item = new QTreeWidgetItem(item_top); + changelog_item->setText(0, tr("Changelog of the latest version")); + + auto changelog = new QTreeWidgetItem(changelog_item); + + auto changelog_area = new QTextEdit(); + HoeDown h; + changelog_area->setText(h.process(info.changelog.toUtf8())); + changelog_area->setReadOnly(true); + if (info.changelog.size() < 250) // heuristic + changelog_area->setSizeAdjustPolicy(QTextEdit::SizeAdjustPolicy::AdjustToContents); + + ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area); + changelog_item->insertChildren(0, { changelog }); + + item_top->insertChildren(0, { old_version_item }); + item_top->insertChildren(1, { new_version_item }); + item_top->insertChildren(2, { changelog_item }); + + ui->modTreeWidget->addTopLevelItem(item_top); +} + +auto ModUpdateDialog::getTasks() -> const std::list +{ + std::list list; + + auto* item = ui->modTreeWidget->topLevelItem(0); + + for (int i = 0; item != nullptr; ++i) { + if (item->checkState(0) == Qt::CheckState::Checked) { + list.push_back(m_tasks.find(item->text(0)).value()); + } + + item = ui->modTreeWidget->topLevelItem(i); + } + + return list; +} diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h new file mode 100644 index 00000000..30cd5cbd --- /dev/null +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -0,0 +1,61 @@ +#pragma once + +#include "BaseInstance.h" +#include "ModDownloadTask.h" +#include "ReviewMessageBox.h" + +#include "minecraft/mod/ModFolderModel.h" + +#include "modplatform/CheckUpdateTask.h" + +class Mod; +class ModrinthCheckUpdate; +class FlameCheckUpdate; + +class ModUpdateDialog final : public ReviewMessageBox { + Q_OBJECT + public: + explicit ModUpdateDialog(QWidget* parent, + BaseInstance* instance, + const std::shared_ptr& mod_model, + std::list& search_for); + + void checkCandidates(); + + void appendMod(const CheckUpdateTask::UpdatableMod& info); + + const std::list getTasks(); + auto indexDir() const -> QDir { return m_mod_model->indexDir(); } + + auto noUpdates() const -> bool { return m_no_updates; }; + auto aborted() const -> bool { return m_aborted; }; + + private: + auto ensureMetadata() -> bool; + + private slots: + void onMetadataEnsured(Mod&); + void onMetadataFailed(Mod&); + + private: + QWidget* m_parent; + + SequentialTask m_check_task; + ModrinthCheckUpdate* m_modrinth_check_task = nullptr; + FlameCheckUpdate* m_flame_check_task = nullptr; + + const std::shared_ptr& m_mod_model; + + std::list& m_candidates; + std::list m_modrinth_to_update; + std::list m_flame_to_update; + + std::list m_failed_metadata; + std::list> m_failed_check_update; + + QHash m_tasks; + BaseInstance* m_instance; + + bool m_no_updates = false; + bool m_aborted = false; +}; diff --git a/launcher/ui/dialogs/ScrollMessageBox.ui b/launcher/ui/dialogs/ScrollMessageBox.ui index 299d2ecc..e684185f 100644 --- a/launcher/ui/dialogs/ScrollMessageBox.ui +++ b/launcher/ui/dialogs/ScrollMessageBox.ui @@ -6,7 +6,7 @@ 0 0 - 400 + 500 455 -- cgit