From e2d503456f668d0a3f58f66c96b0421754f744a8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 12 Feb 2026 16:43:11 +0000 Subject: [PATCH 1/5] Detect resources incompatible with the MC version Signed-off-by: TheKodeToad --- launcher/minecraft/mod/ModFolderModel.cpp | 6 ++++-- launcher/minecraft/mod/Resource.cpp | 20 +++++++++++++++++++ launcher/minecraft/mod/Resource.h | 11 ++++++++++ .../minecraft/mod/ResourceFolderModel.cpp | 11 +++++++++- .../mod/tasks/ResourceFolderLoadTask.h | 4 +++- 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 8b849dc43..e7bb13128 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -258,9 +258,11 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) auto resource = find(mod_id); auto result = cast_task->result(); - if (result && resource) - static_cast(resource.get())->finishResolvingWithDetails(std::move(result->details)); + if (result && resource) { + auto* mod = static_cast(resource.get()); + mod->finishResolvingWithDetails(std::move(result->details)); + } emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); } diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index 8f67d5d94..64bf14b6c 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -7,6 +7,8 @@ #include "FileSystem.h" #include "StringUtils.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" Resource::Resource(QObject* parent) : QObject(parent) {} @@ -111,6 +113,24 @@ void Resource::setMetadata(std::shared_ptr&& metadata) m_metadata = metadata; } +void Resource::determineCompat(const BaseInstance* inst) { + if (m_metadata == nullptr) { + m_isCompatible = true; + return; + } + + auto mcInst = dynamic_cast(inst); + if (mcInst == nullptr) { + m_isCompatible = true; + return; + } + + auto profile = mcInst->getPackProfile(); + QString mcVersion = profile->getComponentVersion("net.minecraft"); + + m_isCompatible = m_metadata->mcVersions.contains(mcVersion); +} + int Resource::compare(const Resource& other, SortType type) const { switch (type) { diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index 3eda4c013..7121a8a6b 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -43,6 +43,8 @@ #include "MetadataHandler.h" #include "QObjectPtr.h" +class BaseInstance; + enum class ResourceType { UNKNOWN, //!< Indicates an unspecified resource type. ZIPFILE, //!< The resource is a zip file containing the resource's class files. @@ -119,6 +121,13 @@ class Resource : public QObject { void setMetadata(std::shared_ptr&& metadata); void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } + /** + * Returns whether the resource is compatible with the instance. + * This is initially true, and may be updated when calling determineCompat with an instance. + */ + bool isCompatible() const { return m_isCompatible; } + void determineCompat(const BaseInstance* inst); + /** Compares two Resources, for sorting purposes, considering a ascending order, returning: * > 0: 'this' comes after 'other' * = 0: 'this' is equal to 'other' @@ -188,6 +197,8 @@ class Resource : public QObject { /* Whether the resource is enabled (e.g. shows up in the game) or not. */ bool m_enabled = true; + bool m_isCompatible = true; + /* Used to keep trach of pending / concluded actions on the resource. */ bool m_is_resolving = false; bool m_is_resolved = false; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 9b7193a4c..532eb8d66 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -810,7 +810,13 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet auto const& current_resource = m_resources.at(row); if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { - // no significant change, ignore... + // no significant change + bool oldCompat = current_resource->isCompatible(); + current_resource->determineCompat(m_instance); + + if (current_resource->isCompatible() != oldCompat) { + emit dataChanged(index(row, 0), index(row, columnCount({}) - 1)); + } continue; } @@ -825,6 +831,8 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet } m_resources[row].reset(new_resource); + new_resource->determineCompat(m_instance); + resolveResource(m_resources.at(row)); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } @@ -872,6 +880,7 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet for (auto& added : added_set) { auto res = new_resources[added]; + res->determineCompat(m_instance); m_resources.append(res); resolveResource(m_resources.last()); } diff --git a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h index 7c872c13d..7f69a1841 100644 --- a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h @@ -41,9 +41,11 @@ #include #include #include -#include "minecraft/mod/Mod.h" +#include "minecraft/mod/Resource.h" #include "tasks/Task.h" +class BaseInstance; + class ResourceFolderLoadTask : public Task { Q_OBJECT public: From 081d2f1e5183ff14f129b695a00e2dad819b87eb Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 12 Feb 2026 17:47:10 +0000 Subject: [PATCH 2/5] Reduce duplication in ResourceFolderModel subclasses Signed-off-by: TheKodeToad --- .../minecraft/mod/DataPackFolderModel.cpp | 60 ++++++++-------- launcher/minecraft/mod/ModFolderModel.cpp | 68 ++++++++----------- .../minecraft/mod/ResourcePackFolderModel.cpp | 63 ++++++++--------- .../minecraft/mod/TexturePackFolderModel.cpp | 68 ++++++++----------- .../mod/tasks/ResourceFolderLoadTask.h | 4 +- 5 files changed, 116 insertions(+), 147 deletions(-) diff --git a/launcher/minecraft/mod/DataPackFolderModel.cpp b/launcher/minecraft/mod/DataPackFolderModel.cpp index 7abab7a06..a975b742e 100644 --- a/launcher/minecraft/mod/DataPackFolderModel.cpp +++ b/launcher/minecraft/mod/DataPackFolderModel.cpp @@ -66,8 +66,6 @@ QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const switch (role) { case Qt::DisplayRole: switch (column) { - case NameColumn: - return m_resources[row]->name(); case PackFormatColumn: { auto& resource = at(row); auto pack_format = resource.packFormat(); @@ -81,53 +79,51 @@ QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const return QString("%1 (%2 - %3)") .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); } - case DateColumn: - return m_resources[row]->dateTimeChanged(); - - default: - return {}; } + break; case Qt::DecorationRole: { - if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } - return {}; + break; } case Qt::ToolTipRole: { if (column == PackFormatColumn) { //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); } - if (column == NameColumn) { - if (at(row).isSymLinkUnder(instDirPath())) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath()); - ; - } - if (at(row).isMoreThanOneHardLink()) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); - } - } - return m_resources[row]->internal_id(); + break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } - return {}; - case Qt::CheckStateRole: - if (column == ActiveColumn) - return at(row).enabled() ? Qt::Checked : Qt::Unchecked; - else - return {}; - default: - return {}; + break; } + + // map the columns to the base equivilents + QModelIndex mappedIndex; + switch (column) { + case ActiveColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); + break; + case NameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); + break; + case DateColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); + break; + case ProviderColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); + break; + // FIXME: there is no size column due to an oversight + } + + if (mappedIndex.isValid()) { + return ResourceFolderModel::data(mappedIndex, role); + } + + return {}; } QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index e7bb13128..cf3219918 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -90,24 +90,15 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const switch (role) { case Qt::DisplayRole: switch (column) { - case NameColumn: - return m_resources[row]->name(); case VersionColumn: { switch (at(row).type()) { case ResourceType::FOLDER: return tr("Folder"); case ResourceType::SINGLEFILE: return tr("File"); - default: - break; } return at(row).version(); } - case DateColumn: - return at(row).dateTimeChanged(); - case ProviderColumn: { - return at(row).provider(); - } case SideColumn: { return at(row).side(); } @@ -120,53 +111,54 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const case ReleaseTypeColumn: { return at(row).releaseType(); } - case SizeColumn: { - return at(row).sizeStr(); - } case RequiredByColumn: { return at(row).requiredByCount(); } case RequiresColumn: { return at(row).requiresCount(); } - default: - return QVariant(); } - - case Qt::ToolTipRole: - if (column == NameColumn) { - if (at(row).isSymLinkUnder(instDirPath())) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath()); - } - if (at(row).isMoreThanOneHardLink()) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); - } - } - return m_resources[row]->internal_id(); + break; case Qt::DecorationRole: { - if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } - return {}; + break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } - return {}; - case Qt::CheckStateRole: - if (column == ActiveColumn) - return at(row).enabled() ? Qt::Checked : Qt::Unchecked; - return QVariant(); + break; default: - return QVariant(); + break; } + + // map the columns to the base equivilents + QModelIndex mappedIndex; + switch (column) { + case ActiveColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); + break; + case NameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); + break; + case DateColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); + break; + case ProviderColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); + break; + case SizeColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); + break; + } + + if (mappedIndex.isValid()) { + return ResourceFolderModel::data(mappedIndex, role); + } + + return {}; } QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index 5680f4c2d..c83f90e2a 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -67,8 +67,6 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const switch (role) { case Qt::DisplayRole: switch (column) { - case NameColumn: - return m_resources[row]->name(); case PackFormatColumn: { auto& resource = at(row); auto pack_format = resource.packFormat(); @@ -82,55 +80,52 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const return QString("%1 (%2 - %3)") .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); } - case DateColumn: - return m_resources[row]->dateTimeChanged(); - case ProviderColumn: - return m_resources[row]->provider(); - case SizeColumn: - return m_resources[row]->sizeStr(); - default: - return {}; } case Qt::DecorationRole: { - if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } - return {}; + break; } case Qt::ToolTipRole: { if (column == PackFormatColumn) { //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); } - if (column == NameColumn) { - if (at(row).isSymLinkUnder(instDirPath())) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath()); - ; - } - if (at(row).isMoreThanOneHardLink()) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); - } - } - return m_resources[row]->internal_id(); + break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } - return {}; - case Qt::CheckStateRole: - if (column == ActiveColumn) - return at(row).enabled() ? Qt::Checked : Qt::Unchecked; - return {}; - default: - return {}; + break; } + + // map the columns to the base equivilents + QModelIndex mappedIndex; + switch (column) { + case ActiveColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); + break; + case NameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); + break; + case DateColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); + break; + case ProviderColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); + break; + case SizeColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); + break; + } + + if (mappedIndex.isValid()) { + return ResourceFolderModel::data(mappedIndex, role); + } + + return {}; } QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 57c5f8ee9..e5f3eef88 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -63,56 +63,44 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const int column = index.column(); switch (role) { - case Qt::DisplayRole: - switch (column) { - case NameColumn: - return m_resources[row]->name(); - case DateColumn: - return m_resources[row]->dateTimeChanged(); - case ProviderColumn: - return m_resources[row]->provider(); - case SizeColumn: - return m_resources[row]->sizeStr(); - default: - return {}; - } - case Qt::ToolTipRole: - if (column == NameColumn) { - if (at(row).isSymLinkUnder(instDirPath())) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath()); - ; - } - if (at(row).isMoreThanOneHardLink()) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); - } - } - - return m_resources[row]->internal_id(); case Qt::DecorationRole: { - if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } - return {}; + break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } - return {}; - case Qt::CheckStateRole: - if (column == ActiveColumn) { - return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; - } - return {}; - default: - return {}; + break; } + + // map the columns to the base equivilents + QModelIndex mappedIndex; + switch (column) { + case ActiveColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); + break; + case NameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); + break; + case DateColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); + break; + case ProviderColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); + break; + case SizeColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); + break; + } + + if (mappedIndex.isValid()) { + return ResourceFolderModel::data(mappedIndex, role); + } + + return {}; } QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const diff --git a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h index 7f69a1841..7c872c13d 100644 --- a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h @@ -41,11 +41,9 @@ #include #include #include -#include "minecraft/mod/Resource.h" +#include "minecraft/mod/Mod.h" #include "tasks/Task.h" -class BaseInstance; - class ResourceFolderLoadTask : public Task { Q_OBJECT public: From 9cb33b519fb16ee70cfd5a724bc62161b4461fa0 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 12 Feb 2026 18:51:27 +0000 Subject: [PATCH 3/5] Render incompatibility highlight and warning Signed-off-by: TheKodeToad --- .../minecraft/mod/DataPackFolderModel.cpp | 2 + launcher/minecraft/mod/ModFolderModel.cpp | 5 +- .../minecraft/mod/ResourceFolderModel.cpp | 49 ++++++++++++++----- launcher/minecraft/mod/ResourceFolderModel.h | 1 + .../minecraft/mod/ResourcePackFolderModel.cpp | 2 + .../minecraft/mod/TexturePackFolderModel.cpp | 2 + 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/launcher/minecraft/mod/DataPackFolderModel.cpp b/launcher/minecraft/mod/DataPackFolderModel.cpp index a975b742e..b934e451e 100644 --- a/launcher/minecraft/mod/DataPackFolderModel.cpp +++ b/launcher/minecraft/mod/DataPackFolderModel.cpp @@ -64,6 +64,8 @@ QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const int column = index.column(); switch (role) { + case Qt::BackgroundRole: + return rowBackground(row); case Qt::DisplayRole: switch (column) { case PackFormatColumn: { diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index cf3219918..50b8985c8 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -88,6 +88,8 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const int column = index.column(); switch (role) { + case Qt::BackgroundRole: + return rowBackground(row); case Qt::DisplayRole: switch (column) { case VersionColumn: { @@ -96,8 +98,9 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const return tr("Folder"); case ResourceType::SINGLEFILE: return tr("File"); + default: + return at(row).version(); } - return at(row).version(); } case SideColumn: { return at(row).side(); diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 532eb8d66..11ef81dbc 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -499,6 +499,17 @@ bool ResourceFolderModel::validateIndex(const QModelIndex& index) const return true; } +// HACK: all subclasses need to call this to have the whole row painted +// and they only delegate to the superclass for compatible columns +QBrush ResourceFolderModel::rowBackground(int row) const +{ + if (!m_resources[row]->isCompatible()) { + return { QColor(255, 0, 0, 40) }; + } else { + return {}; + } +} + QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const { if (!validateIndex(index)) @@ -508,6 +519,8 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const int column = index.column(); switch (role) { + case Qt::BackgroundRole: + return rowBackground(row); case Qt::DisplayRole: switch (column) { case NameColumn: @@ -521,25 +534,37 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const default: return {}; } - case Qt::ToolTipRole: + case Qt::ToolTipRole: { + QString tooltip = m_resources[row]->internal_id(); + if (column == NameColumn) { - if (at(row).isSymLinkUnder(instDirPath())) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath()); - ; + if (!at(row).isCompatible()) { + tooltip += tr("\nResource is not marked as compatible with the instance."); } + + if (at(row).isSymLinkUnder(instDirPath())) { + tooltip += + m_resources[row]->internal_id() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row).fileinfo().canonicalFilePath()); + } + if (at(row).isMoreThanOneHardLink()) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + tooltip += tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } - return m_resources[row]->internal_id(); + return tooltip; + } case Qt::DecorationRole: { - if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return QIcon::fromTheme("status-yellow"); + if (column == NameColumn) { + if (!at(row).isCompatible()) { + return QIcon::fromTheme("status-bad"); + } else if (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()) { + return QIcon::fromTheme("status-yellow"); + } + } return {}; } diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index b6343a807..81bc6f5fc 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -153,6 +153,7 @@ class ResourceFolderModel : public QAbstractListModel { [[nodiscard]] bool validateIndex(const QModelIndex& index) const; + QBrush rowBackground(int row) const; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index c83f90e2a..fd59d5765 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -65,6 +65,8 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const int column = index.column(); switch (role) { + case Qt::BackgroundRole: + return rowBackground(row); case Qt::DisplayRole: switch (column) { case PackFormatColumn: { diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index e5f3eef88..d96b768db 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -63,6 +63,8 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const int column = index.column(); switch (role) { + case Qt::BackgroundRole: + return rowBackground(row); case Qt::DecorationRole: { if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); From b664846030db2e94922da08249afc6b037c69ddc Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 12 Feb 2026 20:38:43 +0000 Subject: [PATCH 4/5] Allow multiple compat issues to be displayed, make game version incompat clearer Signed-off-by: TheKodeToad --- launcher/minecraft/mod/Resource.cpp | 24 +++++++++++++++---- launcher/minecraft/mod/Resource.h | 11 +++++---- .../minecraft/mod/ResourceFolderModel.cpp | 18 +++++++------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index 64bf14b6c..8cffa378e 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -113,22 +113,38 @@ void Resource::setMetadata(std::shared_ptr&& metadata) m_metadata = metadata; } -void Resource::determineCompat(const BaseInstance* inst) { +QStringList Resource::issues() const +{ + QStringList result; + result.reserve(m_issues.length()); + + for (const char* issue : m_issues) { + result.append(tr(issue)); + } + + return result; +} + +void Resource::updateIssues(const BaseInstance* inst) +{ + m_issues.clear(); + if (m_metadata == nullptr) { - m_isCompatible = true; return; } auto mcInst = dynamic_cast(inst); if (mcInst == nullptr) { - m_isCompatible = true; return; } auto profile = mcInst->getPackProfile(); QString mcVersion = profile->getComponentVersion("net.minecraft"); - m_isCompatible = m_metadata->mcVersions.contains(mcVersion); + if (!m_metadata->mcVersions.empty() && !m_metadata->mcVersions.contains(mcVersion)) { + // delay translation until issues() is called + m_issues.append(QT_TR_NOOP("Not marked as compatible with the instance's game version.")); + } } int Resource::compare(const Resource& other, SortType type) const diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index 7121a8a6b..646ee65dd 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -122,11 +122,12 @@ class Resource : public QObject { void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } /** - * Returns whether the resource is compatible with the instance. - * This is initially true, and may be updated when calling determineCompat with an instance. + * Returns compatibility issues with the resource and the instance. + * This is initially empty, and may be updated when calling updateIssues. */ - bool isCompatible() const { return m_isCompatible; } - void determineCompat(const BaseInstance* inst); + QStringList issues() const; + void updateIssues(const BaseInstance* inst); + bool hasIssues() const { return !m_issues.empty(); } /** Compares two Resources, for sorting purposes, considering a ascending order, returning: * > 0: 'this' comes after 'other' @@ -197,7 +198,7 @@ class Resource : public QObject { /* Whether the resource is enabled (e.g. shows up in the game) or not. */ bool m_enabled = true; - bool m_isCompatible = true; + QList m_issues; /* Used to keep trach of pending / concluded actions on the resource. */ bool m_is_resolving = false; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 11ef81dbc..ed0e07574 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -503,7 +503,7 @@ bool ResourceFolderModel::validateIndex(const QModelIndex& index) const // and they only delegate to the superclass for compatible columns QBrush ResourceFolderModel::rowBackground(int row) const { - if (!m_resources[row]->isCompatible()) { + if (m_resources[row]->hasIssues()) { return { QColor(255, 0, 0, 40) }; } else { return {}; @@ -538,8 +538,8 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const QString tooltip = m_resources[row]->internal_id(); if (column == NameColumn) { - if (!at(row).isCompatible()) { - tooltip += tr("\nResource is not marked as compatible with the instance."); + for (const QString& issue : at(row).issues()) { + tooltip += "\n" + issue; } if (at(row).isSymLinkUnder(instDirPath())) { @@ -559,7 +559,7 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const } case Qt::DecorationRole: { if (column == NameColumn) { - if (!at(row).isCompatible()) { + if (at(row).hasIssues()) { return QIcon::fromTheme("status-bad"); } else if (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()) { return QIcon::fromTheme("status-yellow"); @@ -836,10 +836,10 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { // no significant change - bool oldCompat = current_resource->isCompatible(); - current_resource->determineCompat(m_instance); + bool hadIssues = !current_resource->hasIssues(); + current_resource->updateIssues(m_instance); - if (current_resource->isCompatible() != oldCompat) { + if (hadIssues != current_resource->hasIssues()) { emit dataChanged(index(row, 0), index(row, columnCount({}) - 1)); } continue; @@ -856,7 +856,7 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet } m_resources[row].reset(new_resource); - new_resource->determineCompat(m_instance); + new_resource->updateIssues(m_instance); resolveResource(m_resources.at(row)); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); @@ -905,7 +905,7 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet for (auto& added : added_set) { auto res = new_resources[added]; - res->determineCompat(m_instance); + res->updateIssues(m_instance); m_resources.append(res); resolveResource(m_resources.last()); } From 71278ff7ea9df7b17c68371eea66653b647f53cf Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 12 Feb 2026 21:03:59 +0000 Subject: [PATCH 5/5] Add opt-in for showing mod incompatibilities Signed-off-by: TheKodeToad --- launcher/Application.cpp | 1 + launcher/minecraft/mod/ResourceFolderModel.cpp | 10 ++++++---- launcher/ui/pages/global/LauncherPage.cpp | 2 ++ launcher/ui/pages/global/LauncherPage.ui | 16 +++++++++++++--- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ac5cd8f4f..a7c983c1e 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -774,6 +774,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ModMetadataDisabled", false); m_settings->registerSetting("ModDependenciesDisabled", false); m_settings->registerSetting("SkipModpackUpdatePrompt", false); + m_settings->registerSetting("ShowModIncompat", false); // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index ed0e07574..2899b4eee 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -503,7 +503,7 @@ bool ResourceFolderModel::validateIndex(const QModelIndex& index) const // and they only delegate to the superclass for compatible columns QBrush ResourceFolderModel::rowBackground(int row) const { - if (m_resources[row]->hasIssues()) { + if (APPLICATION->settings()->get("ShowModIncompat").toBool() && m_resources[row]->hasIssues()) { return { QColor(255, 0, 0, 40) }; } else { return {}; @@ -538,8 +538,10 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const QString tooltip = m_resources[row]->internal_id(); if (column == NameColumn) { - for (const QString& issue : at(row).issues()) { - tooltip += "\n" + issue; + if (APPLICATION->settings()->get("ShowModIncompat").toBool()) { + for (const QString& issue : at(row).issues()) { + tooltip += "\n" + issue; + } } if (at(row).isSymLinkUnder(instDirPath())) { @@ -559,7 +561,7 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const } case Qt::DecorationRole: { if (column == NameColumn) { - if (at(row).hasIssues()) { + if (APPLICATION->settings()->get("ShowModIncompat").toBool() && at(row).hasIssues()) { return QIcon::fromTheme("status-bad"); } else if (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()) { return QIcon::fromTheme("status-yellow"); diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 7a0f11c83..d6d15a2c4 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -244,6 +244,7 @@ void LauncherPage::applySettings() // Mods s->set("ModMetadataDisabled", !ui->metadataEnableBtn->isChecked()); s->set("ModDependenciesDisabled", !ui->dependenciesEnableBtn->isChecked()); + s->set("ShowModIncompat", ui->showModIncompatCheckBox->isChecked()); s->set("SkipModpackUpdatePrompt", !ui->modpackUpdatePromptBtn->isChecked()); } void LauncherPage::loadSettings() @@ -293,6 +294,7 @@ void LauncherPage::loadSettings() ui->metadataEnableBtn->setChecked(!s->get("ModMetadataDisabled").toBool()); ui->metadataWarningLabel->setHidden(ui->metadataEnableBtn->isChecked()); ui->dependenciesEnableBtn->setChecked(!s->get("ModDependenciesDisabled").toBool()); + ui->showModIncompatCheckBox->setChecked(s->get("ShowModIncompat").toBool()); ui->modpackUpdatePromptBtn->setChecked(!s->get("SkipModpackUpdatePrompt").toBool()); } diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 0debe3f4d..1e7686e5f 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -41,9 +41,9 @@ 0 - 0 + -149 746 - 1194 + 1222 @@ -418,6 +418,16 @@ + + + + Currently this just shows mods which are not marked as compatible with the current Minecraft version. + + + Detect and show mod incompatibilities (experimental) + + + @@ -671,7 +681,7 @@ - +