aboutsummaryrefslogtreecommitdiff
path: root/launcher/minecraft/mod
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/minecraft/mod')
-rw-r--r--launcher/minecraft/mod/Mod.cpp180
-rw-r--r--launcher/minecraft/mod/Mod.h70
-rw-r--r--launcher/minecraft/mod/ModDetails.h61
-rw-r--r--launcher/minecraft/mod/ModFolderModel.cpp624
-rw-r--r--launcher/minecraft/mod/ModFolderModel.h100
-rw-r--r--launcher/minecraft/mod/ModFolderModel_test.cpp92
-rw-r--r--launcher/minecraft/mod/Resource.cpp147
-rw-r--r--launcher/minecraft/mod/Resource.h115
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.cpp522
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.h326
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel_test.cpp275
-rw-r--r--launcher/minecraft/mod/ResourcePack.h13
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.cpp22
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.h9
-rw-r--r--launcher/minecraft/mod/ShaderPackFolderModel.h10
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.cpp22
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.h6
-rw-r--r--launcher/minecraft/mod/tasks/BasicFolderLoadTask.h53
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.cpp140
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.h22
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp24
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.h13
-rw-r--r--launcher/minecraft/mod/testdata/supercoolmod.jar1
23 files changed, 1861 insertions, 986 deletions
diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp
index 588d76e3..39023f69 100644
--- a/launcher/minecraft/mod/Mod.cpp
+++ b/launcher/minecraft/mod/Mod.cpp
@@ -36,130 +36,77 @@
#include "Mod.h"
+#include <QDebug>
#include <QDir>
#include <QString>
+#include <QRegularExpression>
-#include <FileSystem.h>
-#include <QDebug>
-
-#include "Application.h"
#include "MetadataHandler.h"
+#include "Version.h"
-namespace {
-
-ModDetails invalidDetails;
-
-}
-
-Mod::Mod(const QFileInfo& file)
+Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
{
- 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)
-{
- 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_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
+ : Mod(mods_dir.absoluteFilePath(metadata.filename))
+{
+ m_name = metadata.name;
+ m_local_details.metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
}
-void Mod::repath(const QFileInfo& file)
+void Mod::setStatus(ModStatus status)
{
- 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;
- }
+ m_local_details.status = status;
}
-
-auto Mod::enable(bool value) -> bool
+void Mod::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata)
{
- if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER)
- return false;
-
- if (m_enabled == value)
- return false;
-
- QString path = m_file.absoluteFilePath();
- QFile file(path);
- if (value) {
- if (!path.endsWith(".disabled"))
- return false;
- path.chop(9);
-
- if (!file.rename(path))
- return false;
- } else {
- path += ".disabled";
-
- if (!file.rename(path))
- return false;
- }
-
if (status() == ModStatus::NoMetadata)
- repath(QFileInfo(path));
+ setStatus(ModStatus::Installed);
- m_enabled = value;
- return true;
+ m_local_details.metadata = metadata;
}
-void Mod::setStatus(ModStatus status)
+std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
{
- if (m_localDetails) {
- m_localDetails->status = status;
- } else {
- m_temp_status = status;
+ auto cast_other = dynamic_cast<Mod const*>(&other);
+ if (!cast_other)
+ return Resource::compare(other, type);
+
+ switch (type) {
+ default:
+ case SortType::ENABLED:
+ case SortType::NAME:
+ case SortType::DATE: {
+ auto res = Resource::compare(other, type);
+ if (res.first != 0)
+ return res;
+ }
+ case SortType::VERSION: {
+ auto this_ver = Version(version());
+ auto other_ver = Version(cast_other->version());
+ if (this_ver > other_ver)
+ return { 1, type == SortType::VERSION };
+ if (this_ver < other_ver)
+ return { -1, type == SortType::VERSION };
+ }
}
+ return { 0, false };
}
-void Mod::setMetadata(const Metadata::ModStruct& metadata)
+
+bool Mod::applyFilter(QRegularExpression filter) const
{
- if (status() == ModStatus::NoMetadata)
- setStatus(ModStatus::Installed);
+ if (filter.match(description()).hasMatch())
+ return true;
- if (m_localDetails) {
- m_localDetails->metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
- } else {
- m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
+ for (auto& author : authors()) {
+ if (filter.match(author).hasMatch()) {
+ return true;
+ }
}
+
+ return Resource::applyFilter(filter);
}
auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
@@ -175,13 +122,12 @@ 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&
{
- return m_localDetails ? *m_localDetails : invalidDetails;
+ return m_local_details;
}
auto Mod::name() const -> QString
@@ -218,35 +164,29 @@ auto Mod::authors() const -> QStringList
auto Mod::status() const -> ModStatus
{
- if (!m_localDetails)
- return m_temp_status;
return details().status;
}
auto Mod::metadata() -> std::shared_ptr<Metadata::ModStruct>
{
- if (m_localDetails)
- return m_localDetails->metadata;
- return m_temp_metadata;
+ return m_local_details.metadata;
}
auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct>
{
- if (m_localDetails)
- return m_localDetails->metadata;
- return m_temp_metadata;
+ return m_local_details.metadata;
}
-void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details)
+void Mod::finishResolvingWithDetails(ModDetails&& details)
{
- m_resolving = false;
- m_resolved = true;
- m_localDetails = details;
+ m_is_resolving = false;
+ m_is_resolved = true;
- setStatus(m_temp_status);
+ std::shared_ptr<Metadata::ModStruct> metadata = details.metadata;
+ if (details.status == ModStatus::Unknown)
+ details.status = m_local_details.status;
- if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) {
- setMetadata(*m_temp_metadata);
- m_temp_metadata.reset();
- }
+ m_local_details = std::move(details);
+ if (metadata)
+ setMetadata(std::move(metadata));
}
diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h
index 7a13e44b..f336bec4 100644
--- a/launcher/minecraft/mod/Mod.h
+++ b/launcher/minecraft/mod/Mod.h
@@ -39,38 +39,23 @@
#include <QFileInfo>
#include <QList>
-#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>;
+ using WeakPtr = QPointer<Mod>;
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; }
+ Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
+ Mod(QString file_path) : Mod(QFileInfo(file_path)) {}
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;
@@ -81,46 +66,17 @@ public:
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
void setStatus(ModStatus status);
- void setMetadata(const Metadata::ModStruct& metadata);
+ void setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata);
+ void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared<Metadata::ModStruct>(metadata)); }
- auto enable(bool value) -> bool;
+ [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
+ [[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
- // 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<ModDetails> details);
+ void finishResolvingWithDetails(ModDetails&& 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<Metadata::ModStruct> m_temp_metadata;
-
- /* Set the mod status while it doesn't have local details just yet */
- ModStatus m_temp_status = ModStatus::NoMetadata;
-
- std::shared_ptr<ModDetails> m_localDetails;
-
- bool m_enabled = true;
- bool m_resolving = false;
- bool m_resolved = false;
- int m_resolutionTicket = 0;
+ ModDetails m_local_details;
};
diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h
index 3e0a7ab0..dd84b0a3 100644
--- a/launcher/minecraft/mod/ModDetails.h
+++ b/launcher/minecraft/mod/ModDetails.h
@@ -46,34 +46,77 @@ enum class ModStatus {
Installed, // Both JAR and Metadata are present
NotInstalled, // Only the Metadata is present
NoMetadata, // Only the JAR is present
+ Unknown, // Default status
};
struct ModDetails
{
/* Mod ID as defined in the ModLoader-specific metadata */
- QString mod_id;
+ QString mod_id = {};
/* Human-readable name */
- QString name;
+ QString name = {};
/* Human-readable mod version */
- QString version;
+ QString version = {};
/* Human-readable minecraft version */
- QString mcversion;
+ QString mcversion = {};
/* URL for mod's home page */
- QString homeurl;
+ QString homeurl = {};
/* Human-readable description */
- QString description;
+ QString description = {};
/* List of the author's names */
- QStringList authors;
+ QStringList authors = {};
/* Installation status of the mod */
- ModStatus status;
+ ModStatus status = ModStatus::Unknown;
/* Metadata information, if any */
- std::shared_ptr<Metadata::ModStruct> metadata;
+ std::shared_ptr<Metadata::ModStruct> metadata = nullptr;
+
+ ModDetails() = default;
+
+ /** Metadata should be handled manually to properly set the mod status. */
+ ModDetails(ModDetails& other)
+ : mod_id(other.mod_id)
+ , name(other.name)
+ , version(other.version)
+ , mcversion(other.mcversion)
+ , homeurl(other.homeurl)
+ , description(other.description)
+ , authors(other.authors)
+ , status(other.status)
+ {}
+
+ ModDetails& operator=(ModDetails& other)
+ {
+ this->mod_id = other.mod_id;
+ this->name = other.name;
+ this->version = other.version;
+ this->mcversion = other.mcversion;
+ this->homeurl = other.homeurl;
+ this->description = other.description;
+ this->authors = other.authors;
+ this->status = other.status;
+
+ return *this;
+ }
+
+ ModDetails& operator=(ModDetails&& other)
+ {
+ this->mod_id = other.mod_id;
+ this->name = other.name;
+ this->version = other.version;
+ this->mcversion = other.mcversion;
+ this->homeurl = other.homeurl;
+ this->description = other.description;
+ this->authors = other.authors;
+ this->status = other.status;
+
+ return *this;
+ }
};
diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp
index 112d219e..4e264a74 100644
--- a/launcher/minecraft/mod/ModFolderModel.cpp
+++ b/launcher/minecraft/mod/ModFolderModel.cpp
@@ -49,428 +49,53 @@
#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(QDir(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()
-{
- if(is_watching)
- return;
-
- 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();
- }
-}
-
-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();
- }
-}
-
-bool ModFolderModel::update()
-{
- 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_update = task->result();
-
- QThreadPool *threadPool = QThreadPool::globalInstance();
- connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate);
-
- threadPool->start(task);
- return true;
-}
-
-void ModFolderModel::finishUpdate()
-{
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- auto currentList = modsIndex.keys();
- QSet<QString> currentSet(currentList.begin(), currentList.end());
- auto & newMods = m_update->mods;
- auto newList = newMods.keys();
- QSet<QString> newSet(newList.begin(), newList.end());
-#else
- QSet<QString> currentSet = modsIndex.keys().toSet();
- auto& newMods = m_update->mods;
- QSet<QString> newSet = newMods.keys().toSet();
-#endif
-
- // see if the kept mods changed in some way
- {
- QSet<QString> 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<QString> removed = currentSet;
- QList<int> removedRows;
- removed.subtract(newSet);
- for(auto & removedMod: removed) {
- removedRows.append(modsIndex[removedMod]);
- }
- std::sort(removedRows.begin(), removedRows.end(), std::greater<int>());
- 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<QString> 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();
-
- emit updateFinished();
-
- if(scheduled_update) {
- scheduled_update = false;
- update();
- }
-}
-
-void ModFolderModel::resolveMod(Mod::Ptr m)
-{
- 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);
-}
-
-void ModFolderModel::finishModParse(int token)
-{
- auto iter = activeTickets.find(token);
- if(iter == activeTickets.end()) {
- return;
- }
- auto result = *iter;
- activeTickets.remove(token);
- int row = modsIndex[result->id];
- auto mod = mods[row];
- mod->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));
- }
-}
-
-void ModFolderModel::directoryChanged(QString path)
-{
- update();
-}
-
-bool ModFolderModel::isValid()
-{
- return m_dir.exists() && m_dir.isReadable();
-}
-
-auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>
-{
- QList<Mod::Ptr> selected_mods;
- for (auto i : indexes) {
- if(i.column() != 0)
- continue;
-
- selected_mods.push_back(mods[i.row()]);
- }
- return selected_mods;
-}
-
-// FIXME: this does not take disabled mod (with extra .disable extension) into account...
-bool ModFolderModel::installMod(const QString &filename)
-{
- 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;
- }
-
- 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;
- }
-
- 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;
-}
-
-bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
-{
-
- for(auto mod : allMods()){
- if(mod->fileinfo().fileName() == filename){
- auto index_dir = indexDir();
- mod->destroy(index_dir, preserve_metadata);
- return true;
- }
- }
-
- return false;
-}
-
-bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable)
-{
- if(interaction_disabled) {
- return false;
- }
-
- if(indexes.isEmpty())
- return true;
-
- for (auto index: indexes)
- {
- if(index.column() != 0) {
- continue;
- }
- setModStatus(index.row(), enable);
- }
- return true;
-}
-
-bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
-{
- if(interaction_disabled) {
- return false;
- }
-
- if(indexes.isEmpty())
- return true;
-
- for (auto i: indexes)
- {
- if(i.column() != 0) {
- continue;
- }
- auto m = mods[i.row()];
- auto index_dir = indexDir();
- m->destroy(index_dir);
- }
- return true;
-}
-
-int ModFolderModel::columnCount(const QModelIndex &parent) const
-{
- return NUM_COLUMNS;
+ m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE };
}
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();
}
@@ -479,61 +104,6 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
}
}
-bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role)
-{
- if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
- {
- return false;
- }
-
- if (role == Qt::CheckStateRole)
- {
- return setModStatus(index.row(), Toggle);
- }
- return false;
-}
-
-bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action)
-{
- if(row < 0 || row >= mods.size()) {
- return false;
- }
-
- auto &mod = mods[row];
- bool desiredStatus;
- switch(action) {
- case Enable:
- desiredStatus = true;
- break;
- case Disable:
- desiredStatus = false;
- break;
- case Toggle:
- default:
- desiredStatus = !mod->enabled();
- break;
- }
-
- if(desiredStatus == mod->enabled()) {
- return true;
- }
-
- // preserve the row, but change its ID
- auto oldId = mod->internal_id();
- if(!mod->enable(!mod->enabled())) {
- return false;
- }
- auto newId = mod->internal_id();
- if(modsIndex.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;
- emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
- return true;
-}
-
QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
{
switch (role)
@@ -573,65 +143,151 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
return QVariant();
}
-Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const
+int ModFolderModel::columnCount(const QModelIndex &parent) const
+{
+ return NUM_COLUMNS;
+}
+
+Task* ModFolderModel::createUpdateTask()
+{
+ auto index_dir = indexDir();
+ auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, this);
+ m_first_folder_load = false;
+ return task;
+}
+
+Task* ModFolderModel::createParseTask(Resource const& resource)
+{
+ return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo());
+}
+
+bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
+{
+ for(auto mod : allMods()){
+ if(mod->fileinfo().fileName() == filename){
+ auto index_dir = indexDir();
+ mod->destroy(index_dir, preserve_metadata);
+
+ update();
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{
- Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
- auto flags = defaultFlags;
- if(interaction_disabled) {
- flags &= ~Qt::ItemIsDropEnabled;
+ if(!m_can_interact) {
+ return false;
}
- else
+
+ if(indexes.isEmpty())
+ return true;
+
+ for (auto i: indexes)
{
- flags |= Qt::ItemIsDropEnabled;
- if(index.isValid()) {
- flags |= Qt::ItemIsUserCheckable;
+ if(i.column() != 0) {
+ continue;
}
+ auto m = at(i.row());
+ auto index_dir = indexDir();
+ m->destroy(index_dir);
}
- return flags;
+
+ update();
+
+ return true;
}
-Qt::DropActions ModFolderModel::supportedDropActions() const
+bool ModFolderModel::isValid()
{
- // copy from outside, move from within and other mod lists
- return Qt::CopyAction | Qt::MoveAction;
+ return m_dir.exists() && m_dir.isReadable();
}
-QStringList ModFolderModel::mimeTypes() const
+bool ModFolderModel::startWatching()
{
- QStringList types;
- types << "text/uri-list";
- return types;
+ // Remove orphaned metadata next time
+ m_first_folder_load = true;
+ return ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
}
-bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
+bool ModFolderModel::stopWatching()
{
- if (action == Qt::IgnoreAction)
- {
- return true;
+ return ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
+}
+
+auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod*>
+{
+ QList<Mod*> selected_resources;
+ for (auto i : indexes) {
+ if(i.column() != 0)
+ continue;
+
+ selected_resources.push_back(at(i.row()));
}
+ return selected_resources;
+}
- // check if the action is supported
- if (!data || !(action & supportedDropActions()))
- {
- return false;
+auto ModFolderModel::allMods() -> QList<Mod*>
+{
+ QList<Mod*> mods;
+
+ for (auto& res : m_resources) {
+ mods.append(static_cast<Mod*>(res.get()));
}
- // 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 mods;
+}
+
+void ModFolderModel::onUpdateSucceeded()
+{
+ auto update_results = static_cast<ModFolderLoadTask*>(m_current_update_task.get())->result();
+
+ auto& new_mods = update_results->mods;
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ auto current_list = m_resources_index.keys();
+ QSet<QString> current_set(current_list.begin(), current_list.end());
+
+ auto new_list = new_mods.keys();
+ QSet<QString> new_set(new_list.begin(), new_list.end());
+#else
+ QSet<QString> current_set(m_resources_index.keys().toSet());
+ QSet<QString> new_set(new_mods.keys().toSet());
+#endif
+
+ applyUpdates(current_set, new_set, new_mods);
+
+ m_current_update_task.reset();
+
+ if (m_scheduled_update) {
+ m_scheduled_update = false;
+ update();
+ } else {
+ emit updateFinished();
}
- return false;
+}
+
+void ModFolderModel::onParseSucceeded(int ticket, QString mod_id)
+{
+ auto iter = m_active_parse_tasks.constFind(ticket);
+ if (iter == m_active_parse_tasks.constEnd())
+ return;
+
+ int row = m_resources_index[mod_id];
+
+ auto parse_task = *iter;
+ auto cast_task = static_cast<LocalModParseTask*>(parse_task.get());
+
+ Q_ASSERT(cast_task->token() == ticket);
+
+ auto resource = find(mod_id);
+
+ auto result = cast_task->result();
+ if (result && resource)
+ resource->finishResolvingWithDetails(std::move(result->details));
+
+ emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}
diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h
index a7d3ece0..c33195ed 100644
--- a/launcher/minecraft/mod/ModFolderModel.h
+++ b/launcher/minecraft/mod/ModFolderModel.h
@@ -44,6 +44,7 @@
#include <QAbstractListModel>
#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,105 +76,38 @@ 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;
- /// 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();
- }
-
- 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);
+ [[nodiscard]] Task* createUpdateTask() override;
+ [[nodiscard]] Task* createParseTask(Resource const&) override;
+ bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); }
bool uninstallMod(const QString& filename, bool preserve_metadata = false);
/// Deletes all the selected mods
bool deleteMods(const QModelIndexList &indexes);
- /// 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()) };
- }
+ bool startWatching() override;
+ bool stopWatching() override;
- const QList<Mod::Ptr>& allMods()
- {
- return mods;
- }
+ QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; }
- auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>;
+ auto selectedMods(QModelIndexList& indexes) -> QList<Mod*>;
+ auto allMods() -> QList<Mod*>;
-public slots:
- void disableInteraction(bool disabled);
+ RESOURCE_HELPERS(Mod)
private
slots:
- void directoryChanged(QString path);
- void finishUpdate();
- void finishModParse(int token);
-
-signals:
- void updateFinished();
-
-private:
- void resolveMod(Mod::Ptr m);
- bool setModStatus(int index, ModStatusAction action);
+ void onUpdateSucceeded() override;
+ void onParseSucceeded(int ticket, QString resource_id) override;
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;
- QMap<QString, int> modsIndex;
- QMap<int, LocalModParseTask::ResultPtr> activeTickets;
- int nextResolutionTicket = 0;
- QList<Mod::Ptr> mods;
+ bool m_first_folder_load = true;
};
diff --git a/launcher/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp
deleted file mode 100644
index b4d37ce5..00000000
--- a/launcher/minecraft/mod/ModFolderModel_test.cpp
+++ /dev/null
@@ -1,92 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
-* PolyMC - Minecraft Launcher
-* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, version 3.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see <https://www.gnu.org/licenses/>.
-*
-* This file incorporates work covered by the following copyright and
-* permission notice:
-*
-* Copyright 2013-2021 MultiMC Contributors
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
-
-#include <QTest>
-#include <QTemporaryDir>
-
-#include "FileSystem.h"
-#include "minecraft/mod/ModFolderModel.h"
-
-class ModFolderModelTest : public QObject
-{
- Q_OBJECT
-
-private
-slots:
- // test for GH-1178 - install a folder with files to a mod list
- void test_1178()
- {
- // source
- QString source = QFINDTESTDATA("testdata/test_folder");
-
- // sanity check
- QVERIFY(!source.endsWith('/'));
-
- auto verify = [](QString path)
- {
- QDir target_dir(FS::PathCombine(path, "test_folder"));
- QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
- QVERIFY(target_dir.entryList().contains("assets"));
- };
-
- // 1. test with no trailing /
- {
- QString folder = source;
- QTemporaryDir tempDir;
- QEventLoop loop;
- ModFolderModel m(tempDir.path(), true);
- connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
- m.installMod(folder);
- loop.exec();
- verify(tempDir.path());
- }
-
- // 2. test with trailing /
- {
- QString folder = source + '/';
- QTemporaryDir tempDir;
- QEventLoop loop;
- ModFolderModel m(tempDir.path(), true);
- connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
- m.installMod(folder);
- loop.exec();
- verify(tempDir.path());
- }
- }
-};
-
-QTEST_GUILESS_MAIN(ModFolderModelTest)
-
-#include "ModFolderModel_test.moc"
diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp
new file mode 100644
index 00000000..0fbcfd7c
--- /dev/null
+++ b/launcher/minecraft/mod/Resource.cpp
@@ -0,0 +1,147 @@
+#include "Resource.h"
+
+#include <QRegularExpression>
+
+#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);
+ m_enabled = false;
+ }
+
+ 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();
+}
+
+static void removeThePrefix(QString& string)
+{
+ QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption);
+ string.remove(regex);
+ string = string.trimmed();
+}
+
+std::pair<int, bool> Resource::compare(const Resource& other, SortType type) const
+{
+ switch (type) {
+ default:
+ case SortType::ENABLED:
+ if (enabled() && !other.enabled())
+ return { 1, type == SortType::ENABLED };
+ if (!enabled() && other.enabled())
+ return { -1, type == SortType::ENABLED };
+ case SortType::NAME: {
+ QString this_name{ name() };
+ QString other_name{ other.name() };
+
+ removeThePrefix(this_name);
+ removeThePrefix(other_name);
+
+ auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
+ if (compare_result != 0)
+ return { compare_result, type == SortType::NAME };
+ }
+ case SortType::DATE:
+ if (dateTimeChanged() > other.dateTimeChanged())
+ return { 1, type == SortType::DATE };
+ if (dateTimeChanged() < other.dateTimeChanged())
+ return { -1, type == SortType::DATE };
+ }
+
+ return { 0, false };
+}
+
+bool Resource::applyFilter(QRegularExpression filter) const
+{
+ return filter.match(name()).hasMatch();
+}
+
+bool Resource::enable(EnableAction action)
+{
+ if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER)
+ return false;
+
+
+ QString path = m_file_info.absoluteFilePath();
+ QFile file(path);
+
+ bool enable = true;
+ switch (action) {
+ case EnableAction::ENABLE:
+ enable = true;
+ break;
+ case EnableAction::DISABLE:
+ enable = false;
+ break;
+ case EnableAction::TOGGLE:
+ default:
+ enable = !enabled();
+ break;
+ }
+
+ if (m_enabled == enable)
+ return false;
+
+ if (enable) {
+ // m_enabled is false, but there's no '.disabled' suffix.
+ // TODO: Report error?
+ if (!path.endsWith(".disabled"))
+ return false;
+ path.chop(9);
+
+ if (!file.rename(path))
+ return false;
+ } else {
+ path += ".disabled";
+
+ if (!file.rename(path))
+ return false;
+ }
+
+ setFile(QFileInfo(path));
+
+ m_enabled = enable;
+ return true;
+}
+
+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..cee1f172
--- /dev/null
+++ b/launcher/minecraft/mod/Resource.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include <QDateTime>
+#include <QFileInfo>
+#include <QObject>
+#include <QPointer>
+
+#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
+};
+
+enum class SortType {
+ NAME,
+ DATE,
+ VERSION,
+ ENABLED,
+};
+
+enum class EnableAction {
+ ENABLE,
+ DISABLE,
+ TOGGLE
+};
+
+/** 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>;
+ using WeakPtr = QPointer<Resource>;
+
+ Resource(QObject* parent = nullptr);
+ Resource(QFileInfo file_info);
+ Resource(QString file_path) : Resource(QFileInfo(file_path)) {}
+
+ ~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]] bool enabled() const { return m_enabled; }
+
+ [[nodiscard]] virtual auto name() const -> QString { return m_name; }
+ [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; }
+
+ /** Compares two Resources, for sorting purposes, considering a ascending order, returning:
+ * > 0: 'this' comes after 'other'
+ * = 0: 'this' is equal to 'other'
+ * < 0: 'this' comes before 'other'
+ *
+ * The second argument in the pair is true if the sorting type that decided which one is greater was 'type'.
+ */
+ [[nodiscard]] virtual auto compare(Resource const& other, SortType type = SortType::NAME) const -> std::pair<int, bool>;
+
+ /** Returns whether the given filter should filter out 'this' (false),
+ * or if such filter includes the Resource (true).
+ */
+ [[nodiscard]] virtual bool applyFilter(QRegularExpression filter) const;
+
+ /** Changes the enabled property, according to 'action'.
+ *
+ * Returns whether a change was applied to the Resource's properties.
+ */
+ bool enable(EnableAction action);
+
+ [[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;
+
+ /* Whether the resource is enabled (e.g. shows up in the game) or not. */
+ bool m_enabled = true;
+
+ /* 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/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp
new file mode 100644
index 00000000..bc18ddc2
--- /dev/null
+++ b/launcher/minecraft/mod/ResourceFolderModel.cpp
@@ -0,0 +1,522 @@
+#include "ResourceFolderModel.h"
+
+#include <QDebug>
+#include <QMimeData>
+#include <QThreadPool>
+#include <QUrl>
+
+#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;
+
+ 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;
+ }
+
+ update();
+
+ 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);
+
+ if (!m_is_watching)
+ return 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);
+
+ if (!m_is_watching)
+ return update();
+
+ return true;
+ }
+ default:
+ break;
+ }
+ return false;
+}
+
+bool ResourceFolderModel::uninstallResource(QString file_name)
+{
+ for (auto& resource : m_resources) {
+ if (resource->fileinfo().fileName() == file_name) {
+ auto res = resource->destroy();
+
+ update();
+
+ return res;
+ }
+ }
+ 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();
+ }
+
+ update();
+
+ return true;
+}
+
+bool ResourceFolderModel::setResourceEnabled(const QModelIndexList &indexes, EnableAction action)
+{
+ if (!m_can_interact)
+ return false;
+
+ if (indexes.isEmpty())
+ return true;
+
+ bool succeeded = true;
+ for (auto const& idx : indexes) {
+ if (!validateIndex(idx) || idx.column() != 0)
+ continue;
+
+ int row = idx.row();
+
+ auto& resource = m_resources[row];
+
+ // Preserve the row, but change its ID
+ auto old_id = resource->internal_id();
+ if (!resource->enable(action)) {
+ succeeded = false;
+ continue;
+ }
+
+ auto new_id = resource->internal_id();
+ if (m_resources_index.contains(new_id)) {
+ // FIXME: https://github.com/PolyMC/PolyMC/issues/550
+ }
+
+ m_resources_index.remove(old_id);
+ m_resources_index[new_id] = row;
+
+ emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
+ }
+
+ return succeeded;
+}
+
+static QMutex s_update_task_mutex;
+bool ResourceFolderModel::update()
+{
+ // We hold a lock here to prevent race conditions on the m_current_update_task reset.
+ QMutexLocker lock(&s_update_task_mutex);
+
+ // 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());
+ if (!m_current_update_task)
+ return false;
+
+ 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);
+ if (!task)
+ return;
+
+ 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);
+ connect(
+ task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection);
+
+ auto* thread_pool = QThreadPool::globalInstance();
+ thread_pool->start(task);
+}
+
+void ResourceFolderModel::onUpdateSucceeded()
+{
+ auto update_results = static_cast<BasicFolderLoadTask*>(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<QString> current_set(current_list.begin(), current_list.end());
+
+ auto new_list = new_resources.keys();
+ QSet<QString> new_set(new_list.begin(), new_list.end());
+#else
+ QSet<QString> current_set(m_resources_index.keys().toSet());
+ QSet<QString> new_set(new_resources.keys().toSet());
+#endif
+
+ applyUpdates(current_set, new_set, new_resources);
+
+ m_current_update_task.reset();
+
+ if (m_scheduled_update) {
+ m_scheduled_update = false;
+ update();
+ } else {
+ emit updateFinished();
+ }
+}
+
+void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id)
+{
+ auto iter = m_active_parse_tasks.constFind(ticket);
+ if (iter == m_active_parse_tasks.constEnd())
+ return;
+
+ int row = m_resources_index[resource_id];
+ emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
+}
+
+Task* ResourceFolderModel::createUpdateTask()
+{
+ return new BasicFolderLoadTask(m_dir);
+}
+
+bool ResourceFolderModel::hasPendingParseTasks() const
+{
+ return !m_active_parse_tasks.isEmpty();
+}
+
+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;
+
+ int row = index.row();
+ if (row < 0 || row >= m_resources.size())
+ return false;
+
+ return true;
+}
+
+QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
+{
+ if (!validateIndex(index))
+ return {};
+
+ int row = index.row();
+ int column = index.column();
+
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (column) {
+ case NAME_COLUMN:
+ return m_resources[row]->name();
+ case DATE_COLUMN:
+ return m_resources[row]->dateTimeChanged();
+ default:
+ return {};
+ }
+ case Qt::ToolTipRole:
+ return m_resources[row]->internal_id();
+ case Qt::CheckStateRole:
+ switch (column) {
+ case ACTIVE_COLUMN:
+ return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
+ default:
+ return {};
+ }
+ default:
+ return {};
+ }
+}
+
+bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+ int row = index.row();
+ if (row < 0 || row >= rowCount(index) || !index.isValid())
+ return false;
+
+ if (role == Qt::CheckStateRole)
+ return setResourceEnabled({ index }, EnableAction::TOGGLE);
+
+ return false;
+}
+
+QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (section) {
+ case NAME_COLUMN:
+ return tr("Name");
+ case DATE_COLUMN:
+ return tr("Last modified");
+ default:
+ return {};
+ }
+ case Qt::ToolTipRole: {
+ switch (section) {
+ case ACTIVE_COLUMN:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("Is the resource enabled?");
+ case NAME_COLUMN:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("The name of the resource.");
+ case DATE_COLUMN:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("The date and time this resource was last changed (or added).");
+ default:
+ return {};
+ }
+ }
+ default:
+ break;
+ }
+
+ return {};
+}
+
+QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent)
+{
+ return new ProxyModel(parent);
+}
+
+SortType ResourceFolderModel::columnToSortKey(size_t column) const
+{
+ Q_ASSERT(m_column_sort_keys.size() == columnCount());
+ return m_column_sort_keys.at(column);
+}
+
+void ResourceFolderModel::enableInteraction(bool enabled)
+{
+ if (m_can_interact == enabled)
+ return;
+
+ m_can_interact = enabled;
+ if (size())
+ emit dataChanged(index(0), index(size() - 1));
+}
+
+/* Standard Proxy Model for createFilterProxyModel */
+[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
+{
+ auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
+ if (!model)
+ return true;
+
+ const auto& resource = model->at(source_row);
+
+ return resource.applyFilter(filterRegularExpression());
+}
+
+[[nodiscard]] bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const
+{
+ auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
+ if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) {
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+ }
+
+ // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
+ // proceed.
+
+ auto column_sort_key = model->columnToSortKey(source_left.column());
+ auto const& resource_left = model->at(source_left.row());
+ auto const& resource_right = model->at(source_right.row());
+
+ auto compare_result = resource_left.compare(resource_right, column_sort_key);
+ if (compare_result.first == 0)
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+
+ if (compare_result.second || sortOrder() != Qt::DescendingOrder)
+ return (compare_result.first < 0);
+ return (compare_result.first > 0);
+}
diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h
new file mode 100644
index 00000000..e27b5db6
--- /dev/null
+++ b/launcher/minecraft/mod/ResourceFolderModel.h
@@ -0,0 +1,326 @@
+#pragma once
+
+#include <QAbstractListModel>
+#include <QDir>
+#include <QFileSystemWatcher>
+#include <QMutex>
+#include <QSet>
+#include <QSortFilterProxyModel>
+
+#include "Resource.h"
+
+#include "tasks/Task.h"
+
+class QSortFilterProxyModel;
+
+/** A basic model for external resources.
+ *
+ * This model manages a list of resources. As such, external users of such resources do not own them,
+ * and the resource's lifetime is contingent on the model's lifetime.
+ *
+ * TODO: Make the resources unique pointers accessible through weak pointers.
+ */
+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);
+
+ /* Helper methods for subclasses, using a predetermined list of paths. */
+ virtual bool startWatching() { return startWatching({ m_dir.absolutePath() }); };
+ virtual bool stopWatching() { return stopWatching({ m_dir.absolutePath() }); };
+
+ /** 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&);
+
+ /** Applies the given 'action' to the resources in 'indexes'.
+ *
+ * Returns whether the action was successfully applied to all resources.
+ */
+ virtual bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action);
+
+ /** 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]] Resource& at(int index) { return *m_resources.at(index); }
+ [[nodiscard]] Resource const& at(int index) const { return *m_resources.at(index); }
+ [[nodiscard]] QList<Resource::Ptr> const& all() const { return m_resources; }
+
+ [[nodiscard]] QDir const& dir() const { return m_dir; }
+
+ /** Checks whether there's any parse tasks being done.
+ *
+ * Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model while having
+ * such tasks would introduce an undefined behavior, most likely resulting in a crash.
+ */
+ [[nodiscard]] bool hasPendingParseTasks() const;
+
+ /* Qt behavior */
+
+ /* Basic columns */
+ enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS };
+
+ [[nodiscard]] int rowCount(const QModelIndex& = {}) const override { return size(); }
+ [[nodiscard]] int columnCount(const QModelIndex& = {}) const override { return NUM_COLUMNS; };
+
+ [[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;
+ bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
+
+ [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+
+ /** This creates a proxy model to filter / sort the model for a UI.
+ *
+ * The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead!
+ */
+ QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr);
+
+ [[nodiscard]] SortType columnToSortKey(size_t column) const;
+
+ class ProxyModel : public QSortFilterProxyModel {
+ public:
+ explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
+
+ protected:
+ [[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
+ [[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override;
+ };
+
+ public slots:
+ void enableInteraction(bool enabled);
+ void disableInteraction(bool disabled) { enableInteraction(!disabled); }
+
+ 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 <typename T>
+ void applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& 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 disallows 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:
+ // Represents the relationship between a column's index (represented by the list index), and it's sorting key.
+ // As such, the order in with they appear is very important!
+ QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE };
+
+ 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<Resource::Ptr> m_resources;
+
+ // Represents the relationship between a resource's internal ID and it's row position on the model.
+ QMap<QString, int> m_resources_index;
+
+ QMap<int, Task::Ptr> 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<T*>(m_resources[index].get()); \
+ } \
+ [[nodiscard]] T* at(size_t index) \
+ { \
+ return static_cast<T*>(m_resources[index].get()); \
+ } \
+ [[nodiscard]] const T* at(size_t index) const \
+ { \
+ return static_cast<const T*>(m_resources.at(index).get()); \
+ } \
+ [[nodiscard]] T* first() \
+ { \
+ return static_cast<T*>(m_resources.first().get()); \
+ } \
+ [[nodiscard]] T* last() \
+ { \
+ return static_cast<T*>(m_resources.last().get()); \
+ } \
+ [[nodiscard]] T* find(QString id) \
+ { \
+ auto iter = std::find_if(m_resources.constBegin(), m_resources.constEnd(), \
+ [&](Resource::Ptr const& r) { return r->internal_id() == id; }); \
+ if (iter == m_resources.constEnd()) \
+ return nullptr; \
+ return static_cast<T*>((*iter).get()); \
+ }
+
+/* Template definition to avoid some code duplication */
+template <typename T>
+void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& new_resources)
+{
+ // see if the kept resources changed in some way
+ {
+ QSet<QString> kept_set = current_set;
+ kept_set.intersect(new_set);
+
+ for (auto const& kept : kept_set) {
+ auto row_it = m_resources_index.constFind(kept);
+ Q_ASSERT(row_it != m_resources_index.constEnd());
+ auto row = row_it.value();
+
+ auto& new_resource = new_resources[kept];
+ auto const& current_resource = m_resources[row];
+
+ if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) {
+ // no significant change, ignore...
+ continue;
+ }
+
+ // If the resource is resolving, but something about it changed, we don't want to
+ // continue the resolving.
+ if (current_resource->isResolving()) {
+ auto task = (*m_active_parse_tasks.find(current_resource->resolutionTicket())).get();
+ task->abort();
+ }
+
+ m_resources[row].reset(new_resource);
+ resolveResource(m_resources.at(row));
+ emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
+ }
+ }
+
+ // remove resources no longer present
+ {
+ QSet<QString> removed_set = current_set;
+ removed_set.subtract(new_set);
+
+ QList<int> removed_rows;
+ for (auto& removed : removed_set)
+ removed_rows.append(m_resources_index[removed]);
+
+ std::sort(removed_rows.begin(), removed_rows.end(), std::greater<int>());
+
+ for (auto& removed_index : removed_rows) {
+ auto removed_it = m_resources.begin() + removed_index;
+
+ Q_ASSERT(removed_it != m_resources.end());
+ Q_ASSERT(removed_set.contains(removed_it->get()->internal_id()));
+
+ if ((*removed_it)->isResolving()) {
+ auto task = (*m_active_parse_tasks.find((*removed_it)->resolutionTicket())).get();
+ task->abort();
+ }
+
+ beginRemoveRows(QModelIndex(), removed_index, removed_index);
+ m_resources.erase(removed_it);
+ endRemoveRows();
+ }
+ }
+
+ // add new resources to the end
+ {
+ QSet<QString> added_set = new_set;
+ added_set.subtract(current_set);
+
+ // When you have a Qt build with assertions turned on, proceeding here will abort the application
+ if (added_set.size() > 0) {
+ beginInsertRows(QModelIndex(), m_resources.size(), m_resources.size() + added_set.size() - 1);
+
+ for (auto& added : added_set) {
+ auto res = new_resources[added];
+ m_resources.append(res);
+ resolveResource(res);
+ }
+
+ endInsertRows();
+ }
+ }
+
+ // update index
+ {
+ m_resources_index.clear();
+ int idx = 0;
+ for (auto const& mod : m_resources) {
+ m_resources_index[mod->internal_id()] = idx;
+ idx++;
+ }
+ }
+}
diff --git a/launcher/minecraft/mod/ResourceFolderModel_test.cpp b/launcher/minecraft/mod/ResourceFolderModel_test.cpp
new file mode 100644
index 00000000..fe98552e
--- /dev/null
+++ b/launcher/minecraft/mod/ResourceFolderModel_test.cpp
@@ -0,0 +1,275 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+* PolyMC - Minecraft Launcher
+* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, version 3.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see <https://www.gnu.org/licenses/>.
+*
+* This file incorporates work covered by the following copyright and
+* permission notice:
+*
+* Copyright 2013-2021 MultiMC Contributors
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+#include <QTest>
+#include <QTemporaryDir>
+#include <QTimer>
+
+#include "FileSystem.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/ResourceFolderModel.h"
+
+#define EXEC_UPDATE_TASK(EXEC, VERIFY) \
+ QEventLoop loop; \
+ \
+ connect(&model, &ResourceFolderModel::updateFinished, &loop, &QEventLoop::quit); \
+ \
+ QTimer expire_timer; \
+ expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \
+ expire_timer.setSingleShot(true); \
+ expire_timer.start(4000); \
+ \
+ VERIFY(EXEC); \
+ loop.exec(); \
+ \
+ QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); \
+ expire_timer.stop(); \
+ \
+ disconnect(&model, nullptr, nullptr, nullptr);
+
+class ResourceFolderModelTest : public QObject
+{
+ Q_OBJECT
+
+private
+slots:
+ // test for GH-1178 - install a folder with files to a mod list
+ void test_1178()
+ {
+ // source
+ QString source = QFINDTESTDATA("testdata/test_folder");
+
+ // sanity check
+ QVERIFY(!source.endsWith('/'));
+
+ auto verify = [](QString path)
+ {
+ QDir target_dir(FS::PathCombine(path, "test_folder"));
+ QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
+ QVERIFY(target_dir.entryList().contains("assets"));
+ };
+
+ // 1. test with no trailing /
+ {
+ QString folder = source;
+ QTemporaryDir tempDir;
+
+ QEventLoop loop;
+
+ ModFolderModel m(tempDir.path(), true);
+
+ connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
+
+ QTimer expire_timer;
+ expire_timer.callOnTimeout(&loop, &QEventLoop::quit);
+ expire_timer.setSingleShot(true);
+ expire_timer.start(4000);
+
+ m.installMod(folder);
+
+ loop.exec();
+
+ QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished.");
+ expire_timer.stop();
+
+ verify(tempDir.path());
+ }
+
+ // 2. test with trailing /
+ {
+ QString folder = source + '/';
+ QTemporaryDir tempDir;
+ QEventLoop loop;
+ ModFolderModel m(tempDir.path(), true);
+
+ connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
+
+ QTimer expire_timer;
+ expire_timer.callOnTimeout(&loop, &QEventLoop::quit);
+ expire_timer.setSingleShot(true);
+ expire_timer.start(4000);
+
+ m.installMod(folder);
+
+ loop.exec();
+
+ QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished.");
+ expire_timer.stop();
+
+ verify(tempDir.path());
+ }
+ }
+
+ void test_addFromWatch()
+ {
+ QString source = QFINDTESTDATA("testdata");
+
+ ModFolderModel model(source);
+
+ QCOMPARE(model.size(), 0);
+
+ EXEC_UPDATE_TASK(model.startWatching(), )
+
+ for (auto mod : model.allMods())
+ qDebug() << mod->name();
+
+ QCOMPARE(model.size(), 2);
+
+ model.stopWatching();
+
+ while (model.hasPendingParseTasks()) {
+ QTest::qSleep(20);
+ QCoreApplication::processEvents();
+ }
+ }
+
+ void test_removeResource()
+ {
+ QString folder_resource = QFINDTESTDATA("testdata/test_folder");
+ QString file_mod = QFINDTESTDATA("testdata/supercoolmod.jar");
+
+ QTemporaryDir tmp;
+
+ ResourceFolderModel model(QDir(tmp.path()));
+
+ QCOMPARE(model.size(), 0);
+
+ {
+ EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY)
+ }
+
+ QCOMPARE(model.size(), 1);
+ qDebug() << "Added first mod.";
+
+ {
+ EXEC_UPDATE_TASK(model.startWatching(), )
+ }
+
+ QCOMPARE(model.size(), 1);
+ qDebug() << "Started watching the temp folder.";
+
+ {
+ EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY)
+ }
+
+ QCOMPARE(model.size(), 2);
+ qDebug() << "Added second mod.";
+
+ {
+ EXEC_UPDATE_TASK(model.uninstallResource("supercoolmod.jar"), QVERIFY);
+ }
+
+ QCOMPARE(model.size(), 1);
+ qDebug() << "Removed first mod.";
+
+ QString mod_file_name {model.at(0).fileinfo().fileName()};
+ QVERIFY(!mod_file_name.isEmpty());
+
+ {
+ EXEC_UPDATE_TASK(model.uninstallResource(mod_file_name), QVERIFY);
+ }
+
+ QCOMPARE(model.size(), 0);
+ qDebug() << "Removed second mod.";
+
+ model.stopWatching();
+
+ while (model.hasPendingParseTasks()) {
+ QTest::qSleep(20);
+ QCoreApplication::processEvents();
+ }
+ }
+
+ void test_enable_disable()
+ {
+ QString folder_resource = QFINDTESTDATA("testdata/test_folder");
+ QString file_mod = QFINDTESTDATA("testdata/supercoolmod.jar");
+
+ QTemporaryDir tmp;
+ ResourceFolderModel model(tmp.path());
+
+ QCOMPARE(model.size(), 0);
+
+ {
+ EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY)
+ }
+ {
+ EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY)
+ }
+
+ for (auto res : model.all())
+ qDebug() << res->name();
+
+ QCOMPARE(model.size(), 2);
+
+ auto& res_1 = model.at(0).type() != ResourceType::FOLDER ? model.at(0) : model.at(1);
+ auto& res_2 = model.at(0).type() == ResourceType::FOLDER ? model.at(0) : model.at(1);
+ auto id_1 = res_1.internal_id();
+ auto id_2 = res_2.internal_id();
+ bool initial_enabled_res_2 = res_2.enabled();
+ bool initial_enabled_res_1 = res_1.enabled();
+
+ QVERIFY(res_1.type() != ResourceType::FOLDER && res_1.type() != ResourceType::UNKNOWN);
+ qDebug() << "res_1 is of the correct type.";
+ QVERIFY(res_1.enabled());
+ qDebug() << "res_1 is initially enabled.";
+
+ QVERIFY(res_1.enable(EnableAction::TOGGLE));
+
+ QVERIFY(res_1.enabled() == !initial_enabled_res_1);
+ qDebug() << "res_1 got successfully toggled.";
+
+ QVERIFY(res_1.enable(EnableAction::TOGGLE));
+ qDebug() << "res_1 got successfully toggled again.";
+
+ QVERIFY(res_1.enabled() == initial_enabled_res_1);
+ QVERIFY(res_1.internal_id() == id_1);
+ qDebug() << "res_1 got back to its initial state.";
+
+ QVERIFY(!res_2.enable(initial_enabled_res_2 ? EnableAction::ENABLE : EnableAction::DISABLE));
+ QVERIFY(res_2.enabled() == initial_enabled_res_2);
+ QVERIFY(res_2.internal_id() == id_2);
+
+ while (model.hasPendingParseTasks()) {
+ QTest::qSleep(20);
+ QCoreApplication::processEvents();
+ }
+ }
+};
+
+QTEST_GUILESS_MAIN(ResourceFolderModelTest)
+
+#include "ResourceFolderModel_test.moc"
diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h
new file mode 100644
index 00000000..c2cc8690
--- /dev/null
+++ b/launcher/minecraft/mod/ResourcePack.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "Resource.h"
+
+class ResourcePack : public Resource {
+ Q_OBJECT
+ public:
+ using Ptr = shared_qobject_ptr<Resource>;
+
+ ResourcePack(QObject* parent = nullptr) : Resource(parent) {}
+ ResourcePack(QFileInfo file_info) : Resource(file_info) {}
+
+};
diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
index 276804ed..e92be894 100644
--- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
@@ -35,24 +35,4 @@
#include "ResourcePackFolderModel.h"
-ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) {
-}
-
-QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
- if (role == Qt::ToolTipRole) {
- switch (section) {
- case ActiveColumn:
- return tr("Is the resource pack enabled?");
- case NameColumn:
- return tr("The name of the resource pack.");
- case VersionColumn:
- return tr("The version of the resource pack.");
- case DateColumn:
- return tr("The date and time this resource pack was last changed (or added).");
- default:
- return QVariant();
- }
- }
-
- return ModFolderModel::headerData(section, orientation, role);
-}
+ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h
index 0cd6214b..1fe82867 100644
--- a/launcher/minecraft/mod/ResourcePackFolderModel.h
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.h
@@ -1,13 +1,14 @@
#pragma once
-#include "ModFolderModel.h"
+#include "ResourceFolderModel.h"
-class ResourcePackFolderModel : public ModFolderModel
+#include "ResourcePack.h"
+
+class ResourcePackFolderModel : public ResourceFolderModel
{
Q_OBJECT
-
public:
explicit ResourcePackFolderModel(const QString &dir);
- QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ RESOURCE_HELPERS(ResourcePack)
};
diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h
new file mode 100644
index 00000000..a3aa958f
--- /dev/null
+++ b/launcher/minecraft/mod/ShaderPackFolderModel.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "ResourceFolderModel.h"
+
+class ShaderPackFolderModel : public ResourceFolderModel {
+ Q_OBJECT
+
+ public:
+ explicit ShaderPackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) {}
+};
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp
index e3a22219..2c7c945b 100644
--- a/launcher/minecraft/mod/TexturePackFolderModel.cpp
+++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp
@@ -35,24 +35,4 @@
#include "TexturePackFolderModel.h"
-TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) {
-}
-
-QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
- if (role == Qt::ToolTipRole) {
- switch (section) {
- case ActiveColumn:
- return tr("Is the texture pack enabled?");
- case NameColumn:
- return tr("The name of the texture pack.");
- case VersionColumn:
- return tr("The version of the texture pack.");
- case DateColumn:
- return tr("The date and time this texture pack was last changed (or added).");
- default:
- return QVariant();
- }
- }
-
- return ModFolderModel::headerData(section, orientation, role);
-}
+TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h
index a59d5119..69e98661 100644
--- a/launcher/minecraft/mod/TexturePackFolderModel.h
+++ b/launcher/minecraft/mod/TexturePackFolderModel.h
@@ -1,13 +1,11 @@
#pragma once
-#include "ModFolderModel.h"
+#include "ResourceFolderModel.h"
-class TexturePackFolderModel : public ModFolderModel
+class TexturePackFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
explicit TexturePackFolderModel(const QString &dir);
-
- QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
};
diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h
new file mode 100644
index 00000000..cc02a9b9
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <QDir>
+#include <QMap>
+#include <QObject>
+
+#include <memory>
+
+#include "minecraft/mod/Resource.h"
+
+#include "tasks/Task.h"
+
+/** Very simple task that just loads a folder's contents directly.
+ */
+class BasicFolderLoadTask : public Task
+{
+ Q_OBJECT
+public:
+ struct Result {
+ QMap<QString, Resource::Ptr> resources;
+ };
+ using ResultPtr = std::shared_ptr<Result>;
+
+ [[nodiscard]] ResultPtr result() const {
+ return m_result;
+ }
+
+public:
+ BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result) {}
+
+ [[nodiscard]] bool canAbort() const override { return true; }
+ bool abort() override { m_aborted = true; return true; }
+
+ void executeTask() override
+ {
+ m_dir.refresh();
+ for (auto entry : m_dir.entryInfoList()) {
+ auto resource = new Resource(entry);
+ m_result->resources.insert(resource->internal_id(), resource);
+ }
+
+ if (m_aborted)
+ emitAborted();
+ else
+ emitSucceeded();
+ }
+
+private:
+ QDir m_dir;
+ ResultPtr m_result;
+
+ bool m_aborted = false;
+};
diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
index 1519f49d..c486bd46 100644
--- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
@@ -20,22 +20,22 @@ namespace {
// OLD format:
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
-std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
+ModDetails ReadMCModInfo(QByteArray contents)
{
- auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr<ModDetails>
+ auto getInfoFromArray = [&](QJsonArray arr) -> ModDetails
{
if (!arr.at(0).isObject()) {
- return nullptr;
+ return {};
}
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
auto firstObj = arr.at(0).toObject();
- details->mod_id = firstObj.value("modid").toString();
+ details.mod_id = firstObj.value("modid").toString();
auto name = firstObj.value("name").toString();
// NOTE: ignore stupid example mods copies where the author didn't even bother to change the name
if(name != "Example Mod") {
- details->name = name;
+ details.name = name;
}
- details->version = firstObj.value("version").toString();
+ details.version = firstObj.value("version").toString();
auto homeurl = firstObj.value("url").toString().trimmed();
if(!homeurl.isEmpty())
{
@@ -45,8 +45,8 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
homeurl.prepend("http://");
}
}
- details->homeurl = homeurl;
- details->description = firstObj.value("description").toString();
+ details.homeurl = homeurl;
+ details.description = firstObj.value("description").toString();
QJsonArray authors = firstObj.value("authorList").toArray();
if (authors.size() == 0) {
// FIXME: what is the format of this? is there any?
@@ -55,7 +55,7 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
for (auto author: authors)
{
- details->authors.append(author.toString());
+ details.authors.append(author.toString());
}
return details;
};
@@ -83,7 +83,7 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
{
qCritical() << "BAD stuff happened to mod json:";
qCritical() << contents;
- return nullptr;
+ return {};
}
auto arrVal = jsonDoc.object().value("modlist");
if(arrVal.isUndefined()) {
@@ -94,13 +94,13 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
return getInfoFromArray(arrVal.toArray());
}
}
- return nullptr;
+ return {};
}
// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md
-std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
+ModDetails ReadMCModTOML(QByteArray contents)
{
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
char errbuf[200];
// top-level table
@@ -108,7 +108,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlData)
{
- return nullptr;
+ return {};
}
// array defined by [[mods]]
@@ -116,7 +116,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlModsArr)
{
qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!";
- return nullptr;
+ return {};
}
// we only really care about the first element, since multiple mods in one file is not supported by us at the moment
@@ -124,33 +124,33 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlModsTable0)
{
qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!";
- return nullptr;
+ return {};
}
// mandatory properties - always in [[mods]]
toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId");
if(modIdDatum.ok)
{
- details->mod_id = modIdDatum.u.s;
+ details.mod_id = modIdDatum.u.s;
// library says this is required for strings
free(modIdDatum.u.s);
}
toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version");
if(versionDatum.ok)
{
- details->version = versionDatum.u.s;
+ details.version = versionDatum.u.s;
free(versionDatum.u.s);
}
toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName");
if(displayNameDatum.ok)
{
- details->name = displayNameDatum.u.s;
+ details.name = displayNameDatum.u.s;
free(displayNameDatum.u.s);
}
toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description");
if(descriptionDatum.ok)
{
- details->description = descriptionDatum.u.s;
+ details.description = descriptionDatum.u.s;
free(descriptionDatum.u.s);
}
@@ -173,7 +173,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
}
if(!authors.isEmpty())
{
- details->authors.append(authors);
+ details.authors.append(authors);
}
toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL");
@@ -200,7 +200,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
homeurl.prepend("http://");
}
}
- details->homeurl = homeurl;
+ details.homeurl = homeurl;
// this seems to be recursive, so it should free everything
toml_free(tomlData);
@@ -209,20 +209,20 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
}
// https://fabricmc.net/wiki/documentation:fabric_mod_json
-std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
+ModDetails ReadFabricModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0;
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
- details->mod_id = object.value("id").toString();
- details->version = object.value("version").toString();
+ details.mod_id = object.value("id").toString();
+ details.version = object.value("version").toString();
- details->name = object.contains("name") ? object.value("name").toString() : details->mod_id;
- details->description = object.value("description").toString();
+ details.name = object.contains("name") ? object.value("name").toString() : details.mod_id;
+ details.description = object.value("description").toString();
if (schemaVersion >= 1)
{
@@ -230,10 +230,10 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
for (auto author: authors)
{
if(author.isObject()) {
- details->authors.append(author.toObject().value("name").toString());
+ details.authors.append(author.toObject().value("name").toString());
}
else {
- details->authors.append(author.toString());
+ details.authors.append(author.toString());
}
}
@@ -243,7 +243,7 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
if (contact.contains("homepage"))
{
- details->homeurl = contact.value("homepage").toString();
+ details.homeurl = contact.value("homepage").toString();
}
}
}
@@ -251,50 +251,50 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
}
// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md
-std::shared_ptr<ModDetails> ReadQuiltModInfo(QByteArray contents)
+ModDetails ReadQuiltModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version");
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
// https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md
if (schemaVersion == 1)
{
auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info");
- details->mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
- details->version = Json::requireString(modInfo.value("version"), "Mod version");
+ details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
+ details.version = Json::requireString(modInfo.value("version"), "Mod version");
auto modMetadata = Json::ensureObject(modInfo.value("metadata"));
- details->name = Json::ensureString(modMetadata.value("name"), details->mod_id);
- details->description = Json::ensureString(modMetadata.value("description"));
+ details.name = Json::ensureString(modMetadata.value("name"), details.mod_id);
+ details.description = Json::ensureString(modMetadata.value("description"));
auto modContributors = Json::ensureObject(modMetadata.value("contributors"));
// We don't really care about the role of a contributor here
- details->authors += modContributors.keys();
+ details.authors += modContributors.keys();
auto modContact = Json::ensureObject(modMetadata.value("contact"));
if (modContact.contains("homepage"))
{
- details->homeurl = Json::requireString(modContact.value("homepage"));
+ details.homeurl = Json::requireString(modContact.value("homepage"));
}
}
return details;
}
-std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
+ModDetails ReadForgeInfo(QByteArray contents)
{
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
// Read the data
- details->name = "Minecraft Forge";
- details->mod_id = "Forge";
- details->homeurl = "http://www.minecraftforge.net/forum/";
+ details.name = "Minecraft Forge";
+ details.mod_id = "Forge";
+ details.homeurl = "http://www.minecraftforge.net/forum/";
INIFile ini;
if (!ini.loadFile(contents))
return details;
@@ -304,47 +304,47 @@ std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
QString revision = ini.get("forge.revision.number", "0").toString();
QString build = ini.get("forge.build.number", "0").toString();
- details->version = major + "." + minor + "." + revision + "." + build;
+ details.version = major + "." + minor + "." + revision + "." + build;
return details;
}
-std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents)
+ModDetails ReadLiteModInfo(QByteArray contents)
{
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
if (object.contains("name"))
{
- details->mod_id = details->name = object.value("name").toString();
+ details.mod_id = details.name = object.value("name").toString();
}
if (object.contains("version"))
{
- details->version = object.value("version").toString("");
+ details.version = object.value("version").toString("");
}
else
{
- details->version = object.value("revision").toString("");
+ details.version = object.value("revision").toString("");
}
- details->mcversion = object.value("mcversion").toString();
+ details.mcversion = object.value("mcversion").toString();
auto author = object.value("author").toString();
if(!author.isEmpty()) {
- details->authors.append(author);
+ details.authors.append(author);
}
- details->description = object.value("description").toString();
- details->homeurl = object.value("url").toString();
+ details.description = object.value("description").toString();
+ details.homeurl = object.value("url").toString();
return details;
}
}
-LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile):
+LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile):
+ Task(nullptr, false),
m_token(token),
m_type(type),
m_modFile(modFile),
m_result(new Result())
-{
-}
+{}
void LocalModParseTask::processAsZip()
{
@@ -366,7 +366,7 @@ void LocalModParseTask::processAsZip()
file.close();
// to replace ${file.jarVersion} with the actual version, as needed
- if (m_result->details && m_result->details->version == "${file.jarVersion}")
+ if (m_result->details.version == "${file.jarVersion}")
{
if (zip.setCurrentFile("META-INF/MANIFEST.MF"))
{
@@ -395,7 +395,7 @@ void LocalModParseTask::processAsZip()
manifestVersion = "NONE";
}
- m_result->details->version = manifestVersion;
+ m_result->details.version = manifestVersion;
file.close();
}
@@ -497,21 +497,31 @@ void LocalModParseTask::processAsLitemod()
zip.close();
}
-void LocalModParseTask::run()
+bool LocalModParseTask::abort()
+{
+ m_aborted = true;
+ return true;
+}
+
+void LocalModParseTask::executeTask()
{
switch(m_type)
{
- case Mod::MOD_ZIPFILE:
+ case ResourceType::ZIPFILE:
processAsZip();
break;
- case Mod::MOD_FOLDER:
+ case ResourceType::FOLDER:
processAsFolder();
break;
- case Mod::MOD_LITEMOD:
+ case ResourceType::LITEMOD:
processAsLitemod();
break;
default:
break;
}
- emit finished(m_token);
+
+ if (m_aborted)
+ emitAborted();
+ else
+ emitSucceeded();
}
diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h
index ed92394c..4bbf3c85 100644
--- a/launcher/minecraft/mod/tasks/LocalModParseTask.h
+++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h
@@ -2,29 +2,31 @@
#include <QDebug>
#include <QObject>
-#include <QRunnable>
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModDetails.h"
-class LocalModParseTask : public QObject, public QRunnable
+#include "tasks/Task.h"
+
+class LocalModParseTask : public Task
{
Q_OBJECT
public:
struct Result {
- QString id;
- std::shared_ptr<ModDetails> details;
+ ModDetails details;
};
using ResultPtr = std::shared_ptr<Result>;
ResultPtr result() const {
return m_result;
}
- LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile);
- void run();
+ [[nodiscard]] bool canAbort() const override { return true; }
+ bool abort() override;
+
+ LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile);
+ void executeTask() override;
-signals:
- void finished(int token);
+ [[nodiscard]] int token() const { return m_token; }
private:
void processAsZip();
@@ -33,7 +35,9 @@ private:
private:
int m_token;
- Mod::ModType m_type;
+ ResourceType m_type;
QFileInfo m_modFile;
ResultPtr m_result;
+
+ bool m_aborted = false;
};
diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
index 9b70e7a1..a56ba8ab 100644
--- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
+++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
@@ -38,11 +38,11 @@
#include "minecraft/mod/MetadataHandler.h"
-ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed)
- : m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_result(new Result())
+ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan, QObject* parent)
+ : Task(parent, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result())
{}
-void ModFolderLoadTask::run()
+void ModFolderLoadTask::executeTask()
{
if (m_is_indexed) {
// Read metadata first
@@ -52,7 +52,7 @@ void ModFolderLoadTask::run()
// Read JAR files that don't have metadata
m_mods_dir.refresh();
for (auto entry : m_mods_dir.entryInfoList()) {
- Mod::Ptr mod(new Mod(entry));
+ Mod* mod(new Mod(entry));
if (mod->enabled()) {
if (m_result->mods.contains(mod->internal_id())) {
@@ -85,16 +85,18 @@ void ModFolderLoadTask::run()
// Remove orphan metadata to prevent issues
// See https://github.com/PolyMC/PolyMC/issues/996
- QMutableMapIterator<QString, Mod::Ptr> iter(m_result->mods);
- while (iter.hasNext()) {
- auto mod = iter.next().value();
- if (mod->status() == ModStatus::NotInstalled) {
- mod->destroy(m_index_dir, false);
- iter.remove();
+ if (m_clean_orphan) {
+ QMutableMapIterator<QString, Mod::Ptr> iter(m_result->mods);
+ while (iter.hasNext()) {
+ auto mod = iter.next().value();
+ if (mod->status() == ModStatus::NotInstalled) {
+ mod->destroy(m_index_dir, false);
+ iter.remove();
+ }
}
}
- emit succeeded();
+ emitSucceeded();
}
void ModFolderLoadTask::getFromMetadata()
diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
index 0b6bb6cc..840e95e1 100644
--- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
+++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
@@ -42,8 +42,9 @@
#include <QRunnable>
#include <memory>
#include "minecraft/mod/Mod.h"
+#include "tasks/Task.h"
-class ModFolderLoadTask : public QObject, public QRunnable
+class ModFolderLoadTask : public Task
{
Q_OBJECT
public:
@@ -56,16 +57,16 @@ public:
}
public:
- ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed);
- void run();
-signals:
- void succeeded();
+ ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false, QObject* parent = nullptr);
+
+ void executeTask() override;
private:
void getFromMetadata();
private:
- QDir& m_mods_dir, m_index_dir;
+ QDir m_mods_dir, m_index_dir;
bool m_is_indexed;
+ bool m_clean_orphan;
ResultPtr m_result;
};
diff --git a/launcher/minecraft/mod/testdata/supercoolmod.jar b/launcher/minecraft/mod/testdata/supercoolmod.jar
new file mode 100644
index 00000000..d8cf9860
--- /dev/null
+++ b/launcher/minecraft/mod/testdata/supercoolmod.jar
@@ -0,0 +1 @@
+the best mod.