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 ¤tSet,
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