mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
* [Client] Show localized card names, texts and pictures
Localization wiring now runs end to end: the oracle importer collects
foreignData for the configured language and the client renders it.
- [Oracle] Import localized names and rules texts for the selected cardLang
- single-face cards store their foreignData name and full text
- multi-face (split/adventure/aftermath/prepare) cards collect the joined
name once and join each face's translated text with the same separator
as the English merge; an incomplete translation falls back to English;
the joined text follows the same highest-priority-set policy as the
single-face path and is only collected when localization is enabled
- the wizard switching languages re-imports the card database
- [Client] Display localized card info throughout the client
- card info text/picture widgets and the game board re-render on language
change
- pictures resolve cardLang art through Scryfall's named endpoint using the
localized name, falling back to id-based art when no match exists
- deck editor keeps canonical English names as card identity (EditRole)
while showing localized names (DisplayRole), so decks and wire names
stay stable
- [Card] Add CardLocalization-backed name/text lookup and cards.xml v4
localization elements with a bounded-size translation cache
- [Tests] Cover oracle foreignData import (incl. multi-face joins, priority
and fallback paths), XML v4 localization parsing, deck model localized
display and the language-aware settings default
Existing installations need to re-run Oracle to see translations: localized
data only lands in cards.xml when the Oracle app is started with the
preferred language selected — launch the separate "Oracle" program that
ships with Cockatrice, pick the language in the wizard and let it re-import
the card database.
The client's database cache (cards.xml.cache) is invalidated by the cache
format bump and the source-hash checks, but a cache written before the
re-import can still hold English-only entries (the hash uses file size and
mtime, so a same-size/same-timestamp rewrite may be served as-is); delete
cards.xml.cache and relaunch if no localized names/texts show up after
re-importing.
* [Card] Pass localized card names and texts into CardInfo construction
Address review: instead of constructing the card and then calling
setLocalizedName/setLocalizedText (which emit a cardInfoChanged signal per
language), both constructors, both newInstance overloads and their callers
(cards.xml v4 parser and the binary cache reader) now pass the localized maps
as constructor arguments.
* [Client] Rename LocalizedCard:: helpers namespace to CardLocalization
The namespace now matches its header file name, as the review pointed out;
LocalizedCard reads more like a class or struct. Callers (card info text
widget, board card name rendering) are updated to match.
* [Client] Drop unused info member from the card info text widget
The CardInfoPtr member was only ever initialized to nullptr and never read;
remove it together with its initializer.
* [PictureLoader] Add the localized picture URL explicitly, not implicitly
Address review: silently prepending the Scryfall named-picture URL to the
download list whenever a non-English card language was active was surprising,
consumed quota per card when it failed, and could grab the wrong (canon) art on
name collisions, with no way to turn it off.
The insert is now opt-in and user-controlled: changing the card language adds
the template to the top of the download URLs once (persisted, documented in the
re-import prompt, and editable/removable in the deck editor settings), while the
picture loader no longer injects it at request time.
* [Card] Show card languages in the same native (English) format as the UI
Address review: the card text & images language dropdown listed bare native
names, some in inconsistent lowercase (e.g. "čeština", "español de España"),
which makes the languages easy to mix up for users that do not read the script
(e.g. 日本語 vs 한국어). It now mirrors the UI language dropdown and always pairs
the native name with its English name (e.g. "Deutsch (German)",
"日本語 (Japanese)"), using the same fixed casing.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
309 lines
9.7 KiB
C++
309 lines
9.7 KiB
C++
#ifndef ORACLEIMPORTER_H
|
|
#define ORACLEIMPORTER_H
|
|
|
|
#include "raw_json_scanner.h"
|
|
|
|
#include <QAtomicInt>
|
|
#include <QByteArray>
|
|
#include <QJsonArray>
|
|
#include <QJsonObject>
|
|
#include <QMap>
|
|
#include <QRegularExpression>
|
|
#include <QVariant>
|
|
#include <libcockatrice/card/card_info.h>
|
|
#include <utility>
|
|
|
|
// 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<QString, CardSet::Priority> 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<QString, QString> &_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<QString, QString> &getProperties() const
|
|
{
|
|
return properties;
|
|
}
|
|
inline const PrintingInfo &getPrintingInfo() const
|
|
{
|
|
return printingInfo;
|
|
}
|
|
|
|
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
|
|
private:
|
|
static const QRegularExpression formatRegex;
|
|
|
|
/**
|
|
* The cards, indexed by name.
|
|
*/
|
|
CardNameMap cards;
|
|
|
|
/**
|
|
* The sets, indexed by short name.
|
|
*/
|
|
SetNameMap sets;
|
|
|
|
QList<SetToDownload> 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<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 ¤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<SetToDownload> &getSets()
|
|
{
|
|
return allSets;
|
|
}
|
|
const QByteArray &getRawSetsData() const
|
|
{
|
|
return rawSetsData;
|
|
}
|
|
void releaseSetData();
|
|
void clear();
|
|
};
|
|
|
|
#endif
|