mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-31 01:48:50 +00:00
Minor: Add Asian Pokemon Card Support (#18)
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -20,6 +21,12 @@ enum class Game {
|
||||
Pokemon,
|
||||
YuGiOh,
|
||||
DigiBattle99,
|
||||
JapanesePokemon, // internal Asia sets/preview routing; not in allGames()
|
||||
};
|
||||
|
||||
enum class PokemonRegion {
|
||||
West,
|
||||
Asia,
|
||||
};
|
||||
|
||||
enum class Language {
|
||||
@@ -28,8 +35,10 @@ enum class Language {
|
||||
French,
|
||||
Spanish,
|
||||
Italian,
|
||||
Chinese,
|
||||
SimplifiedChinese, // JSON / display: "S-Chinese" (legacy "Chinese" accepted)
|
||||
TraditionalChinese, // JSON / display: "T-Chinese"
|
||||
Japanese,
|
||||
Korean,
|
||||
Russian,
|
||||
};
|
||||
|
||||
@@ -49,24 +58,34 @@ enum class Theme {
|
||||
};
|
||||
|
||||
std::string_view to_string(Game g) noexcept;
|
||||
std::string_view to_string(PokemonRegion r) noexcept;
|
||||
std::string_view to_string(Language l) noexcept;
|
||||
std::string_view to_string(Condition c) noexcept;
|
||||
std::string_view to_string(Theme t) noexcept;
|
||||
|
||||
std::optional<Game> gameFromString(std::string_view s) noexcept;
|
||||
std::optional<Language> languageFromString(std::string_view s) noexcept;
|
||||
std::optional<Condition> conditionFromString(std::string_view s) noexcept;
|
||||
std::optional<Theme> themeFromString(std::string_view s) noexcept;
|
||||
std::optional<Game> gameFromString(std::string_view s) noexcept;
|
||||
std::optional<PokemonRegion> pokemonRegionFromString(std::string_view s) noexcept;
|
||||
std::optional<Language> languageFromString(std::string_view s) noexcept;
|
||||
std::optional<Condition> conditionFromString(std::string_view s) noexcept;
|
||||
std::optional<Theme> themeFromString(std::string_view s) noexcept;
|
||||
|
||||
// User-facing games (Game menu / Settings). JapanesePokemon is internal-only.
|
||||
const std::array<Game, 4>& allGames() noexcept;
|
||||
const std::array<Language, 8>& allLanguages() noexcept;
|
||||
const std::array<Language, 10>& allLanguages() noexcept;
|
||||
const std::array<Condition, 7>& allConditions() noexcept;
|
||||
const std::array<Theme, 2>& allThemes() noexcept;
|
||||
|
||||
[[nodiscard]] std::span<const Language> languagesForPokemonRegion(PokemonRegion r) noexcept;
|
||||
[[nodiscard]] Game pokemonBackendGame(PokemonRegion r) noexcept;
|
||||
[[nodiscard]] Language defaultLanguageForPokemonRegion(PokemonRegion r) noexcept;
|
||||
|
||||
// nlohmann/json hooks - serialize as plain strings, matching Rust serde.
|
||||
void to_json(nlohmann::json& j, Game v);
|
||||
void from_json(const nlohmann::json& j, Game& v);
|
||||
|
||||
void to_json(nlohmann::json& j, PokemonRegion v);
|
||||
void from_json(const nlohmann::json& j, PokemonRegion& v);
|
||||
|
||||
void to_json(nlohmann::json& j, Language v);
|
||||
void from_json(const nlohmann::json& j, Language& v);
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
// JapanesePokemonCard - Japanese Pokémon TCG collection model.
|
||||
// Pokémon-shaped field set (setNo / holo / firstEdition / signed / altered).
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/domain/Set.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
struct JapanesePokemonCard {
|
||||
std::uint32_t id{0};
|
||||
std::uint8_t amount{1};
|
||||
std::string name;
|
||||
Set set;
|
||||
std::string setNo;
|
||||
std::string note;
|
||||
std::vector<std::string> images;
|
||||
Language language{Language::Japanese};
|
||||
Condition condition{Condition::NearMint};
|
||||
bool firstEdition{false};
|
||||
bool holo{false};
|
||||
bool signed_{false};
|
||||
bool altered{false};
|
||||
|
||||
friend bool operator==(const JapanesePokemonCard&, const JapanesePokemonCard&) = default;
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json& j, const JapanesePokemonCard& c);
|
||||
void from_json(const nlohmann::json& j, JapanesePokemonCard& c);
|
||||
|
||||
} // namespace ccm
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
// PokemonCard - faithful port of pokemon/card_services.rs::Card.
|
||||
// Same established JSON shape (with `setNo` and `firstEdition` aliases).
|
||||
// Same established JSON shape (with `setNo` and `firstEdition` aliases),
|
||||
// plus `region` (West/Asia) for unified West+Asia collections.
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/domain/Set.hpp"
|
||||
@@ -28,6 +29,7 @@ struct PokemonCard {
|
||||
bool holo{false};
|
||||
bool signed_{false};
|
||||
bool altered{false};
|
||||
PokemonRegion region{PokemonRegion::West};
|
||||
|
||||
friend bool operator==(const PokemonCard&, const PokemonCard&) = default;
|
||||
};
|
||||
|
||||
@@ -23,6 +23,10 @@ public:
|
||||
// Implementations return a vector that has already been filtered
|
||||
// (e.g. no digital-only sets) and sorted by release date ascending.
|
||||
virtual Result<std::vector<Set>> fetchAll() = 0;
|
||||
|
||||
// Optional post-process for locally cached set lists (e.g. inject products
|
||||
// the upstream API omits). Default is a no-op. Called by SetService::getSets.
|
||||
virtual void augmentCachedSets(std::vector<Set>& /*sets*/) const {}
|
||||
};
|
||||
|
||||
class IGameModule {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
// JapanesePokemonCardPreviewSource: TCGdex ja localId-based preview + variants.
|
||||
// Image URLs use /high.png (wxImage decodes PNG/JPEG, not webp).
|
||||
|
||||
#include "ccm/games/pokemonjp/JapanesePokemonEnCatalog.hpp"
|
||||
#include "ccm/ports/ICardPreviewSource.hpp"
|
||||
#include "ccm/ports/IHttpClient.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
class JapanesePokemonCardPreviewSource final : public ICardPreviewSource {
|
||||
public:
|
||||
JapanesePokemonCardPreviewSource(IHttpClient& http,
|
||||
const JapanesePokemonEnCatalog& catalog);
|
||||
|
||||
[[nodiscard]] bool supportsAutoDetectPrint() const noexcept override { return true; }
|
||||
|
||||
Result<std::string, PreviewLookupError>
|
||||
fetchImageUrl(std::string_view name,
|
||||
std::string_view setId,
|
||||
std::string_view setNo) override;
|
||||
Result<AutoDetectedPrint> detectFirstPrint(std::string_view name,
|
||||
std::string_view setId) override;
|
||||
Result<std::vector<AutoDetectedPrint>> detectPrintVariants(std::string_view name,
|
||||
std::string_view setId) override;
|
||||
|
||||
static std::string normalizeLocalId(std::string_view setNo);
|
||||
static std::string buildSetDetailUrl(std::string_view setId);
|
||||
static std::string buildCardUrl(std::string_view setId, std::string_view localId);
|
||||
static std::string imageUrlFromBase(std::string_view imageBase);
|
||||
|
||||
// Parse set-detail body; optionally filter by name (EN catalog / JA) and/or localId.
|
||||
struct SetCardRow {
|
||||
std::string localId;
|
||||
std::string nameJa;
|
||||
std::string imageBase; // empty when TCGdex has no scan
|
||||
std::string rarity;
|
||||
};
|
||||
|
||||
static Result<std::vector<SetCardRow>, PreviewLookupError>
|
||||
parseSetCards(const std::string& body);
|
||||
|
||||
static Result<std::string, PreviewLookupError>
|
||||
parseCardImageUrl(const std::string& body);
|
||||
|
||||
static Result<std::vector<AutoDetectedPrint>>
|
||||
parsePrintVariants(const std::string& body,
|
||||
std::string_view setId,
|
||||
std::string_view wantedCardName,
|
||||
const JapanesePokemonEnCatalog& catalog);
|
||||
|
||||
// Catalog-only Auto-detect when TCGdex has no set detail (theme decks, etc.).
|
||||
static Result<std::vector<AutoDetectedPrint>>
|
||||
detectPrintVariantsFromCatalog(std::string_view setId,
|
||||
std::string_view wantedCardName,
|
||||
const JapanesePokemonEnCatalog& catalog);
|
||||
|
||||
private:
|
||||
IHttpClient& http_;
|
||||
const JapanesePokemonEnCatalog& catalog_;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
// JapanesePokemonEnCatalog - bundled English name layer for Japanese Pokémon.
|
||||
// Loaded from assets/pokemon_jp_en_catalog.json (generated offline). Missing
|
||||
// entries fall through to TCGdex Japanese names at runtime.
|
||||
|
||||
#include "ccm/util/Result.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
struct JapanesePokemonSetEnInfo {
|
||||
std::string nameEn;
|
||||
std::string nameJa;
|
||||
std::string releaseDate; // YYYY/MM/DD when known; may be empty
|
||||
};
|
||||
|
||||
struct JapanesePokemonPrintEnInfo {
|
||||
std::string setId;
|
||||
std::string localId;
|
||||
std::string nameEn;
|
||||
std::string nameJa;
|
||||
std::string nameEnSource; // bulbapedia | species-table | manual
|
||||
// Classic JA gap-fill when TCGdex has no CDN scan (optional).
|
||||
std::string imageUrl; // explicit HTTPS URL, preferred when set
|
||||
std::string tcgplayerId; // TCGPlayer product id → product-images CDN
|
||||
};
|
||||
|
||||
class JapanesePokemonEnCatalog {
|
||||
public:
|
||||
[[nodiscard]] static Result<JapanesePokemonEnCatalog>
|
||||
parse(const std::string& jsonBody);
|
||||
|
||||
[[nodiscard]] bool empty() const noexcept {
|
||||
return sets_.empty() && printsByKey_.empty();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<JapanesePokemonSetEnInfo>
|
||||
findSet(std::string_view setId) const;
|
||||
|
||||
[[nodiscard]] std::optional<JapanesePokemonPrintEnInfo>
|
||||
findPrint(std::string_view setId, std::string_view localId) const;
|
||||
|
||||
// Case-insensitive match of nameEn or nameJa within a set.
|
||||
// Also matches qualified English titles: wanted "Mewtwo" hits
|
||||
// "Mewtwo (CoroCoro promo)" (prefix + " (").
|
||||
[[nodiscard]] std::vector<JapanesePokemonPrintEnInfo>
|
||||
findPrintsByName(std::string_view setId, std::string_view cardName) const;
|
||||
|
||||
[[nodiscard]] bool hasPrintsForSet(std::string_view setId) const noexcept;
|
||||
|
||||
// TCGPlayer product-image CDN URL for classic JA gap-fill.
|
||||
[[nodiscard]] static std::string tcgplayerImageUrl(std::string_view productId);
|
||||
|
||||
// Prefer imageUrl; else build from tcgplayerId; else empty.
|
||||
[[nodiscard]] static std::string previewImageUrlFromPrint(
|
||||
const JapanesePokemonPrintEnInfo& print);
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, JapanesePokemonSetEnInfo> sets_;
|
||||
std::unordered_map<std::string, JapanesePokemonPrintEnInfo> printsByKey_;
|
||||
// setId -> print keys for name scans
|
||||
std::unordered_map<std::string, std::vector<std::string>> printKeysBySet_;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
// JapanesePokemonGameModule: Japanese Pokémon TCG via TCGdex ja + EN catalog.
|
||||
|
||||
#include "ccm/games/IGameModule.hpp"
|
||||
#include "ccm/games/pokemonjp/JapanesePokemonCardPreviewSource.hpp"
|
||||
#include "ccm/games/pokemonjp/JapanesePokemonEnCatalog.hpp"
|
||||
#include "ccm/games/pokemonjp/JapanesePokemonSetSource.hpp"
|
||||
|
||||
namespace ccm {
|
||||
|
||||
class JapanesePokemonGameModule final : public IGameModule {
|
||||
public:
|
||||
explicit JapanesePokemonGameModule(IHttpClient& http,
|
||||
JapanesePokemonEnCatalog catalog = {});
|
||||
|
||||
[[nodiscard]] Game id() const noexcept override { return Game::JapanesePokemon; }
|
||||
[[nodiscard]] std::string dirName() const override { return "pokemon"; }
|
||||
[[nodiscard]] std::string displayName() const override { return "Pokemon (Japan)"; }
|
||||
|
||||
ISetSource& setSource() override { return setSource_; }
|
||||
ICardPreviewSource* cardPreviewSource() noexcept override { return &previewSource_; }
|
||||
|
||||
[[nodiscard]] const JapanesePokemonEnCatalog& catalog() const noexcept {
|
||||
return catalog_;
|
||||
}
|
||||
|
||||
private:
|
||||
JapanesePokemonEnCatalog catalog_;
|
||||
JapanesePokemonSetSource setSource_;
|
||||
JapanesePokemonCardPreviewSource previewSource_;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
// JapanesePokemonSetSource: TCGdex ja set list + per-set detail for release
|
||||
// dates. English display names come from JapanesePokemonEnCatalog when present.
|
||||
|
||||
#include "ccm/games/IGameModule.hpp"
|
||||
#include "ccm/games/pokemonjp/JapanesePokemonEnCatalog.hpp"
|
||||
#include "ccm/ports/IHttpClient.hpp"
|
||||
|
||||
namespace ccm {
|
||||
|
||||
class JapanesePokemonSetSource final : public ISetSource {
|
||||
public:
|
||||
static constexpr const char* kListEndpoint = "https://api.tcgdex.net/v2/ja/sets";
|
||||
|
||||
JapanesePokemonSetSource(IHttpClient& http, const JapanesePokemonEnCatalog& catalog);
|
||||
|
||||
Result<std::vector<Set>> fetchAll() override;
|
||||
|
||||
void augmentCachedSets(std::vector<Set>& sets) const override;
|
||||
|
||||
// Pure parsers for hermetic tests.
|
||||
static Result<std::vector<Set>> parseListResponse(const std::string& body);
|
||||
static Result<std::string> parseReleaseDate(const std::string& detailBody);
|
||||
static bool shouldExcludeSetId(std::string_view setId) noexcept;
|
||||
static std::string applySetNameOverride(std::string_view setId,
|
||||
std::string nameJa);
|
||||
static std::string rewriteReleaseDate(std::string_view isoDate);
|
||||
static std::string buildSetDetailUrl(std::string_view setId);
|
||||
|
||||
// Original-era theme decks / sheets omitted by TCGdex JA. Idempotent by id.
|
||||
static void appendMissingClassicProducts(std::vector<Set>& sets);
|
||||
|
||||
private:
|
||||
IHttpClient& http_;
|
||||
const JapanesePokemonEnCatalog& catalog_;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
// JsonSetRepository: persists vector<Set> to `<dataStorage>/<game>/sets.json`.
|
||||
// JsonSetRepository: persists vector<Set> under `<dataStorage>/<dirName>/`.
|
||||
// Most games use `sets.json`. Pokemon West/Asia share dir `pokemon` with
|
||||
// `sets-west.json` / `sets-asia.json` (migrate-on-load from legacy paths).
|
||||
|
||||
#include "ccm/games/IGameModule.hpp"
|
||||
#include "ccm/ports/IFileSystem.hpp"
|
||||
@@ -27,6 +29,8 @@ private:
|
||||
DirNameFn dirName_;
|
||||
|
||||
[[nodiscard]] std::filesystem::path setsPath(Game game) const;
|
||||
[[nodiscard]] std::filesystem::path legacySetsPath(Game game) const;
|
||||
[[nodiscard]] Result<std::vector<Set>> parseSetsText(const std::string& text) const;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
// ISetRepository - persistence port for the cached `sets.json` of a game.
|
||||
// ISetRepository - persistence port for the cached set list of a game.
|
||||
// Typical layout: `<dataStorage>/<dirName>/sets.json`. Pokemon West/Asia use
|
||||
// `sets-west.json` / `sets-asia.json` under the shared `pokemon/` directory.
|
||||
// Stored as a flat list to mirror the original Rust file layout.
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
// `.includes("")` returns true.
|
||||
|
||||
#include "ccm/domain/DigiBattle99Card.hpp"
|
||||
#include "ccm/domain/JapanesePokemonCard.hpp"
|
||||
#include "ccm/domain/MagicCard.hpp"
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
#include "ccm/domain/YuGiOhCard.hpp"
|
||||
@@ -35,7 +36,7 @@ namespace ccm {
|
||||
std::string_view filter);
|
||||
|
||||
// Pokemon value-key columns from PokemonTable.tsx tableFields list:
|
||||
// name, set.name, setNo, language, condition, amount, note.
|
||||
// name, set.name, setNo, language, condition, amount, note, region.
|
||||
// Holo/FirstEdition/Signed/Altered are bool-typed and excluded.
|
||||
[[nodiscard]] bool matchesPokemonFilter(const PokemonCard& card,
|
||||
std::string_view filter);
|
||||
@@ -46,4 +47,8 @@ namespace ccm {
|
||||
[[nodiscard]] bool matchesDigiBattle99Filter(const DigiBattle99Card& card,
|
||||
std::string_view filter);
|
||||
|
||||
// Japanese Pokemon mirrors Pokemon searchable columns (includes setNo).
|
||||
[[nodiscard]] bool matchesJapanesePokemonFilter(const JapanesePokemonCard& card,
|
||||
std::string_view filter);
|
||||
|
||||
} // namespace ccm
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/games/IGameModule.hpp"
|
||||
#include "ccm/ports/ICardPreviewSource.hpp"
|
||||
#include "ccm/ports/IFileSystem.hpp"
|
||||
#include "ccm/ports/IHttpClient.hpp"
|
||||
#include "ccm/ports/IPreviewByteCache.hpp"
|
||||
#include "ccm/util/Result.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -32,7 +34,9 @@ namespace ccm {
|
||||
class CardPreviewService {
|
||||
public:
|
||||
explicit CardPreviewService(IHttpClient& http,
|
||||
IPreviewByteCache* persistentCache = nullptr);
|
||||
IPreviewByteCache* persistentCache = nullptr,
|
||||
IFileSystem* fs = nullptr,
|
||||
std::filesystem::path assetRoot = {});
|
||||
|
||||
// Register a game module's preview source. Calling this with a module
|
||||
// whose `cardPreviewSource()` returns nullptr is a no-op (the game has
|
||||
@@ -96,6 +100,9 @@ private:
|
||||
|
||||
Result<std::string> fetchAndCache(const std::string& cacheKey,
|
||||
std::string_view url);
|
||||
Result<std::string, PreviewLookupError> fetchAssetAndCache(
|
||||
const std::string& cacheKey,
|
||||
std::string_view assetUrl);
|
||||
|
||||
// Returns the kind of in-memory cache entry for `key`. On Hit the
|
||||
// payload is copied into `outPayload`; on NegativeHit `outPayload` is
|
||||
@@ -107,6 +114,8 @@ private:
|
||||
|
||||
IHttpClient& http_;
|
||||
IPreviewByteCache* persistentCache_{nullptr};
|
||||
IFileSystem* fs_{nullptr};
|
||||
std::filesystem::path assetRoot_;
|
||||
std::unordered_map<Game, ICardPreviewSource*> sources_;
|
||||
|
||||
// LRU: list holds entries in MRU-first order; map points at list nodes
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// (e.g. sort by name, then by set => grouped by set, name-sorted within each).
|
||||
|
||||
#include "ccm/domain/DigiBattle99Card.hpp"
|
||||
#include "ccm/domain/JapanesePokemonCard.hpp"
|
||||
#include "ccm/domain/MagicCard.hpp"
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
#include "ccm/domain/YuGiOhCard.hpp"
|
||||
@@ -81,6 +82,20 @@ enum class DigiBattle99SortColumn {
|
||||
Note,
|
||||
};
|
||||
|
||||
// Japanese Pokemon mirrors Pokemon columns.
|
||||
enum class JapanesePokemonSortColumn {
|
||||
Name,
|
||||
SetReleaseDate,
|
||||
Language,
|
||||
Condition,
|
||||
Amount,
|
||||
Holo,
|
||||
FirstEdition,
|
||||
Signed,
|
||||
Altered,
|
||||
Note,
|
||||
};
|
||||
|
||||
// Stable in-place sort. `ascending=false` runs the same comparator with
|
||||
// inverted sign, matching `byField(field, asc)` semantics.
|
||||
void sortMagicCards(std::vector<MagicCard>& cards, MagicSortColumn column,
|
||||
@@ -92,5 +107,8 @@ void sortYuGiOhCards(std::vector<YuGiOhCard>& cards, YuGiOhSortColumn column,
|
||||
void sortDigiBattle99Cards(std::vector<DigiBattle99Card>& cards,
|
||||
DigiBattle99SortColumn column,
|
||||
bool ascending);
|
||||
void sortJapanesePokemonCards(std::vector<JapanesePokemonCard>& cards,
|
||||
JapanesePokemonSortColumn column,
|
||||
bool ascending);
|
||||
|
||||
} // namespace ccm
|
||||
|
||||
Reference in New Issue
Block a user