[Card] Add a setting for the language used in card search (#7314)

* [Card] Add a setting for the language used in card search

Localized card names and texts can now be searched too, controlled by a
'Language used in card search' toggle (English, selected card language, or
both) on the general settings page. Untranslated cards always keep matching
in English.

Removed the redundant local copy of the URL templates list in the localized
picture loader while here.

* [Card] Bind search language per FilterString instance

The peg parser rules are set up once per process, so the GenericQuery and
OracleQuery rule actions could not capture per-instance state. Instead of
storing the search language in a process-global that FilterString instance
methods mutate, hand it to the rule actions through a thread-local parse
context and copy it into the filter closures they produce. Card evaluation
in FilterString::check no longer reads any process-global state, and each
instance keeps the language it was built with; constructing one instance no
longer changes what unrelated instances (deck filter, drop-to-hand, zone
views) match against.

The card database display model stores the raw query and rebuilds the
FilterString when the search language changes, since the language is now
bound at parse time.

Add tests for the English/Selected/Both search modes, the English fallback
for untranslated cards, and per-instance language independence.

* [Card] Pass the card search language to deck and zone card searches

Wire the two remaining FilterString consumers to the configured card search
language so card-name matches respect it everywhere:

- DeckFilterString now takes the search language and mode, exposes them to its
  [[card name]] rule action via a thread-local parse context (same pattern as
  FilterString), and the engine's card database uses them for content search.
- ZoneViewZone reads the card language from CardsDisplaySettings when applying
  its search filter, and the reveal-zone widget re-applies the active search
  when the language setting changes.
- The deck-storage search re-runs its filter against the current card language
  setting, including live re-application when the setting changes.

Game-action targeting (DlgMoveTopCardsUntil) intentionally keeps evaluating
against English card names.

* [Card] Rename CardSearchLanguage to SearchLanguageMode

* [Card] Restore displaced namespace doc in card_localization.h

* [Filters] Pass CardSearchLanguage as a single struct

* [CardSearchModel] Match English and localized names in Both mode

Card names are stored in both English and localized forms, so search for
matches in both during the 'Both' search mode instead of checking only
the localized name.

* [CreateTokenDialog] Fetch cardsDisplay settings inside the apply lambda

Avoid capturing the raw settings pointer in the lambda: resolve the card
language and card search language from the settings cache at call time so
the values are always current when the search language is re-applied.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-09-21 08:50:48 +02:00 • committed by GitHub
parent 12299abcc8
commit ef68a7bdcc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 488 additions and 68 deletions

View file

@ -74,6 +74,36 @@ NumericValue <- [0-9]+
static std::once_flag init;
// The peglib parser rules (and therefore their rule actions) are set up once per
// process, so a rule action cannot capture per-instance state. The card language
// plain-text name and text queries search in is therefore handed to the GenericQuery
// and OracleQuery rule actions through this thread-local context, which is live only
// while a FilterString is being parsed. The rule actions copy it into the filter
// closures they produce, so card evaluation never reads process-global state.
thread_local CardSearchLanguage searchLanguageContext;
namespace
{
bool matchesInSearchLanguage(const QString &english,
const QString &localized,
const CardSearchLanguage &searchLanguage,
const StringMatcher &matcher)
{
if (searchLanguage.mode == SearchLanguageMode::English) {
return matcher(english);
}
if (searchLanguage.mode == SearchLanguageMode::Both) {
if (!searchLanguage.isEnglishOnly() && matcher(localized)) {
return true;
}
return matcher(english);
}
return searchLanguage.isEnglishOnly() ? matcher(english) : matcher(localized);
}
} // namespace
static void setupParserRules()
{
auto passthru = [](const peg::SemanticValues &sv) -> Filter {
@ -333,7 +363,11 @@ static void setupParserRules()
search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<StringMatcher>(sv[0]);
return [=](const CardData &x) { return matcher(x->getText()); };
const CardSearchLanguage searchLanguage = searchLanguageContext;
return [=](const CardData &x) {
return matchesInSearchLanguage(x->getText(), x->getLocalizedText(searchLanguage.language), searchLanguage,
matcher);
};
};
search["ColorQuery"] = [](const peg::SemanticValues &sv) -> Filter {
@ -410,7 +444,11 @@ static void setupParserRules()
};
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<StringMatcher>(sv[0]);
return [=](const CardData &x) { return matcher(x->getName()); };
const CardSearchLanguage searchLanguage = searchLanguageContext;
return [=](const CardData &x) {
return matchesInSearchLanguage(x->getName(), x->getLocalizedName(searchLanguage.language), searchLanguage,
matcher);
};
};
search["Color"] = [](const peg::SemanticValues &sv) -> char { return "WUBRGU"[sv.choice()]; };
@ -425,7 +463,7 @@ FilterString::FilterString()
_error = "Not initialized";
}
FilterString::FilterString(const QString &expr)
FilterString::FilterString(const QString &expr, const CardSearchLanguage &searchLanguage)
{
QByteArray ba = expr.simplified().toUtf8();
@ -438,6 +476,8 @@ FilterString::FilterString(const QString &expr)
return;
}
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));
});

