Merge upstream/master into scryfall-tagger-tags

Conflict resolution: keep both the tag database and the upstream database writer in the oracle test targets and both includes in the importer test; restore the tags.svg resource entry that the rerere replay dropped.

Upstream's icon rules also landed mid-branch: use loadThemedIcon() for the tag quick-filter button and re-author tags.svg as a 24x24 currentColor icon so IconAssets.AssetsFollowTheRules passes.
This commit is contained in:
Lukas Brübach 2026-11-08 20:44:00 +01:00
commit f154335f5c
553 changed files with 21045 additions and 11980 deletions

View file

@ -1,10 +1,10 @@
set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_string.h
libcockatrice/filters/filter_tree.h
set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_mode.h
libcockatrice/filters/filter_string.h libcockatrice/filters/filter_tree.h
)
add_library(
libcockatrice_filters STATIC ${HEADERS} libcockatrice/filters/filter_card.cpp libcockatrice/filters/filter_string.cpp
libcockatrice/filters/filter_tree.cpp
libcockatrice_filters STATIC ${HEADERS} libcockatrice/filters/filter_card.cpp libcockatrice/filters/filter_mode.cpp
libcockatrice/filters/filter_string.cpp libcockatrice/filters/filter_tree.cpp
)
add_dependencies(libcockatrice_filters libcockatrice_card)

View file

@ -69,6 +69,8 @@ const QString CardFilter::attrName(Attr a)
return tr("Type");
case AttrColor:
return tr("Color");
case AttrColorIdentity:
return tr("Color Identity");
case AttrText:
return tr("Text");
case AttrSet:

View file

