Track dependencies in Mods page (#3738)

This commit is contained in:
Alexandru Ionut Tripon 2026-01-31 20:28:01 +02:00 committed by GitHub
commit ef1e35d585
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 577 additions and 18 deletions

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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;

View file

@ -38,7 +38,9 @@
#include "ModFolderModel.h"
#include <FileSystem.h>
#include <QAbstractButton>
#include <QDebug>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QHeaderView>
#include <QIcon>
@ -48,23 +50,33 @@
#include <QThreadPool>
#include <QUrl>
#include <QUuid>
#include <algorithm>
#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<Mod*>(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<Mod*> 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<Mod*> collectMods(QSet<Mod*> mods, QHash<QString, QSet<Mod*>> relation, std::set<QString>& seen, bool shouldBeEnabled)
{
QSet<Mod*> affectedList = {};
QSet<Mod*> 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<QString> 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<Mod*> toEnable = {};
QSet<Mod*> toDisable = {};
std::set<QString> 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<Mod*> 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<Mod*> 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<Mod*>& 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;
}

View file

@ -39,13 +39,15 @@
#include <QAbstractListModel>
#include <QDir>
#include <QList>
#include <QHash>
#include <QMap>
#include <QSet>
#include <QString>
#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<QString, QSet<Mod*>> m_requiredBy;
QHash<QString, QSet<Mod*>> m_requires;
};

View file

@ -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;

View file

@ -906,6 +906,7 @@ QList<Resource*> ResourceFolderModel::allResources()
result.append((resource.get()));
return result;
}
QList<Resource*> ResourceFolderModel::selectedResources(const QModelIndexList& indexes)
{
QList<Resource*> result;

View file

@ -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) {

View file

@ -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<QString, DependencyType> 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

View file

@ -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);

View file

@ -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;
}

View file

@ -55,6 +55,8 @@ class V1 {
QVariant project_id{};
QString version_number{};
QList<ModPlatform::Dependency> dependencies;
public:
// This is a totally heuristic, but should work for now.
auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); }

View file

@ -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()