Highlight resources incompatible with the Minecraft version (#5010)

This commit is contained in:
Alexandru Ionut Tripon 2026-02-17 20:16:13 +00:00 committed by GitHub
commit 2b84053011
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 242 additions and 162 deletions

View file

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

View file

@ -64,10 +64,10 @@ 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 NameColumn:
return m_resources[row]->name();
case PackFormatColumn: {
auto& resource = at(row);
auto pack_format = resource.packFormat();
@ -81,53 +81,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

View file

@ -88,10 +88,10 @@ 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 NameColumn:
return m_resources[row]->name();
case VersionColumn: {
switch (at(row).type()) {
case ResourceType::FOLDER:
@ -99,14 +99,8 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
case ResourceType::SINGLEFILE:
return tr("File");
default:
break;
return at(row).version();
}
return at(row).version();
}
case DateColumn:
return at(row).dateTimeChanged();
case ProviderColumn: {
return at(row).provider();
}
case SideColumn: {
return at(row).side();
@ -120,53 +114,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
@ -258,9 +253,11 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id)
auto resource = find(mod_id);
auto result = cast_task->result();
if (result && resource)
static_cast<Mod*>(resource.get())->finishResolvingWithDetails(std::move(result->details));
if (result && resource) {
auto* mod = static_cast<Mod*>(resource.get());
mod->finishResolvingWithDetails(std::move(result->details));
}
emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn));
}

View file

@ -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,40 @@ void Resource::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata)
m_metadata = metadata;
}
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) {
return;
}
auto mcInst = dynamic_cast<const MinecraftInstance*>(inst);
if (mcInst == nullptr) {
return;
}
auto profile = mcInst->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");
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
{
switch (type) {

View file

@ -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,14 @@ class Resource : public QObject {
void setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata);
void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared<Metadata::ModStruct>(metadata)); }
/**
* Returns compatibility issues with the resource and the instance.
* This is initially empty, and may be updated when calling updateIssues.
*/
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'
* = 0: 'this' is equal to 'other'
@ -188,6 +198,8 @@ class Resource : public QObject {
/* Whether the resource is enabled (e.g. shows up in the game) or not. */
bool m_enabled = true;
QList<const char*> m_issues;
/* Used to keep trach of pending / concluded actions on the resource. */
bool m_is_resolving = false;
bool m_is_resolved = false;

View file

@ -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 (APPLICATION->settings()->get("ShowModIncompat").toBool() && m_resources[row]->hasIssues()) {
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,39 @@ 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 (APPLICATION->settings()->get("ShowModIncompat").toBool()) {
for (const QString& issue : at(row).issues()) {
tooltip += "\n" + issue;
}
}
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 (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");
}
}
return {};
}
@ -810,7 +837,13 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
auto const& current_resource = m_resources.at(row);
if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) {
// no significant change, ignore...
// no significant change
bool hadIssues = !current_resource->hasIssues();
current_resource->updateIssues(m_instance);
if (hadIssues != current_resource->hasIssues()) {
emit dataChanged(index(row, 0), index(row, columnCount({}) - 1));
}
continue;
}
@ -825,6 +858,8 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
}
m_resources[row].reset(new_resource);
new_resource->updateIssues(m_instance);
resolveResource(m_resources.at(row));
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
}
@ -872,6 +907,7 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
for (auto& added : added_set) {
auto res = new_resources[added];
res->updateIssues(m_instance);
m_resources.append(res);
resolveResource(m_resources.last());
}

View file

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

View file

@ -65,10 +65,10 @@ 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 NameColumn:
return m_resources[row]->name();
case PackFormatColumn: {
auto& resource = at(row);
auto pack_format = resource.packFormat();
@ -82,55 +82,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

View file

@ -63,56 +63,46 @@ 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::BackgroundRole:
return rowBackground(row);
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

View file

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

View file

@ -41,9 +41,9 @@
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<y>-149</y>
<width>746</width>
<height>1194</height>
<height>1222</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
@ -418,6 +418,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showModIncompatCheckBox">
<property name="toolTip">
<string>Currently this just shows mods which are not marked as compatible with the current Minecraft version.</string>
</property>
<property name="text">
<string>Detect and show mod incompatibilities (experimental)</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="modpackUpdatePromptBtn">
<property name="toolTip">
@ -671,7 +681,7 @@
<resources/>
<connections/>
<buttongroups>
<buttongroup name="sortingModeGroup"/>
<buttongroup name="renamingBehaviorGroup"/>
<buttongroup name="sortingModeGroup"/>
</buttongroups>
</ui>