View file

@ -14,6 +14,7 @@
#include <QString>
#include <functional>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/card_localization.h>
#include <utility>
inline Q_LOGGING_CATEGORY(FilterStringLog, "filter_string");
@ -35,7 +36,7 @@ class FilterString
{
public:
FilterString();
explicit FilterString(const QString &exp);
explicit FilterString(const QString &exp, const CardSearchLanguage &searchLanguage = {});
[[nodiscard]] bool check(const CardData &card) const
{
if (card.isNull()) {

View file

@ -100,14 +100,16 @@ FilterTreeNode *FilterItemList::termNode(const QString &term)
return childNodes.at(i);
}
bool FilterItemList::testTypeAnd(const CardInfoPtr info, CardFilter::Attr attr) const
bool FilterItemList::testTypeAnd(const CardInfoPtr info,
CardFilter::Attr attr,
const CardSearchLanguage &searchLanguage) const
{
for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) {
if (!(*i)->isEnabled()) {
continue;
}
if (!(*i)->acceptCardAttr(info, attr)) {
if (!(*i)->acceptCardAttr(info, attr, searchLanguage)) {
return false;
}
}
@ -115,13 +117,17 @@ bool FilterItemList::testTypeAnd(const CardInfoPtr info, CardFilter::Attr attr)
return true;
}
bool FilterItemList::testTypeAndNot(const CardInfoPtr info, CardFilter::Attr attr) const
bool FilterItemList::testTypeAndNot(const CardInfoPtr info,
CardFilter::Attr attr,
const CardSearchLanguage &searchLanguage) const
{
// if any one in the list is true, return false
return !testTypeOr(info, attr);
return !testTypeOr(info, attr, searchLanguage);
}
bool FilterItemList::testTypeOr(const CardInfoPtr info, CardFilter::Attr attr) const
bool FilterItemList::testTypeOr(const CardInfoPtr info,
CardFilter::Attr attr,
const CardSearchLanguage &searchLanguage) const
{
bool noChildEnabledChild = true;
@ -134,7 +140,7 @@ bool FilterItemList::testTypeOr(const CardInfoPtr info, CardFilter::Attr attr) c
noChildEnabledChild = false;
}
if ((*i)->acceptCardAttr(info, attr)) {
if ((*i)->acceptCardAttr(info, attr, searchLanguage)) {
return true;
}
}
@ -142,20 +148,58 @@ bool FilterItemList::testTypeOr(const CardInfoPtr info, CardFilter::Attr attr) c
return noChildEnabledChild;
}
bool FilterItemList::testTypeOrNot(const CardInfoPtr info, CardFilter::Attr attr) const
bool FilterItemList::testTypeOrNot(const CardInfoPtr info,
CardFilter::Attr attr,
const CardSearchLanguage &searchLanguage) const
{
// if any one in the list is false, return true
return !testTypeAnd(info, attr);
return !testTypeAnd(info, attr, searchLanguage);
}
bool FilterItem::acceptName(const CardInfoPtr info) const
bool FilterItem::acceptName(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const
{
return info->getName().contains(term, Qt::CaseInsensitive);
const QString &englishName = info->getName();
const QString &localizedName = info->getLocalizedName(searchLanguage.language);
switch (searchLanguage.mode) {
case SearchLanguageMode::English:
return englishName.contains(term, Qt::CaseInsensitive);
case SearchLanguageMode::Both:
if (englishName.contains(term, Qt::CaseInsensitive)) {
return true;
}
return !searchLanguage.isEnglishOnly() && localizedName.contains(term, Qt::CaseInsensitive);
case SearchLanguageMode::Selected:
if (searchLanguage.isEnglishOnly()) {
return englishName.contains(term, Qt::CaseInsensitive);
}
return localizedName.contains(term, Qt::CaseInsensitive);
}
return false;
}
bool FilterItem::acceptNameExact(const CardInfoPtr info) const
bool FilterItem::acceptNameExact(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const
{
return info->getName() == term;
const QString &englishName = info->getName();
const QString &localizedName = info->getLocalizedName(searchLanguage.language);
switch (searchLanguage.mode) {
case SearchLanguageMode::English:
return englishName == term;
case SearchLanguageMode::Both:
if (englishName == term) {
return true;
}
return !searchLanguage.isEnglishOnly() && localizedName == term;
case SearchLanguageMode::Selected:
if (searchLanguage.isEnglishOnly()) {
return englishName == term;
}
return localizedName == term;
}
return false;
}
bool FilterItem::acceptType(const CardInfoPtr info) const
@ -213,9 +257,27 @@ bool FilterItem::acceptColor(const CardInfoPtr info) const
return match_count == converted_term.length();
}
bool FilterItem::acceptText(const CardInfoPtr info) const
bool FilterItem::acceptText(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const
{
return info->getText().contains(term, Qt::CaseInsensitive);
const QString &englishText = info->getText();
const QString &localizedText = info->getLocalizedText(searchLanguage.language);
switch (searchLanguage.mode) {
case SearchLanguageMode::English:
return englishText.contains(term, Qt::CaseInsensitive);
case SearchLanguageMode::Both:
if (englishText.contains(term, Qt::CaseInsensitive)) {
return true;
}
return !searchLanguage.isEnglishOnly() && localizedText.contains(term, Qt::CaseInsensitive);
case SearchLanguageMode::Selected:
if (searchLanguage.isEnglishOnly()) {
return englishText.contains(term, Qt::CaseInsensitive);
}
return localizedText.contains(term, Qt::CaseInsensitive);
}
return false;
}
bool FilterItem::acceptSet(const CardInfoPtr info) const
@ -402,19 +464,21 @@ bool FilterItem::relationCheck(int cardInfo) const
return result;
}
bool FilterItem::acceptCardAttr(const CardInfoPtr info, CardFilter::Attr attr) const
bool FilterItem::acceptCardAttr(const CardInfoPtr info,
CardFilter::Attr attr,
const CardSearchLanguage &searchLanguage) const
{
switch (attr) {
case CardFilter::AttrName:
return acceptName(info);
return acceptName(info, searchLanguage);
case CardFilter::AttrNameExact:
return acceptNameExact(info);
return acceptNameExact(info, searchLanguage);
case CardFilter::AttrType:
return acceptType(info);
case CardFilter::AttrColor:
return acceptColor(info);
case CardFilter::AttrText:
return acceptText(info);
return acceptText(info, searchLanguage);
case CardFilter::AttrSet:
return acceptSet(info);
case CardFilter::AttrManaCost:
@ -484,18 +548,18 @@ FilterTreeNode *FilterTree::termNode(const CardFilter *f)
return termNode(f->attr(), f->type(), f->term());
}
bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm) const
bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm, const CardSearchLanguage &searchLanguage) const
{
const FilterItemList *fil;
bool status = true;
fil = lm->findTypeList(CardFilter::TypeAnd);
if (fil && fil->isEnabled() && !fil->testTypeAnd(info, lm->attr)) {
if (fil && fil->isEnabled() && !fil->testTypeAnd(info, lm->attr, searchLanguage)) {
return false;
}
fil = lm->findTypeList(CardFilter::TypeAndNot);
if (fil && fil->isEnabled() && !fil->testTypeAndNot(info, lm->attr)) {
if (fil && fil->isEnabled() && !fil->testTypeAndNot(info, lm->attr, searchLanguage)) {
return false;
}
@ -504,23 +568,23 @@ bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm) const
status = false;
// if this is true we can return because it is OR'd with the OrNot list
if (fil->testTypeOr(info, lm->attr)) {
if (fil->testTypeOr(info, lm->attr, searchLanguage)) {
return true;
}
}
fil = lm->findTypeList(CardFilter::TypeOrNot);
if (fil && fil->isEnabled() && fil->testTypeOrNot(info, lm->attr)) {
if (fil && fil->isEnabled() && fil->testTypeOrNot(info, lm->attr, searchLanguage)) {
return true;
}
return status;
}
bool FilterTree::acceptsCard(const CardInfoPtr info) const
bool FilterTree::acceptsCard(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const
{
for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) {
if ((*i)->isEnabled() && !testAttr(info, *i)) {
if ((*i)->isEnabled() && !testAttr(info, *i, searchLanguage)) {
return false;
}
}

View file

@ -11,6 +11,7 @@
#include <QList>
#include <QObject>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/card/database/card_database.h>
#include <utility>
@ -167,10 +168,14 @@ public:
return CardFilter::typeName(type);
}
[[nodiscard]] bool testTypeAnd(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool testTypeOr(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool
testTypeAnd(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool
testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool
testTypeOr(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool
testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const;
};
class FilterItem : public FilterTreeNode
@ -207,20 +212,21 @@ public:
return true;
}
[[nodiscard]] bool acceptName(CardInfoPtr info) const;
[[nodiscard]] bool acceptNameExact(CardInfoPtr info) const;
[[nodiscard]] bool acceptName(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool acceptNameExact(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool acceptType(CardInfoPtr info) const;
[[nodiscard]] bool acceptMainType(CardInfoPtr info) const;
[[nodiscard]] bool acceptSubType(CardInfoPtr info) const;
[[nodiscard]] bool acceptColor(CardInfoPtr info) const;
[[nodiscard]] bool acceptText(CardInfoPtr info) const;
[[nodiscard]] bool acceptText(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool acceptSet(CardInfoPtr info) const;
[[nodiscard]] bool acceptManaCost(CardInfoPtr info) const;
[[nodiscard]] bool acceptCmc(CardInfoPtr info) const;
[[nodiscard]] bool acceptPowerToughness(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool acceptLoyalty(CardInfoPtr info) const;
[[nodiscard]] bool acceptRarity(CardInfoPtr info) const;
[[nodiscard]] bool acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool
acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool acceptFormat(CardInfoPtr info) const;
[[nodiscard]] bool relationCheck(int cardInfo) const;
};
@ -240,7 +246,7 @@ private:
LogicMap *attrLogicMap(CardFilter::Attr attr);
FilterItemList *attrTypeList(CardFilter::Attr attr, CardFilter::Type type);
bool testAttr(CardInfoPtr info, const LogicMap *lm) const;
bool testAttr(CardInfoPtr info, const LogicMap *lm, const CardSearchLanguage &searchLanguage) const;
void nodeChanged() const override
{
@ -279,7 +285,7 @@ public:
return 0;
}
[[nodiscard]] bool acceptsCard(CardInfoPtr info) const;
[[nodiscard]] bool acceptsCard(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const;
void removeFiltersByAttr(CardFilter::Attr filterType);
void removeFilter(const CardFilter *toRemove);
void clear();