diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 32b940c5b..661192d67 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -105,6 +105,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; } @@ -283,3 +297,25 @@ bool Mod::valid() const { return !m_local_details.mod_id.isEmpty(); } + +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 c548f5350..0d24409bf 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -70,6 +70,13 @@ class Mod : public Resource { auto loaders() const -> QString; auto mcVersions() const -> QString; 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; } @@ -102,4 +109,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/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 9b81f561f..02cf42a38 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -138,6 +138,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. */ @@ -152,6 +154,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/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 45ec76f19..8b849dc43 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -38,7 +38,9 @@ #include "ModFolderModel.h" #include +#include #include +#include #include #include #include @@ -48,23 +50,33 @@ #include #include #include +#include +#include "minecraft/Component.h" +#include "minecraft/mod/Resource.h" +#include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/tasks/LocalModParseTask.h" +#include "modplatform/ModIndex.h" +#include "ui/dialogs/CustomMessageBox.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 @@ -108,8 +120,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(); } @@ -166,6 +185,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(); @@ -193,6 +214,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("For each mod, the number of other mods which depend on it."); + case RequiresColumn: + return tr("For each mod, the number of other mods it depends on."); default: return QVariant(); } @@ -236,5 +261,259 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) if (result && resource) static_cast(resource.get())->finishResolvingWithDetails(std::move(result->details)); - emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); + emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); +} + +Mod* findById(QSet 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()) { + return; + } + auto modsList = allMods(); + auto mods = QSet(modsList.begin(), modsList.end()); + + m_requires.clear(); + m_requiredBy.clear(); + + 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() && m->metadata()->provider == provider && m->metadata()->project_id == modId; + }); + return found != mods.end() ? *found : nullptr; + }; + for (auto mod : mods) { + auto id = mod->mod_id(); + for (auto dep : mod->dependencies()) { + auto d = findById(mods, dep); + if (d) { + m_requires[id] << d; + m_requiredBy[d->mod_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; + } + } + } + } + } + for (auto mod : mods) { + auto id = mod->mod_id(); + if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) { + mod->setRequiredByCount(m_requiredBy[id].count()); + mod->setRequiresCount(m_requires[id].count()); + int row = m_resources_index[mod->internal_id()]; + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); + } + } +} + +QSet collectMods(QSet mods, QHash> relation, std::set& seen, bool shouldBeEnabled) +{ + QSet affectedList = {}; + QSet needToCheck = {}; + 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); + if (shouldBeEnabled != affected->enabled()) { + affectedList << affected; + } + needToCheck << affected; + } + } + } + } + // collect the affected mods until all of them are included in the list + if (!needToCheck.isEmpty()) { + affectedList += collectMods(needToCheck, relation, seen, shouldBeEnabled); + } + return affectedList; +} + +QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, EnableAction action) +{ + if (indexes.isEmpty()) + return {}; + + QModelIndexList affectedList = {}; + auto affectedModsList = selectedMods(indexes); + auto affectedMods = QSet(affectedModsList.begin(), affectedModsList.end()); + std::set seen; + + switch (action) { + case EnableAction::ENABLE: { + affectedMods = collectMods(affectedMods, m_requires, seen, true); + break; + } + case EnableAction::DISABLE: { + affectedMods = collectMods(affectedMods, m_requiredBy, seen, false); + break; + } + case EnableAction::TOGGLE: { + return {}; // this function should not be called with TOGGLE + } + } + for (auto affected : affectedMods) { + auto affectedId = affected->mod_id(); + auto row = m_resources_index[affected->internal_id()]; + affectedList << index(row, 0); + } + return affectedList; +} + +bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) +{ + if (indexes.isEmpty()) + return {}; + + auto indexedModsList = selectedMods(indexes); + auto indexedMods = QSet(indexedModsList.begin(), indexedModsList.end()); + + QSet toEnable = {}; + QSet 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; + } + } + + auto requiredToEnable = collectMods(toEnable, m_requires, seen, true); + auto requiredToDisable = collectMods(toDisable, m_requiredBy, seen, false); + + toDisable.removeIf([toEnable](Mod* m) { return toEnable.contains(m); }); + auto toList = [this](QSet mods) { + QModelIndexList list; + for (auto mod : mods) { + auto row = m_resources_index[mod->internal_id()]; + list << index(row, 0); + } + return list; + }; + + if (requiredToEnable.size() > 0 || requiredToDisable.size() > 0) { + QString title; + QString message; + QString noButton; + QString yesButton; + if (requiredToEnable.size() > 0 && requiredToDisable.size() > 0) { + title = tr("Confirm toggle"); + message = tr("Toggling these mod(s) will cause changes to other mods.\n") + + tr("%n mod(s) will be enabled\n", "", requiredToEnable.size()) + + tr("%n mod(s) will be disabled\n", "", requiredToDisable.size()) + + tr("Do you want to automatically apply these related changes?\nIgnoring them may break the game."); + noButton = tr("Only Toggle Selected"); + yesButton = tr("Toggle Required Mods"); + } else if (requiredToEnable.size() > 0) { + title = tr("Confirm enable"); + message = tr("The enabled mod(s) require %n mod(s).\n", "", requiredToEnable.size()) + + tr("Would you like to enable them as well?\nIgnoring them may break the game."); + noButton = tr("Only Enable Selected"); + yesButton = tr("Enable Required"); + } else { + title = tr("Confirm disable"); + message = tr("The disabled mod(s) are required by %n mod(s).\n", "", requiredToDisable.size()) + + tr("Would you like to disable them as well?\nIgnoring them may break the game."); + noButton = tr("Only Disable Selected"); + yesButton = tr("Disable Required"); + } + + auto box = CustomMessageBox::selectable(nullptr, title, message, QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No); + box->button(QMessageBox::No)->setText(noButton); + box->button(QMessageBox::Yes)->setText(yesButton); + auto response = box->exec(); + + if (response == QMessageBox::Yes) { + toEnable |= requiredToEnable; + toDisable |= requiredToDisable; + } else if (response == QMessageBox::Cancel) { + return false; + } + } + + auto disableStatus = ResourceFolderModel::setResourceEnabled(toList(toDisable), EnableAction::DISABLE); + auto enableStatus = ResourceFolderModel::setResourceEnabled(toList(toEnable), EnableAction::ENABLE); + return disableStatus && enableStatus; +} + +QStringList reqToList(QSet 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]); +} + +bool ModFolderModel::deleteResources(const QModelIndexList& indexes) +{ + auto deleteInvalid = [](QSet& mods) { + for (auto it = mods.begin(); it != mods.end();) { + auto mod = *it; + // the QFileInfo::exists is used instead of mod->fileinfo().exists + // because the later somehow caches that the file exists + if (!mod || !QFileInfo::exists(mod->fileinfo().absoluteFilePath())) { + it = mods.erase(it); + } else { + ++it; + } + } + }; + auto rsp = ResourceFolderModel::deleteResources(indexes); + for (auto mod : allMods()) { + auto id = mod->mod_id(); + deleteInvalid(m_requiredBy[id]); + deleteInvalid(m_requires[id]); + if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) { + mod->setRequiredByCount(m_requiredBy[id].count()); + mod->setRequiresCount(m_requires[id].count()); + int row = m_resources_index[mod->internal_id()]; + emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); + } + } + return rsp; } diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 42868dc91..4de875abc 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -39,13 +39,15 @@ #include #include -#include +#include #include #include #include #include "Mod.h" #include "ResourceFolderModel.h" +#include "minecraft/Component.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); @@ -85,8 +89,22 @@ class ModFolderModel : public ResourceFolderModel { bool isValid(); + bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action) override; + bool deleteResources(const QModelIndexList& indexes) override; + + QModelIndexList getAffectedMods(const QModelIndexList& indexes, EnableAction action); + RESOURCE_HELPERS(Mod) + public: + QStringList requiresList(QString id); + QStringList requiredByList(QString id); + 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 87bfd4345..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 }; @@ -152,9 +166,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 +176,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/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index e5c707e24..9b7193a4c 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -906,6 +906,7 @@ QList ResourceFolderModel::allResources() result.append((resource.get())); return result; } + QList ResourceFolderModel::selectedResources(const QModelIndexList& indexes) { QList result; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 59d3876b3..7d76317f2 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -61,6 +61,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("requiredMods").toArray()) { + addDep(dep.toString()); + } + } else if (firstObj.contains("dependencies")) { + for (auto dep : firstObj.value("dependencies").toArray()) { + addDep(dep.toString()); + } + } + return details; }; QJsonParseError jsonError; @@ -198,6 +228,52 @@ ModDetails ReadMCModTOML(QByteArray contents) } details.icon_file = logoFile; + auto parseDep = [&details](toml::array* dependencies) { + 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())); + } + } + }; + + 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; } @@ -285,6 +361,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; } @@ -372,6 +460,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) { diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index 7b20d37ec..e365fa801 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -178,4 +178,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 5cde2274f..f225208da 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -58,6 +58,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 a9d75b4ae..787dd7918 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,23 @@ 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"); + if (dep->contains("version")) { + 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(); } diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index c39c5ed9d..296ec00ec 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/dialogs/InstallLoaderDialog.h" #include "ui_ExternalResourcesPage.h" @@ -91,7 +92,7 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* 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); @@ -136,7 +137,25 @@ 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 trying to delete are required by %1 mods.\n" + "Do you want to disable them?") + .arg(affected.length()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, + QMessageBox::Cancel) + ->exec(); + + if (response != QMessageBox::Yes) { + m_model->setResourceEnabled(affected, EnableAction::DISABLE); + } else if (response != QMessageBox::Cancel) { + return; + } + } + m_model->deleteResources(indexes); } void ModFolderPage::downloadMods()