mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-24 02:13:02 -07:00
Support tab-separated deck imports (#4222)
Third-party deck tools (e.g. Delver Lens) export decks as tab-separated rows such as "2 Card Name Set Name". Card names never contain tab characters, so a leading numeric column marks a tabular record: the first two columns are read as the quantity and card name, and any further columns are discarded as printer/set metadata. - deck_list: parse tab-delimited rows before whitespace collapsing - tests: cover tab-separated mainboard and sideboard imports - docs: document the accepted plain-text import format (fills the TODO)
This commit is contained in:
parent
fb482f037e
commit
7d907e54d0
3 changed files with 60 additions and 7 deletions
|
|
@ -38,7 +38,18 @@ Selecting this action will open a new text editor dialog with the contents of yo
|
||||||
|
|
||||||
The import dialog expects each line to be a card with the following format:
|
The import dialog expects each line to be a card with the following format:
|
||||||
|
|
||||||
TODO
|
`[quantity] Card Name [options]`
|
||||||
|
|
||||||
|
- `quantity` is the number of copies and can be written as `2`, `2x` or `2X`. If omitted, a single copy is imported.
|
||||||
|
- `options` are appended after the card name and are understood on a best-effort basis:
|
||||||
|
- a set code and collector number, either as `Card Name (SET) 123` or `Card Name (SET) 123-1`
|
||||||
|
- a foil marker, `Card Name *F*`
|
||||||
|
- Lines can also carry a `SB:` prefix or a set code prefix (`[SET]`) — Cockatrice tries to stay compatible with the most
|
||||||
|
common deck formats.
|
||||||
|
|
||||||
|
Tab-separated exports from third-party apps (such as Delver Lens) are also accepted: any line whose first column is a
|
||||||
|
plain quantity treats the following columns as the card name and printer/set metadata (which is discarded), for example
|
||||||
|
`2\tCard Name\tSet Name`.
|
||||||
|
|
||||||
Each card should be on a separate line and there should be no empty lines between cards. The first empty line between
|
Each card should be on a separate line and there should be no empty lines between cards. The first empty line between
|
||||||
two blocks of cards will be considered as the divider between mainboard and sideboard.
|
two blocks of cards will be considered as the divider between mainboard and sideboard.
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,7 @@ bool DeckList::loadFromStream_Plain(QTextStream &in,
|
||||||
|
|
||||||
// Regex for advanced card parsing
|
// Regex for advanced card parsing
|
||||||
const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))");
|
const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))");
|
||||||
|
const QRegularExpression reTabQuantity(R"(^\d+[xX]?$)");
|
||||||
|
|
||||||
// Regex for extracting set code and collector number with attached symbols
|
// Regex for extracting set code and collector number with attached symbols
|
||||||
const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))");
|
const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))");
|
||||||
|
|
@ -337,8 +338,34 @@ bool DeckList::loadFromStream_Plain(QTextStream &in,
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString cardName = match.captured().simplified();
|
const QString rawLine = match.captured();
|
||||||
|
QString cardName = rawLine.simplified();
|
||||||
bool sideboard = false;
|
bool sideboard = false;
|
||||||
|
int amount = 1;
|
||||||
|
|
||||||
|
// Card names never contain tab characters, so a leading numeric field
|
||||||
|
// marks a tabular record emitted by third-party deck tools (e.g. Delver
|
||||||
|
// Lens "2\tCard Name\tSet Name"). The first two columns are the quantity
|
||||||
|
// and the card name; any further columns are printer/set metadata and
|
||||||
|
// are discarded.
|
||||||
|
bool hasTabQuantity = false;
|
||||||
|
const auto tabFields = rawLine.split('\t');
|
||||||
|
int firstField = 0;
|
||||||
|
while (firstField < tabFields.size() && tabFields.at(firstField).trimmed().isEmpty()) {
|
||||||
|
++firstField;
|
||||||
|
}
|
||||||
|
if (firstField + 1 < tabFields.size()) {
|
||||||
|
const QString quantity = tabFields.at(firstField).trimmed();
|
||||||
|
if (reTabQuantity.match(quantity).hasMatch()) {
|
||||||
|
hasTabQuantity = true;
|
||||||
|
QString digits = quantity;
|
||||||
|
if (digits.endsWith('x') || digits.endsWith('X')) {
|
||||||
|
digits.chop(1);
|
||||||
|
}
|
||||||
|
amount = digits.toInt();
|
||||||
|
cardName = tabFields.at(firstField + 1).simplified();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sideboard detection
|
// Sideboard detection
|
||||||
if (sBStart < 0) {
|
if (sBStart < 0) {
|
||||||
|
|
@ -383,11 +410,12 @@ bool DeckList::loadFromStream_Plain(QTextStream &in,
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if a specific amount is mentioned
|
// check if a specific amount is mentioned
|
||||||
int amount = 1;
|
if (!hasTabQuantity) {
|
||||||
match = reMultiplier.match(cardName);
|
match = reMultiplier.match(cardName);
|
||||||
if (match.hasMatch()) {
|
if (match.hasMatch()) {
|
||||||
amount = match.captured(1).toInt();
|
amount = match.captured(1).toInt();
|
||||||
cardName = match.captured(2);
|
cardName = match.captured(2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize the card name
|
// Normalize the card name
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,20 @@ TEST(LoadingFromClipboardTest, WeirdWhitespaceIsIgnored)
|
||||||
testDeck(clipboard, result);
|
testDeck(clipboard, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(LoadingFromClipboardTest, TabSeparatedImport)
|
||||||
|
{
|
||||||
|
QString clipboard("2\tMystic Snake\tStreets of New Capenna\n"
|
||||||
|
"3x\tCounterspell\tNPH\n"
|
||||||
|
"\t2\tSol Ring\tFMB\n"
|
||||||
|
"1\tForest\n"
|
||||||
|
"\n"
|
||||||
|
"2x\tDoom Blade\tM11\n");
|
||||||
|
|
||||||
|
Result result("", "", {{"Mystic Snake", 2}, {"Counterspell", 3}, {"Sol Ring", 2}, {"Forest", 1}},
|
||||||
|
{{"Doom Blade", 2}});
|
||||||
|
testDeck(clipboard, result);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(LoadingFromClipboardTest, RemoveBlankEntriesFromBeginningAndEnd)
|
TEST(LoadingFromClipboardTest, RemoveBlankEntriesFromBeginningAndEnd)
|
||||||
{
|
{
|
||||||
QString clipboard("\n"
|
QString clipboard("\n"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue