Merge branch 'master' into tooomm-qt5

This commit is contained in:
tooomm 2026-09-19 16:05:00 +02:00 committed by GitHub
commit b3813c202b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
162 changed files with 4862 additions and 653 deletions

View file

@ -12,6 +12,7 @@
#include <QSet>
#include <algorithm>
#include <climits>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
#include <libcockatrice/card/relation/card_relation.h>
@ -22,8 +23,9 @@ static const QList<AllowedCount> kSingletonCounts = {{1, "legal"}, {0, "banned"}
SplitCardPart::SplitCardPart(const QString &_name,
const QString &_text,
const QHash<QString, QString> &_properties,
const PrintingInfo &_printingInfo)
: name(_name), text(_text), properties(_properties), printingInfo(_printingInfo)
const PrintingInfo &_printingInfo,
const QString &_localizedText)
: name(_name), text(_text), localizedText(_localizedText), properties(_properties), printingInfo(_printingInfo)
{
}
@ -33,6 +35,42 @@ OracleImporter::OracleImporter(QObject *parent) : QObject(parent)
{
}
void OracleImporter::setCardLang(const QString &lang)
{
cardLang = lang.trimmed().toLower();
localizationEnabled = cardLang != "en" && CardLocalization::supportedLanguages().contains(cardLang);
}
/**
* @brief Maps the MTGJSON foreignData language names to the short codes used by
* Scryfall and stored in cards.xml (e.g. "German" -> "de").
* @param language The language name found in the MTGJSON foreignData entries.
* @return The short language code, or an empty string if unknown.
*/
static QString mtgjsonLanguageToCode(const QString &language)
{
static const QHash<QString, QString> map = {
{"Chinese Simplified", "zhs"},
{"Chinese Traditional", "zht"},
{"English", "en"},
{"French", "fr"},
{"German", "de"},
{"Greek", "grc"},
{"Ancient Greek", "grc"},
{"Hebrew", "he"},
{"Italian", "it"},
{"Japanese", "ja"},
{"Korean", "ko"},
{"Latin", "la"},
{"Phyrexian", "ph"},
{"Portuguese (Brazil)", "pt"},
{"Russian", "ru"},
{"Sanskrit", "sa"},
{"Spanish", "es"},
};
return map.value(language);
}
static CardSet::Priority getSetPriority(const QString &setType, const QString &shortName)
{
if (!setTypePriorities.contains(setType.toLower())) {
@ -254,6 +292,101 @@ static QString getJsonString(const QJsonObject &obj, const QString &key)
return obj.value(key).toVariant().toString();
}
static QString normalizeCardName(QString name)
{
// Mirror of the name cleanup applied in addCard(), so collected localization
// keys line up with the card map keys (Æ → AE, curly apostrophe → straight).
name = name.replace("Æ", "AE");
name = name.replace("", "'");
return name;
}
static QString matchingForeignEntryText(const QJsonObject &card, const QString &cardLang)
{
// Multi-face cards (split/aftermath/adventure/prepare) expose each face as a
// separate card object, each with its own foreignData entry carrying that
// face's rules text; single-face cards carry the full text in one entry.
const QJsonArray foreignData = card.value("foreignData").toArray();
for (const QJsonValue &entryValue : foreignData) {
const QJsonObject entry = entryValue.toObject();
// MTGJSON reports languages by long-form name ("German"); match the
// short code ("de") that Scryfall and cards.xml use.
if (mtgjsonLanguageToCode(getJsonString(entry, "language")) == cardLang) {
return getJsonString(entry, "text");
}
}
return QString();
}
void OracleImporter::collectForeignData(const QString &cardKey,
const CardSetPtr &currentSet,
const QJsonObject &card,
bool collectText)
{
if (!localizationEnabled) {
return;
}
LocalizedCardEntry incoming;
bool found = false;
const QJsonArray foreignData = card.value("foreignData").toArray();
for (const QJsonValue &entryValue : foreignData) {
const QJsonObject entry = entryValue.toObject();
// MTGJSON reports languages by long-form name ("German"); match the
// short code ("de") that Scryfall and cards.xml use.
if (mtgjsonLanguageToCode(getJsonString(entry, "language")) != cardLang) {
continue;
}
incoming.name = getJsonString(entry, "name");
incoming.text = collectText ? getJsonString(entry, "text") : QString();
found = true;
break;
}
if (!found) {
return;
}
// Prefer the entry from the highest-priority set (lower enum value = more
// authoritative); printings of equal priority keep the first one seen.
incoming.priority = currentSet->getPriority();
const auto existing = localizedEntries.constFind(cardKey);
if (existing == localizedEntries.constEnd() || incoming.priority < existing->priority) {
localizedEntries.insert(cardKey, incoming);
}
}
void OracleImporter::applyLocalizedData()
{
if (!localizationEnabled) {
return;
}
for (auto it = localizedEntries.constBegin(); it != localizedEntries.constEnd(); ++it) {
CardInfoPtr card = cards.value(it.key());
if (card.isNull()) {
continue;
}
const LocalizedCardEntry &entry = it.value();
if (!entry.name.isEmpty()) {
card->setLocalizedName(cardLang, entry.name);
}
if (!entry.text.isEmpty()) {
card->setLocalizedText(cardLang, entry.text);
}
}
localizedEntries.clear();
for (auto it = splitLocalizedTexts.constBegin(); it != splitLocalizedTexts.constEnd(); ++it) {
CardInfoPtr card = cards.value(it.key());
if (card.isNull()) {
continue;
}
if (!it.value().text.isEmpty()) {
card->setLocalizedText(cardLang, it.value().text);
}
}
splitLocalizedTexts.clear();
}
int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QJsonArray &cardsList)
{
// mtgjson name => xml name
@ -397,13 +530,21 @@ int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QJson
// split cards are considered a single card, enqueue for later merging
if (layout == "split" || layout == "aftermath" || layout == "adventure" || layout == "prepare") {
auto _faceName = getJsonString(card, "faceName");
SplitCardPart split(_faceName, text, properties, printingInfo);
// MTGJSON exposes each face as a separate card object, each with its
// own foreignData entry holding that face's rules text; collect it so
// the per-face texts can be joined the same way as the English text.
const QString faceLocalizedText =
localizationEnabled ? matchingForeignEntryText(card, cardLang) : QString();
SplitCardPart split(_faceName, text, properties, printingInfo, faceLocalizedText);
auto found_iter = splitCards.find(name + numProperty);
if (found_iter == splitCards.end()) {
splitCards.insert(name + numProperty, {{split}, name});
} else {
found_iter->first.append(split);
}
// MTGJSON's foreignData name is the joined name present on every
// face, so collect the name once.
collectForeignData(normalizeCardName(name), currentSet, card, false);
} else {
// relations
QList<CardRelation *> relatedCards;
@ -446,6 +587,8 @@ int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QJson
}
}
collectForeignData(normalizeCardName(name + numComponent), currentSet, card);
CardInfoPtr newCard =
addCard(name + numComponent, text, isToken, std::move(properties), relatedCards, printingInfo);
numCards++;
@ -459,6 +602,8 @@ int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QJson
QList<QPair<QList<SplitCardPart>, QString>> partsAndNames = splitCards.values();
for (auto [splitCardParts, name] : partsAndNames) {
QString text;
QString localizedText;
bool localizedTextComplete = true;
QHash<QString, QString> properties;
PrintingInfo printingInfo;
@ -468,6 +613,21 @@ int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QJson
}
text.append(tmp.getText());
// Build the cardLang text by joining each face's translated text with
// the same separator as the English text. Any face missing a complete
// translation abandons the whole join, falling back to the English text.
if (localizedTextComplete) {
const QString partLocalizedText = tmp.getLocalizedText();
if (partLocalizedText.isEmpty()) {
localizedTextComplete = false;
} else {
if (!localizedText.isEmpty()) {
localizedText.append(splitCardTextSeparator);
}
localizedText.append(partLocalizedText);
}
}
if (properties.isEmpty()) {
properties = tmp.getProperties();
printingInfo = tmp.getPrintingInfo();
@ -500,6 +660,18 @@ int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QJson
}
}
CardInfoPtr newCard = addCard(name, text, isToken, std::move(properties), {}, printingInfo);
if (localizationEnabled && localizedTextComplete && !localizedText.isEmpty()) {
// Same priority policy as collectForeignData(): the joined text from the
// highest-priority set seen so far wins, applied once all printings are in.
LocalizedCardEntry entry;
entry.text = localizedText;
entry.priority = currentSet->getPriority();
const QString entryKey = normalizeCardName(name);
const auto existing = splitLocalizedTexts.constFind(entryKey);
if (existing == splitLocalizedTexts.constEnd() || entry.priority < existing->priority) {
splitLocalizedTexts.insert(entryKey, entry);
}
}
numCards++;
}
@ -654,6 +826,8 @@ int OracleImporter::startImport()
emit setIndexChanged(numCardsInSet, setIndex, curSetToParse.getLongName());
}
applyLocalizedData();
emit setIndexChanged(0, setIndex, QString());
// total number of sets
@ -679,4 +853,6 @@ void OracleImporter::clear()
cards.clear();
allSets.clear();
rawSetsData.clear();
localizedEntries.clear();
splitLocalizedTexts.clear();
}