@ -32,6 +32,7 @@ public:
{
AttrCmc = 0,
AttrColor,
AttrColorIdentity,
AttrLoyalty,
AttrManaCost,
AttrName,

View file

@ -0,0 +1,77 @@
/**
* @file filter_mode.cpp
* @ingroup Filters
*/
#include "libcockatrice/filters/filter_mode.h"
#include <QString>
FilterModeInfo getFilterModeInfo(FilterMode filterMode)
{
switch (filterMode) {
case FilterMode::Includes:
return {CardFilter::Type::TypeOr, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrColor};
case FilterMode::Excludes:
return {CardFilter::Type::TypeAndNot, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrColor};
case FilterMode::IncludeExclude:
return {CardFilter::Type::TypeOr, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor};
case FilterMode::ExactMatch:
return {CardFilter::Type::TypeAnd, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor};
case FilterMode::ColorIdentity:
return {CardFilter::Type::TypeEnd, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity};
case FilterMode::Custom:
case FilterMode::End:
break;
}
return {CardFilter::Type::TypeEnd, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrEnd};
}
FilterMode deriveFilterMode(const QList<const CardFilter *> &filters, FilterMode currentMode)
{
constexpr int modeCount = static_cast<int>(FilterMode::End);
bool modeImpossible[modeCount] = {};
const QString colors = "WUBRG";
for (int i = 0; i < modeCount; ++i) {
const FilterModeInfo modeInfo = getFilterModeInfo(static_cast<FilterMode>(i));
// Remove the modes that cannot create one of the filters.
for (const CardFilter *filter : filters) {
if (filter->attr() != modeInfo.attr ||
(filter->type() != modeInfo.includeType && filter->type() != modeInfo.excludeType)) {
modeImpossible[i] = true;
break;
}
}
// Modes that have both an include and an exclude filter should always have a filter for each color.
if (modeInfo.includeType != CardFilter::Type::TypeEnd && modeInfo.excludeType != CardFilter::Type::TypeEnd &&
!modeImpossible[i]) {
for (const QChar &color : colors) {
int count = 0;
for (const CardFilter *filter : filters) {
count += filter->term() == color;
}
if (count != 1) {
modeImpossible[i] = true;
break;
}
}
}
}
// Keep the current mode if it can produce the filters, so that a live mode is never swapped for another.
// Switching to and from the custom mode is always possible.
if (!modeImpossible[static_cast<int>(currentMode)] && currentMode != FilterMode::Custom) {
return currentMode;
}
for (int i = 0; i < modeCount; ++i) {
if (!modeImpossible[i] || i == static_cast<int>(FilterMode::Custom)) {
return static_cast<FilterMode>(i);
}
}
return FilterMode::Custom;
}

View file

@ -0,0 +1,62 @@
/**
* @file filter_mode.h
* @ingroup Filters
*/
#ifndef LIBCOCKATRICE_FILTERS_FILTER_MODE_H
#define LIBCOCKATRICE_FILTERS_FILTER_MODE_H
#include "libcockatrice/filters/filter_card.h"
#include <QChar>
#include <QList>
#include <QMetaType>
/**
* @brief The way selected and unselected colors are combined into card filters.
*/
enum class FilterMode
{
Includes = 0, // Include selected colors (OR condition).
Excludes, // Exclude selected colors (AND NOT).
IncludeExclude, // Include selected colors (OR) and exclude unselected colors (AND NOT).
ExactMatch, // Only selected colors are included, all others are excluded.
ColorIdentity, // Keep cards whose color identity is a subset of the selected colors (stored as AND NOT for each
// unselected color).
Custom, // The color filters do not match any of the above modes. Not included in the combo box.
End
};
Q_DECLARE_METATYPE(FilterMode)
/**
* @brief The filters a single color contributes in a given mode.
*/
struct FilterModeInfo
{
// When a color symbol is enabled, add filters of this type. Skipped if equal to CardFilter::Type::TypeEnd.
CardFilter::Type includeType;
// When a color symbol is disabled, add filters of this type. Skipped if equal to CardFilter::Type::TypeEnd.
CardFilter::Type excludeType;
// The attribute to filter for when adding the filters.
CardFilter::Attr attr;
};
/** @brief Return the filters the given mode contributes for one color. */
FilterModeInfo getFilterModeInfo(FilterMode filterMode);
/**
* @brief Determine the mode that produced the given color filters.
*
* Returns @p currentMode unchanged when that mode can still produce @p filters, so that a live mode is never
* replaced by an equally valid one. Otherwise returns the first mode that can produce them, falling back to
* FilterMode::Custom when none can.
*
* @param filters The filters on CardFilter::Attr::AttrColor and CardFilter::Attr::AttrColorIdentity.
* @param currentMode The mode currently selected in the UI.
*/
FilterMode deriveFilterMode(const QList<const CardFilter *> &filters, FilterMode currentMode);
#endif // LIBCOCKATRICE_FILTERS_FILTER_MODE_H

View file

@ -425,7 +425,7 @@ static void setupParserRules()
const bool identity = sv.tokens[0].empty() || sv.tokens[0][0] != 'i';
if (sv.tokens[1][0] == ':') {
return [=](const CardData &x) {
const QString match = identity ? x->getColors() : x->getProperty("coloridentity");
const QString match = identity ? x->getColors() : x->getColorIdentity();
if (parts.contains("m") && match.length() < 2) {
return false;
}
@ -443,7 +443,7 @@ static void setupParserRules()
}
return [=](const CardData &x) {
const QString match = identity ? x->getColors() : x->getProperty("colorIdentity");
const QString match = identity ? x->getColors() : x->getColorIdentity();
if (parts.contains("m") && match.length() < 2) {
return false;
}
@ -522,6 +522,14 @@ FilterString::FilterString()
_error = "Not initialized";
}
QString FilterString::error() const
{
if (_errorColumn == 0) {
return _error;
}
return tr("Error at position %1: %2").arg(_errorColumn).arg(_error);
}
FilterString::FilterString(const QString &expr, const CardSearchLanguage &searchLanguage)
{
QByteArray ba = expr.simplified().toUtf8();
@ -538,7 +546,8 @@ FilterString::FilterString(const QString &expr, const CardSearchLanguage &search
searchLanguageContext = searchLanguage;
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) {
_error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
_error = QString::fromStdString(msg);
_errorColumn = col;
});
if (!search.parse(ba.data(), result)) {

View file

@ -7,6 +7,7 @@
#ifndef FILTER_STRING_H
#define FILTER_STRING_H
#include <QCoreApplication>
#include <QLoggingCategory>
#include <QString>
#include <functional>
@ -33,6 +34,8 @@ typedef AstBase<EmptyType> Ast;
class FilterString
{
Q_DECLARE_TR_FUNCTIONS(FilterString)
public:
FilterString();
explicit FilterString(const QString &exp, const CardSearchLanguage &searchLanguage = {});
@ -45,18 +48,18 @@ public:
return result(card);
}
bool valid()
[[nodiscard]] bool valid() const
{
return _error.isEmpty();
}
QString error()
{
return _error;
}
[[nodiscard]] QString error() const;
private:
// Untranslated parser message; error() adds the translated prefix, logging uses it as-is.
QString _error;
// Column reported by the parser, or 0 when the message carries no position.
size_t _errorColumn = 0;
Filter result;
};

View file

@ -177,23 +177,24 @@ FilterItem::FilterItem(QString trm, FilterItemList *parent) : p(parent), term(st
lowercaseTerm = term.toLower();
switch (attr()) {
case CardFilter::AttrColor: {
QString converted = term.trimmed();
case CardFilter::AttrColor:
case CardFilter::AttrColorIdentity: {
QString converted = term.trimmed().toLower();
converted.replace("green", "g", Qt::CaseInsensitive);
converted.replace("grn", "g", Qt::CaseInsensitive);
converted.replace("blue", "u", Qt::CaseInsensitive);
converted.replace("blu", "u", Qt::CaseInsensitive);
converted.replace("black", "b", Qt::CaseInsensitive);
converted.replace("blk", "b", Qt::CaseInsensitive);
converted.replace("red", "r", Qt::CaseInsensitive);
converted.replace("white", "w", Qt::CaseInsensitive);
converted.replace("wht", "w", Qt::CaseInsensitive);
converted.replace("colorless", "c", Qt::CaseInsensitive);
converted.replace("colourless", "c", Qt::CaseInsensitive);
converted.replace("none", "c", Qt::CaseInsensitive);
converted.replace("green", "g");
converted.replace("grn", "g");
converted.replace("blue", "u");
converted.replace("blu", "u");
converted.replace("black", "b");
converted.replace("blk", "b");
converted.replace("red", "r");
converted.replace("white", "w");
converted.replace("wht", "w");
converted.replace("colorless", "c");
converted.replace("colourless", "c");
converted.replace("none", "c");
converted.replace(QString(" "), QString(""), Qt::CaseInsensitive);
converted.replace(QString(" "), QString(""));
convertedColorTerm = converted;
break;
@ -359,12 +360,17 @@ bool FilterItem::acceptSubType(const CardInfoPtr &info) const
return subType.contains(term, Qt::CaseInsensitive);
}
bool FilterItem::acceptColor(const CardInfoPtr &info) const
/**
* @brief Return true when every letter of the given term appears in the given colors string.
* If the term is equal to "c", then only return true if the color string is empty.
*
* @param term The letters which should be included, or "c"
* @param colors The string to check for letters in
*/
static bool matchesColor(const QString &term, const QString &colors)
{
const QString colors = info->getColors();
// Colorless card filter
if (convertedColorTerm.toLower() == "c" && colors.length() < 1) {
if (term == "c" && colors.length() < 1) {
return true;
}
@ -373,13 +379,23 @@ bool FilterItem::acceptColor(const CardInfoPtr &info) const
* then we should match all of them to the card's colors
*/
int match_count = 0;
for (const QChar &color : convertedColorTerm) {
for (const QChar &color : term) {
if (colors.contains(color, Qt::CaseInsensitive)) {
match_count++;
}
}
return match_count == convertedColorTerm.length();
return match_count == term.length();
}
bool FilterItem::acceptColor(const CardInfoPtr &info) const
{
return matchesColor(convertedColorTerm, info->getColors());
}
bool FilterItem::acceptColorIdentity(const CardInfoPtr &info) const
{
return matchesColor(convertedColorTerm, info->getColorIdentity());
}
bool FilterItem::acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const
@ -551,6 +567,8 @@ bool FilterItem::acceptCardAttr(const CardInfoPtr &info,
return acceptType(info);
case CardFilter::AttrColor:
return acceptColor(info);
case CardFilter::AttrColorIdentity:
return acceptColorIdentity(info);
case CardFilter::AttrText:
return acceptText(info, searchLanguage);
case CardFilter::AttrSet:

View file

@ -244,6 +244,7 @@ public:
[[nodiscard]] bool acceptMainType(const CardInfoPtr &info) const;
[[nodiscard]] bool acceptSubType(const CardInfoPtr &info) const;
[[nodiscard]] bool acceptColor(const CardInfoPtr &info) const;
[[nodiscard]] bool acceptColorIdentity(const CardInfoPtr &info) const;
[[nodiscard]] bool acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool acceptSet(const CardInfoPtr &info) const;
[[nodiscard]] bool acceptManaCost(const CardInfoPtr &info) const;