#ifndef ORACLEIMPORTER_H #define ORACLEIMPORTER_H #include "raw_json_scanner.h" #include #include #include #include #include #include #include #include #include // many users prefer not to see these sets with non english arts // they will given priority PriorityLowest const QStringList nonEnglishSets = {"4BB", "FBB", "PS11", "PSAL", "REN", "RIN"}; const QMap setTypePriorities{ {"core", CardSet::PriorityPrimary}, {"expansion", CardSet::PriorityPrimary}, {"commander", CardSet::PrioritySecondary}, {"starter", CardSet::PrioritySecondary}, {"draft_innovation", CardSet::PrioritySecondary}, {"duel_deck", CardSet::PrioritySecondary}, {"archenemy", CardSet::PriorityReprint}, {"arsenal", CardSet::PriorityReprint}, {"box", CardSet::PriorityReprint}, {"eternal", CardSet::PriorityReprint}, {"from_the_vault", CardSet::PriorityReprint}, {"masterpiece", CardSet::PriorityReprint}, {"masters", CardSet::PriorityReprint}, {"memorabilia", CardSet::PriorityReprint}, {"planechase", CardSet::PriorityReprint}, {"premium_deck", CardSet::PriorityReprint}, {"promo", CardSet::PriorityReprint}, {"spellbook", CardSet::PriorityReprint}, {"token", CardSet::PriorityReprint}, {"treasure_chest", CardSet::PriorityReprint}, {"alchemy", CardSet::PriorityOther}, {"funny", CardSet::PriorityOther}, {"minigame", CardSet::PriorityOther}, {"vanguard", CardSet::PriorityOther}, }; class SetToDownload { private: QString shortName, longName; QDate releaseDate; QString setType; CardSet::Priority priority; // Byte range of this set's object within the importer's raw JSON text. Parsing // one set at a time keeps peak memory low instead of holding the whole document. RawJson::SetDataRange rawRange; public: const QString &getShortName() const { return shortName; } const QString &getLongName() const { return longName; } const QString &getSetType() const { return setType; } const QDate &getReleaseDate() const { return releaseDate; } CardSet::Priority getPriority() const { return priority; } const RawJson::SetDataRange &getRawRange() const { return rawRange; } SetToDownload(QString _shortName, QString _longName, CardSet::Priority _priority, QString _setType = QString(), const QDate &_releaseDate = QDate()) : shortName(std::move(_shortName)), longName(std::move(_longName)), releaseDate(_releaseDate), setType(std::move(_setType)), priority(_priority) { } void setRawRange(const RawJson::SetDataRange &_rawRange) { rawRange = _rawRange; } bool operator<(const SetToDownload &set) const { return longName.compare(set.longName, Qt::CaseInsensitive) < 0; } }; class SplitCardPart { public: SplitCardPart(const QString &_name, const QString &_text, const QHash &_properties, const PrintingInfo &_printingInfo, const QString &_localizedText = QString()); inline const QString &getName() const { return name; } inline const QString &getText() const { return text; } /** * @brief The cardLang rules text of this face's foreignData entry, if any. */ inline const QString &getLocalizedText() const { return localizedText; } inline const QHash &getProperties() const { return properties; } inline const PrintingInfo &getPrintingInfo() const { return printingInfo; } private: QString name; QString text; QString localizedText; QHash properties; PrintingInfo printingInfo; }; struct LocalizedCardEntry { QString name; QString text; CardSet::Priority priority = CardSet::PriorityLowest; }; class OracleImporter : public QObject { Q_OBJECT private: static const QRegularExpression formatRegex; /** * The cards, indexed by name. */ CardNameMap cards; /** * The sets, indexed by short name. */ SetNameMap sets; QList allSets; /** * The raw JSON text of the source document, retained for lazy per-set * parsing during startImport(). Frees the card data as each set is imported. */ QByteArray rawSetsData; /** * Whether readSetsFromByteArray() should report scan progress via * dataReadProgress. A background run routes that signal to stdout (for the * hosting Cockatrice client to parse); the flag exists to skip the scanner * instrumentation entirely when no consumer needs it. */ bool progressReporting = true; /** * Atomic "please stop importing" flag. startImport() checks it between sets * so a wizard being closed mid-import can be torn down without waiting for * the whole import (or racing it). */ QAtomicInt importCancelled; /** * The ISO-639 language code whose foreignData is imported; "en" by default. */ QString cardLang = "en"; /** * Whether cardLang is a supported language other than English, so per-card * foreignData scanning can be skipped entirely when disabled. */ bool localizationEnabled = false; /** * Localized name/text collected per imported card key while parsing sets, * applied to the CardInfo objects by applyLocalizedData() once all * printings have been seen so the best-priority one wins. */ QMap localizedEntries; /** * cardLang rules text collected for split-card names while parsing sets, * applied by applyLocalizedData(). Kept apart from localizedEntries because * MTGJSON emits each split face as its own card object with the joined name * on every foreignData entry: names and the per-face text join have different * completeness and must not overwrite each other under the same key. */ QMap splitLocalizedTexts; CardInfoPtr addCard(QString name, const QString &text, bool isToken, QHash properties, const QList &relatedCards, const PrintingInfo &printingInfo); /** * Records the first foreignData entry matching cardLang for the given card * key, keeping the entry from the highest-priority set seen so far. * * Multi-face cards (split, adventure, aftermath, prepare) pass collectText = * false: MTGJSON emits one foreignData entry per face with the same joined * name but only that face's text, so the name is collected here while the * per-face texts are joined during the split-card merge. */ void collectForeignData(const QString &cardKey, const CardSetPtr ¤tSet, const QJsonObject &card, bool collectText = true); signals: void setIndexChanged(int cardsImported, int setIndex, const QString &setName); void dataReadProgress(int bytesRead, int totalBytes); public: explicit OracleImporter(QObject *parent = nullptr); /** * @brief Controls whether readSetsFromByteArray() instruments the raw scan. * * When enabled (the default) the raw scanner reports progress via * dataReadProgress(), which an interactive wizard shows on its progress bar * and a background run routes to stdout for the hosting client. Switch it * off only when nothing will consume scan progress. */ void setProgressReporting(bool enabled) { progressReporting = enabled; } /** * Selects the ISO-639 language code whose foreignData is imported. * English (the default) and unsupported codes disable localization. */ void setCardLang(const QString &lang); const QString &getCardLang() const { return cardLang; } /** * Scans the given JSON document for set metadata. Takes the data by value so * the wizard can hand over its decompressed buffer without copying it. */ bool readSetsFromByteArray(QByteArray data); int startImport(); /** * @brief Requests an in-flight startImport() to stop at the next set boundary. * * Works by setting an atomic flag that startImport() polls between sets, so * cancelImport() followed by a short waitForFinished() on the running future is * safe the moment the wizard is about to be destroyed. */ void cancelImport() { importCancelled.storeRelease(1); } /** * Applies the collected localized names/texts to the imported cards. * Called automatically at the end of startImport(); exposed separately so * tests can drive it after importing sets directly. */ void applyLocalizedData(); bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion); int importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList); /** * @brief Returns the default format rules. The result is memoized on first use and must be treated as immutable. */ const FormatRulesNameMap &createDefaultMagicFormats(); const CardNameMap &getCardList() const { return cards; } QList &getSets() { return allSets; } const QByteArray &getRawSetsData() const { return rawSetsData; } void releaseSetData(); void clear(); }; #endif