[VDS] Add ability to search by deck contents

This commit is contained in:
RickyRister 2025-05-14 16:26:47 -07:00
parent d51620640b
commit d3db83b73d
6 changed files with 229 additions and 12 deletions

View file

@ -190,6 +190,7 @@ set(cockatrice_SOURCES
src/game/cards/card_search_model.cpp src/game/cards/card_search_model.cpp
src/game/deckview/deck_view.cpp src/game/deckview/deck_view.cpp
src/game/deckview/deck_view_container.cpp src/game/deckview/deck_view_container.cpp
src/game/filters/deck_filter_string.cpp
src/game/filters/filter_builder.cpp src/game/filters/filter_builder.cpp
src/game/filters/filter_card.cpp src/game/filters/filter_card.cpp
src/game/filters/filter_string.cpp src/game/filters/filter_string.cpp

View file

@ -60,4 +60,5 @@
# pixel_map_generator = false # pixel_map_generator = false
# deck_filter_string = false
# filter_string = false # filter_string = false

View file

@ -1,5 +1,6 @@
#include "visual_deck_storage_search_widget.h" #include "visual_deck_storage_search_widget.h"
#include "../../../../game/filters/deck_filter_string.h"
#include "../../../../settings/cache_settings.h" #include "../../../../settings/cache_settings.h"
/** /**
@ -52,11 +53,11 @@ static QString getFileSearchName(const QString &filePath, bool includeFolderName
{ {
QString deckPath = SettingsCache::instance().getDeckPath(); QString deckPath = SettingsCache::instance().getDeckPath();
if (includeFolderName && filePath.startsWith(deckPath)) { if (includeFolderName && filePath.startsWith(deckPath)) {
return filePath.mid(deckPath.length()).toLower(); return filePath.mid(deckPath.length());
} }
QFileInfo fileInfo(filePath); QFileInfo fileInfo(filePath);
QString fileName = fileInfo.fileName().toLower(); QString fileName = fileInfo.fileName();
return fileName; return fileName;
} }
@ -64,14 +65,10 @@ void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> wid
const QString &searchText, const QString &searchText,
bool includeFolderName) bool includeFolderName)
{ {
if (searchText.isEmpty() || searchText.isNull()) { auto filterString = DeckFilterString(searchText);
for (auto widget : widgets) {
widget->filteredBySearch = false;
}
}
for (auto file : widgets) { for (auto widget : widgets) {
QString fileSearchName = getFileSearchName(file->filePath, includeFolderName); QString fileSearchName = getFileSearchName(widget->filePath, includeFolderName);
file->filteredBySearch = !fileSearchName.contains(searchText.toLower()); widget->filteredBySearch = !filterString.check(widget, {fileSearchName});
} }
} }

View file

@ -0,0 +1,167 @@
#include "deck_filter_string.h"
#include "../cards/card_database_manager.h"
#include "filter_string.h"
#include "lib/peglib.h"
static peg::parser search(R"(
Start <- QueryPartList
~ws <- [ ]+
QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart
SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
QueryPart <- NotQuery / DeckContentQuery / GenericQuery
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
DeckContentQuery <- CardSearch NumericExpression?
CardSearch <- '[[' CardFilterString ']]'
CardFilterString <- (!']]'.)*
GenericQuery <- String
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].
UnescapedStringListPart <- !['":<>=! ].
SingleApostropheString <- (UnescapedStringListPart+ ws*)* ['] (UnescapedStringListPart+ ws*)*
String <- SingleApostropheString / UnescapedStringListPart+ / ["] <NonDoubleQuoteUnlessEscaped*> ["] / ['] <NonSingleQuoteUnlessEscaped*> [']
NumericExpression <- NumericOperator ws? NumericValue
NumericOperator <- [=:] / <[><!][=]?>
NumericValue <- [0-9]+
)");
static std::once_flag init;
static void setupParserRules()
{
// plumbing
auto passthru = [](const peg::SemanticValues &sv) -> DeckFilter {
return !sv.empty() ? std::any_cast<DeckFilter>(sv[0]) : nullptr;
};
search["Start"] = passthru;
search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter {
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
auto matchesFilter = [&deck, &info](const std::any &query) {
return std::any_cast<DeckFilter>(query)(deck, info);
};
return std::all_of(sv.begin(), sv.end(), matchesFilter);
};
};
search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter {
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
auto matchesFilter = [&deck, &info](const std::any &query) {
return std::any_cast<DeckFilter>(query)(deck, info);
};
return std::any_of(sv.begin(), sv.end(), matchesFilter);
};
};
search["SomewhatComplexQueryPart"] = passthru;
search["QueryPart"] = passthru;
search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
const auto dependent = std::any_cast<DeckFilter>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) -> bool {
return !dependent(deck, info);
};
};
search["String"] = [](const peg::SemanticValues &sv) -> QString {
if (sv.choice() == 0) {
return QString::fromStdString(std::string(sv.sv()));
}
return QString::fromStdString(std::string(sv.token(0)));
};
search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher {
const auto arg = std::any_cast<int>(sv[1]);
const auto op = std::any_cast<QString>(sv[0]);
if (op == ">")
return [=](const int s) { return s > arg; };
if (op == ">=")
return [=](const int s) { return s >= arg; };
if (op == "<")
return [=](const int s) { return s < arg; };
if (op == "<=")
return [=](const int s) { return s <= arg; };
if (op == "=")
return [=](const int s) { return s == arg; };
if (op == ":")
return [=](const int s) { return s == arg; };
if (op == "!=")
return [=](const int s) { return s != arg; };
return [](int) { return false; };
};
search["NumericValue"] = [](const peg::SemanticValues &sv) -> int {
return QString::fromStdString(std::string(sv.sv())).toInt();
};
search["NumericOperator"] = [](const peg::SemanticValues &sv) -> QString {
return QString::fromStdString(std::string(sv.sv()));
};
// actual functionality
search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]));
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool {
int count = 0;
deck->deckLoader->forEachCard([&](InnerDecklistNode *, const DecklistCardNode *node) {
auto cardInfoPtr = CardDatabaseManager::getInstance()->getCard(node->getName());
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
count += node->getNumber();
}
});
return numberMatcher(count);
};
};
search["CardSearch"] = [](const peg::SemanticValues &sv) -> QString { return std::any_cast<QString>(sv[0]); };
search["CardFilterString"] = [](const peg::SemanticValues &sv) -> QString {
return QString::fromStdString(std::string(sv.sv()));
};
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) {
return info.fileSearchName.contains(name, Qt::CaseInsensitive);
};
};
}
DeckFilterString::DeckFilterString()
{
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
_error = "Not initialized";
}
DeckFilterString::DeckFilterString(const QString &expr)
{
QByteArray ba = expr.simplified().toUtf8();
std::call_once(init, setupParserRules);
_error = QString();
if (ba.isEmpty()) {
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return true; };
return;
}
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));
});
if (!search.parse(ba.data(), filter)) {
qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error);
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
}
}

View file

@ -0,0 +1,51 @@
#ifndef DECK_FILTER_STRING_H
#define DECK_FILTER_STRING_H
#include "../../client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h"
#include <QLoggingCategory>
#include <QMap>
#include <QString>
#include <functional>
#include <utility>
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
/**
* Extra info relevant to filtering that isn't present in the DeckPreviewWidget
*/
struct ExtraDeckSearchInfo
{
/**
* The filename used for filtering. Varies based on settings.
*/
QString fileSearchName;
};
typedef std::function<bool(const DeckPreviewWidget *, const ExtraDeckSearchInfo &)> DeckFilter;
class DeckFilterString
{
public:
DeckFilterString();
explicit DeckFilterString(const QString &expr);
bool check(const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) const
{
return filter(deck, info);
}
bool valid() const
{
return _error.isEmpty();
}
QString error()
{
return _error;
}
private:
QString _error;
DeckFilter filter;
};
#endif // DECK_FILTER_STRING_H

View file

@ -7,7 +7,7 @@
#include <QString> #include <QString>
#include <functional> #include <functional>
peg::parser search(R"( static peg::parser search(R"(
Start <- QueryPartList Start <- QueryPartList
~ws <- [ ]+ ~ws <- [ ]+
QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws* QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
@ -63,7 +63,7 @@ NumericOperator <- [=:] / <[><!][=]?>
NumericValue <- [0-9]+ NumericValue <- [0-9]+
)"); )");
std::once_flag init; static std::once_flag init;
static void setupParserRules() static void setupParserRules()
{ {