1 #include "oracleimporter.h"
2 
3 #include "carddbparser/cockatricexml4.h"
4 #include "qt-json/json.h"
5 
6 #include <QtWidgets>
7 #include <algorithm>
8 #include <climits>
9 
SplitCardPart(const QString & _name,const QString & _text,const QVariantHash & _properties,const CardInfoPerSet _setInfo)10 SplitCardPart::SplitCardPart(const QString &_name,
11                              const QString &_text,
12                              const QVariantHash &_properties,
13                              const CardInfoPerSet _setInfo)
14     : name(_name), text(_text), properties(_properties), setInfo(_setInfo)
15 {
16 }
17 
OracleImporter(const QString & _dataDir,QObject * parent)18 OracleImporter::OracleImporter(const QString &_dataDir, QObject *parent) : CardDatabase(parent), dataDir(_dataDir)
19 {
20 }
21 
readSetsFromByteArray(const QByteArray & data)22 bool OracleImporter::readSetsFromByteArray(const QByteArray &data)
23 {
24     QList<SetToDownload> newSetList;
25 
26     bool ok;
27     setsMap = QtJson::Json::parse(QString(data), ok).toMap().value("data").toMap();
28     if (!ok) {
29         qDebug() << "error: QtJson::Json::parse()";
30         return false;
31     }
32 
33     QListIterator<QVariant> it(setsMap.values());
34     QVariantMap map;
35 
36     QString shortName;
37     QString longName;
38     QList<QVariant> setCards;
39     QString setType;
40     QDate releaseDate;
41 
42     while (it.hasNext()) {
43         map = it.next().toMap();
44         shortName = map.value("code").toString().toUpper();
45         longName = map.value("name").toString();
46         setCards = map.value("cards").toList();
47         setType = map.value("type").toString();
48         // capitalize set type
49         if (setType.length() > 0) {
50             // basic grammar for words that aren't capitalized, like in "From the Vault"
51             const QStringList noCapitalize = {"the", "a", "an", "on", "to", "for", "of", "in", "and", "with", "or"};
52             QStringList words = setType.split("_");
53             setType.clear();
54             bool first = false;
55             for (auto &item : words) {
56                 if (first && noCapitalize.contains(item)) {
57                     setType += item + QString(" ");
58                 } else {
59                     setType += item[0].toUpper() + item.mid(1, -1) + QString(" ");
60                     first = true;
61                 }
62             }
63             setType = setType.trimmed();
64         }
65         if (!nonEnglishSets.contains(shortName)) {
66             releaseDate = map.value("releaseDate").toDate();
67         } else {
68             releaseDate = QDate();
69         }
70         newSetList.append(SetToDownload(shortName, longName, setCards, setType, releaseDate));
71     }
72 
73     std::sort(newSetList.begin(), newSetList.end());
74 
75     if (newSetList.isEmpty()) {
76         return false;
77     }
78     allSets = newSetList;
79     return true;
80 }
81 
getMainCardType(const QStringList & typeList)82 QString OracleImporter::getMainCardType(const QStringList &typeList)
83 {
84     if (typeList.isEmpty()) {
85         return {};
86     }
87 
88     for (const auto &type : mainCardTypes) {
89         if (typeList.contains(type)) {
90             return type;
91         }
92     }
93 
94     return typeList.first();
95 }
96 
addCard(QString name,QString text,bool isToken,QVariantHash properties,QList<CardRelation * > & relatedCards,CardInfoPerSet setInfo)97 CardInfoPtr OracleImporter::addCard(QString name,
98                                     QString text,
99                                     bool isToken,
100                                     QVariantHash properties,
101                                     QList<CardRelation *> &relatedCards,
102                                     CardInfoPerSet setInfo)
103 {
104     // Workaround for card name weirdness
105     name = name.replace("Æ", "AE");
106     name = name.replace("’", "'");
107     if (cards.contains(name)) {
108         CardInfoPtr card = cards.value(name);
109         card->addToSet(setInfo.getPtr(), setInfo);
110         return card;
111     }
112 
113     // Remove {} around mana costs, except if it's split cost
114     QString manacost = properties.value("manacost").toString();
115     if (!manacost.isEmpty()) {
116         QStringList symbols = manacost.split("}");
117         QString formattedCardCost;
118         for (QString symbol : symbols) {
119             if (symbol.contains(QRegExp("[0-9WUBGRP]/[0-9WUBGRP]"))) {
120                 symbol.append("}");
121             } else {
122                 symbol.remove(QChar('{'));
123             }
124             formattedCardCost.append(symbol);
125         }
126         properties.insert("manacost", formattedCardCost);
127     }
128 
129     // fix colors
130     QString allColors = properties.value("colors").toString();
131     if (allColors.size() > 1) {
132         sortAndReduceColors(allColors);
133         properties.insert("colors", allColors);
134     }
135     QString allColorIdent = properties.value("coloridentity").toString();
136     if (allColorIdent.size() > 1) {
137         sortAndReduceColors(allColorIdent);
138         properties.insert("coloridentity", allColorIdent);
139     }
140 
141     // DETECT CARD POSITIONING INFO
142 
143     // cards that enter the field tapped
144     bool cipt = text.contains("Hideaway") || text.contains(" it enters the battlefield tapped") ||
145                 (text.contains(name + " enters the battlefield tapped") &&
146                  !text.contains(name + " enters the battlefield tapped unless"));
147 
148     // table row
149     int tableRow = 1;
150     QString mainCardType = properties.value("maintype").toString();
151     if ((mainCardType == "Land"))
152         tableRow = 0;
153     else if ((mainCardType == "Sorcery") || (mainCardType == "Instant"))
154         tableRow = 3;
155     else if (mainCardType == "Creature")
156         tableRow = 2;
157 
158     // card side
159     QString side = properties.value("side").toString() == "b" ? "back" : "front";
160     properties.insert("side", side);
161 
162     // upsideDown (flip cards)
163     QString layout = properties.value("layout").toString();
164     bool upsideDown = layout == "flip" && side == "back";
165 
166     // insert the card and its properties
167     QList<CardRelation *> reverseRelatedCards;
168     CardInfoPerSetMap setsInfo;
169     setsInfo.insert(setInfo.getPtr()->getShortName(), setInfo);
170     CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, reverseRelatedCards,
171                                                 setsInfo, cipt, tableRow, upsideDown);
172 
173     if (name.isEmpty()) {
174         qDebug() << "warning: an empty card was added to set" << setInfo.getPtr()->getShortName();
175     }
176     cards.insert(name, newCard);
177 
178     return newCard;
179 }
180 
getStringPropertyFromMap(const QVariantMap & card,const QString & propertyName)181 QString OracleImporter::getStringPropertyFromMap(const QVariantMap &card, const QString &propertyName)
182 {
183     return card.contains(propertyName) ? card.value(propertyName).toString() : QString("");
184 }
185 
importCardsFromSet(const CardSetPtr & currentSet,const QList<QVariant> & cardsList,bool skipSpecialCards)186 int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet,
187                                        const QList<QVariant> &cardsList,
188                                        bool skipSpecialCards)
189 {
190     // mtgjson name => xml name
191     static const QMap<QString, QString> cardProperties{
192         {"manaCost", "manacost"}, {"convertedManaCost", "cmc"}, {"type", "type"},
193         {"loyalty", "loyalty"},   {"layout", "layout"},         {"side", "side"},
194     };
195 
196     // mtgjson name => xml name
197     static const QMap<QString, QString> setInfoProperties{{"number", "num"}, {"rarity", "rarity"}};
198 
199     // mtgjson name => xml name
200     static const QMap<QString, QString> identifierProperties{{"multiverseId", "muid"}, {"scryfallId", "uuid"}};
201 
202     int numCards = 0;
203     QMultiMap<QString, SplitCardPart> splitCards;
204     QString ptSeparator("/");
205     QVariantMap card;
206     QString layout, name, text, colors, colorIdentity, maintype, power, toughness, faceName;
207     static const bool isToken = false;
208     QVariantHash properties;
209     CardInfoPerSet setInfo;
210     QList<CardRelation *> relatedCards;
211     static const QList<QString> specialNumChars = {"★", "s", "†"};
212     QMap<QString, QVariant> specialPromoCards;
213     QList<QString> allNameProps;
214 
215     for (const QVariant &cardVar : cardsList) {
216         card = cardVar.toMap();
217 
218         // skip alternatives
219         if (getStringPropertyFromMap(card, "isAlternative") == "true") {
220             continue;
221         }
222 
223         /* Currently used layouts are:
224          * augment, double_faced_token, flip, host, leveler, meld, normal, planar,
225          * saga, scheme, split, token, transform, vanguard
226          */
227         layout = getStringPropertyFromMap(card, "layout");
228 
229         // don't import tokens from the json file
230         if (layout == "token") {
231             continue;
232         }
233 
234         // normal cards handling
235         name = getStringPropertyFromMap(card, "name");
236         text = getStringPropertyFromMap(card, "text");
237         faceName = getStringPropertyFromMap(card, "faceName");
238         if (faceName.isEmpty()) {
239             faceName = name;
240         }
241 
242         // card properties
243         properties.clear();
244         QMapIterator<QString, QString> it(cardProperties);
245         while (it.hasNext()) {
246             it.next();
247             QString mtgjsonProperty = it.key();
248             QString xmlPropertyName = it.value();
249             QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
250             if (!propertyValue.isEmpty())
251                 properties.insert(xmlPropertyName, propertyValue);
252         }
253 
254         // per-set properties
255         setInfo = CardInfoPerSet(currentSet);
256         QMapIterator<QString, QString> it2(setInfoProperties);
257         while (it2.hasNext()) {
258             it2.next();
259             QString mtgjsonProperty = it2.key();
260             QString xmlPropertyName = it2.value();
261             QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
262             if (!propertyValue.isEmpty())
263                 setInfo.setProperty(xmlPropertyName, propertyValue);
264         }
265 
266         // Identifiers
267         QMapIterator<QString, QString> it3(identifierProperties);
268         while (it3.hasNext()) {
269             it3.next();
270             auto mtgjsonProperty = it3.key();
271             auto xmlPropertyName = it3.value();
272             auto propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty);
273             if (!propertyValue.isEmpty()) {
274                 setInfo.setProperty(xmlPropertyName, propertyValue);
275             }
276         }
277 
278         QString numComponent{};
279         if (skipSpecialCards) {
280             QString numProperty = setInfo.getProperty("num");
281             // skip promo cards if it's not the only print, cards with two faces are different cards
282             if (allNameProps.contains(faceName)) {
283                 // check for alternative versions
284                 if (layout != "normal")
285                     continue;
286 
287                 // alternative versions have a letter in the end of num like abc
288                 // note this will also catch p and s, those will get removed later anyway
289                 QChar lastChar = numProperty.at(numProperty.size() - 1);
290                 if (!lastChar.isLetter())
291                     continue;
292 
293                 numComponent = " (" + QString(lastChar) + ")";
294                 faceName += numComponent; // add to facename to make it unique
295             }
296             if (getStringPropertyFromMap(card, "isPromo") == "true") {
297                 specialPromoCards.insert(faceName, cardVar);
298                 continue;
299             }
300             bool skip = false;
301             // skip cards containing special stuff in the collectors number like promo cards
302             for (const QString &specialChar : specialNumChars) {
303                 if (numProperty.contains(specialChar)) {
304                     skip = true;
305                     break;
306                 }
307             }
308             if (skip) {
309                 specialPromoCards.insert(faceName, cardVar);
310                 continue;
311             } else {
312                 allNameProps.append(faceName);
313             }
314         }
315 
316         // special handling properties
317         colors = card.value("colors").toStringList().join("");
318         if (!colors.isEmpty()) {
319             properties.insert("colors", colors);
320         }
321 
322         // special handling properties
323         colorIdentity = card.value("colorIdentity").toStringList().join("");
324         if (!colorIdentity.isEmpty()) {
325             properties.insert("coloridentity", colorIdentity);
326         }
327 
328         const auto &mainCardType = getMainCardType(card.value("types").toStringList());
329         if (mainCardType.isEmpty()) {
330             qDebug() << "warning: no mainCardType for card:" << name;
331         } else {
332             properties.insert("maintype", mainCardType);
333         }
334 
335         power = getStringPropertyFromMap(card, "power");
336         toughness = getStringPropertyFromMap(card, "toughness");
337         if (!(power.isEmpty() && toughness.isEmpty())) {
338             properties.insert("pt", power + ptSeparator + toughness);
339         }
340 
341         auto legalities = card.value("legalities").toMap();
342         for (const QString &fmtName : legalities.keys()) {
343             properties.insert(QString("format-%1").arg(fmtName), legalities.value(fmtName).toString().toLower());
344         }
345 
346         // split cards are considered a single card, enqueue for later merging
347         if (layout == "split" || layout == "aftermath" || layout == "adventure") {
348             auto faceName = getStringPropertyFromMap(card, "faceName");
349             SplitCardPart split(faceName, text, properties, setInfo);
350             splitCards.insert(name, split);
351         } else {
352             // relations
353             relatedCards.clear();
354 
355             // add other face for split cards as card relation
356             if (!getStringPropertyFromMap(card, "side").isEmpty()) {
357                 properties["cmc"] = getStringPropertyFromMap(card, "faceConvertedManaCost");
358                 if (layout == "meld") { // meld cards don't work
359                     QRegularExpression meldNameRegex{"then meld them into ([\\.]*)"};
360                     QString additionalName = meldNameRegex.match(text).captured(1);
361                     if (!additionalName.isNull()) {
362                         relatedCards.append(new CardRelation(additionalName, true));
363                     }
364                 } else {
365                     for (const QString &additionalName : name.split(" // ")) {
366                         if (additionalName != faceName) {
367                             relatedCards.append(new CardRelation(additionalName, true));
368                         }
369                     }
370                 }
371                 name = faceName;
372             }
373 
374             CardInfoPtr newCard = addCard(name + numComponent, text, isToken, properties, relatedCards, setInfo);
375             numCards++;
376         }
377     }
378 
379     // split cards handling
380     QString splitCardPropSeparator = QString(" // ");
381     QString splitCardTextSeparator = QString("\n\n---\n\n");
382     for (const QString &nameSplit : splitCards.uniqueKeys()) {
383         // get all parts for this specific card
384         QList<SplitCardPart> splitCardParts = splitCards.values(nameSplit);
385         // sort them by face name
386         std::sort(splitCardParts.begin(), splitCardParts.end(),
387                   [](const SplitCardPart &a, const SplitCardPart &b) -> bool { return a.getName() < b.getName(); });
388 
389         text = QString("");
390         properties.clear();
391         relatedCards.clear();
392 
393         QString lastName{};
394         for (const SplitCardPart &tmp : splitCardParts) {
395             // some sets have 2 different variations of the same split card,
396             // eg. Fire // Ice in WC02. Avoid adding duplicates.
397             if (lastName == tmp.getName())
398                 continue;
399 
400             lastName = tmp.getName();
401 
402             if (!text.isEmpty())
403                 text.append(splitCardTextSeparator);
404             text.append(tmp.getText());
405 
406             if (properties.isEmpty()) {
407                 properties = tmp.getProperties();
408                 setInfo = tmp.getSetInfo();
409             } else {
410                 const QVariantHash &props = tmp.getProperties();
411                 for (const QString &prop : props.keys()) {
412                     QString originalPropertyValue = properties.value(prop).toString();
413                     QString thisCardPropertyValue = props.value(prop).toString();
414                     if (originalPropertyValue != thisCardPropertyValue) {
415                         if (prop == "colors") {
416                             properties.insert(prop, originalPropertyValue + thisCardPropertyValue);
417                         } else if (prop == "maintype") { // don't create maintypes with //es in them
418                             properties.insert(prop, originalPropertyValue);
419                         } else {
420                             properties.insert(prop,
421                                               originalPropertyValue + splitCardPropSeparator + thisCardPropertyValue);
422                         }
423                     }
424                 }
425             }
426         }
427         CardInfoPtr newCard = addCard(nameSplit, text, isToken, properties, relatedCards, setInfo);
428         numCards++;
429     }
430 
431     // only add the unique promo cards that didn't already exist in the set
432     if (skipSpecialCards) {
433         QList<QVariant> nonDuplicatePromos;
434         for (auto cardIter = specialPromoCards.constBegin(); cardIter != specialPromoCards.constEnd(); ++cardIter) {
435             if (!allNameProps.contains(cardIter.key())) {
436                 nonDuplicatePromos.append(cardIter.value());
437             }
438         }
439         if (!nonDuplicatePromos.isEmpty()) {
440             numCards += importCardsFromSet(currentSet, nonDuplicatePromos, false);
441         }
442     }
443     return numCards;
444 }
445 
sortAndReduceColors(QString & colors)446 void OracleImporter::sortAndReduceColors(QString &colors)
447 {
448     // sort
449     const QHash<QChar, unsigned int> colorOrder{{'W', 0}, {'U', 1}, {'B', 2}, {'R', 3}, {'G', 4}};
450     std::sort(colors.begin(), colors.end(), [&colorOrder](const QChar a, const QChar b) {
451         return colorOrder.value(a, INT_MAX) < colorOrder.value(b, INT_MAX);
452     });
453     // reduce
454     QChar lastChar = '\0';
455     for (int i = 0; i < colors.size(); ++i) {
456         if (colors.at(i) == lastChar)
457             colors.remove(i, 1);
458         else
459             lastChar = colors.at(i);
460     }
461 }
462 
startImport()463 int OracleImporter::startImport()
464 {
465     int setCards = 0, setIndex = 0;
466     // add an empty set for tokens
467     CardSetPtr tokenSet = CardSet::newInstance(TOKENS_SETNAME, tr("Dummy set containing tokens"), "Tokens");
468     sets.insert(TOKENS_SETNAME, tokenSet);
469 
470     for (const SetToDownload &curSetToParse : allSets) {
471         CardSetPtr newSet = CardSet::newInstance(curSetToParse.getShortName(), curSetToParse.getLongName(),
472                                                  curSetToParse.getSetType(), curSetToParse.getReleaseDate());
473         if (!sets.contains(newSet->getShortName()))
474             sets.insert(newSet->getShortName(), newSet);
475 
476         int numCardsInSet = importCardsFromSet(newSet, curSetToParse.getCards());
477 
478         ++setIndex;
479 
480         emit setIndexChanged(numCardsInSet, setIndex, curSetToParse.getLongName());
481     }
482 
483     emit setIndexChanged(setCards, setIndex, QString());
484 
485     // total number of sets
486     return setIndex;
487 }
488 
saveToFile(const QString & fileName,const QString & sourceUrl,const QString & sourceVersion)489 bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion)
490 {
491     CockatriceXml4Parser parser;
492     return parser.saveToFile(sets, cards, fileName, sourceUrl, sourceVersion);
493 }
494 
clear()495 void OracleImporter::clear()
496 {
497     CardDatabase::clear();
498     allSets.clear();
499 }
500