From 2d63c860227f4a539526a85d8999f867ae67ce43 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 9 Aug 2022 01:26:53 -0300 Subject: feat: make Task a QRunnable This makes it possible to run a task in another thread. I added a variable to toggle debug prints because they seem to trigger an assertion on Qt internals when the task in on another thread. Of course, this isn't awesome, but can wait until we improve our logging. Signed-off-by: flow --- launcher/tasks/Task.cpp | 24 ++++++++++++++++-------- launcher/tasks/Task.h | 12 ++++++++++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index bb71b98c..b4babdd4 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -37,8 +37,9 @@ #include -Task::Task(QObject *parent) : QObject(parent) +Task::Task(QObject *parent, bool show_debug) : QObject(parent), m_show_debug(show_debug) { + setAutoDelete(false); } void Task::setStatus(const QString &new_status) @@ -63,27 +64,32 @@ void Task::start() { case State::Inactive: { - qDebug() << "Task" << describe() << "starting for the first time"; + if (m_show_debug) + qDebug() << "Task" << describe() << "starting for the first time"; break; } case State::AbortedByUser: { - qDebug() << "Task" << describe() << "restarting for after being aborted by user"; + if (m_show_debug) + qDebug() << "Task" << describe() << "restarting for after being aborted by user"; break; } case State::Failed: { - qDebug() << "Task" << describe() << "restarting for after failing at first"; + if (m_show_debug) + qDebug() << "Task" << describe() << "restarting for after failing at first"; break; } case State::Succeeded: { - qDebug() << "Task" << describe() << "restarting for after succeeding at first"; + if (m_show_debug) + qDebug() << "Task" << describe() << "restarting for after succeeding at first"; break; } case State::Running: { - qWarning() << "The launcher tried to start task" << describe() << "while it was already running!"; + if (m_show_debug) + qWarning() << "The launcher tried to start task" << describe() << "while it was already running!"; return; } } @@ -118,7 +124,8 @@ void Task::emitAborted() } m_state = State::AbortedByUser; m_failReason = "Aborted."; - qDebug() << "Task" << describe() << "aborted."; + if (m_show_debug) + qDebug() << "Task" << describe() << "aborted."; emit aborted(); emit finished(); } @@ -132,7 +139,8 @@ void Task::emitSucceeded() return; } m_state = State::Succeeded; - qDebug() << "Task" << describe() << "succeeded"; + if (m_show_debug) + qDebug() << "Task" << describe() << "succeeded"; emit succeeded(); emit finished(); } diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index aafaf68c..2baf0188 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -35,9 +35,11 @@ #pragma once +#include + #include "QObjectPtr.h" -class Task : public QObject { +class Task : public QObject, public QRunnable { Q_OBJECT public: using Ptr = shared_qobject_ptr; @@ -45,7 +47,7 @@ class Task : public QObject { enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser }; public: - explicit Task(QObject* parent = 0); + explicit Task(QObject* parent = 0, bool show_debug_log = true); virtual ~Task() = default; bool isRunning() const; @@ -95,6 +97,9 @@ class Task : public QObject { void stepStatus(QString status); public slots: + // QRunnable's interface + void run() override { start(); } + virtual void start(); virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); }; @@ -117,4 +122,7 @@ class Task : public QObject { QString m_status; int m_progress = 0; int m_progressTotal = 100; + + // TODO: Nuke in favor of QLoggingCategory + bool m_show_debug = true; }; -- cgit From 3225f514f64533394e14bf7aee4e61c19a72ed2f Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 9 Aug 2022 01:53:50 -0300 Subject: refactor: move general info from Mod to Resource This allows us to create other resources that are not Mods, but can still share a significant portion of code. Signed-off-by: flow --- launcher/CMakeLists.txt | 2 + launcher/MMCZip.cpp | 6 +-- launcher/minecraft/MinecraftInstance.cpp | 2 +- launcher/minecraft/mod/Mod.cpp | 74 +++++------------------------ launcher/minecraft/mod/Mod.h | 45 ++---------------- launcher/minecraft/mod/Resource.cpp | 53 +++++++++++++++++++++ launcher/minecraft/mod/Resource.h | 74 +++++++++++++++++++++++++++++ launcher/modplatform/EnsureMetadataTask.cpp | 2 +- launcher/ui/widgets/MCModInfoFrame.cpp | 2 +- 9 files changed, 150 insertions(+), 110 deletions(-) create mode 100644 launcher/minecraft/mod/Resource.cpp create mode 100644 launcher/minecraft/mod/Resource.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index cff07b4b..70997e51 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -318,6 +318,8 @@ set(MINECRAFT_SOURCES minecraft/mod/ModDetails.h minecraft/mod/ModFolderModel.h minecraft/mod/ModFolderModel.cpp + minecraft/mod/Resource.h + minecraft/mod/Resource.cpp minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.h diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 04ca5094..9f4e968f 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -148,7 +148,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const // do not merge disabled mods. if (!mod->enabled()) continue; - if (mod->type() == Mod::MOD_ZIPFILE) + if (mod->type() == ResourceType::ZIPFILE) { if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { @@ -158,7 +158,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const return false; } } - else if (mod->type() == Mod::MOD_SINGLEFILE) + else if (mod->type() == ResourceType::SINGLEFILE) { // FIXME: buggy - does not work with addedFiles auto filename = mod->fileinfo(); @@ -171,7 +171,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const } addedFiles.insert(filename.fileName()); } - else if (mod->type() == Mod::MOD_FOLDER) + else if (mod->type() == ResourceType::FOLDER) { // untested, but seems to be unused / not possible to reach // FIXME: buggy - does not work with addedFiles diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index c677b677..b42aeda3 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -714,7 +714,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr }); for(auto mod: modList) { - if(mod->type() == Mod::MOD_FOLDER) + if(mod->type() == ResourceType::FOLDER) { out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)"; continue; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 588d76e3..f28fd32a 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -36,13 +36,10 @@ #include "Mod.h" +#include #include #include -#include -#include - -#include "Application.h" #include "MetadataHandler.h" namespace { @@ -51,75 +48,27 @@ ModDetails invalidDetails; } -Mod::Mod(const QFileInfo& file) +Mod::Mod(const QFileInfo& file) : Resource(file) { - repath(file); - m_changedDateTime = file.lastModified(); + m_enabled = (file.suffix() != "disabled"); } Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) - : m_file(mods_dir.absoluteFilePath(metadata.filename)) - , m_internal_id(metadata.filename) - , m_name(metadata.name) + : Mod(mods_dir.absoluteFilePath(metadata.filename)) { - if (m_file.isDir()) { - m_type = MOD_FOLDER; - } else { - if (metadata.filename.endsWith(".zip") || metadata.filename.endsWith(".jar")) - m_type = MOD_ZIPFILE; - else if (metadata.filename.endsWith(".litemod")) - m_type = MOD_LITEMOD; - else - m_type = MOD_SINGLEFILE; - } - - m_enabled = true; - m_changedDateTime = m_file.lastModified(); - + m_name = metadata.name; m_temp_metadata = std::make_shared(std::move(metadata)); } -void Mod::repath(const QFileInfo& file) -{ - m_file = file; - QString name_base = file.fileName(); - - m_type = Mod::MOD_UNKNOWN; - - m_internal_id = name_base; - - if (m_file.isDir()) { - m_type = MOD_FOLDER; - m_name = name_base; - } else if (m_file.isFile()) { - if (name_base.endsWith(".disabled")) { - m_enabled = false; - name_base.chop(9); - } else { - m_enabled = true; - } - if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) { - m_type = MOD_ZIPFILE; - name_base.chop(4); - } else if (name_base.endsWith(".litemod")) { - m_type = MOD_LITEMOD; - name_base.chop(8); - } else { - m_type = MOD_SINGLEFILE; - } - m_name = name_base; - } -} - auto Mod::enable(bool value) -> bool { - if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) + if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) return false; if (m_enabled == value) return false; - QString path = m_file.absoluteFilePath(); + QString path = m_file_info.absoluteFilePath(); QFile file(path); if (value) { if (!path.endsWith(".disabled")) @@ -136,7 +85,7 @@ auto Mod::enable(bool value) -> bool } if (status() == ModStatus::NoMetadata) - repath(QFileInfo(path)); + setFile(QFileInfo(path)); m_enabled = value; return true; @@ -175,8 +124,7 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool } } - m_type = MOD_UNKNOWN; - return FS::deletePath(m_file.filePath()); + return Resource::destroy(); } auto Mod::details() const -> const ModDetails& @@ -239,8 +187,8 @@ auto Mod::metadata() const -> const std::shared_ptr void Mod::finishResolvingWithDetails(std::shared_ptr details) { - m_resolving = false; - m_resolved = true; + m_is_resolving = false; + m_is_resolved = true; m_localDetails = details; setStatus(m_temp_status); diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 7a13e44b..313c478c 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -39,38 +39,23 @@ #include #include -#include "QObjectPtr.h" +#include "Resource.h" #include "ModDetails.h" -class Mod : public QObject +class Mod : public Resource { Q_OBJECT public: - enum ModType - { - MOD_UNKNOWN, //!< Indicates an unspecified mod type. - MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files. - MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). - MOD_FOLDER, //!< The mod is in a folder on the filesystem. - MOD_LITEMOD, //!< The mod is a litemod - }; - using Ptr = shared_qobject_ptr; Mod() = default; Mod(const QFileInfo &file); explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); - auto fileinfo() const -> QFileInfo { return m_file; } - auto dateTimeChanged() const -> QDateTime { return m_changedDateTime; } - auto internal_id() const -> QString { return m_internal_id; } - auto type() const -> ModType { return m_type; } auto enabled() const -> bool { return m_enabled; } - auto valid() const -> bool { return m_type != MOD_UNKNOWN; } - auto details() const -> const ModDetails&; - auto name() const -> QString; + auto name() const -> QString override; auto version() const -> QString; auto homeurl() const -> QString; auto description() const -> QString; @@ -85,31 +70,12 @@ public: auto enable(bool value) -> bool; - // delete all the files of this mod + // Delete all the files of this mod auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool; - // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) - void repath(const QFileInfo &file); - - auto shouldResolve() const -> bool { return !m_resolving && !m_resolved; } - auto isResolving() const -> bool { return m_resolving; } - auto resolutionTicket() const -> int { return m_resolutionTicket; } - - void setResolving(bool resolving, int resolutionTicket) { - m_resolving = resolving; - m_resolutionTicket = resolutionTicket; - } void finishResolvingWithDetails(std::shared_ptr details); protected: - QFileInfo m_file; - QDateTime m_changedDateTime; - - QString m_internal_id; - /* Name as reported via the file name */ - QString m_name; - ModType m_type = MOD_UNKNOWN; - /* If the mod has metadata, this will be filled in the constructor, and passed to * the ModDetails when calling finishResolvingWithDetails */ std::shared_ptr m_temp_metadata; @@ -120,7 +86,4 @@ protected: std::shared_ptr m_localDetails; bool m_enabled = true; - bool m_resolving = false; - bool m_resolved = false; - int m_resolutionTicket = 0; }; diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp new file mode 100644 index 00000000..8771a20f --- /dev/null +++ b/launcher/minecraft/mod/Resource.cpp @@ -0,0 +1,53 @@ +#include "Resource.h" + +#include "FileSystem.h" + +Resource::Resource(QObject* parent) : QObject(parent) {} + +Resource::Resource(QFileInfo file_info) : QObject() +{ + setFile(file_info); +} + +void Resource::setFile(QFileInfo file_info) +{ + m_file_info = file_info; + parseFile(); +} + +void Resource::parseFile() +{ + QString file_name{ m_file_info.fileName() }; + + m_type = ResourceType::UNKNOWN; + + m_internal_id = file_name; + + if (m_file_info.isDir()) { + m_type = ResourceType::FOLDER; + m_name = file_name; + } else if (m_file_info.isFile()) { + if (file_name.endsWith(".disabled")) + file_name.chop(9); + + if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) { + m_type = ResourceType::ZIPFILE; + file_name.chop(4); + } else if (file_name.endsWith(".litemod")) { + m_type = ResourceType::LITEMOD; + file_name.chop(8); + } else { + m_type = ResourceType::SINGLEFILE; + } + + m_name = file_name; + } + + m_changed_date_time = m_file_info.lastModified(); +} + +bool Resource::destroy() +{ + m_type = ResourceType::UNKNOWN; + return FS::deletePath(m_file_info.filePath()); +} diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h new file mode 100644 index 00000000..c348c7e2 --- /dev/null +++ b/launcher/minecraft/mod/Resource.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include + +#include "QObjectPtr.h" + +enum class ResourceType { + UNKNOWN, //!< Indicates an unspecified resource type. + ZIPFILE, //!< The resource is a zip file containing the resource's class files. + SINGLEFILE, //!< The resource is a single file (not a zip file). + FOLDER, //!< The resource is in a folder on the filesystem. + LITEMOD, //!< The resource is a litemod +}; + +/** General class for managed resources. It mirrors a file in disk, with some more info + * for display and house-keeping purposes. + * + * Subclass it to add additional data / behavior, such as Mods or Resource packs. + */ +class Resource : public QObject { + Q_OBJECT + Q_DISABLE_COPY(Resource) + public: + using Ptr = shared_qobject_ptr; + + Resource(QObject* parent = nullptr); + Resource(QFileInfo file_info); + ~Resource() override = default; + + void setFile(QFileInfo file_info); + void parseFile(); + + [[nodiscard]] auto fileinfo() const -> QFileInfo { return m_file_info; } + [[nodiscard]] auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; } + [[nodiscard]] auto internal_id() const -> QString { return m_internal_id; } + [[nodiscard]] auto type() const -> ResourceType { return m_type; } + + [[nodiscard]] virtual auto name() const -> QString { return m_name; } + [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } + + [[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; } + [[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; } + [[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; } + + void setResolving(bool resolving, int resolutionTicket) + { + m_is_resolving = resolving; + m_resolution_ticket = resolutionTicket; + } + + // Delete all files of this resource. + bool destroy(); + + protected: + /* The file corresponding to this resource. */ + QFileInfo m_file_info; + /* The cached date when this file was last changed. */ + QDateTime m_changed_date_time; + + /* Internal ID for internal purposes. Properties such as human-readability should not be assumed. */ + QString m_internal_id; + /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */ + QString m_name; + + /* The type of file we're dealing with. */ + ResourceType m_type = ResourceType::UNKNOWN; + + /* Used to keep trach of pending / concluded actions on the resource. */ + bool m_is_resolving = false; + bool m_is_resolved = false; + int m_resolution_ticket = 0; +}; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 60c54c4e..21c20b28 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -110,7 +110,7 @@ void EnsureMetadataTask::executeTask() } // Folders don't have metadata - if (mod->type() == Mod::MOD_FOLDER) { + if (mod->type() == ResourceType::FOLDER) { emitReady(mod); } } diff --git a/launcher/ui/widgets/MCModInfoFrame.cpp b/launcher/ui/widgets/MCModInfoFrame.cpp index 7d78006b..22475abc 100644 --- a/launcher/ui/widgets/MCModInfoFrame.cpp +++ b/launcher/ui/widgets/MCModInfoFrame.cpp @@ -23,7 +23,7 @@ void MCModInfoFrame::updateWithMod(Mod &m) { - if (m.type() == m.MOD_FOLDER) + if (m.type() == ResourceType::FOLDER) { clear(); return; -- cgit From ec62d8e97334d3b5a30cea00858e7035468f3609 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 9 Aug 2022 01:58:22 -0300 Subject: refactor: move general code from mod model to its own model This aims to continue decoupling other types of resources (e.g. resource packs, shader packs, etc) from mods, so that we don't have to continuously watch our backs for changes to one of them affecting the others. To do so, this creates a more general list model for resources, based on the mods one, that allows you to extend it with functionality for other resources. I had to do some template and preprocessor stuff to get around the QObject limitation of not allowing templated classes, so that's sadge :c On the other hand, I tried cleaning up most general-purpose code in the mod model, and added some documentation, because it looks nice :D Signed-off-by: flow --- launcher/CMakeLists.txt | 3 + launcher/minecraft/mod/ModFolderModel.cpp | 424 ++++----------------- launcher/minecraft/mod/ModFolderModel.h | 93 +---- launcher/minecraft/mod/ResourceFolderModel.cpp | 336 ++++++++++++++++ launcher/minecraft/mod/ResourceFolderModel.h | 274 +++++++++++++ launcher/minecraft/mod/tasks/BasicFolderLoadTask.h | 44 +++ launcher/minecraft/mod/tasks/LocalModParseTask.cpp | 16 +- launcher/minecraft/mod/tasks/LocalModParseTask.h | 15 +- launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp | 8 +- launcher/minecraft/mod/tasks/ModFolderLoadTask.h | 12 +- .../ui/pages/instance/ExternalResourcesPage.cpp | 8 +- 11 files changed, 778 insertions(+), 455 deletions(-) create mode 100644 launcher/minecraft/mod/ResourceFolderModel.cpp create mode 100644 launcher/minecraft/mod/ResourceFolderModel.h create mode 100644 launcher/minecraft/mod/tasks/BasicFolderLoadTask.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 70997e51..d1e0befd 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -320,10 +320,13 @@ set(MINECRAFT_SOURCES minecraft/mod/ModFolderModel.cpp minecraft/mod/Resource.h minecraft/mod/Resource.cpp + minecraft/mod/ResourceFolderModel.h + minecraft/mod/ResourceFolderModel.cpp minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp + minecraft/mod/tasks/BasicFolderLoadTask.h minecraft/mod/tasks/ModFolderLoadTask.h minecraft/mod/tasks/ModFolderLoadTask.cpp minecraft/mod/tasks/LocalModParseTask.h diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index d4c5e819..597f9807 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -49,226 +49,91 @@ #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" -ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : QAbstractListModel(), m_dir(dir), m_is_indexed(is_indexed) +ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(dir), m_is_indexed(is_indexed) { FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); - m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); - m_watcher = new QFileSystemWatcher(this); - connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); } -void ModFolderModel::startWatching() +Task* ModFolderModel::createUpdateTask() { - if(is_watching) - return; + auto index_dir = indexDir(); + auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load); + m_first_folder_load = false; + return task; +} +void ModFolderModel::startWatching() +{ // Remove orphaned metadata next time m_first_folder_load = true; - - update(); - - // Watch the mods folder - is_watching = m_watcher->addPath(m_dir.absolutePath()); - if (is_watching) { - qDebug() << "Started watching " << m_dir.absolutePath(); - } else { - qDebug() << "Failed to start watching " << m_dir.absolutePath(); - } - - // Watch the mods index folder - is_watching = m_watcher->addPath(indexDir().absolutePath()); - if (is_watching) { - qDebug() << "Started watching " << indexDir().absolutePath(); - } else { - qDebug() << "Failed to start watching " << indexDir().absolutePath(); - } + ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); } void ModFolderModel::stopWatching() { - if(!is_watching) - return; - - is_watching = !m_watcher->removePath(m_dir.absolutePath()); - if (!is_watching) { - qDebug() << "Stopped watching " << m_dir.absolutePath(); - } else { - qDebug() << "Failed to stop watching " << m_dir.absolutePath(); - } - - is_watching = !m_watcher->removePath(indexDir().absolutePath()); - if (!is_watching) { - qDebug() << "Stopped watching " << indexDir().absolutePath(); - } else { - qDebug() << "Failed to stop watching " << indexDir().absolutePath(); - } + ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); } -bool ModFolderModel::update() +void ModFolderModel::onUpdateSucceeded() { - if (!isValid()) { - return false; - } - if(m_update) { - scheduled_update = true; - return true; - } - - auto index_dir = indexDir(); - auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load); - m_first_folder_load = false; - - m_update = task->result(); + auto update_results = static_cast(m_current_update_task.get())->result(); - QThreadPool *threadPool = QThreadPool::globalInstance(); - connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate); - - threadPool->start(task); - return true; -} + auto& new_mods = update_results->mods; -void ModFolderModel::finishUpdate() -{ #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - auto currentList = modsIndex.keys(); - QSet currentSet(currentList.begin(), currentList.end()); - auto & newMods = m_update->mods; - auto newList = newMods.keys(); - QSet newSet(newList.begin(), newList.end()); + auto current_list = m_resources_index.keys(); + QSet current_set(current_list.begin(), current_list.end()); + + auto new_list = new_mods.keys(); + QSet new_set(new_list.begin(), new_list.end()); #else - QSet currentSet = modsIndex.keys().toSet(); - auto& newMods = m_update->mods; - QSet newSet = newMods.keys().toSet(); + QSet current_set(m_resources_index.keys().toSet()); + QSet new_set(new_mods.keys().toSet()); #endif - // see if the kept mods changed in some way - { - QSet kept = currentSet; - kept.intersect(newSet); - for(auto& keptMod : kept) { - auto newMod = newMods[keptMod]; - auto row = modsIndex[keptMod]; - auto currentMod = mods[row]; - if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) { - // no significant change, ignore... - continue; - } - auto oldMod = mods[row]; - if(oldMod->isResolving()) { - activeTickets.remove(oldMod->resolutionTicket()); - } - - mods[row] = newMod; - resolveMod(mods[row]); - emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); - } - } - - // remove mods no longer present - { - QSet removed = currentSet; - QList removedRows; - removed.subtract(newSet); - for(auto & removedMod: removed) { - removedRows.append(modsIndex[removedMod]); - } - std::sort(removedRows.begin(), removedRows.end(), std::greater()); - for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) { - int removedIndex = *iter; - beginRemoveRows(QModelIndex(), removedIndex, removedIndex); - auto removedIter = mods.begin() + removedIndex; - if((*removedIter)->isResolving()) { - activeTickets.remove((*removedIter)->resolutionTicket()); - } - - mods.erase(removedIter); - endRemoveRows(); - } - } - - // add new mods to the end - { - QSet added = newSet; - added.subtract(currentSet); - - // When you have a Qt build with assertions turned on, proceeding here will abort the application - if (added.size() > 0) { - beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1); - for (auto& addedMod : added) { - mods.append(newMods[addedMod]); - resolveMod(mods.last()); - } - endInsertRows(); - } - } - - // update index - { - modsIndex.clear(); - int idx = 0; - for(auto mod: mods) { - modsIndex[mod->internal_id()] = idx; - idx++; - } - } - - m_update.reset(); + applyUpdates(current_set, new_set, new_mods); + + update_results.reset(); + m_current_update_task.reset(); emit updateFinished(); - if(scheduled_update) { - scheduled_update = false; + if(m_scheduled_update) { + m_scheduled_update = false; update(); } } -void ModFolderModel::resolveMod(Mod::Ptr m) +Task* ModFolderModel::createParseTask(Resource const& resource) { - if(!m->shouldResolve()) { - return; - } - - auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo()); - auto result = task->result(); - result->id = m->internal_id(); - activeTickets.insert(nextResolutionTicket, result); - m->setResolving(true, nextResolutionTicket); - nextResolutionTicket++; - QThreadPool *threadPool = QThreadPool::globalInstance(); - connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse); - threadPool->start(task); + return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); } -void ModFolderModel::finishModParse(int token) +void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) { - auto iter = activeTickets.find(token); - if(iter == activeTickets.end()) { + auto iter = m_active_parse_tasks.constFind(ticket); + if (iter == m_active_parse_tasks.constEnd()) return; - } - auto result = *iter; - activeTickets.remove(token); - int row = modsIndex[result->id]; - auto mod = mods[row]; - mod->finishResolvingWithDetails(result->details); + + int row = m_resources_index[mod_id]; + + auto parse_task = *iter; + auto cast_task = static_cast(parse_task.get()); + + Q_ASSERT(cast_task->token() == ticket); + + auto resource = find(mod_id); + + auto result = cast_task->result(); + if (result && resource) + resource->finishResolvingWithDetails(result->details); + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); -} -void ModFolderModel::disableInteraction(bool disabled) -{ - if (interaction_disabled == disabled) { - return; - } - interaction_disabled = disabled; - if(size()) { - emit dataChanged(index(0), index(size() - 1)); - } + parse_task->deleteLater(); + m_active_parse_tasks.remove(ticket); } -void ModFolderModel::directoryChanged(QString path) -{ - update(); -} bool ModFolderModel::isValid() { @@ -277,104 +142,28 @@ bool ModFolderModel::isValid() auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList { - QList selected_mods; + QList selected_resources; for (auto i : indexes) { if(i.column() != 0) continue; - selected_mods.push_back(mods[i.row()]); + selected_resources.push_back(at(i.row())); } - return selected_mods; + return selected_resources; } -// FIXME: this does not take disabled mod (with extra .disable extension) into account... -bool ModFolderModel::installMod(const QString &filename) +auto ModFolderModel::allMods() -> QList { - if(interaction_disabled) { - return false; - } - - // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName - auto originalPath = FS::NormalizePath(filename); - QFileInfo fileinfo(originalPath); - - if (!fileinfo.exists() || !fileinfo.isReadable()) - { - qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath; - return false; - } - qDebug() << "installing: " << fileinfo.absoluteFilePath(); - - Mod installedMod(fileinfo); - if (!installedMod.valid()) - { - qDebug() << originalPath << "is not a valid mod. Ignoring it."; - return false; - } - - auto type = installedMod.type(); - if (type == Mod::MOD_UNKNOWN) - { - qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it."; - return false; - } - - auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName())); - if(originalPath == newpath) - { - qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense..."; - return false; - } + QList mods; - if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) - { - if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled"))) - { - if(!QFile::remove(newpath)) - { - // FIXME: report error in a user-visible way - qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed."; - return false; - } - qDebug() << newpath << "has been deleted."; - } - if (!QFile::copy(fileinfo.filePath(), newpath)) - { - qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed."; - // FIXME: report error in a user-visible way - return false; - } - FS::updateTimestamp(newpath); - QFileInfo newpathInfo(newpath); - installedMod.repath(newpathInfo); - update(); - return true; - } - else if (type == Mod::MOD_FOLDER) - { - QString from = fileinfo.filePath(); - if(QFile::exists(newpath)) - { - qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath; - return false; - } + for (auto res : m_resources) + mods.append(static_cast(res.get())); - if (!FS::copy(from, newpath)()) - { - qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed."; - return false; - } - QFileInfo newpathInfo(newpath); - installedMod.repath(newpathInfo); - update(); - return true; - } - return false; + return mods; } bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) { - for(auto mod : allMods()){ if(mod->fileinfo().fileName() == filename){ auto index_dir = indexDir(); @@ -388,7 +177,7 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable) { - if(interaction_disabled) { + if(!m_can_interact) { return false; } @@ -407,7 +196,7 @@ bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusActio bool ModFolderModel::deleteMods(const QModelIndexList& indexes) { - if(interaction_disabled) { + if(!m_can_interact) { return false; } @@ -419,7 +208,7 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes) if(i.column() != 0) { continue; } - auto m = mods[i.row()]; + auto m = at(i.row()); auto index_dir = indexDir(); m->destroy(index_dir); } @@ -433,48 +222,45 @@ int ModFolderModel::columnCount(const QModelIndex &parent) const QVariant ModFolderModel::data(const QModelIndex &index, int role) const { - if (!index.isValid()) - return QVariant(); + if (!validateIndex(index)) + return {}; int row = index.row(); int column = index.column(); - if (row < 0 || row >= mods.size()) - return QVariant(); - switch (role) { case Qt::DisplayRole: switch (column) { case NameColumn: - return mods[row]->name(); + return m_resources[row]->name(); case VersionColumn: { - switch(mods[row]->type()) { - case Mod::MOD_FOLDER: + switch(m_resources[row]->type()) { + case ResourceType::FOLDER: return tr("Folder"); - case Mod::MOD_SINGLEFILE: + case ResourceType::SINGLEFILE: return tr("File"); default: break; } - return mods[row]->version(); + return at(row)->version(); } case DateColumn: - return mods[row]->dateTimeChanged(); + return m_resources[row]->dateTimeChanged(); default: return QVariant(); } case Qt::ToolTipRole: - return mods[row]->internal_id(); + return m_resources[row]->internal_id(); case Qt::CheckStateRole: switch (column) { case ActiveColumn: - return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked; + return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; default: return QVariant(); } @@ -499,11 +285,11 @@ bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, in bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action) { - if(row < 0 || row >= mods.size()) { + if(row < 0 || row >= m_resources.size()) { return false; } - auto &mod = mods[row]; + auto mod = at(row); bool desiredStatus; switch(action) { case Enable: @@ -528,12 +314,12 @@ bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction actio return false; } auto newId = mod->internal_id(); - if(modsIndex.contains(newId)) { + if(m_resources_index.contains(newId)) { // NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled // But is it necessary? } - modsIndex.remove(oldId); - modsIndex[newId] = row; + m_resources_index.remove(oldId); + m_resources_index[newId] = row; emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); return true; } @@ -577,65 +363,3 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in return QVariant(); } -Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - auto flags = defaultFlags; - if(interaction_disabled) { - flags &= ~Qt::ItemIsDropEnabled; - } - else - { - flags |= Qt::ItemIsDropEnabled; - if(index.isValid()) { - flags |= Qt::ItemIsUserCheckable; - } - } - return flags; -} - -Qt::DropActions ModFolderModel::supportedDropActions() const -{ - // copy from outside, move from within and other mod lists - return Qt::CopyAction | Qt::MoveAction; -} - -QStringList ModFolderModel::mimeTypes() const -{ - QStringList types; - types << "text/uri-list"; - return types; -} - -bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) -{ - if (action == Qt::IgnoreAction) - { - return true; - } - - // check if the action is supported - if (!data || !(action & supportedDropActions())) - { - return false; - } - - // files dropped from outside? - if (data->hasUrls()) - { - auto urls = data->urls(); - for (auto url : urls) - { - // only local files may be dropped... - if (!url.isLocalFile()) - { - continue; - } - // TODO: implement not only copy, but also move - // FIXME: handle errors here - installMod(url.toLocalFile()); - } - return true; - } - return false; -} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 3d6efac3..a90457d5 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -44,6 +44,7 @@ #include #include "Mod.h" +#include "ResourceFolderModel.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" #include "minecraft/mod/tasks/LocalModParseTask.h" @@ -56,7 +57,7 @@ class QFileSystemWatcher; * A legacy mod list. * Backed by a folder. */ -class ModFolderModel : public QAbstractListModel +class ModFolderModel : public ResourceFolderModel { Q_OBJECT public: @@ -75,48 +76,18 @@ public: }; ModFolderModel(const QString &dir, bool is_indexed = false); - virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - Qt::DropActions supportedDropActions() const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - /// flags, mostly to support drag&drop - virtual Qt::ItemFlags flags(const QModelIndex &index) const override; - QStringList mimeTypes() const override; - bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex &parent) const override; - virtual int rowCount(const QModelIndex &) const override - { - return size(); - } + [[nodiscard]] Task* createUpdateTask() override; + [[nodiscard]] Task* createParseTask(Resource const&) override; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - virtual int columnCount(const QModelIndex &parent) const override; - - size_t size() const - { - return mods.size(); - } - ; - bool empty() const - { - return size() == 0; - } - Mod& operator[](size_t index) - { - return *mods[index]; - } - const Mod& at(size_t index) const - { - return *mods.at(index); - } - - /// Reloads the mod list and returns true if the list changed. - bool update(); - - /** - * Adds the given mod to the list at the given index - if the list supports custom ordering - */ - bool installMod(const QString& filename); + // Alias for old code, consider those deprecated and don't use in new code :gun: + bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); } + void disableInteraction(bool disabled) { ResourceFolderModel::enableInteraction(!disabled); } bool uninstallMod(const QString& filename, bool preserve_metadata = false); @@ -126,55 +97,27 @@ public: /// Enable or disable listed mods bool setModStatus(const QModelIndexList &indexes, ModStatusAction action); - void startWatching(); - void stopWatching(); - bool isValid(); - QDir& dir() - { - return m_dir; - } - - QDir indexDir() - { - return { QString("%1/.index").arg(dir().absolutePath()) }; - } + void startWatching(); + void stopWatching(); - const QList& allMods() - { - return mods; - } + QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; } auto selectedMods(QModelIndexList& indexes) -> QList; + auto allMods() -> QList; -public slots: - void disableInteraction(bool disabled); + RESOURCE_HELPERS(Mod) private slots: - void directoryChanged(QString path); - void finishUpdate(); - void finishModParse(int token); - -signals: - void updateFinished(); + void onUpdateSucceeded() override; + void onParseSucceeded(int ticket, QString resource_id) override; private: - void resolveMod(Mod::Ptr m); bool setModStatus(int index, ModStatusAction action); protected: - QFileSystemWatcher *m_watcher; - bool is_watching = false; - ModFolderLoadTask::ResultPtr m_update; - bool scheduled_update = false; - bool interaction_disabled = false; - QDir m_dir; bool m_is_indexed; bool m_first_folder_load = true; - QMap modsIndex; - QMap activeTickets; - int nextResolutionTicket = 0; - QList mods; }; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp new file mode 100644 index 00000000..4867a8c2 --- /dev/null +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -0,0 +1,336 @@ +#include "ResourceFolderModel.h" + +#include +#include +#include +#include + +#include "FileSystem.h" + +#include "minecraft/mod/tasks/BasicFolderLoadTask.h" + +#include "tasks/Task.h" + +ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_watcher(this) +{ + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); +} + +bool ResourceFolderModel::startWatching(const QStringList paths) +{ + if (m_is_watching) + return false; + + update(); + + auto couldnt_be_watched = m_watcher.addPaths(paths); + for (auto path : paths) { + if (couldnt_be_watched.contains(path)) + qDebug() << "Failed to start watching " << path; + else + qDebug() << "Started watching " << path; + } + + m_is_watching = !m_is_watching; + return m_is_watching; +} + +bool ResourceFolderModel::stopWatching(const QStringList paths) +{ + if (!m_is_watching) + return false; + + auto couldnt_be_stopped = m_watcher.removePaths(paths); + for (auto path : paths) { + if (couldnt_be_stopped.contains(path)) + qDebug() << "Failed to stop watching " << path; + else + qDebug() << "Stopped watching " << path; + } + + m_is_watching = !m_is_watching; + return !m_is_watching; +} + +bool ResourceFolderModel::installResource(QString original_path) +{ + if (!m_can_interact) { + return false; + } + + // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName + original_path = FS::NormalizePath(original_path); + QFileInfo file_info(original_path); + + if (!file_info.exists() || !file_info.isReadable()) { + qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path; + return false; + } + qDebug() << "Installing: " << file_info.absoluteFilePath(); + + Resource resource(file_info); + if (!resource.valid()) { + qWarning() << original_path << "is not a valid resource. Ignoring it."; + return false; + } + + auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName())); + if (original_path == new_path) { + qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense..."; + return false; + } + + switch (resource.type()) { + case ResourceType::SINGLEFILE: + case ResourceType::ZIPFILE: + case ResourceType::LITEMOD: { + if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) { + if (!QFile::remove(new_path)) { + qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; + return false; + } + qDebug() << new_path << "has been deleted."; + } + + if (!QFile::copy(original_path, new_path)) { + qCritical() << "Copy from" << original_path << "to" << new_path << "has failed."; + return false; + } + + FS::updateTimestamp(new_path); + + QFileInfo new_path_file_info(new_path); + resource.setFile(new_path_file_info); + + update(); + + return true; + } + case ResourceType::FOLDER: { + if (QFile::exists(new_path)) { + qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path; + return false; + } + + if (!FS::copy(original_path, new_path)()) { + qWarning() << "Copy of folder from" << original_path << "to" << new_path << "has (potentially partially) failed."; + return false; + } + + QFileInfo newpathInfo(new_path); + resource.setFile(newpathInfo); + + update(); + + return true; + } + default: + break; + } + return false; +} + +bool ResourceFolderModel::uninstallResource(QString file_name) +{ + for (auto resource : m_resources) { + if (resource->fileinfo().fileName() == file_name) + return resource->destroy(); + } + return false; +} + +bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) +{ + if (!m_can_interact) + return false; + + if (indexes.isEmpty()) + return true; + + for (auto i : indexes) { + if (i.column() != 0) { + continue; + } + + auto resource = m_resources.at(i.row()); + resource->destroy(); + } + return true; +} + +bool ResourceFolderModel::update() +{ + // Already updating, so we schedule a future update and return. + if (m_current_update_task) { + m_scheduled_update = true; + return false; + } + + m_current_update_task.reset(createUpdateTask()); + + connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, Qt::ConnectionType::QueuedConnection); + connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); + + auto* thread_pool = QThreadPool::globalInstance(); + thread_pool->start(m_current_update_task.get()); + + return true; +} + +void ResourceFolderModel::resolveResource(Resource::Ptr res) +{ + if (!res->shouldResolve()) { + return; + } + + auto task = createParseTask(*res); + + m_ticket_mutex.lock(); + int ticket = m_next_resolution_ticket; + m_next_resolution_ticket += 1; + m_ticket_mutex.unlock(); + + res->setResolving(true, ticket); + m_active_parse_tasks.insert(ticket, task); + + connect(task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + connect(task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + + auto* thread_pool = QThreadPool::globalInstance(); + thread_pool->start(task); +} + +void ResourceFolderModel::onUpdateSucceeded() +{ + auto update_results = static_cast(m_current_update_task.get())->result(); + + auto& new_resources = update_results->resources; + +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + auto current_list = m_resources_index.keys(); + QSet current_set(current_list.begin(), current_list.end()); + + auto new_list = new_resources.keys(); + QSet new_set(new_list.begin(), new_list.end()); +#else + QSet current_set(m_resources_index.keys().toSet()); + QSet new_set(new_resources.keys().toSet()); +#endif + + applyUpdates(current_set, new_set, new_resources); + + update_results.reset(); + m_current_update_task->deleteLater(); + m_current_update_task.reset(); + + emit updateFinished(); + + if (m_scheduled_update) { + m_scheduled_update = false; + update(); + } +} + +void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) +{ + auto iter = m_active_parse_tasks.constFind(ticket); + if (iter == m_active_parse_tasks.constEnd()) + return; + + (*iter)->deleteLater(); + m_active_parse_tasks.remove(ticket); + + int row = m_resources_index[resource_id]; + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); +} + +Task* ResourceFolderModel::createUpdateTask() +{ + return new BasicFolderLoadTask(m_dir); +} + +void ResourceFolderModel::directoryChanged(QString path) +{ + update(); +} + +Qt::DropActions ResourceFolderModel::supportedDropActions() const +{ + // copy from outside, move from within and other resource lists + return Qt::CopyAction | Qt::MoveAction; +} + +Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + auto flags = defaultFlags; + if (!m_can_interact) { + flags &= ~Qt::ItemIsDropEnabled; + } else { + flags |= Qt::ItemIsDropEnabled; + if (index.isValid()) { + flags |= Qt::ItemIsUserCheckable; + } + } + return flags; +} + +QStringList ResourceFolderModel::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) +{ + if (action == Qt::IgnoreAction) { + return true; + } + + // check if the action is supported + if (!data || !(action & supportedDropActions())) { + return false; + } + + // files dropped from outside? + if (data->hasUrls()) { + auto urls = data->urls(); + for (auto url : urls) { + // only local files may be dropped... + if (!url.isLocalFile()) { + continue; + } + // TODO: implement not only copy, but also move + // FIXME: handle errors here + installResource(url.toLocalFile()); + } + return true; + } + return false; +} + +bool ResourceFolderModel::validateIndex(const QModelIndex& index) const +{ + if (!index.isValid()) + return false; + + size_t row = index.row(); + if (row < 0 || row >= size()) + return false; + + return true; +} + +void ResourceFolderModel::enableInteraction(bool enabled) +{ + if (m_can_interact == enabled) + return; + + m_can_interact = enabled; + if (size()) + emit dataChanged(index(0), index(size() - 1)); +} diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h new file mode 100644 index 00000000..31fd7414 --- /dev/null +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -0,0 +1,274 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Resource.h" + +#include "tasks/Task.h" + +class QRunnable; + +/** A basic model for external resources. + * + * To implement one such model, you need to implement, at the very minimum: + * - columnCount: The number of columns in your model. + * - data: How the model data is displayed and accessed. + * - headerData: Display properties of the header. + */ +class ResourceFolderModel : public QAbstractListModel { + Q_OBJECT + public: + ResourceFolderModel(QDir, QObject* parent = nullptr); + + /** Starts watching the paths for changes. + * + * Returns whether starting to watch all the paths was successful. + * If one or more fails, it returns false. + */ + bool startWatching(const QStringList paths); + + /** Stops watching the paths for changes. + * + * Returns whether stopping to watch all the paths was successful. + * If one or more fails, it returns false. + */ + bool stopWatching(const QStringList paths); + + /** Given a path in the system, install that resource, moving it to its place in the + * instance file hierarchy. + * + * Returns whether the installation was succcessful. + */ + virtual bool installResource(QString path); + + /** Uninstall (i.e. remove all data about it) a resource, given its file name. + * + * Returns whether the removal was successful. + */ + virtual bool uninstallResource(QString file_name); + + virtual bool deleteResources(const QModelIndexList&); + + /** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */ + virtual bool update(); + + /** Creates a new parse task, if needed, for 'res' and start it.*/ + virtual void resolveResource(Resource::Ptr res); + + [[nodiscard]] size_t size() const { return m_resources.size(); }; + [[nodiscard]] bool empty() const { return size() == 0; } + + [[nodiscard]] QDir const& dir() const { return m_dir; } + + /* Qt behavior */ + + [[nodiscard]] int rowCount(const QModelIndex&) const override { return size(); } + [[nodiscard]] int columnCount(const QModelIndex&) const override = 0; + + [[nodiscard]] Qt::DropActions supportedDropActions() const override; + + /// flags, mostly to support drag&drop + [[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override; + [[nodiscard]] QStringList mimeTypes() const override; + bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; + + [[nodiscard]] bool validateIndex(const QModelIndex& index) const; + + [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override = 0; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override { return false; }; + + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override = 0; + + public slots: + void enableInteraction(bool enabled); + + signals: + void updateFinished(); + + protected: + /** This creates a new update task to be executed by update(). + * + * The task should load and parse all resources necessary, and provide a way of accessing such results. + * + * This Task is normally executed when opening a page, so it shouldn't contain much heavy work. + * If such work is needed, try using it in the Task create by createParseTask() instead! + */ + [[nodiscard]] virtual Task* createUpdateTask(); + + /** This creates a new parse task to be executed by onUpdateSucceeded(). + * + * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed + * in the background, so it slowly updates the UI as tasks get done. + */ + [[nodiscard]] virtual Task* createParseTask(Resource const&) { return nullptr; }; + + /** Standard implementation of the model update logic. + * + * It uses set operations to find differences between the current state and the updated state, + * to act only on those disparities. + * + * The implementation is at the end of this header. + */ + template + void applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources); + + protected slots: + void directoryChanged(QString); + + /** Called when the update task is successful. + * + * This usually calls static_cast on the specific Task type returned by createUpdateTask, + * so care must be taken in such cases. + * TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro dissalows that). + */ + virtual void onUpdateSucceeded(); + virtual void onUpdateFailed() {} + + /** Called when the parse task with the given ticket is successful. + * + * This is just a simple reference implementation. You probably want to override it with your own logic in a subclass + * if the resource is complex and has more stuff to parse. + */ + virtual void onParseSucceeded(int ticket, QString resource_id); + virtual void onParseFailed(int ticket, QString resource_id) {} + + protected: + bool m_can_interact = true; + + QDir m_dir; + QFileSystemWatcher m_watcher; + bool m_is_watching = false; + + Task::Ptr m_current_update_task = nullptr; + bool m_scheduled_update = false; + + QList m_resources; + + // Represents the relationship between a resource's internal ID and it's row position on the model. + QMap m_resources_index; + + QMap m_active_parse_tasks; + int m_next_resolution_ticket = 0; + QMutex m_ticket_mutex; +}; + +/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ +#define RESOURCE_HELPERS(T) \ + [[nodiscard]] T* operator[](size_t index) \ + { \ + return static_cast(m_resources[index].get()); \ + } \ + [[nodiscard]] T* at(size_t index) \ + { \ + return static_cast(m_resources[index].get()); \ + } \ + [[nodiscard]] const T* at(size_t index) const \ + { \ + return static_cast(m_resources.at(index).get()); \ + } \ + [[nodiscard]] T* first() \ + { \ + return static_cast(m_resources.first().get()); \ + } \ + [[nodiscard]] T* last() \ + { \ + return static_cast(m_resources.last().get()); \ + } \ + [[nodiscard]] T* find(QString id) \ + { \ +