mirror of
https://github.com/PrismLauncher/PrismLauncher.git
synced 2026-06-29 01:54:20 +03:00
Track dependencies in Mods page (#3738)
This commit is contained in:
commit
ef1e35d585
13 changed files with 577 additions and 18 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -906,6 +906,7 @@ QList<Resource*> ResourceFolderModel::allResources()
|
|||
result.append((resource.get()));
|
||||
return result;
|
||||
}
|
||||
|
||||
QList<Resource*> ResourceFolderModel::selectedResources(const QModelIndexList& indexes)
|
||||
{
|
||||
QList<Resource*> result;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(); }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue