From d2da8b5c3755710dc8194f36de74fa8d78ed6c1e Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 2 May 2025 21:53:20 +0300 Subject: [PATCH 1/8] feat: decode dependencies from mod jar Signed-off-by: Trial97 --- launcher/minecraft/mod/Mod.cpp | 5 + launcher/minecraft/mod/Mod.h | 1 + launcher/minecraft/mod/ModDetails.h | 3 + .../minecraft/mod/tasks/LocalModParseTask.cpp | 101 ++++++++++++++++++ 4 files changed, 110 insertions(+) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index e9ca2e682..0384e9226 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -284,3 +284,8 @@ bool Mod::valid() const { return !m_local_details.mod_id.isEmpty(); } + +QStringList Mod::dependencies() const +{ + return details().dependencies; +} diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index eceb8c256..63257429e 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -72,6 +72,7 @@ class Mod : public Resource { auto loaders() const -> QString; auto mcVersions() const -> QString; auto releaseType() const -> QString; + QStringList dependencies() const; /** Get the intneral path to the mod's icon file*/ QString iconPath() const { return m_local_details.icon_file; } diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 9195c0368..d866cb1bc 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -142,6 +142,8 @@ struct ModDetails { /* Path of mod logo */ QString icon_file = {}; + QStringList dependencies = {}; + ModDetails() = default; /** Metadata should be handled manually to properly set the mod status. */ @@ -156,6 +158,7 @@ struct ModDetails { , issue_tracker(other.issue_tracker) , licenses(other.licenses) , icon_file(other.icon_file) + , dependencies(other.dependencies) {} ModDetails& operator=(const ModDetails& other) = default; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 952115bed..d0eb5949c 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -62,6 +62,36 @@ ModDetails ReadMCModInfo(QByteArray contents) for (auto author : authors) { details.authors.append(author.toString()); } + + if (details.mod_id.startsWith("mod_")) { + details.mod_id = details.mod_id.mid(4); + } + + auto addDep = [&details](QString dep) { + if (dep == "mod_MinecraftForge" || dep == "Forge") + return; + if (dep.contains(":")) { + dep = dep.section(":", 1); + } + if (dep.contains("@")) { + dep = dep.section("@", 0, 0); + } + if (dep.startsWith("mod_")) { + dep = dep.mid(4); + } + details.dependencies.append(dep); + }; + + if (firstObj.contains("requiredMods")) { + for (auto dep : firstObj.value("dependencies").toArray().toVariantList()) { + addDep(dep.toString()); + } + } else if (firstObj.contains("dependencies")) { + for (auto dep : firstObj.value("dependencies").toArray().toVariantList()) { + addDep(dep.toString()); + } + } + return details; }; QJsonParseError jsonError; @@ -199,6 +229,42 @@ ModDetails ReadMCModTOML(QByteArray contents) } details.icon_file = logoFile; + auto parseDep = [&details](toml::array* dependencies) { + if (dependencies) { + for (auto& dep : *dependencies) { + auto dep_table = dep.as_table(); + if (dep_table) { + auto modId = dep_table->get("modId")->value_or(""); + if (modId != "forge" && modId != "neoforge" && modId != "minecraft") { + if (dep_table->contains("type") && (dep_table->get("type"))->value_or("") == "required") { + details.dependencies.append(QString::fromStdString(modId)); + } else if (dep_table->contains("mandatory") && (dep_table->get("mandatory"))->value_or(false)) { + details.dependencies.append(QString::fromStdString(modId)); + } + } + } + } + } + }; + + if (tomlData.contains("dependencies")) { + auto depValue = tomlData["dependencies"]; + if (auto array = depValue.as_array()) { + parseDep(array); + } else if (auto depTable = depValue.as_table()) { + auto expectedKey = details.mod_id.toStdString(); + if (!depTable->contains(expectedKey)) { + for (auto [k, v] : *depTable) { + expectedKey = k; + break; + } + } + if (auto array = (*depTable)[expectedKey].as_array()) { + parseDep(array); + } + } + } + return details; } @@ -286,6 +352,18 @@ ModDetails ReadFabricModInfo(QByteArray contents) details.icon_file = icon.toString(); } } + + if (object.contains("depends")) { + auto depends = object.value("depends"); + if (depends.isObject()) { + auto obj = depends.toObject(); + for (auto key : obj.keys()) { + if (key != "fabricloader" && key != "minecraft" && !key.startsWith("fabric-")) { + details.dependencies.append(key); + } + } + } + } } return details; } @@ -373,6 +451,29 @@ ModDetails ReadQuiltModInfo(QByteArray contents) details.icon_file = icon.toString(); } } + if (object.contains("depends")) { + auto depends = object.value("depends"); + if (depends.isArray()) { + auto array = depends.toArray(); + for (auto obj : array) { + QString modId; + if (obj.isString()) { + modId = obj.toString(); + } else if (obj.isObject()) { + auto objValue = obj.toObject(); + modId = objValue.value("id").toString(); + if (objValue.contains("optional") && objValue.value("optional").toBool()) { + continue; + } + } else { + continue; + } + if (modId != "minecraft" && !modId.startsWith("quilt_")) { + details.dependencies.append(modId); + } + } + } + } } } catch (const Exception& e) { From bc383abfdaad203fb32714de639ec9e1607590f7 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 3 May 2025 12:25:18 +0300 Subject: [PATCH 2/8] feat: store provider dependencies Signed-off-by: Trial97 --- launcher/minecraft/mod/Resource.h | 4 -- .../mod/tasks/GetModDependenciesTask.cpp | 1 - launcher/modplatform/ModIndex.cpp | 38 ++++++++++++++++++- launcher/modplatform/ModIndex.h | 5 +++ launcher/modplatform/packwiz/Packwiz.cpp | 27 +++++++++++++ launcher/modplatform/packwiz/Packwiz.h | 2 + 6 files changed, 71 insertions(+), 6 deletions(-) diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index 87bfd4345..242b8a30d 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -152,9 +152,6 @@ class Resource : public QObject { bool isMoreThanOneHardLink() const; - auto mod_id() const -> QString { return m_mod_id; } - void setModId(const QString& modId) { m_mod_id = modId; } - protected: /* The file corresponding to this resource. */ QFileInfo m_file_info; @@ -165,7 +162,6 @@ class Resource : public QObject { 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; - QString m_mod_id; /* The type of file we're dealing with. */ ResourceType m_type = ResourceType::UNKNOWN; diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index 21e7c5a2a..29ee3c5ba 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -224,7 +224,6 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen pDep->version.is_currently_selected = true; pDep->pack->versions = { pDep->version }; pDep->pack->versionsLoaded = true; - } catch (const JSONValidationError& e) { removePack(dep.addonId); qDebug() << doc; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index edb5e5aa1..6665ea99d 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -122,7 +122,7 @@ auto getModLoaderAsString(ModLoaderType type) -> const QString case Cauldron: return "cauldron"; case LiteLoader: - return "liteloader"; + return "liteloader"; case Fabric: return "fabric"; case Quilt: @@ -185,4 +185,40 @@ Side SideUtils::fromString(QString side) return Side::UniversalSide; return Side::UniversalSide; } + +QString DependencyTypeUtils::toString(DependencyType type) +{ + switch (type) { + case DependencyType::REQUIRED: + return "REQUIRED"; + case DependencyType::OPTIONAL: + return "OPTIONAL"; + case DependencyType::INCOMPATIBLE: + return "INCOMPATIBLE"; + case DependencyType::EMBEDDED: + return "EMBEDDED"; + case DependencyType::TOOL: + return "TOOL"; + case DependencyType::INCLUDE: + return "INCLUDE"; + case DependencyType::UNKNOWN: + return "UNKNOWN"; + } + return "UNKNOWN"; +} + +DependencyType DependencyTypeUtils::fromString(const QString& str) +{ + static const QHash map = { + { "REQUIRED", DependencyType::REQUIRED }, + { "OPTIONAL", DependencyType::OPTIONAL }, + { "INCOMPATIBLE", DependencyType::INCOMPATIBLE }, + { "EMBEDDED", DependencyType::EMBEDDED }, + { "TOOL", DependencyType::TOOL }, + { "INCLUDE", DependencyType::INCLUDE }, + { "UNKNOWN", DependencyType::UNKNOWN }, + }; + + return map.value(str.toUpper(), DependencyType::UNKNOWN); +} } // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 2935eda76..ded66c7d6 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -54,6 +54,11 @@ QString toString(Side side); Side fromString(QString side); } // namespace SideUtils +namespace DependencyTypeUtils { +QString toString(DependencyType type); +DependencyType fromString(const QString& str); +} // namespace DependencyTypeUtils + namespace ProviderCapabilities { const char* name(ResourceProvider); QString readableName(ResourceProvider); diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 0660d611c..15cab589e 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -122,6 +122,7 @@ auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a version number mod.version_number = mod_version.version; + mod.dependencies = mod_version.dependencies; return mod; } @@ -190,6 +191,16 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod) return; } + toml::array deps; + for (auto dep : mod.dependencies) { + auto tbl = toml::table{ { "addonId", dep.addonId.toString().toStdString() }, + { "type", ModPlatform::DependencyTypeUtils::toString(dep.type).toStdString() } }; + if (!dep.version.isEmpty()) { + tbl.emplace("version", dep.version.toStdString()); + } + deps.push_back(tbl); + } + // Put TOML data into the file QTextStream in_stream(&index_file); { @@ -200,6 +211,7 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod) { "x-prismlauncher-mc-versions", mcVersions }, { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() }, { "x-prismlauncher-version-number", mod.version_number.toStdString() }, + { "x-prismlauncher-dependencies", deps }, { "download", toml::table{ { "mode", mod.mode.toStdString() }, @@ -330,6 +342,21 @@ auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod return {}; } } + { // dependencies + auto deps = table["x-prismlauncher-dependencies"].as_array(); + if (deps) { + for (auto&& depNode : *deps) { + auto dep = depNode.as_table(); + if (dep) { + ModPlatform::Dependency d; + d.addonId = stringEntry(*dep, "addonId"); + d.version = stringEntry(*dep, "version"); + d.type = ModPlatform::DependencyTypeUtils::fromString(stringEntry(*dep, "type")); + mod.dependencies << d; + } + } + } + } return mod; } diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index ba9a0fe75..b5b8894f3 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -55,6 +55,8 @@ class V1 { QVariant project_id{}; QString version_number{}; + QList dependencies; + public: // This is a totally heuristic, but should work for now. auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); } From 2b62b4281f0c302d6abd456aff0dfede573367e9 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 3 May 2025 13:27:40 +0300 Subject: [PATCH 3/8] feat: add requireBy and requires columns Signed-off-by: Trial97 --- launcher/minecraft/mod/Mod.cpp | 30 +++++++ launcher/minecraft/mod/Mod.h | 9 +++ launcher/minecraft/mod/ModFolderModel.cpp | 98 ++++++++++++++++++++--- launcher/minecraft/mod/ModFolderModel.h | 9 +++ launcher/minecraft/mod/Resource.h | 16 +++- 5 files changed, 152 insertions(+), 10 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 0384e9226..ed151bc5b 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -106,6 +106,20 @@ int Mod::compare(const Resource& other, SortType type) const return compare_result; break; } + case SortType::REQUIRED_BY: { + if (requiredByCount() > cast_other->requiredByCount()) + return 1; + if (requiredByCount() < cast_other->requiredByCount()) + return -1; + break; + } + case SortType::REQUIRES: { + if (requiresCount() > cast_other->requiresCount()) + return 1; + if (requiresCount() < cast_other->requiresCount()) + return -1; + break; + } } return 0; } @@ -289,3 +303,19 @@ QStringList Mod::dependencies() const { return details().dependencies; } +int Mod::requiredByCount() const +{ + return m_requiredByCount; +} +int Mod::requiresCount() const +{ + return m_requiresCount; +} +void Mod::setRequiredByCount(int value) +{ + m_requiredByCount = value; +} +void Mod::setRequiresCount(int value) +{ + m_requiresCount = value; +} diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 63257429e..16f4b32f7 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -74,6 +74,12 @@ class Mod : public Resource { auto releaseType() const -> QString; QStringList dependencies() const; + int requiredByCount() const; + int requiresCount() const; + + void setRequiredByCount(int value); + void setRequiresCount(int value); + /** Get the intneral path to the mod's icon file*/ QString iconPath() const { return m_local_details.icon_file; } /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ @@ -105,4 +111,7 @@ class Mod : public Resource { bool wasEverUsed = false; bool wasReadAttempt = false; } mutable m_packImageCacheKey; + + int m_requiredByCount = 0; + int m_requiresCount = 0; }; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index b613e0af1..cd8a0d609 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -48,25 +48,31 @@ #include #include #include +#include #include "Application.h" #include "minecraft/mod/tasks/LocalModParseTask.h" +#include "modplatform/ModIndex.h" ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders", - "Minecraft Versions", "Release Type" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), - tr("Size"), tr("Side"), tr("Loaders"), tr("Minecraft Versions"), tr("Release Type") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, - SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE, - SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE }; + "Minecraft Versions", "Release Type", "Requires", "Required by" }); + m_column_names_translated = + QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("Side"), + tr("Loaders"), tr("Minecraft Versions"), tr("Release Type"), tr("Requires "), tr("Required by") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, + SortType::PROVIDER, SortType::SIZE, SortType::SIDE, SortType::LOADERS, SortType::MC_VERSIONS, + SortType::RELEASE_TYPE, SortType::REQUIRES, SortType::REQUIRED_BY }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, - QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true }; + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, + QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true, true, true }; + + connect(this, &ModFolderModel::parseFinished, this, &ModFolderModel::onParseFinished); } QVariant ModFolderModel::data(const QModelIndex& index, int role) const @@ -110,8 +116,15 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const case ReleaseTypeColumn: { return at(row).releaseType(); } - case SizeColumn: + case SizeColumn: { return at(row).sizeStr(); + } + case RequiredByColumn: { + return at(row).requiredByCount(); + } + case RequiresColumn: { + return at(row).requiresCount(); + } default: return QVariant(); } @@ -168,6 +181,8 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio case McVersionsColumn: case ReleaseTypeColumn: case SizeColumn: + case RequiredByColumn: + case RequiresColumn: return columnNames().at(section); default: return QVariant(); @@ -195,6 +210,10 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio return tr("The release type."); case SizeColumn: return tr("The size of the mod."); + case RequiredByColumn: + return tr("Number of mods for what this is needed."); + case RequiresColumn: + return tr("Number of mods that this requires."); default: return QVariant(); } @@ -240,3 +259,64 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } + +void ModFolderModel::onParseFinished() +{ + if (hasPendingParseTasks()) { + return; + } + auto mods = allMods(); + + auto findById = [mods](QString modId) -> Mod* { + auto found = std::find_if(mods.begin(), mods.end(), [modId](Mod* m) { return m->mod_id() == modId; }); + return found != mods.end() ? *found : nullptr; + }; + auto findByProjectID = [mods](QVariant modId, ModPlatform::ResourceProvider provider) -> Mod* { + auto found = std::find_if(mods.begin(), mods.end(), [modId, provider](Mod* m) { + return m->metadata()->provider == provider && m->metadata()->project_id == modId; + }); + return found != mods.end() ? *found : nullptr; + }; + for (auto mod : mods) { + auto id = mod->internal_id(); + for (auto dep : mod->dependencies()) { + auto d = findById(dep); + if (d) { + m_requires[id] << d; + m_requiredBy[d->internal_id()] << mod; + } + } + for (auto dep : mod->metadata()->dependencies) { + auto d = findByProjectID(dep.addonId, mod->metadata()->provider); + if (d) { + m_requires[id] << d; + m_requiredBy[d->internal_id()] << mod; + } + } + } + auto removeDuplicates = [](QList& list) { + std::set seen; + auto it = std::remove_if(list.begin(), list.end(), [&seen](Mod* m) { + auto id = m->internal_id(); + if (seen.count(id) > 0) { + return true; + } + seen.insert(id); + return false; + }); + list.erase(it, list.end()); + }; + for (auto key : m_requiredBy.keys()) { + removeDuplicates(m_requiredBy[key]); + } + for (auto key : m_requires.keys()) { + removeDuplicates(m_requires[key]); + } + for (auto mod : mods) { + auto id = mod->internal_id(); + mod->setRequiredByCount(m_requiredBy[id].count()); + mod->setRequiresCount(m_requires[id].count()); + int row = m_resources_index[id]; + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); + } +} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 42868dc91..40f9eb733 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -46,6 +47,7 @@ #include "Mod.h" #include "ResourceFolderModel.h" +#include "minecraft/mod/Resource.h" class BaseInstance; class QFileSystemWatcher; @@ -69,6 +71,8 @@ class ModFolderModel : public ResourceFolderModel { LoadersColumn, McVersionsColumn, ReleaseTypeColumn, + RequiresColumn, + RequiredByColumn, NUM_COLUMNS }; ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); @@ -89,4 +93,9 @@ class ModFolderModel : public ResourceFolderModel { private slots: void onParseSucceeded(int ticket, QString resource_id) override; + void onParseFinished(); + + private: + QHash> m_requiredBy; + QHash> m_requires; }; diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index 242b8a30d..3eda4c013 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -58,7 +58,21 @@ enum class ResourceStatus { UNKNOWN, // Default status }; -enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, MC_VERSIONS, LOADERS, RELEASE_TYPE }; +enum class SortType { + NAME, + DATE, + VERSION, + ENABLED, + PACK_FORMAT, + PROVIDER, + SIZE, + SIDE, + MC_VERSIONS, + LOADERS, + RELEASE_TYPE, + REQUIRES, + REQUIRED_BY, +}; enum class EnableAction { ENABLE, DISABLE, TOGGLE }; From 8277fd41ae658480e23dd5bd8df0cfc32a77a0e5 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 5 May 2025 00:35:35 +0300 Subject: [PATCH 4/8] feat: make dependencies auto disable/enable Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 97 ++++++++++++++++--- launcher/minecraft/mod/ModFolderModel.h | 4 + .../minecraft/mod/ResourceFolderModel.cpp | 1 + launcher/ui/pages/instance/ModFolderPage.cpp | 20 +++- 4 files changed, 104 insertions(+), 18 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index cd8a0d609..60ba0e6e2 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -52,6 +52,9 @@ #include "Application.h" +#include "minecraft/Component.h" +#include "minecraft/mod/Resource.h" +#include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/tasks/LocalModParseTask.h" #include "modplatform/ModIndex.h" @@ -260,6 +263,12 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } +Mod* findById(QList mods, QString modId) +{ + auto found = std::find_if(mods.begin(), mods.end(), [modId](Mod* m) { return m->mod_id() == modId; }); + return found != mods.end() ? *found : nullptr; +} + void ModFolderModel::onParseFinished() { if (hasPendingParseTasks()) { @@ -267,37 +276,37 @@ void ModFolderModel::onParseFinished() } auto mods = allMods(); - auto findById = [mods](QString modId) -> Mod* { - auto found = std::find_if(mods.begin(), mods.end(), [modId](Mod* m) { return m->mod_id() == modId; }); - return found != mods.end() ? *found : nullptr; - }; auto findByProjectID = [mods](QVariant modId, ModPlatform::ResourceProvider provider) -> Mod* { auto found = std::find_if(mods.begin(), mods.end(), [modId, provider](Mod* m) { - return m->metadata()->provider == provider && m->metadata()->project_id == modId; + return m->metadata() && m->metadata()->provider == provider && m->metadata()->project_id == modId; }); return found != mods.end() ? *found : nullptr; }; for (auto mod : mods) { - auto id = mod->internal_id(); + auto id = mod->mod_id(); for (auto dep : mod->dependencies()) { - auto d = findById(dep); + auto d = findById(mods, dep); if (d) { m_requires[id] << d; - m_requiredBy[d->internal_id()] << mod; + m_requiredBy[d->mod_id()] << mod; } } - for (auto dep : mod->metadata()->dependencies) { - auto d = findByProjectID(dep.addonId, mod->metadata()->provider); - if (d) { - m_requires[id] << d; - m_requiredBy[d->internal_id()] << mod; + if (mod->metadata()) { + for (auto dep : mod->metadata()->dependencies) { + if (dep.type == ModPlatform::DependencyType::REQUIRED) { + auto d = findByProjectID(dep.addonId, mod->metadata()->provider); + if (d) { + m_requires[id] << d; + m_requiredBy[d->mod_id()] << mod; + } + } } } } auto removeDuplicates = [](QList& list) { std::set seen; auto it = std::remove_if(list.begin(), list.end(), [&seen](Mod* m) { - auto id = m->internal_id(); + auto id = m->mod_id(); if (seen.count(id) > 0) { return true; } @@ -313,10 +322,66 @@ void ModFolderModel::onParseFinished() removeDuplicates(m_requires[key]); } for (auto mod : mods) { - auto id = mod->internal_id(); + auto id = mod->mod_id(); mod->setRequiredByCount(m_requiredBy[id].count()); mod->setRequiresCount(m_requires[id].count()); - int row = m_resources_index[id]; + int row = m_resources_index[mod->internal_id()]; emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } } + +QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, EnableAction action) +{ + if (indexes.isEmpty()) + return {}; + + QModelIndexList affectedList = {}; + auto indexedMods = selectedMods(indexes); + if (action == EnableAction::TOGGLE) { + if (indexedMods.length() != 1) { + return {}; // not sure how to handle a bunch of rows that are toggled(not even sure it is posible) + } + action = indexedMods.first()->enabled() ? EnableAction::DISABLE : EnableAction::ENABLE; + } + + std::set seen; + bool shouldBeEnabled = action == EnableAction::ENABLE; + for (auto mod : indexedMods) { + auto id = mod->mod_id(); + QList mods; + switch (action) { + case EnableAction::DISABLE: { + mods = m_requiredBy[id]; + break; + } + case EnableAction::ENABLE: { + mods = m_requires[id]; + break; + } + case EnableAction::TOGGLE: + break; + } + for (auto affected : mods) { + auto affectedId = affected->mod_id(); + + if (findById(indexedMods, affectedId) == nullptr && seen.count(affectedId) == 0) { + seen.insert(affectedId); + if (shouldBeEnabled != affected->enabled()) { + auto row = m_resources_index[affected->internal_id()]; + affectedList << index(row, 0); + } + } + } + } + // collect the affected mods until all of them are included in the list + if (!affectedList.isEmpty()) { + affectedList += getAffectedMods(indexes + affectedList, action); + } + return affectedList; +} + +bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) +{ + auto affected = getAffectedMods(indexes, action); + return ResourceFolderModel::setResourceEnabled(indexes + affected, action); +} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 40f9eb733..ec1c60daa 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -47,6 +47,7 @@ #include "Mod.h" #include "ResourceFolderModel.h" +#include "minecraft/Component.h" #include "minecraft/mod/Resource.h" class BaseInstance; @@ -89,6 +90,9 @@ class ModFolderModel : public ResourceFolderModel { bool isValid(); + bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action) override; + QModelIndexList getAffectedMods(const QModelIndexList& indexes, EnableAction action); + RESOURCE_HELPERS(Mod) private slots: diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index f93002f06..af3ec68e1 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -882,6 +882,7 @@ QList ResourceFolderModel::allResources() result.append((resource.get())); return result; } + QList ResourceFolderModel::selectedResources(const QModelIndexList& indexes) { QList result; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 7b79766ee..5951a5e33 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -37,6 +37,7 @@ */ #include "ModFolderPage.h" +#include "minecraft/mod/Resource.h" #include "ui/dialogs/ExportToModListDialog.h" #include "ui_ExternalResourcesPage.h" @@ -90,7 +91,7 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr auto depsDisabled = APPLICATION->settings()->getSetting("ModDependenciesDisabled"); ui->actionVerifyItemDependencies->setVisible(!depsDisabled->get().toBool()); connect(depsDisabled.get(), &Setting::SettingChanged, this, - [this](const Setting& setting, const QVariant& value) { ui->actionVerifyItemDependencies->setVisible(!value.toBool()); }); + [this](const Setting&, const QVariant& value) { ui->actionVerifyItemDependencies->setVisible(!value.toBool()); }); updateMenu->addAction(ui->actionResetItemMetadata); connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); @@ -133,7 +134,22 @@ void ModFolderPage::removeItems(const QItemSelection& selection) if (response != QMessageBox::Yes) return; } - m_model->deleteResources(selection.indexes()); + + auto indexes = selection.indexes(); + auto affected = m_model->getAffectedMods(indexes, EnableAction::DISABLE); + if (!affected.isEmpty()) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Disable"), + tr("The mods you are tring to disable are required by %1 mods.\n" + "Do you want to disable them?") + .arg(affected.length()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) { + m_model->setResourceEnabled(affected, EnableAction::DISABLE); + } + } + m_model->deleteResources(indexes); } void ModFolderPage::downloadMods() From 7dd1690947033efefdfd45565ff23290fe8a542d Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 28 Jun 2025 23:50:54 +0300 Subject: [PATCH 5/8] feat: display mod dependencies Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 17 ++ launcher/minecraft/mod/ModFolderModel.h | 4 + launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- launcher/ui/widgets/InfoFrame.cpp | 110 +++++--- launcher/ui/widgets/InfoFrame.h | 4 +- launcher/ui/widgets/InfoFrame.ui | 258 ++++++++++++------- 6 files changed, 258 insertions(+), 137 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 60ba0e6e2..fae832940 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -385,3 +385,20 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc auto affected = getAffectedMods(indexes, action); return ResourceFolderModel::setResourceEnabled(indexes + affected, action); } + +QStringList reqToList(QList l) +{ + QStringList req; + for (auto m : l) { + req << m->name(); + } + return req; +} +QStringList ModFolderModel::requiresList(QString id) +{ + return reqToList(m_requires[id]); +} +QStringList ModFolderModel::requiredByList(QString id) +{ + return reqToList(m_requiredBy[id]); +} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index ec1c60daa..e794cacdc 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -95,6 +95,10 @@ class ModFolderModel : public ResourceFolderModel { RESOURCE_HELPERS(Mod) + public: + QStringList requiresList(QString id); + QStringList requiredByList(QString id); + private slots: void onParseSucceeded(int ticket, QString resource_id) override; void onParseFinished(); diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 5951a5e33..bbaf7be94 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -119,7 +119,7 @@ void ModFolderPage::updateFrame(const QModelIndex& current, [[maybe_unused]] con auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); const Mod& mod = m_model->at(row); - ui->frame->updateWithMod(mod); + ui->frame->updateWithMod(mod, m_model->requiresList(mod.mod_id()), m_model->requiredByList(mod.mod_id())); } void ModFolderPage::removeItems(const QItemSelection& selection) diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 2363b6592..cab2889ac 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -54,28 +54,34 @@ void setupLinkToolTip(QLabel* label) }); } -InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), ui(new Ui::InfoFrame) +InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), m_ui(new Ui::InfoFrame) { - ui->setupUi(this); - ui->descriptionLabel->setHidden(true); - ui->nameLabel->setHidden(true); - ui->licenseLabel->setHidden(true); - ui->issueTrackerLabel->setHidden(true); + m_ui->setupUi(this); + m_ui->descriptionLabel->setHidden(true); + m_ui->nameLabel->setHidden(true); + m_ui->licenseLabel->setHidden(true); + m_ui->issueTrackerLabel->setHidden(true); - setupLinkToolTip(ui->iconLabel); - setupLinkToolTip(ui->descriptionLabel); - setupLinkToolTip(ui->nameLabel); - setupLinkToolTip(ui->licenseLabel); - setupLinkToolTip(ui->issueTrackerLabel); + setupLinkToolTip(m_ui->iconLabel); + setupLinkToolTip(m_ui->descriptionLabel); + setupLinkToolTip(m_ui->nameLabel); + setupLinkToolTip(m_ui->licenseLabel); + setupLinkToolTip(m_ui->issueTrackerLabel); updateHiddenState(); + connect(m_ui->moreInfoBtn, &QPushButton::clicked, this, [this]() { + auto nextIndex = (m_ui->infoStacked->currentIndex() + 1) % 2; + m_ui->infoStacked->setCurrentIndex(nextIndex); + m_ui->moreInfoBtn->setText(nextIndex == 0 ? ">" : "<"); + }); + m_ui->moreInfoBtn->hide(); } InfoFrame::~InfoFrame() { - delete ui; + delete m_ui; } -void InfoFrame::updateWithMod(Mod const& m) +void InfoFrame::updateWithMod(Mod const& m, QStringList requiresList, QStringList requiredByList) { if (m.type() == ResourceType::FOLDER) { clear(); @@ -141,6 +147,26 @@ void InfoFrame::updateWithMod(Mod const& m) issueTracker += "" + m.issueTracker() + ""; } setIssueTracker(issueTracker); + if (requiredByList.isEmpty()) { + m_ui->requiredGB->hide(); + } else { + m_ui->requiredGB->show(); + m_ui->requiredView->clear(); + m_ui->requiredView->addItems(requiredByList); + } + + if (requiresList.isEmpty()) { + m_ui->requiresGB->hide(); + } else { + m_ui->requiresGB->show(); + m_ui->requiresView->clear(); + m_ui->requiresView->addItems(requiresList); + } + if (requiresList.isEmpty() && requiredByList.isEmpty()) { + m_ui->infoStacked->setCurrentIndex(0); + m_ui->moreInfoBtn->setText(">"); + } + m_ui->moreInfoBtn->setHidden(requiresList.isEmpty() && requiredByList.isEmpty()); } void InfoFrame::updateWithResource(const Resource& resource) @@ -227,7 +253,8 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) setImage(resource_pack.image({ 64, 64 })); } -void InfoFrame::updateWithDataPack(DataPack& data_pack) { +void InfoFrame::updateWithDataPack(DataPack& data_pack) +{ setName(renderColorCodes(data_pack.name())); setDescription(renderColorCodes(data_pack.description())); setImage(data_pack.image({ 64, 64 })); @@ -254,12 +281,13 @@ void InfoFrame::clear() setImage(); setLicense(); setIssueTracker(); + m_ui->moreInfoBtn->hide(); } void InfoFrame::updateHiddenState() { - if (ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden() && ui->licenseLabel->isHidden() && - ui->issueTrackerLabel->isHidden()) { + if (m_ui->descriptionLabel->isHidden() && m_ui->nameLabel->isHidden() && m_ui->licenseLabel->isHidden() && + m_ui->issueTrackerLabel->isHidden()) { setHidden(true); } else { setHidden(false); @@ -269,10 +297,10 @@ void InfoFrame::updateHiddenState() void InfoFrame::setName(QString text) { if (text.isEmpty()) { - ui->nameLabel->setHidden(true); + m_ui->nameLabel->setHidden(true); } else { - ui->nameLabel->setText(text); - ui->nameLabel->setHidden(false); + m_ui->nameLabel->setText(text); + m_ui->nameLabel->setHidden(false); } updateHiddenState(); } @@ -280,14 +308,14 @@ void InfoFrame::setName(QString text) void InfoFrame::setDescription(QString text) { if (text.isEmpty()) { - ui->descriptionLabel->setHidden(true); + m_ui->descriptionLabel->setHidden(true); updateHiddenState(); return; } else { - ui->descriptionLabel->setHidden(false); + m_ui->descriptionLabel->setHidden(false); updateHiddenState(); } - ui->descriptionLabel->setToolTip(""); + m_ui->descriptionLabel->setToolTip(""); QString intermediatetext = text.trimmed(); bool prev(false); QChar rem('\n'); @@ -309,8 +337,8 @@ void InfoFrame::setDescription(QString text) doc.setHtml(text); if (doc.characterCount() > maxCharacterElide) { - ui->descriptionLabel->setOpenExternalLinks(false); - ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here. + m_ui->descriptionLabel->setOpenExternalLinks(false); + m_ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here. m_description = text; // move the cursor to the character elide, doesn't see html @@ -323,25 +351,25 @@ void InfoFrame::setDescription(QString text) cursor.insertHtml("..."); labeltext.append(doc.toHtml()); - connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); + connect(m_ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); } else { - ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); + m_ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); } - ui->descriptionLabel->setText(labeltext); + m_ui->descriptionLabel->setText(labeltext); } void InfoFrame::setLicense(QString text) { if (text.isEmpty()) { - ui->licenseLabel->setHidden(true); + m_ui->licenseLabel->setHidden(true); updateHiddenState(); return; } else { - ui->licenseLabel->setHidden(false); + m_ui->licenseLabel->setHidden(false); updateHiddenState(); } - ui->licenseLabel->setToolTip(""); + m_ui->licenseLabel->setToolTip(""); QString intermediatetext = text.trimmed(); bool prev(false); QChar rem('\n'); @@ -357,26 +385,26 @@ void InfoFrame::setLicense(QString text) QString labeltext; labeltext.reserve(300); if (finaltext.length() > 290) { - ui->licenseLabel->setOpenExternalLinks(false); - ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); + m_ui->licenseLabel->setOpenExternalLinks(false); + m_ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); m_license = text; // This allows injecting HTML here. labeltext.append("" + finaltext.left(287) + "..."); - connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); + connect(m_ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); } else { - ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); + m_ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); } - ui->licenseLabel->setText(labeltext); + m_ui->licenseLabel->setText(labeltext); } void InfoFrame::setIssueTracker(QString text) { if (text.isEmpty()) { - ui->issueTrackerLabel->setHidden(true); + m_ui->issueTrackerLabel->setHidden(true); } else { - ui->issueTrackerLabel->setText(text); - ui->issueTrackerLabel->setHidden(false); + m_ui->issueTrackerLabel->setText(text); + m_ui->issueTrackerLabel->setHidden(false); } updateHiddenState(); } @@ -384,10 +412,10 @@ void InfoFrame::setIssueTracker(QString text) void InfoFrame::setImage(QPixmap img) { if (img.isNull()) { - ui->iconLabel->setHidden(true); + m_ui->iconLabel->setHidden(true); } else { - ui->iconLabel->setHidden(false); - ui->iconLabel->setPixmap(img); + m_ui->iconLabel->setHidden(false); + m_ui->iconLabel->setPixmap(img); } } diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h index 20c54e2e5..9cec3d2f2 100644 --- a/launcher/ui/widgets/InfoFrame.h +++ b/launcher/ui/widgets/InfoFrame.h @@ -61,7 +61,7 @@ class InfoFrame : public QFrame { void clear(); - void updateWithMod(Mod const& m); + void updateWithMod(Mod const& m, QStringList requiresList = {}, QStringList requiredByList = {}); void updateWithResource(Resource const& resource); void updateWithResourcePack(ResourcePack& rp); void updateWithDataPack(DataPack& rp); @@ -78,7 +78,7 @@ class InfoFrame : public QFrame { void updateHiddenState(); private: - Ui::InfoFrame* ui; + Ui::InfoFrame* m_ui; QString m_description; QString m_license; class QMessageBox* m_current_box = nullptr; diff --git a/launcher/ui/widgets/InfoFrame.ui b/launcher/ui/widgets/InfoFrame.ui index c4d8c83d3..3e044f06f 100644 --- a/launcher/ui/widgets/InfoFrame.ui +++ b/launcher/ui/widgets/InfoFrame.ui @@ -7,7 +7,7 @@ 0 0 527 - 113 + 130 @@ -19,7 +19,7 @@ 16777215 - 120 + 130 @@ -35,6 +35,169 @@ 0 + + + + > + + + + + + + + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + Requires + + + + + + true + + + QListView::Adjust + + + 10 + + + QListView::IconMode + + + + + + + + + + Required by + + + + + + true + + + QListView::Static + + + QListView::Adjust + + + 10 + + + QListView::IconMode + + + + + + + + + + @@ -60,97 +223,6 @@ - - - - - - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - From 15a5e99a8aae047774a12cdb4f7980990ae7f6c7 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 20 Jul 2025 20:10:00 +0300 Subject: [PATCH 6/8] change to QSet to remove duplicates Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 22 ++-------------------- launcher/minecraft/mod/ModFolderModel.h | 5 ++--- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index fae832940..3a7962e50 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -303,24 +303,6 @@ void ModFolderModel::onParseFinished() } } } - auto removeDuplicates = [](QList& list) { - std::set seen; - auto it = std::remove_if(list.begin(), list.end(), [&seen](Mod* m) { - auto id = m->mod_id(); - if (seen.count(id) > 0) { - return true; - } - seen.insert(id); - return false; - }); - list.erase(it, list.end()); - }; - for (auto key : m_requiredBy.keys()) { - removeDuplicates(m_requiredBy[key]); - } - for (auto key : m_requires.keys()) { - removeDuplicates(m_requires[key]); - } for (auto mod : mods) { auto id = mod->mod_id(); mod->setRequiredByCount(m_requiredBy[id].count()); @@ -348,7 +330,7 @@ QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, bool shouldBeEnabled = action == EnableAction::ENABLE; for (auto mod : indexedMods) { auto id = mod->mod_id(); - QList mods; + QSet mods; switch (action) { case EnableAction::DISABLE: { mods = m_requiredBy[id]; @@ -386,7 +368,7 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc return ResourceFolderModel::setResourceEnabled(indexes + affected, action); } -QStringList reqToList(QList l) +QStringList reqToList(QSet l) { QStringList req; for (auto m : l) { diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index e794cacdc..e47c18405 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -40,7 +40,6 @@ #include #include #include -#include #include #include #include @@ -104,6 +103,6 @@ class ModFolderModel : public ResourceFolderModel { void onParseFinished(); private: - QHash> m_requiredBy; - QHash> m_requires; + QHash> m_requiredBy; + QHash> m_requires; }; From 1346783c5e21f02563ce901b61d6783d8ead1f41 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 24 Jul 2025 18:42:48 +0300 Subject: [PATCH 7/8] fix toggle action Signed-off-by: Trial97 --- launcher/minecraft/mod/ModFolderModel.cpp | 130 +++++++++++++++------- 1 file changed, 92 insertions(+), 38 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 3a7962e50..5e217e90c 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -38,6 +38,7 @@ #include "ModFolderModel.h" #include +#include #include #include #include @@ -311,6 +312,29 @@ void ModFolderModel::onParseFinished() emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } } +QList collectMods(QList mods, QHash> relation, std::set& seen) +{ + QList affectedList = {}; + for (auto mod : mods) { + auto id = mod->mod_id(); + if (seen.count(id) == 0) { + seen.insert(id); + for (auto affected : relation[id]) { + auto affectedId = affected->mod_id(); + + if (findById(mods, affectedId) == nullptr && seen.count(affectedId) == 0) { + seen.insert(affectedId); + affectedList << affected; + } + } + } + } + // collect the affected mods until all of them are included in the list + if (!affectedList.isEmpty()) { + affectedList += collectMods(affectedList, relation, seen); + } + return affectedList; +} QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, EnableAction action) { @@ -318,54 +342,84 @@ QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, return {}; QModelIndexList affectedList = {}; - auto indexedMods = selectedMods(indexes); - if (action == EnableAction::TOGGLE) { - if (indexedMods.length() != 1) { - return {}; // not sure how to handle a bunch of rows that are toggled(not even sure it is posible) - } - action = indexedMods.first()->enabled() ? EnableAction::DISABLE : EnableAction::ENABLE; - } - + auto affectedMods = selectedMods(indexes); std::set seen; - bool shouldBeEnabled = action == EnableAction::ENABLE; - for (auto mod : indexedMods) { - auto id = mod->mod_id(); - QSet mods; - switch (action) { - case EnableAction::DISABLE: { - mods = m_requiredBy[id]; - break; - } - case EnableAction::ENABLE: { - mods = m_requires[id]; - break; - } - case EnableAction::TOGGLE: - break; - } - for (auto affected : mods) { - auto affectedId = affected->mod_id(); - if (findById(indexedMods, affectedId) == nullptr && seen.count(affectedId) == 0) { - seen.insert(affectedId); - if (shouldBeEnabled != affected->enabled()) { - auto row = m_resources_index[affected->internal_id()]; - affectedList << index(row, 0); - } - } + switch (action) { + case EnableAction::ENABLE: { + affectedMods << collectMods(affectedMods, m_requires, seen); + break; + } + case EnableAction::DISABLE: { + affectedMods << collectMods(affectedMods, m_requiredBy, seen); + break; + } + case EnableAction::TOGGLE: { + return {}; // this function should not be called with TOGGLE } } - // collect the affected mods until all of them are included in the list - if (!affectedList.isEmpty()) { - affectedList += getAffectedMods(indexes + affectedList, action); + bool shouldBeEnabled = action == EnableAction::ENABLE; + for (auto affected : affectedMods) { + auto affectedId = affected->mod_id(); + if (shouldBeEnabled != affected->enabled()) { + auto row = m_resources_index[affected->internal_id()]; + affectedList << index(row, 0); + } } return affectedList; } bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) { - auto affected = getAffectedMods(indexes, action); - return ResourceFolderModel::setResourceEnabled(indexes + affected, action); + if (indexes.isEmpty()) + return {}; + + QModelIndexList affectedList = {}; + auto indexedMods = selectedMods(indexes); + + QList toEnable = {}; + QList toDisable = {}; + std::set seen; + + switch (action) { + case EnableAction::ENABLE: { + toEnable = indexedMods; + break; + } + case EnableAction::DISABLE: { + toDisable = indexedMods; + break; + } + case EnableAction::TOGGLE: { + for (auto mod : indexedMods) { + if (mod->enabled()) { + toDisable << mod; + } else { + toEnable << mod; + } + } + break; + } + } + + toEnable << collectMods(toEnable, m_requires, seen); + toDisable << collectMods(toDisable, m_requiredBy, seen); + + toDisable.removeIf([toEnable](Mod* m) { return toEnable.contains(m); }); + auto toList = [this](QList mods, bool shouldBeEnabled) { + QModelIndexList list; + for (auto mod : mods) { + if (shouldBeEnabled != mod->enabled()) { + auto row = m_resources_index[mod->internal_id()]; + list << index(row, 0); + } + } + return list; + }; + + auto disableStatus = ResourceFolderModel::setResourceEnabled(toList(toDisable, false), EnableAction::DISABLE); + auto enableStatus = ResourceFolderModel::setResourceEnabled(toList(toEnable, true), EnableAction::ENABLE); + return disableStatus && enableStatus; } QStringList reqToList(QSet l) From 0f5a890051d968c4b6bec9121fb3f718ed6777ad Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 29 Jul 2025 17:50:29 +0300 Subject: [PATCH 8/8] rework parse dependency function Signed-off-by: Trial97 --- .../minecraft/mod/tasks/LocalModParseTask.cpp | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index d0eb5949c..c3b2dbdd9 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -83,11 +83,11 @@ ModDetails ReadMCModInfo(QByteArray contents) }; if (firstObj.contains("requiredMods")) { - for (auto dep : firstObj.value("dependencies").toArray().toVariantList()) { + for (auto dep : firstObj.value("requiredMods").toArray()) { addDep(dep.toString()); } } else if (firstObj.contains("dependencies")) { - for (auto dep : firstObj.value("dependencies").toArray().toVariantList()) { + for (auto dep : firstObj.value("dependencies").toArray()) { addDep(dep.toString()); } } @@ -230,19 +230,29 @@ ModDetails ReadMCModTOML(QByteArray contents) details.icon_file = logoFile; auto parseDep = [&details](toml::array* dependencies) { - if (dependencies) { - for (auto& dep : *dependencies) { - auto dep_table = dep.as_table(); - if (dep_table) { - auto modId = dep_table->get("modId")->value_or(""); - if (modId != "forge" && modId != "neoforge" && modId != "minecraft") { - if (dep_table->contains("type") && (dep_table->get("type"))->value_or("") == "required") { - details.dependencies.append(QString::fromStdString(modId)); - } else if (dep_table->contains("mandatory") && (dep_table->get("mandatory"))->value_or(false)) { - details.dependencies.append(QString::fromStdString(modId)); - } - } - } + static const QStringList ignoreModIds = { "", "forge", "neoforge", "minecraft" }; + if (!dependencies) { + return; + } + auto isNeoForgeDep = [](toml::table* t) { + auto type = (*t)["type"].as_string(); + return type && type->get() == "required"; + }; + auto isForgeDep = [](toml::table* t) { + auto mandatory = (*t)["mandatory"].as_boolean(); + return mandatory && mandatory->get(); + }; + for (auto& dep : *dependencies) { + auto dep_table = dep.as_table(); + if (!dep_table) { + continue; + } + auto modId = (*dep_table)["modId"].as_string(); + if (!modId || ignoreModIds.contains(modId->get())) { + continue; + } + if (isNeoForgeDep(dep_table) || isForgeDep(dep_table)) { + details.dependencies.append(QString::fromStdString(modId->get())); } } };