View file

@ -107,7 +107,8 @@ public:
SplitCardPart(const QString &_name,
const QString &_text,
const QHash<QString, QString> &_properties,
const PrintingInfo &_printingInfo);
const PrintingInfo &_printingInfo,
const QString &_localizedText = QString());
inline const QString &getName() const
{
return name;
@ -116,6 +117,13 @@ public:
{
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<QString, QString> &getProperties() const
{
return properties;
@ -128,10 +136,18 @@ public:
private:
QString name;
QString text;
QString localizedText;
QHash<QString, QString> properties;
PrintingInfo printingInfo;
};
struct LocalizedCardEntry
{
QString name;
QString text;
CardSet::Priority priority = CardSet::PriorityLowest;
};
class OracleImporter : public QObject
{
Q_OBJECT
@ -171,12 +187,53 @@ private:
*/
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<QString, LocalizedCardEntry> 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<QString, LocalizedCardEntry> splitLocalizedTexts;
CardInfoPtr addCard(QString name,
const QString &text,
bool isToken,
QHash<QString, QString> properties,
const QList<CardRelation *> &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 &currentSet,
const QJsonObject &card,
bool collectText = true);
signals:
void setIndexChanged(int cardsImported, int setIndex, const QString &setName);
void dataReadProgress(int bytesRead, int totalBytes);
@ -195,6 +252,15 @@ public:
{
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.
@ -212,6 +278,12 @@ public:
{
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 &currentSet, const QJsonArray &cardsList);
/**

View file

@ -15,6 +15,7 @@
#include <QScrollBar>
#include <QtConcurrent>
#include <QtGui>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/personal_settings.h>
OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent)
@ -37,6 +38,9 @@ OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent)
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &OracleWizard::updateLanguage);
importer = new OracleImporter(this);
// Import card text in the language the client displays, if supported:
// foreignData for any other language is never imported.
importer->setCardLang(SettingsCache::instance().cardsDisplay().getCardLang());
nam = new QNetworkAccessManager(this);