40 KiB
#documentation #apis #integrations #ccm3
Asset And Info APIs
This document explains which external APIs Card Collection Manager 3 uses, and what each API is responsible for in the app. Use this page with adding-a-new-game.md when you are wiring a new game module or debugging API behavior.
API Roles
The code separates remote APIs into two roles: info APIs and asset APIs. Info APIs provide set metadata used to populate local set lists (ID, name, release date). Asset APIs resolve a card lookup into an image URL, then CardPreviewService downloads the raw preview image bytes for the UI.
Magic: The Gathering APIs
Info API: https://api.scryfall.com/sets
Used by MagicSetSource to fetch all sets. The parser drops digital-only sets, maps Scryfall fields to the internal Set type, rewrites released_at from YYYY-MM-DD to YYYY/MM/DD, and sorts ascending by release date.
Asset API: https://api.scryfall.com/cards/search?q=...
Used by MagicCardPreviewSource to find a card printing from name + setId, then extract data[0].image_uris.normal as the preview URL. The search query is percent-encoded and card names apply & -> and normalization before lookup.
Pokemon APIs
Unified Pokemon Game menu entry. Per-card region (West / Asia) selects the backend below. Collection: pokemon/collection.json. West sets: pokemon/sets-west.json. Asia sets: pokemon/sets-asia.json. Language choices: West → English/German/French/Spanish/Italian/Russian; Asia → Japanese/S-Chinese/T-Chinese/Korean.
West (Game::Pokemon, TCGdex EN)
Upstream: TCGdex REST API locale en. No API key. Canonical West set ids are TCGdex EN ids (e.g. base1, sv01, swsh12.5tg). Legacy pokemontcg.io ids (sv1, pgo, swsh12tg, …) are rewritten via canonicalizeWestSetId on West collection load, preview/auto-detect lookups, and set-completion matching so existing collections keep working; the next save persists TCGdex ids.
Info API: https://api.tcgdex.net/v2/en/sets
Used by PokemonSetSource to fetch the slim set list (id, name). Release dates are not on the list endpoint — each set’s GET /v2/en/sets/{id} supplies releaseDate as YYYY-MM-DD, rewritten to YYYY/MM/DD, then the list is sorted ascending by release date.
Asset API: https://api.tcgdex.net/v2/en/cards/{setId}-{localId} (by id), https://api.tcgdex.net/v2/en/cards?… (filtered search), and set-detail cards[] for auto-detect. Image CDN bases live on assets.tcgdex.net; the preview source appends /high.png (wxImage decodes PNG, not webp).
Used by PokemonCardPreviewSource in two ways:
-
Preview lookup (
fetchImageUrl). When both set id and collector number are present, prefersGET /v2/en/cards/{setId}-{localId}(card object withimagebase). On HTTP failure or missing image, falls back to a filtered searchset.id=eq:…&localId=eq:…(collector numbers are unique within a set). When Set # or set id is missing, usesname=eq:…with optionalset.id/localId. Legacy set ids are canonicalized before URL build. -
Auto-detect print (
detectFirstPrint/detectPrintVariants, Pokémon edit dialog). PrefersGET /v2/en/sets/{setId}and filterscards[]by exact case-insensitive card name. MapslocalId→AutoDetectedPrint::setNoandrarity→AutoDetectedPrint::rarity(the edit dialog does not auto-sync holo flags from rarity). If set detail fails, falls back to a filtered cards search and still restricts rows to the chosen set id when present. Distinct(setNo, rarity)pairs are deduped. The edit dialog offers Auto detect, Next, silent prefetch on Edit open, and clears cached variants when Name or Set changes. The Set # field and persistedPokemonCard::setNokeep only the printed-number portion; values such as4/104are trimmed to4on load and save.
The preview path normalizes collector numbers before request build. For example, 4/102 is reduced to 4 because the remote localId path expects only the printed-number component.
Set-completion catalog (West)
Sets → Update Pokemon uses PokemonSetSource::fetchAllWithCatalog() so the West path writes:
- The set list (
pokemon/sets-west.json) from/v2/en/sets+ per-set detail dates - A pack checklist at
<dataStorage>/pokemon/set-catalog-west.jsonfrom each set’s detailcards[](localId→setNo,name→ name)
Each catalog pack stores id (TCGdex EN set id), name (display), and cards[] of { setNo, name } keyed by localId (normalized by stripping anything after /). Duplicate collector numbers within a pack collapse to one checklist row. The Pokemon Set Completion tab reads this file offline; ownership for a West pack requires PokemonRegion::West, a canonicalized card.set.id match, and a normalized collector number match. Amount / holo / 1st Edition are ignored for completion counts.
After a successful Update, PokemonGameView also runs syncPokemonCollectionSets against the refreshed set lists: West cards get legacy set-id migration plus set.name / releaseDate refresh when the id is present; Asia cards refresh name/date the same way. Changed cards are persisted via CollectionService::saveAll.
If set-catalog-west.json is missing (and the active region filter is West or All with no Asia catalog either), the Set Completion tab prompts the user to run Update Pokemon.
Yu-Gi-Oh! APIs (Yugipedia + YGOPRODeck)
Yu-Gi-Oh! splits its remote calls across two upstreams. Yugipedia is the primary preview source because it hosts actual per-printing card scans; YGOPRODeck continues to drive set listings and the auto-detect-first-print helper, plus a last-resort image fallback.
Upstream documentation:
- Yugipedia MediaWiki API help (standard MediaWiki action API; we only need
prop=imageinfo). - Yu-Gi-Oh! API Guide — YGOPRODeck. CCM3 uses v7 endpoints only.
Info API: YGOPRODeck cardsets.php
https://db.ygoprodeck.com/api/v7/cardsets.php
Used by YuGiOhSetSource. The response is a top-level JSON array. Each object maps set_code → internal Set.id, set_name → Set.name, and tcg_date → Set.releaseDate with - rewritten to / for consistency with other games’ date strings. Results are sorted ascending by releaseDate.
CCM3 also applies a deterministic local patch step in YuGiOhSetSource::appendMissingSetAliases(...) after parsing: if upstream omits known 25th Anniversary TCG reprints, the app injects missing aliases for LOB-25TH, MRD-25TH, SRL-25TH, PSV-25TH, DCR-25TH, and IOC-25TH (with fixed release dates) so users can still select those products in the set picker.
UI note (set code entry, no extra HTTP): The Yu-Gi-Oh! Add/Edit dialog can resolve a typed product code against the already cached set vector (same data as the set dropdown). Matching is implemented in core/include/ccm/util/YuGiOhSetLookup.hpp as lookupYuGiOhSetByShorthand(...): trim ASCII whitespace, ASCII case-fold, then require an exact match on Set.id (the YGOPRODeck set_code). Zero matches → user error; more than one row with the same normalized id → ambiguous error (defensive). On a unique hit the dialog returns to the dropdown and selects that set.
Asset API: Yugipedia api.php (primary)
https://yugipedia.com/api.php?action=query&prop=imageinfo&iiprop=url&titles=...
Used by YuGiOhCardPreviewSource::fetchImageUrl for the actual per-printing card scan. Yugipedia is the only public source we have found that distinguishes art between same-passcode reprints (LOB Blue-Eyes vs SDK Blue-Eyes, for example), and uses a deterministic file-name convention of the shape <Slug>-<SET>-<REGION>-<RARITY>-<EDITION>[-Misc].<png|jpg> per Yugipedia’s image policy.
The UI passes a positional tuple in setNo of the form set_code||rarity||edition (for example SDK-001||Ultra Rare||UE); the source splits on || before building filenames. Field meanings:
set_code— full code as printed (LOB-005,SDK-001,RA04-EN001). Everything before the first-becomes the Yugipedia<SET>slot (LOB,SDK,RA04).rarity— full English rarity name from the edit dialog (Ultra Rare→UR,Quarter Century Secret Rare→QCScR, …). The canonical short-form mapping lives inygoRarityShortCode(...)(core/include/ccm/util/YuGiOhPrintingSlot.hpp) and is reused by both the Yu-Gi-Oh overview-table rarity rendering and preview filename construction (rarityCodeFor(...)). Unknown values fall back to the rarity-less filename pattern.edition—1Ewhen the user marked the card as 1st Edition, otherwiseUE(Unlimited).
buildCandidateFilenames(...) then produces a priority-ordered list:
- Printed edition first (
1EthenUE, orUEthen1Efor non-first), withLElast for promo-style prints. - English regions only —
EN, thenNA, thenEU, thenAU. Yugipedia is queried with English regions regardless of the card’s stored Language, so a German-language card still shows the English scan; this matches the user-visible policy in the edit dialog and avoids querying region-specific scans that are sparser on Yugipedia. - Both
.pngand.jpgextensions per combo (older LOB-era uploads are.jpg, modern reprints are.png). - A rarity-less fallback round so cards with unknown rarities still resolve in single-rarity sets.
buildYugipediaQueryUrl(...) joins all candidates into one MediaWiki batch query (titles=File:A|File:B|... URL-encoded), so the entire list resolves in a single HTTP call. parseYugipediaResponse(...) walks the candidate list in order and returns the URL of the first filename that came back with imageinfo[0].url; missing files come back with "missing": "" and are skipped.
Asset API: YGOPRODeck cardinfo.php (fallback + auto-detect)
https://db.ygoprodeck.com/api/v7/cardinfo.php?fname=...
Used in two situations:
-
Last-resort preview fallback. If Yugipedia returns no candidate match (cards without an English scan yet, transient API errors),
fetchImageUrlfalls through toparseFallbackImageUrl(...), which prefers an exact-name match in YGOPRODeck’sdata[], otherwise the first row, and returns the first entry fromcard_images[0]. This is intentionally not filtered bycardset=: when YGOPRODeck applies that filter, it reorderscard_imagesso alt-art passcodes are promoted ahead of the standard art, which would re-introduce the “wrong artwork” bug we fixed by switching to Yugipedia. -
Auto-detect print (
detectFirstPrint/detectPrintVariants, Yu-Gi-Oh! edit dialog). Usesfname=pluscardset=set to the display set name from the picker (must matchcard_sets[].set_namein the payload). If that request fails (for example unknown set label), it retries withfname=only and still filters prints by preferredset_name.YuGiOhCardPreviewSource::parsePrintVariants(...)walks every(set_code, set_rarity)pair for rows whose card name matches exactly (case-insensitive) so the dialog can offer ring-buffer Next controls: one cycles distinctset_codevalues for that name+set (and resets rarity to the first upstream rarity for the newly selected code); another cycles distinctset_rarityvalues for the currentset_codewithout changing the collector number. Shared HTTP and parsing rules live besideparseFirstPrint. When the dialog passes both an exact card name and a displayset_name, an upstream miss on that label returns an error instead of falling back to unfilteredcard_sets[]rows — otherwise unrelated products (same card name, differentset_nameon each printing) could be blended into one bogus variant list. The Yu-Gi-Oh! edit dialog additionally drops European alternateset_coderows that use the-E###pattern (singleEbefore digits, e.g.LOB-E003) when the card language is English, because YGOPRODeck keeps those alongside NA numbering (LOB-005) under the same Englishset_name; it also collapsesLOB-005-style andLOB-EN005-style codes to one Next slot via digit-tail matching (ccm/util/YuGiOhPrintingSlot.hpp). No image data is needed for this path, so Yugipedia is not consulted.
YGOPRODeck publishes rate limits and asks clients to cache responses and avoid abusive hotlinking; treat failures after burst traffic as an upstream policy signal, not an app bug. Yugipedia’s MediaWiki API is similarly polite — one batched call per preview lookup keeps us well under any normal threshold.
Set-completion catalog (cardinfo.php all-cards dump)
Sets → Update Yu-Gi-Oh! uses YuGiOhSetSource::fetchAllWithCatalog() so two HTTP responses write:
- The set list (
yugioh/sets.json) fromcardsets.php(same as before, including local 25th Anniversary aliases) - A pack checklist at
<dataStorage>/yugioh/set-catalog.jsonfrom the unfilteredcardinfo.phpdump
Each catalog pack stores id (YGOPRODeck product set_code / Set.id, e.g. LOB), name (display set_name), and cards[] of { setNo, name } drawn from each card’s card_sets[]. European -E### alternate codes are dropped; LOB-005 / LOB-EN005-style equivalents collapse to one checklist row (preferring an EN-embedded code when present). The Yu-Gi-Oh! Set Completion tab reads this file offline; ownership for a pack requires matching card.set.id plus a printing-slot match (ygoPrintingSlotsMatch — same abbrev + digit run). Rarity and 1st Edition are ignored for completion counts.
If set-catalog.json is missing, the Set Completion tab prompts the user to run Update Yu-Gi-Oh!.
Digimon Digi-Battle (1999) APIs (digimoncard.io)
English Digi-Battle is wired as Game::DigiBattle99 (dirName digibattle99, UI label Digimon (Digi-Battle)). Upstream docs: digimoncard.io Public API. Always scope requests with series=Digimon Digi-Battle Card Game so modern Digimon Card Game rows are never mixed in. Rate limit: 15 requests / 10 seconds / IP (429 then temporary block on abuse).
Info API: derived set list from search.php
There is no dedicated sets endpoint. DigiBattle99SetSource calls:
https://digimoncard.io/api-public/search.php?series=Digimon%20Digi-Battle%20Card%20Game&limit=1000&sort=name&sortdirection=asc
and collects unique set_name[] pack strings. Each pack becomes a Set with:
Set.name— exact pack display name (used aspack=on search / auto-detect)Set.id— stable slug (Series 1 Starter Set→series-1-starter-set); never rename after shipSet.releaseDate— curated table in the set source (Series 1 Starter =1999/06/01verified; other packs use documented year/month anchors)
Unknown future packs get an empty release date and sort last.
Cached on disk as <dataStorage>/digibattle99/sets.json via SetService / JsonSetRepository.
Set-completion catalog (same search.php payload)
Sets → Update Digimon (Digi-Battle) uses DigiBattle99SetSource::fetchAllWithCatalog() so one HTTP response writes both:
- The set list (
sets.json) as above - A pack checklist at
<dataStorage>/digibattle99/set-catalog.json
Each catalog pack stores id (slug), name (display), and cards[] of { setNo, name } (API id normalized like preview — alphabetic prefix uppercased). A card listed in multiple set_name[] packs appears under each pack. The Digimon Set Completion tab reads this file offline (no live HTTP while browsing); ownership for a pack requires matching card.set.id plus normalized setNo.
If set-catalog.json is missing, the Set Completion tab prompts the user to run Update Digimon (Digi-Battle).
Asset API: CDN images + search.php lookup
Card scans live at:
https://images.digimoncard.io/images/cards/{id}.jpg
where {id} is the API card number (ST-01, BO-115, MO-06). The CDN also serves .webp, but CCM3 uses .jpg because OnInit only registers wxPNGHandler / wxJPEGHandler (WebP bytes would surface as “image decode failed”).
DigiBattle99CardPreviewSource::fetchImageUrl:
- If
setNois non-empty → normalize alphabetic prefix to uppercase (no invented zero-padding) and return the CDN URL with no search round-trip. - Otherwise search with
n=+ optionalpack=(display set name) +series=, take the first exact name match’sid, then build the CDN URL.
Preview key: (name, set.name, setNo) — middle slot is the pack display name (same idea as Yu-Gi-Oh! passing set.name for YGOPRODeck cardset=), not the slug id.
Auto-detect (detectPrintVariants): same search; distinct id values become AutoDetectedPrint::setNo. Digi-Battle UI is Pokémon-like (no persisted rarity).
Empty search array / {"error":"..."} → NotFound; bad JSON / HTTP → Transient.
Japanese Pokémon TCG APIs (TCGdex ja) — Asia region backend
Asia Pokémon is routed internally as Game::JapanesePokemon (dirName pokemon, same data directory as West). It is not a separate Game menu entry: the unified Pokemon UI stores both West and Asia cards in pokemon/collection.json with a per-card region (West / Asia). Set caches are split by filename under that directory (pokemon/sets-west.json vs pokemon/sets-asia.json). JsonSetRepository migrate-on-load promotes legacy pokemon/sets.json → sets-west.json and pokemonjp/sets.json → sets-asia.json when the new files are missing. Sets > Update Pokemon refreshes both lists. Upstream: TCGdex REST API. No API key. Japanese set IDs (e.g. PMCG1, SV1a) are never merged into Western TCGdex EN ids.
Info API: TCGdex GET /v2/ja/sets (+ per-set detail)
https://api.tcgdex.net/v2/ja/sets returns a slim array (id, name, cardCount). Release dates require GET /v2/ja/sets/{id} (releaseDate as YYYY-MM-DD, rewritten to YYYY/MM/DD). JapanesePokemonSetSource:
- Excludes Chinese-region
CS*junk rows mislabeled on the JA endpoint. - Applies field overrides (e.g.
SV4aJapanese name →シャイニートレジャーex). - Prefers English display names and release dates from the bundled EN catalog when present; otherwise keeps the TCGdex Japanese name and fetches detail for the date.
- After parsing the TCGdex list, injects Original-era / catalog-only products TCGdex omits (idempotent by set id — skipped if upstream later adds them). The same injection runs when loading a cached Asia set list (
sets-asia.json) viaISetSource::augmentCachedSets, so these products appear without requiring Update Sets first. Stable ids and English names:
| Id | English name |
|---|---|
UnnumberedPromo |
Unnumbered Promotional cards (Bulbapedia catch-all; synthetic 001… localIds; preview via catalog image_url preferring Japanese / Unnumbered Bulbagarden scans) |
ExpSheet1 / ExpSheet2 / ExpSheet3 |
Expansion Sheet Series 1–3 |
NiviCG |
Nivi City Gym |
HanadaCG |
Hanada City Gym |
KuchibaCG |
Kuchiba City Gym |
TamamushiCG |
Tamamushi City Gym |
YamabukiCG |
Yamabuki City Gym |
GurenTG |
Guren Town Gym |
SouthernIslands |
Southern Islands |
Seed data lives in tools/pokemon_jp/classic_missing_sets.json + classic_missing_prints.json (merged into the EN catalog via merge_classic_missing.py). LocalIds for these products are sequential 001… within each product (cards were unnumbered in print). Refresh UnnumberedPromo prints from Bulbapedia with python tools/pokemon_jp/harvest_unnumbered_promos.py, then fill preview images with python tools/pokemon_jp/enrich_unnumbered_promo_images.py (prefers Unnumbered / Japanese reprint-gallery scans over English Wizards |image= primaries; EN-only Bulbapedia pages leave image_url empty), then re-run merge_classic_missing.py. Numbered Japanese promo eras (SV-P, S-P, …) remain out of scope — TCGdex does not expose them, and they are not part of this curated set.
Set-completion catalog (Asia)
Sets → Update Pokemon uses JapanesePokemonSetSource::fetchAllWithCatalog() so the Asia path writes:
- The set list (
pokemon/sets-asia.json) as above (EN names + classic product injection) - A pack checklist at
<dataStorage>/pokemon/set-catalog-asia.json
For each set, the source GETs /v2/ja/sets/{id} and builds checklist rows from cards[] (localId → setNo, display name prefers EN catalog nameEn, else TCGdex Japanese name). Prints present in the bundled EN catalog but missing from TCGdex cards[] are gap-filled into the pack (covers UnnumberedPromo / City Gym / Expansion Sheets / Southern Islands and sparse classic sets). Catalog-only products with no TCGdex detail become packs entirely from JapanesePokemonEnCatalog::printsForSet.
The Pokemon Set Completion tab also loads this file offline; ownership for an Asia pack requires PokemonRegion::Asia, matching card.set.id, and normalizeLocalId on setNo. Region and language filters on the tab restrict which packs/cards count. West and Asia never cross-count.
If set-catalog-asia.json is missing (and the active region filter needs it), the Set Completion tab prompts the user to run Update Pokemon.
Sets without printed collector numbers (UnnumberedPromo)
Physically unnumbered Japanese promos (and the other classic catalog-only products above) have no printed set number. The app still stores a synthetic setNo / catalog local_id (001, 002, …) so preview and collection JSON stay keyed by (setId, localId) — but that value must not be treated as something the user can read off the card.
Edit dialog (PokemonCardEditDialog, Asia region) for set id UnnumberedPromo:
- The Set # text field is hidden (row label becomes Print). Auto detect and Next remain.
- Auto-detect / silent Edit prefetch lists catalog prints matching the typed name (exact EN/JA, plus qualified titles such as
Mewtwo→Mewtwo (CoroCoro promo)). Distinct synthetic localIds form the Next ring. - Next on the edit form shows a position counter (
Next (2/5)), not the synthetic id. For ordinary numbered JP sets, Next still shows the current collector number (Next (42)). - A modeless Print preview popup (
VariantImagePreviewDialog) opens ~20px to the right of the Add/Edit dialog. It loads the current print viaCardPreviewService::fetchPreviewBytesand refreshes on each ring step. The popup has its own<< Prev/Next >>controls that drive the same ring as the edit dialog (buttons disabled when fewer than two variants). - On save, the dialog writes the ring’s synthetic
setNointoPokemonCard::setNoeven though the text field was hidden.
Other classic unnumbered products (City Gyms, Expansion Sheets, Southern Islands) currently keep the normal Set # field; only UnnumberedPromo uses the print-preview UX above.
Asset API: TCGdex card / set-detail images
Preview is local-id based. JapanesePokemonCardPreviewSource:
- With
setId+setNo(localId), triesGET /v2/ja/cards/{setId}-{localId}and readsimage. - Falls back to set-detail
cards[](which often already carriesimageon modern sets). - Appends
/high.pngto the TCGdex image base URL (PNG — wxImage does not decode webp). - If the set-specific card still has no scan (classic sets like
PMCG1), looks up the bundled EN catalog print for that exactsetId+localIdand uses optionalimage_urlor a TCGPlayer product image built fromtcgplayer_id(https://product-images.tcgplayer.com/fit-in/437x437/{id}.jpg). Gap-fill sources differ by era:- PMCG and other data-asia sets with
thirdParty.tcgplayer: printing-accuratetcgplayer_idharvested offline from tcgdex/cards-databasedata-asia(the live TCGdex API does not expose them). - neo1–neo4: data-asia has no
tcgplayer_idand TCGdex JAimageis null; the catalog may carry an ETL-writtenimage_urlfrom a Japanese CardIndex set scan (enrich_neo_image_urls.pyscrapes the JA neo set pages and matches by English card name). No English pokemontcg.io fallback — if CardIndex has no JP image,image_urlis left empty and the UI shows the card-back. Use--overwriteto re-resolve / clear stale EN URLs. Runtime still resolves only by exact JAsetId+localId— no C++ name search across printings. This is printing-accurate gap-fill — not a name search across other Charizard printings at runtime.
- PMCG and other data-asia sets with
- For catalog-only products (Unnumbered Promotional cards, City Gym theme decks, Expansion Sheets, Southern Islands), when TCGdex set/card GETs fail, Auto-detect and preview fall back to the bundled catalog prints for that
setId(EN/JA name →localId; optionaltcgplayer_id/image_urlfor preview).UnnumberedPromorows typically carry Bulbagarden Archivesimage_urlvalues written byenrich_unnumbered_promo_images.py, which prefers Japanese / Unnumbered Promotional reprint scans and omits English-only Wizards Black Star primaries when no JP file is available. Without a catalog image field, preview returnsNotFoundand the UI shows the card-back. Auto-detect matches exact EN/JA names and also qualified English titles (Mewtwo→Mewtwo (CoroCoro promo)). - City Gym deck exclusives must stay printing-accurate. Do not reuse Leaders' Stadium / PMCG donor
tcgplayer_ids for those prints; that shows the wrong set art. Instead, bundle local scans underassets/pokemon_jp_classic/<setId>/<localId>.jpgand point the catalog row atimage_url: "asset:pokemon_jp_classic/<setId>/<localId>.jpg".CardPreviewServiceloadsasset:URLs from disk next to the executable, bypassing HTTP entirely.
It does not substitute another printing of the same Pokémon when both TCGdex and the catalog lack an image. Then preview returns NotFound and the UI shows the Japanese TCG card-back.
Auto-detect / Next uses the same set-detail cards[], matching the typed name against catalog English names or TCGdex Japanese names. Catalog EN aliases are applied only when the catalog name_ja agrees with the TCGdex row (stale seed mappings like Charmander→001 are ignored).
Pokémon English aliases in the catalog come from National Dex → species table (dexId) for ordinary Pokémon. When name_ja carries a known owner / Rocket's / Dark / Light / Shining prefix, enrich_preview_images.py composes the full English product title (e.g. エリカのナゾノクサ → Erika's Oddish, わるいリザードン → Dark Charizard, R団のサンダー → Rocket's Zapdos, neo garbled 輝くセレビ → Shining Celebi). Those rows use name_en_source: "species-table-variant". Trainer/Energy English aliases come from the offline JA→EN map tools/pokemon_jp/non_pokemon_en_by_ja.json (e.g. Switch ← ポケモンいれかえ).
That trainer/energy map is maintained to cover at least the first 15 chronological main Japanese expansions present in TCGdex (PMCG1–PMCG6, neo1–neo4, VS1, web1, E1–E3). The same JA→EN entry also applies to later reprints that reuse the Japanese name.
Variant Pokémon English titles
Auto-detect for English owner / Rocket's / Dark / Light / Shining Pokémon names requires the bundled catalog's full name_en for that print (same rule as City Gym manuals that already store Erika's Oddish). Typing the Japanese TCGdex name still works when name_ja is correct.
To extend variant coverage:
- Add new JA prefix → English title prefix pairs to
VARIANT_JA_PREFIXESintools/pokemon_jp/enrich_preview_images.py(longest prefixes first). - Re-run:
python tools/pokemon_jp/enrich_preview_images.py
- For neo1–neo4 Japanese preview images (CardIndex JP scans only; clears EN pokemontcg.io URLs on miss), run:
python tools/pokemon_jp/enrich_neo_image_urls.py
python tools/pokemon_jp/enrich_neo_image_urls.py --overwrite
- Rebuild so
assets/pokemon_jp_en_catalog.jsonnext to the exe is updated.
Rows with name_en_source: "manual" (City Gym theme decks in classic_missing_prints.json) are never overwritten. Prefer stable English TCG product names (Bulbapedia / Limitless English titles).
Extending Trainer/Energy English aliases
Auto-detect for English Trainer/Energy names only works when the bundled catalog has a name_en for that print. Pokémon get name_en automatically from dexId (bare species) or from variant prefix composition (full titles); Trainers and Energy do not. To add more sets or staples:
- Collect unique Japanese Trainer/Energy names for the sets you care about (from TCGdex set detail
cards[].name, or fromtools/pokemon_jp/_tcgdex_cards_database/data-asia/<serie>/<setId>/*.tsafter running enrich once). - Add each missing
name_ja→ English display name totools/pokemon_jp/non_pokemon_en_by_ja.json. One entry covers every set that reprints that Japanese title. - Re-run:
python tools/pokemon_jp/enrich_preview_images.py
- Confirm
enrich_preview_images.pyprintsFIRST15 trainer/energy coverage OK(or extendFIRST15_SETSin that script if you raise the coverage baseline). Copy/rebuild soassets/pokemon_jp_en_catalog.jsonnext to the exe is updated. - Prefer stable English TCG product names (Bulbapedia / Limitless English titles). Do not invent per-set aliases that differ for the same
name_ja.
Bundled English catalog
ui_wx/assets/pokemon_jp_en_catalog.json is copied next to the exe on build (assets/pokemon_jp_en_catalog.json). It supplies English set/card names TCGdex JA cannot provide, plus optional classic-image gap-fill fields (tcgplayer_id / image_url). Generated offline via tools/pokemon_jp/ (set EN merge + enrich_preview_images.py using species, variant, and trainer/energy tables + optional enrich_neo_image_urls.py for neo image_url). Missing catalog → Japanese-only labels still work; missing image fields → card-back for unscanned printings. Missing EN aliases for a Trainer still allow Auto-detect when the Japanese name is typed.
Card-back fallback uses the Japanese TCG Bulbagarden scan
(TCG_Card_Back_Japanese.jpg), not the Western Cardback.jpg.
Runtime Flow In CCM3
The app uses the same flow for every game that registers a module:
SetServiceasks the game'sISetSource(info API) for the latest set list.CardPreviewServiceasks the game'sICardPreviewSource(asset API) for a preview image URL.CardPreviewServiceperforms a second HTTP GET to that URL and returns raw bytes to the UI layer.- If preview lookup fails (or returns empty bytes), the UI loads a per-game card-back fallback in
BaseSelectedCardPanel: Magic / Pokémon / Japanese Pokémon callCardPreviewService::fetchImageBytesByUrl(...)against fixed HTTPS URLs. Yu-Gi-Oh! tries two Yugipedia URLs (thumbnail then fullBack-EN.png), then readsassets/ygo_card_back.pngnext to the executable if both downloads fail (bundled asset; seeapp/CMakeLists.txt).
Caching And Connection Reuse
See caching.md for a dedicated reference on preview cache tiers, internal keys, eviction, clearing, and HTTP session reuse.
Three mechanisms reduce preview latency for all games (Magic, Pokemon West/Asia backends, Yu-Gi-Oh!, DigiBattle99). In addition, the shared HTTP session speeds every IHttpClient::get call (including set-list fetches), not only previews:
- In-memory preview LRU (
CardPreviewService). SuccessfulfetchPreviewBytesresults are cached keyed by(game, name, setId, setNo); successfulfetchImageBytesByUrlresults are cached keyed by URL (used for the per-game card-back fallback). Re-selecting a previously viewed row is decode-only — no HTTP at all. The cache is bounded byCardPreviewService::kCacheCapacity(currently 128 entries) and uses a list+map LRU under a mutex (the preview pipeline is invoked from a worker thread inBaseSelectedCardPanel). Source errors are split byPreviewLookupError::Kind:NotFound(the upstream answered cleanly that the record has no image) is negative-cached in this tier so subsequent selections short-circuit without HTTP, whileTransient(HTTP/network/parse failures) is never cached so a brief outage cannot permanently disable a card's preview. - Persistent disk byte cache (
LocalPreviewByteCache, portIPreviewByteCache). Wraps the in-memory tier with an on-disk store under<exeDir>/.cache/preview-cache/— pinned next to the executable, in the same scope asconfig.json, not under the user-configurableConfiguration.dataStoragepath. The cache stays put when the user reconfigures or relocates their collection data, and it is not part of the user's data directory backups; it is install-scoped, not collection-scoped. Both positive previews andNotFoundverdicts survive an app restart. Each entry is a mutually-exclusive<hash>.bin(positive payload) or<hash>.neg(negative marker) plus a<hash>.idxsidecar containing the original key — load-time mismatch on the sidecar treats the entry as a miss, so a hash collision degrades to a one-time HTTP refetch instead of serving the wrong card's bytes (or the wrong card's "no image" verdict). Hashing is FNV-1a 64-bit (no crypto dependency). The cache is bounded by total.binpayload bytes (defaultkDefaultMaxBytes = 64 MiB) and evicts oldest entries by mtime when a new write would exceed the cap; reading an entry touches its mtime so frequently-viewed cards survive eviction. Negative.negmarkers are tiny and not counted against the cap — their count is naturally bounded by the user's actively-viewed records. Filesystem mutations route throughIFileSystem; size and mtime queries (which the port does not expose) usestd::filesystemdirectly inside the adapter. The persistent tier is fire-and-forget on the way down — every adapter operation swallows I/O errors so a flaky or full disk never breaks the preview path. - Persistent HTTP session (
CprHttpClient). The adapter owns one long-livedcpr::Session(libcurl easy handle) for the lifetime of the app. Per-request configuration is limited toSetUrl(...); headers, timeout, and redirect policy are configured once in the constructor. DefaultAccept: */*keeps JSON responses and raw image bodies working on the same session (avoid tying every GET toapplication/json). libcurl's connection pool keeps the TLS connection to each host warm, so repeat calls toapi.scryfall.com,api.tcgdex.net,assets.tcgdex.net,db.ygoprodeck.com,yugipedia.com,ms.yugipedia.com,digimoncard.io, andimages.digimoncard.ioskip the TLS handshake. Astd::mutexserializes callers — libcurl easy handles are not thread-safe, and the preview pipeline is single-flight per panel anyway.
CardPreviewService consults the tiers in order memory → disk → source/HTTP. On a disk hit (positive or negative) the entry is promoted into the in-memory LRU so the next click on the same row never re-touches the disk cache. On HTTP success the bytes are written through to both tiers in one shot. On a NotFound source error the negative marker is written through to both tiers; on Transient source errors nothing is written, so the next selection retries cleanly.
The combined effect on the preview path: first selection of a previously-unseen card pays one TLS handshake per new host this session (typically two hops for Yu-Gi-Oh!: yugipedia.com for the API, ms.yugipedia.com for the image; Digi-Battle often hits images.digimoncard.io only when setNo is already known), each subsequent fresh card on the same host skips the handshake, any re-selection of an already-viewed card is instant, after the first run with the disk cache populated even a fresh app launch is decode-only for previously-seen cards until eviction or a manual cache clear, and records the upstream cleanly has no image for stay "instant card-back" across restarts instead of re-paying the lookup every launch. Editing a lookup-relevant field of a record (name, set, setNo, or for Yu-Gi-Oh! the rarity / edition packed into setNo) changes the cache key automatically, so a fresh resolution attempt happens on the next click.
To clear the persistent cache (for example to recover from a bad upstream image), delete the <exeDir>/.cache/preview-cache/ subdirectory or the umbrella <exeDir>/.cache/ folder. Note: the in-app "Reset" / data-storage-relocation flow does not touch this directory — the cache is install-scoped, not collection-scoped, so it is preserved across data-dir moves and only cleared by deleting the directory above explicitly (or by reinstalling / relocating the executable).
For the full caching design and contributor rules see the dedicated caching.md.
Fallback card-back sources (BaseSelectedCardPanel; Magic/Pokémon URLs match CCM2):
- Magic:
https://gamepedia.cursecdn.com/mtgsalvation_gamepedia/f/f8/Magic_card_back.jpg - Pokémon:
https://archives.bulbagarden.net/media/upload/1/17/Cardback.jpg - Japanese Pokémon:
https://archives.bulbagarden.net/media/upload/2/2a/TCG_Card_Back_Japanese.jpg - Yu-Gi-Oh!: Yugipedia English TCG back — try
https://ms.yugipedia.com/thumb/e/e5/Back-EN.png/250px-Back-EN.png, thenhttps://ms.yugipedia.com/e/e5/Back-EN.png; if both fail, load<exeDir>/assets/ygo_card_back.png(shipped fromui_wx/assets/ygo_card_back.pngat link time).fallbackImageUrlForGame(Game::YuGiOh)returns the thumbnail URL for helpers that only consult a single string. - Digimon (Digi-Battle): no stable public back URL; load
<exeDir>/assets/digibattle99_card_back.png(shipped fromui_wx/assets/digibattle99_card_back.pngat link time).
If a game module does not provide a preview source (cardPreviewSource() == nullptr), preview registration is skipped and the UI behaves as "no remote preview API available."
Error Surface And Debugging Intent
All source types return Result<T, std::string> errors so failures cross boundaries without exceptions. In practice, this keeps failures debuggable by separating:
- info API failures (bad set payload, schema mismatch, endpoint/network failure), and
- asset API failures (query mismatch, no matching card, missing image fields, image download failure).
When previews fail, verify request construction first (name sanitization, number normalization, percent encoding), then verify response shape assumptions: Scryfall (data, image_uris), Pokemon West (GET /v2/cards/{setId}-{number} → data object, or search data[]; images.large/images.small; auto-detect also needs name, number, rarity, and set.id on each matching row), Yu-Gi-Oh! Yugipedia (query.pages.<id>.imageinfo[0].url per filename, missing files tagged "missing": ""), Yu-Gi-Oh! YGOPRODeck fallback (data, name, card_images), Digi-Battle digimoncard.io (top-level array with name/id/set_name; CDN images.digimoncard.io/images/cards/{id}.jpg), Japanese Pokémon TCGdex (image base + /high.png; set-detail cards[] with localId). If the UI fallback path succeeds (network card-back and/or bundled PNG), the panel shows the card-back image and the inline label (image preview unavailable); only if every fallback fails does the preview stay empty with status text.
For Yu-Gi-Oh! specifically, when a printing shows the wrong art compared with Yugipedia’s gallery, debug in this order: (1) verify the candidate list via YuGiOhCardPreviewSource::buildCandidateFilenames(...) against the actual file names on Yugipedia’s Card_Gallery:<Card> page; (2) confirm the dialog rarity name maps to the expected short code in ygoRarityShortCode(...) / rarityCodeFor(...) (extend the mapping when a new rarity surfaces); (3) confirm the firstEdition flag matches the printed edition stamp — the candidate ordering puts the printed edition first.