* don't sort split card halves alphabetically fixes #4241 introduces new issue: aftermath cards are now switched upside down * use list instead of multimap to enforce preservation of a given order
507 lines
19 KiB
C++
507 lines
19 KiB
C++
#include "oracleimporter.h"
|
||
|
||
#include "carddbparser/cockatricexml4.h"
|
||
#include "qt-json/json.h"
|
||
|
||
#include <QtWidgets>
|
||
#include <algorithm>
|
||
#include <climits>
|
||
|
||
SplitCardPart::SplitCardPart(const QString &_name,
|
||
const QString &_text,
|
||
const QVariantHash &_properties,
|
||
const CardInfoPerSet _setInfo)
|
||
: name(_name), text(_text), properties(_properties), setInfo(_setInfo)
|
||
{
|
||
}
|
||
|
||
OracleImporter::OracleImporter(const QString &_dataDir, QObject *parent) : CardDatabase(parent), dataDir(_dataDir)
|
||
{
|
||
}
|
||
|
||
bool OracleImporter::readSetsFromByteArray(const QByteArray &data)
|
||
{
|
||
QList<SetToDownload> newSetList;
|
||
|
||
bool ok;
|
||
setsMap = QtJson::Json::parse(QString(data), ok).toMap().value("data").toMap();
|
||
if (!ok) {
|
||
qDebug() << "error: QtJson::Json::parse()";
|
||
return false;
|
||
}
|
||
|
||
QListIterator<QVariant> it(setsMap.values());
|
||
QVariantMap map;
|
||
|
||
QString shortName;
|
||
QString longName;
|
||
QList<QVariant> setCards;
|
||
QString setType;
|
||
QDate releaseDate;
|
||
|
||
while (it.hasNext()) {
|
||
map = it.next().toMap();
|
||
shortName = map.value("code").toString().toUpper();
|
||
longName = map.value("name").toString();
|
||
setCards = map.value("cards").toList();
|
||
setType = map.value("type").toString();
|
||
// capitalize set type
|
||
if (setType.length() > 0) {
|
||
// basic grammar for words that aren't capitalized, like in "From the Vault"
|
||
const QStringList noCapitalize = {"the", "a", "an", "on", "to", "for", "of", "in", "and", "with", "or"};
|
||
QStringList words = setType.split("_");
|
||
setType.clear();
|
||
bool first = false;
|
||
for (auto &item : words) {
|
||
if (first && noCapitalize.contains(item)) {
|
||
setType += item + QString(" ");
|
||
} else {
|
||
setType += item[0].toUpper() + item.mid(1, -1) + QString(" ");
|
||
first = true;
|
||
}
|
||
}
|
||
setType = setType.trimmed();
|
||
}
|
||
if (!nonEnglishSets.contains(shortName)) {
|
||
releaseDate = map.value("releaseDate").toDate();
|
||
} else {
|
||
releaseDate = QDate();
|
||
}
|
||
newSetList.append(SetToDownload(shortName, longName, setCards, setType, releaseDate));
|
||
}
|
||
|
||
std::sort(newSetList.begin(), newSetList.end());
|
||
|
||
if (newSetList.isEmpty()) {
|
||
return false;
|
||
}
|
||
allSets = newSetList;
|
||
return true;
|
||
}
|
||
|
||
QString OracleImporter::getMainCardType(const QStringList &typeList)
|
||
{
|
||
if (typeList.isEmpty()) {
|
||
return {};
|
||
}
|
||
|
||
for (const auto &type : mainCardTypes) {
|
||
if (typeList.contains(type)) {
|
||
return type;
|
||
}
|
||
}
|
||
|
||
return typeList.first();
|
||
}
|
||
|
||
CardInfoPtr OracleImporter::addCard(QString name,
|
||
QString text,
|
||
bool isToken,
|
||
QVariantHash properties,
|
||
QList<CardRelation *> &relatedCards,
|
||
CardInfoPerSet setInfo)
|
||
{
|
||
// Workaround for card name weirdness
|
||
name = name.replace("Æ", "AE");
|
||
name = name.replace("’", "'");
|
||
if (cards.contains(name)) {
|
||
CardInfoPtr card = cards.value(name);
|
||
card->addToSet(setInfo.getPtr(), setInfo);
|
||
return card;
|
||
}
|
||
|
||
// Remove {} around mana costs, except if it's split cost
|
||
QString manacost = properties.value("manacost").toString();
|
||
if (!manacost.isEmpty()) {
|
||
QStringList symbols = manacost.split("}");
|
||
QString formattedCardCost;
|
||
for (QString symbol : symbols) {
|
||
if (symbol.contains(QRegExp("[0-9WUBGRP]/[0-9WUBGRP]"))) {
|
||
symbol.append("}");
|
||
} else {
|
||
symbol.remove(QChar('{'));
|
||
}
|
||
formattedCardCost.append(symbol);
|
||
}
|
||
properties.insert("manacost", formattedCardCost);
|
||
}
|
||
|
||
// fix colors
|
||
QString allColors = properties.value("colors").toString();
|
||
if (allColors.size() > 1) {
|
||
sortAndReduceColors(allColors);
|
||
properties.insert("colors", allColors);
|
||
}
|
||
QString allColorIdent = properties.value("coloridentity").toString();
|
||
if (allColorIdent.size() > 1) {
|
||
sortAndReduceColors(allColorIdent);
|
||
properties.insert("coloridentity", allColorIdent);
|
||
}
|
||
|
||
// DETECT CARD POSITIONING INFO
|
||
|
||
// cards that enter the field tapped
|
||
bool cipt = text.contains("Hideaway") || text.contains(" it enters the battlefield tapped") ||
|
||
(text.contains(name + " enters the battlefield tapped") &&
|
||
!text.contains(name + " enters the battlefield tapped unless"));
|
||
|
||
// table row
|
||
int tableRow = 1;
|
||
QString mainCardType = properties.value("maintype").toString();
|
||
if ((mainCardType == "Land"))
|
||
tableRow = 0;
|
||
else if ((mainCardType == "Sorcery") || (mainCardType == "Instant"))
|
||
tableRow = 3;
|
||
else if (mainCardType == "Creature")
|
||
tableRow = 2;
|
||
|
||
// card side
|
||
QString side = properties.value("side").toString() == "b" ? "back" : "front";
|
||
properties.insert("side", side);
|
||
|
||
// upsideDown (flip cards)
|
||
QString layout = properties.value("layout").toString();
|
||
bool upsideDown = layout == "flip" && side == "back";
|
||
|
||
// insert the card and its properties
|
||
QList<CardRelation *> reverseRelatedCards;
|
||
CardInfoPerSetMap setsInfo;
|
||
setsInfo.insert(setInfo.getPtr()->getShortName(), setInfo);
|
||
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, reverseRelatedCards,
|
||
setsInfo, cipt, tableRow, upsideDown);
|
||
|
||
if (name.isEmpty()) {
|
||
qDebug() << "warning: an empty card was added to set" << setInfo.getPtr()->getShortName();
|
||
}
|
||
cards.insert(name, newCard);
|
||
|
||
return newCard;
|
||
}
|
||
|
||
QString OracleImporter::getStringPropertyFromMap(const QVariantMap &card, const QString &propertyName)
|
||
{
|
||
return card.contains(propertyName) ? card.value(propertyName).toString() : QString("");
|
||
}
|
||
|
||
int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet,
|
||
const QList<QVariant> &cardsList,
|
||
bool skipSpecialCards)
|
||
{
|
||
// mtgjson name => xml name
|
||
static const QMap<QString, QString> cardProperties{
|
||
{"manaCost", "manacost"}, {"convertedManaCost", "cmc"}, {"type", "type"},
|
||
{"loyalty", "loyalty"}, {"layout", "layout"}, {"side", "side"},
|
||
};
|
||
|
||
// mtgjson name => xml name
|
||
static const QMap<QString, QString> setInfoProperties{{"number", "num"}, {"rarity", "rarity"}};
|
||
|
||
// mtgjson name => xml name
|
||
static const QMap<QString, QString> identifierProperties{{"multiverseId", "muid"}, {"scryfallId", "uuid"}};
|
||
|
||
int numCards = 0;
|
||
QMap<QString, QList<SplitCardPart>> splitCards;
|
||
QString ptSeparator("/");
|
||
QVariantMap card;
|
||
QString layout, name, text, colors, colorIdentity, maintype, power, toughness, faceName;
|
||
static const bool isToken = false;
|
||
QVariantHash properties;
|
||
CardInfoPerSet setInfo;
|
||
QList<CardRelation *> relatedCards;
|
||
static const QList<QString> specialNumChars = {"★", "s", "†"};
|
||
QMap<QString, QVariant> specialPromoCards;
|
||
QList<QString> allNameProps;
|
||
|
||
for (const QVariant &cardVar : cardsList) {
|
||
card = cardVar.toMap();
|
||
|
||
// skip alternatives
|
||
if (getStringPropertyFromMap(card, "isAlternative") == "true") {
|
||
continue;
|
||
}
|
||
|
||
/* Currently used layouts are:
|
||
* augment, double_faced_token, flip, host, leveler, meld, normal, planar,
|
||
* saga, scheme, split, token, transform, vanguard
|
||
*/
|
||
layout = getStringPropertyFromMap(card, "layout");
|
||
|
||
// don't import tokens from the json file
|
||
if (layout == "token") {
|
||
continue;
|
||
}
|
||
|
||
// normal cards handling
|
||
name = getStringPropertyFromMap(card, "name");
|
||
text = getStringPropertyFromMap(card, "text");
|
||
faceName = getStringPropertyFromMap(card, "faceName");
|
||
if (faceName.isEmpty()) {
|
||
faceName = name;
|
||
}
|
||
|
||
// card properties
|
||
properties.clear();
|
||
QMapIterator<QString, QString> it(cardProperties);
|
||
while (it.hasNext()) {
|
||
it.next();
|
||
QString mtgjsonProperty = it.key();
|
||
QString xmlPropertyName = it.value();
|
||
QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
|
||
if (!propertyValue.isEmpty())
|
||
properties.insert(xmlPropertyName, propertyValue);
|
||
}
|
||
|
||
// per-set properties
|
||
setInfo = CardInfoPerSet(currentSet);
|
||
QMapIterator<QString, QString> it2(setInfoProperties);
|
||
while (it2.hasNext()) {
|
||
it2.next();
|
||
QString mtgjsonProperty = it2.key();
|
||
QString xmlPropertyName = it2.value();
|
||
QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
|
||
if (!propertyValue.isEmpty())
|
||
setInfo.setProperty(xmlPropertyName, propertyValue);
|
||
}
|
||
|
||
// Identifiers
|
||
QMapIterator<QString, QString> it3(identifierProperties);
|
||
while (it3.hasNext()) {
|
||
it3.next();
|
||
auto mtgjsonProperty = it3.key();
|
||
auto xmlPropertyName = it3.value();
|
||
auto propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty);
|
||
if (!propertyValue.isEmpty()) {
|
||
setInfo.setProperty(xmlPropertyName, propertyValue);
|
||
}
|
||
}
|
||
|
||
QString numComponent{};
|
||
if (skipSpecialCards) {
|
||
QString numProperty = setInfo.getProperty("num");
|
||
// skip promo cards if it's not the only print, cards with two faces are different cards
|
||
if (allNameProps.contains(faceName)) {
|
||
// check for alternative versions
|
||
if (layout != "normal")
|
||
continue;
|
||
|
||
// alternative versions have a letter in the end of num like abc
|
||
// note this will also catch p and s, those will get removed later anyway
|
||
QChar lastChar = numProperty.at(numProperty.size() - 1);
|
||
if (!lastChar.isLetter())
|
||
continue;
|
||
|
||
numComponent = " (" + QString(lastChar) + ")";
|
||
faceName += numComponent; // add to facename to make it unique
|
||
}
|
||
if (getStringPropertyFromMap(card, "isPromo") == "true") {
|
||
specialPromoCards.insert(faceName, cardVar);
|
||
continue;
|
||
}
|
||
bool skip = false;
|
||
// skip cards containing special stuff in the collectors number like promo cards
|
||
for (const QString &specialChar : specialNumChars) {
|
||
if (numProperty.contains(specialChar)) {
|
||
skip = true;
|
||
break;
|
||
}
|
||
}
|
||
if (skip) {
|
||
specialPromoCards.insert(faceName, cardVar);
|
||
continue;
|
||
} else {
|
||
allNameProps.append(faceName);
|
||
}
|
||
}
|
||
|
||
// special handling properties
|
||
colors = card.value("colors").toStringList().join("");
|
||
if (!colors.isEmpty()) {
|
||
properties.insert("colors", colors);
|
||
}
|
||
|
||
// special handling properties
|
||
colorIdentity = card.value("colorIdentity").toStringList().join("");
|
||
if (!colorIdentity.isEmpty()) {
|
||
properties.insert("coloridentity", colorIdentity);
|
||
}
|
||
|
||
const auto &mainCardType = getMainCardType(card.value("types").toStringList());
|
||
if (mainCardType.isEmpty()) {
|
||
qDebug() << "warning: no mainCardType for card:" << name;
|
||
} else {
|
||
properties.insert("maintype", mainCardType);
|
||
}
|
||
|
||
power = getStringPropertyFromMap(card, "power");
|
||
toughness = getStringPropertyFromMap(card, "toughness");
|
||
if (!(power.isEmpty() && toughness.isEmpty())) {
|
||
properties.insert("pt", power + ptSeparator + toughness);
|
||
}
|
||
|
||
auto legalities = card.value("legalities").toMap();
|
||
for (const QString &fmtName : legalities.keys()) {
|
||
properties.insert(QString("format-%1").arg(fmtName), legalities.value(fmtName).toString().toLower());
|
||
}
|
||
|
||
// split cards are considered a single card, enqueue for later merging
|
||
if (layout == "split" || layout == "aftermath" || layout == "adventure") {
|
||
auto faceName = getStringPropertyFromMap(card, "faceName");
|
||
SplitCardPart split(faceName, text, properties, setInfo);
|
||
auto found_iter = splitCards.find(name);
|
||
if (found_iter == splitCards.end()) {
|
||
splitCards.insert(name, {split});
|
||
} else if (layout == "adventure") {
|
||
found_iter->insert(0, split);
|
||
} else {
|
||
found_iter->append(split);
|
||
}
|
||
} else {
|
||
// relations
|
||
relatedCards.clear();
|
||
|
||
// add other face for split cards as card relation
|
||
if (!getStringPropertyFromMap(card, "side").isEmpty()) {
|
||
properties["cmc"] = getStringPropertyFromMap(card, "faceConvertedManaCost");
|
||
if (layout == "meld") { // meld cards don't work
|
||
static const QRegularExpression meldNameRegex{"then meld them into ([^\\.]*)"};
|
||
QString additionalName = meldNameRegex.match(text).captured(1);
|
||
if (!additionalName.isNull()) {
|
||
relatedCards.append(new CardRelation(additionalName, true));
|
||
}
|
||
} else {
|
||
for (const QString &additionalName : name.split(" // ")) {
|
||
if (additionalName != faceName) {
|
||
relatedCards.append(new CardRelation(additionalName, true));
|
||
}
|
||
}
|
||
}
|
||
name = faceName;
|
||
}
|
||
|
||
CardInfoPtr newCard = addCard(name + numComponent, text, isToken, properties, relatedCards, setInfo);
|
||
numCards++;
|
||
}
|
||
}
|
||
|
||
// split cards handling
|
||
static const QString splitCardPropSeparator = QString(" // ");
|
||
static const QString splitCardTextSeparator = QString("\n\n---\n\n");
|
||
for (const QString &nameSplit : splitCards.keys()) {
|
||
// get all parts for this specific card
|
||
QList<SplitCardPart> splitCardParts = splitCards.value(nameSplit);
|
||
QSet<QString> done{};
|
||
|
||
text.clear();
|
||
properties.clear();
|
||
relatedCards.clear();
|
||
|
||
for (const SplitCardPart &tmp : splitCardParts) {
|
||
// some sets have 2 different variations of the same split card,
|
||
// eg. Fire // Ice in WC02. Avoid adding duplicates.
|
||
QString splitName = tmp.getName();
|
||
if (done.contains(splitName)) {
|
||
continue;
|
||
}
|
||
done.insert(splitName);
|
||
|
||
if (!text.isEmpty()) {
|
||
text.append(splitCardTextSeparator);
|
||
}
|
||
text.append(tmp.getText());
|
||
|
||
if (properties.isEmpty()) {
|
||
properties = tmp.getProperties();
|
||
setInfo = tmp.getSetInfo();
|
||
} else {
|
||
const QVariantHash &tmpProps = tmp.getProperties();
|
||
for (const QString &prop : tmpProps.keys()) {
|
||
QString originalPropertyValue = properties.value(prop).toString();
|
||
QString thisCardPropertyValue = tmpProps.value(prop).toString();
|
||
if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) {
|
||
if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty
|
||
properties.insert(prop, thisCardPropertyValue);
|
||
} else if (prop == "colors") { // the card is both colors
|
||
properties.insert(prop, originalPropertyValue + thisCardPropertyValue);
|
||
} else if (prop == "maintype") { // don't create maintypes with //es in them
|
||
continue;
|
||
} else {
|
||
properties.insert(prop,
|
||
originalPropertyValue + splitCardPropSeparator + thisCardPropertyValue);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
CardInfoPtr newCard = addCard(nameSplit, text, isToken, properties, relatedCards, setInfo);
|
||
numCards++;
|
||
}
|
||
|
||
// only add the unique promo cards that didn't already exist in the set
|
||
if (skipSpecialCards) {
|
||
QList<QVariant> nonDuplicatePromos;
|
||
for (auto cardIter = specialPromoCards.constBegin(); cardIter != specialPromoCards.constEnd(); ++cardIter) {
|
||
if (!allNameProps.contains(cardIter.key())) {
|
||
nonDuplicatePromos.append(cardIter.value());
|
||
}
|
||
}
|
||
if (!nonDuplicatePromos.isEmpty()) {
|
||
numCards += importCardsFromSet(currentSet, nonDuplicatePromos, false);
|
||
}
|
||
}
|
||
return numCards;
|
||
}
|
||
|
||
void OracleImporter::sortAndReduceColors(QString &colors)
|
||
{
|
||
// sort
|
||
const QHash<QChar, unsigned int> colorOrder{{'W', 0}, {'U', 1}, {'B', 2}, {'R', 3}, {'G', 4}};
|
||
std::sort(colors.begin(), colors.end(), [&colorOrder](const QChar a, const QChar b) {
|
||
return colorOrder.value(a, INT_MAX) < colorOrder.value(b, INT_MAX);
|
||
});
|
||
// reduce
|
||
QChar lastChar = '\0';
|
||
for (int i = 0; i < colors.size(); ++i) {
|
||
if (colors.at(i) == lastChar)
|
||
colors.remove(i, 1);
|
||
else
|
||
lastChar = colors.at(i);
|
||
}
|
||
}
|
||
|
||
int OracleImporter::startImport()
|
||
{
|
||
int setCards = 0, setIndex = 0;
|
||
// add an empty set for tokens
|
||
CardSetPtr tokenSet = CardSet::newInstance(TOKENS_SETNAME, tr("Dummy set containing tokens"), "Tokens");
|
||
sets.insert(TOKENS_SETNAME, tokenSet);
|
||
|
||
for (const SetToDownload &curSetToParse : allSets) {
|
||
CardSetPtr newSet = CardSet::newInstance(curSetToParse.getShortName(), curSetToParse.getLongName(),
|
||
curSetToParse.getSetType(), curSetToParse.getReleaseDate());
|
||
if (!sets.contains(newSet->getShortName()))
|
||
sets.insert(newSet->getShortName(), newSet);
|
||
|
||
int numCardsInSet = importCardsFromSet(newSet, curSetToParse.getCards());
|
||
|
||
++setIndex;
|
||
|
||
emit setIndexChanged(numCardsInSet, setIndex, curSetToParse.getLongName());
|
||
}
|
||
|
||
emit setIndexChanged(setCards, setIndex, QString());
|
||
|
||
// total number of sets
|
||
return setIndex;
|
||
}
|
||
|
||
bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion)
|
||
{
|
||
CockatriceXml4Parser parser;
|
||
return parser.saveToFile(sets, cards, fileName, sourceUrl, sourceVersion);
|
||
}
|
||
|
||
void OracleImporter::clear()
|
||
{
|
||
CardDatabase::clear();
|
||
allSets.clear();
|
||
}
|