diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index e9ca2e682..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; } @@ -284,3 +298,24 @@ 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 eceb8c256..16f4b32f7 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -72,6 +72,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; } @@ -104,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/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/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index b613e0af1..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 @@ -48,25 +49,34 @@ #include #include #include +#include #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" 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 +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(); } @@ -168,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(); @@ -195,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("Number of mods for what this is needed."); + case RequiresColumn: + return tr("Number of mods that this requires."); default: return QVariant(); } @@ -240,3 +263,178 @@ 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()) { + return; + } + auto mods = allMods(); + + 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(); + 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)); + } +} +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) +{ + if (indexes.isEmpty()) + return {}; + + QModelIndexList affectedList = {}; + auto affectedMods = selectedMods(indexes); + std::set seen; + + 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 + } + } + 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) +{ + 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) +{ + 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 42868dc91..e47c18405 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,20 @@ class ModFolderModel : public ResourceFolderModel { bool isValid(); + bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action) 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 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/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/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 952115bed..c3b2dbdd9 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("requiredMods").toArray()) { + addDep(dep.toString()); + } + } else if (firstObj.contains("dependencies")) { + for (auto dep : firstObj.value("dependencies").toArray()) { + addDep(dep.toString()); + } + } + return details; }; QJsonParseError jsonError; @@ -199,6 +229,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; } @@ -286,6 +362,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 +461,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 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(); } diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 7b79766ee..bbaf7be94 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); @@ -118,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) @@ -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() 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 - - -