From 208ed73e59c46ee7966f463558c07805a9b541e6 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 8 Jul 2022 13:00:44 -0300 Subject: [PATCH] feat: add early modrinth pack updating Still some FIXMEs and TODOs to consider, but the general thing is here! Signed-off-by: flow --- launcher/InstanceImportTask.cpp | 11 +- launcher/InstanceList.cpp | 66 ++++++++-- launcher/InstanceList.h | 7 +- launcher/InstanceTask.h | 7 ++ .../modrinth/ModrinthInstanceCreationTask.cpp | 118 ++++++++++++++++-- .../modrinth/ModrinthInstanceCreationTask.h | 2 +- 6 files changed, 185 insertions(+), 26 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index b19b5fa6..4bdf9cd2 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -593,15 +593,16 @@ void InstanceImportTask::processModrinth() inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); - connect(inst_creation_task, &Task::succeeded, this, &InstanceImportTask::emitSucceeded); + connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] { + setOverride(inst_creation_task->shouldOverride()); + emitSucceeded(); + }); connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress); connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus); - connect(inst_creation_task, &Task::finished, this, [inst_creation_task]{ inst_creation_task->deleteLater(); }); + connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater); - connect(this, &Task::aborted, inst_creation_task, [inst_creation_task] { - inst_creation_task->abort(); - }); + connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort); inst_creation_task->start(); } diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 4447a17c..698aa24e 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -535,7 +535,20 @@ InstancePtr InstanceList::getInstanceById(QString instId) const return InstancePtr(); } -QModelIndex InstanceList::getInstanceIndexById(const QString& id) const +InstancePtr InstanceList::getInstanceByManagedName(QString managed_name) const +{ + if (managed_name.isEmpty()) + return {}; + + for (auto instance : m_instances) { + if (instance->getManagedPackName() == managed_name) + return instance; + } + + return {}; +} + +QModelIndex InstanceList::getInstanceIndexById(const QString &id) const { return index(getInstIndex(getInstanceById(id).get())); } @@ -764,9 +777,8 @@ class InstanceStaging : public Task { Q_OBJECT const unsigned minBackoff = 1; const unsigned maxBackoff = 16; - public: - InstanceStaging(InstanceList* parent, Task* child, const QString& stagingPath, const QString& instanceName, const QString& groupName) + InstanceStaging(InstanceList* parent, InstanceTask* child, const QString& stagingPath, const QString& instanceName, const QString& groupName) : backoff(minBackoff, maxBackoff) { m_parent = parent; @@ -808,7 +820,8 @@ class InstanceStaging : public Task { void childSucceded() { unsigned sleepTime = backoff(); - if (m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) { + if (m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName, m_child->shouldOverride())) + { emitSucceeded(); return; } @@ -834,8 +847,8 @@ class InstanceStaging : public Task { */ ExponentialSeries backoff; QString m_stagingPath; - InstanceList* m_parent; - unique_qobject_ptr m_child; + InstanceList * m_parent; + unique_qobject_ptr m_child; QString m_instanceName; QString m_groupName; QTimer m_backoffTimer; @@ -866,23 +879,52 @@ QString InstanceList::getStagedInstancePath() return path; } -bool InstanceList::commitStagedInstance(const QString& path, const QString& instanceName, const QString& groupName) +bool InstanceList::commitStagedInstance(const QString& path, const QString& instanceName, const QString& groupName, bool should_override) { QDir dir; - QString instID = FS::DirNameFromString(instanceName, m_instDir); + QString instID; + InstancePtr inst; + + QString raw_inst_name = instanceName.section(' ', 0, -2); + if (should_override) { + // This is to avoid problems when the instance folder gets manually renamed + if ((inst = getInstanceByManagedName(raw_inst_name))) { + instID = QFileInfo(inst->instanceRoot()).fileName(); + } else { + instID = FS::RemoveInvalidFilenameChars(raw_inst_name, '-'); + } + } else { + instID = FS::DirNameFromString(raw_inst_name, m_instDir); + } + { WatchLock lock(m_watcher, m_instDir); QString destination = FS::PathCombine(m_instDir, instID); - if (!dir.rename(path, destination)) { - qWarning() << "Failed to move" << path << "to" << destination; - return false; + + if (should_override) { + if (!FS::overrideFolder(destination, path)) { + qWarning() << "Failed to override" << path << "to" << destination; + return false; + } + + if (!inst) + inst = getInstanceById(instID); + if (inst) + inst->setName(instanceName); + } else { + if (!dir.rename(path, destination)) { + qWarning() << "Failed to move" << path << "to" << destination; + return false; + } } m_instanceGroupIndex[instID] = groupName; - instanceSet.insert(instID); m_groupNameCache.insert(groupName); + instanceSet.insert(instID); + emit instancesChanged(); emit instanceSelectRequest(instID); } + saveGroupList(); return true; } diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index 62282f04..6b4dcfa4 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -101,7 +101,10 @@ public: InstListError loadList(); void saveNow(); + /* O(n) */ InstancePtr getInstanceById(QString id) const; + /* O(n) */ + InstancePtr getInstanceByManagedName(QString managed_name) const; QModelIndex getInstanceIndexById(const QString &id) const; QStringList getGroups(); bool isGroupCollapsed(const QString &groupName); @@ -127,8 +130,10 @@ public: /** * Commit the staging area given by @keyPath to the provider - used when creation succeeds. * Used by instance manipulation tasks. + * should_override is used when another similar instance already exists, and we want to override it + * - for instance, when updating it. */ - bool commitStagedInstance(const QString & keyPath, const QString& instanceName, const QString & groupName); + bool commitStagedInstance(const QString & keyPath, const QString& instanceName, const QString & groupName, bool should_override); /** * Destroy a previously created staging area given by @keyPath - used when creation fails. diff --git a/launcher/InstanceTask.h b/launcher/InstanceTask.h index 82e23f11..02810a52 100644 --- a/launcher/InstanceTask.h +++ b/launcher/InstanceTask.h @@ -43,10 +43,17 @@ public: return m_instGroup; } + bool shouldOverride() const { return m_override_existing; } + +protected: + void setOverride(bool override) { m_override_existing = override; } + protected: /* data */ SettingsObjectPtr m_globalSettings; QString m_instName; QString m_instIcon; QString m_instGroup; QString m_stagingPath; + + bool m_override_existing = false; }; diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 7eb6cc8f..efb8c99b 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -2,25 +2,128 @@ #include "Application.h" #include "FileSystem.h" +#include "InstanceList.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "net/NetJob.h" +#include "modplatform/ModIndex.h" + #include "net/ChecksumValidator.h" #include "settings/INISettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" +#include + +bool ModrinthCreationTask::abort() +{ + if (m_files_job) + return m_files_job->abort(); + return true; +} + +bool ModrinthCreationTask::updateInstance() +{ + auto instance_list = APPLICATION->instances(); + + // FIXME: How to handle situations when there's more than one install already for a given modpack? + // Based on the way we create the instance name (name + " " + version). Is there a better way? + auto inst = instance_list->getInstanceByManagedName(m_instName.section(' ', 0, -2)); + + if (!inst) { + inst = instance_list->getInstanceById(m_instName); + + if (!inst) + return false; + } + + QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (!parseManifest(index_path, m_files)) + return false; + + auto version_id = inst->getManagedPackVersionID(); + auto version_str = !version_id.isEmpty() ? tr(" (version %1)").arg(version_id) : ""; + + auto info = CustomMessageBox::selectable(m_parent, tr("Similar modpack was found!"), + tr("One or more of your instances are from this same modpack%1. Do you want to create a " + "separate instance, or update the existing one?") + .arg(version_str), + QMessageBox::Information, QMessageBox::Ok | QMessageBox::Abort); + info->setButtonText(QMessageBox::Ok, tr("Update existing instance")); + info->setButtonText(QMessageBox::Abort, tr("Create new instance")); + + if (info->exec() && info->clickedButton() == info->button(QMessageBox::Abort)) + return false; + + // Remove repeated files, we don't need to download them! + QDir old_inst_dir(inst->instanceRoot()); + + QString old_index_path(FS::PathCombine(old_inst_dir.absolutePath(), "mrpack", "modrinth.index.json")); + QFileInfo old_index_file(old_index_path); + if (old_index_file.exists()) { + std::vector old_files; + parseManifest(old_index_path, old_files); + + // Let's remove all duplicated, identical resources! + auto files_iterator = m_files.begin(); +begin: + while (files_iterator != m_files.end()) { + auto const& file = *files_iterator; + + auto old_files_iterator = old_files.begin(); + while (old_files_iterator != old_files.end()) { + auto const& old_file = *old_files_iterator; + + if (old_file.hash == file.hash) { + qDebug() << "Removed file at" << file.path << "from list of downloads"; + files_iterator = m_files.erase(files_iterator); + old_files_iterator = old_files.erase(old_files_iterator); + goto begin; // Sorry :c + } + + old_files_iterator++; + } + + files_iterator++; + } + + // Some files were removed from the old version, and some will be downloaded in an updated version, + // so we're fine removing them! + if (!old_files.empty()) { + QDir old_minecraft_dir(inst->gameRoot()); + for (auto const& file : old_files) { + qWarning() << "Removing" << file.path; + old_minecraft_dir.remove(file.path); + } + } + } + + // TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides? + + setOverride(true); + qDebug() << "Will override instance!"; + + // We let it go through the createInstance() stage, just with a couple modifications for updating + return false; +} + +// https://docs.modrinth.com/docs/modpacks/format_definition/ bool ModrinthCreationTask::createInstance() { QEventLoop loop; - if (m_files.empty() && !parseManifest()) + QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (m_files.empty() && !parseManifest(index_path, m_files)) return false; + // Keep index file in case we need it some other time (like when changing versions) + QString new_index_place(FS::PathCombine(m_stagingPath, "mrpack", "modrinth.index.json")); + FS::ensureFilePathExists(new_index_place); + QFile::rename(index_path, new_index_place); + auto mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); auto override_path = FS::PathCombine(m_stagingPath, "overrides"); @@ -43,6 +146,7 @@ bool ModrinthCreationTask::createInstance() QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", minecraftVersion, true); @@ -53,6 +157,7 @@ bool ModrinthCreationTask::createInstance() components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion); if (!forgeVersion.isEmpty()) components->setComponentVersion("net.minecraftforge", forgeVersion); + if (m_instIcon != "default") { instance.setIconKey(m_instIcon); } else { @@ -101,11 +206,10 @@ bool ModrinthCreationTask::createInstance() return ended_well; } -bool ModrinthCreationTask::parseManifest() +bool ModrinthCreationTask::parseManifest(QString index_path, std::vector& files) { try { - QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); - auto doc = Json::requireDocument(indexPath); + auto doc = Json::requireDocument(index_path); auto obj = Json::requireObject(doc, "modrinth.index.json"); int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); if (formatVersion == 1) { @@ -184,7 +288,7 @@ bool ModrinthCreationTask::parseManifest() } } - m_files.push_back(file); + files.push_back(file); } auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); @@ -205,7 +309,7 @@ bool ModrinthCreationTask::parseManifest() } else { throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); } - QFile::remove(indexPath); + } catch (const JSONValidationError& e) { setError(tr("Could not understand pack index:\n") + e.cause()); return false; diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h index 61f7dd5c..4e804e58 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -24,7 +24,7 @@ class ModrinthCreationTask final : public InstanceCreationTask { bool createInstance() override; private: - bool parseManifest(); + bool parseManifest(QString, std::vector&); QString getManagedPackID() const; private: