Minor: Add Asian Pokemon Card Support (#18)

This commit is contained in:
Sebastian Dine
2026-07-22 11:13:42 +02:00
committed by GitHub
parent e5c830e945
commit c9e6bc2b6b
87 changed files with 78068 additions and 162 deletions
+5 -4
View File
@@ -5,14 +5,14 @@ The `ccm` executable — composition root only. The single place where concrete
## File pointers
- `main.cpp` — the entire app. Defines `CcmApp : public wxApp`, builds the dependency graph in `OnInit()`, then hands an `AppContext` to `MainFrame`.
- `CMakeLists.txt` — declares the `ccm` target. Sets `WIN32_EXECUTABLE TRUE` on Windows so no console window appears. Links `ccm_core`, `ccm_ui_wx`, `ccm_warnings`. **`POST_BUILD`**: creates `$<TARGET_FILE_DIR:ccm>/assets/` and copies `ui_wx/assets/ygo_card_back.png` and `ui_wx/assets/digibattle99_card_back.png` there so Yu-Gi-Oh! / Digi-Battle preview fallbacks work offline (see `BaseSelectedCardPanel` / `docs/assets-and-info-apis.md`).
- `CMakeLists.txt` — declares the `ccm` target. Sets `WIN32_EXECUTABLE TRUE` on Windows so no console window appears. Links `ccm_core`, `ccm_ui_wx`, `ccm_warnings`. **`POST_BUILD`**: creates `$<TARGET_FILE_DIR:ccm>/assets/` and copies `ui_wx/assets/ygo_card_back.png`, `ui_wx/assets/digibattle99_card_back.png`, and `ui_wx/assets/pokemon_jp_en_catalog.json` there so Yu-Gi-Oh! / Digi-Battle preview fallbacks and Japanese Pokémon EN names work offline (see `BaseSelectedCardPanel` / `docs/assets-and-info-apis.md`).
## Conventions
1. **Composition root is the only place** that names concrete adapters: `StdFileSystem`, `CprHttpClient`, `JsonCollectionRepository<MagicCard>`, `JsonCollectionRepository<PokemonCard>`, `JsonCollectionRepository<YuGiOhCard>`, `JsonCollectionRepository<DigiBattle99Card>`, `JsonSetRepository`, `LocalImageStore`, `LocalPreviewByteCache`, `MagicGameModule`, `PokemonGameModule`, `YuGiOhGameModule`, `DigiBattle99GameModule`, `MagicGameView`, `PokemonGameView`, `YuGiOhGameView`, `DigiBattle99GameView`, etc. If a concrete adapter type appears anywhere else in the codebase, move the wiring here.
1. **Composition root is the only place** that names concrete adapters: `StdFileSystem`, `CprHttpClient`, `JsonCollectionRepository<MagicCard>`, `JsonCollectionRepository<PokemonCard>`, `JsonCollectionRepository<YuGiOhCard>`, `JsonCollectionRepository<DigiBattle99Card>`, `JsonSetRepository`, `LocalImageStore`, `LocalPreviewByteCache`, `MagicGameModule`, `PokemonGameModule`, `JapanesePokemonGameModule` (Asia sets/preview backend for unified Pokemon), `YuGiOhGameModule`, `DigiBattle99GameModule`, `MagicGameView`, `PokemonGameView`, `YuGiOhGameView`, `DigiBattle99GameView`, etc. If a concrete adapter type appears anywhere else in the codebase, move the wiring here.
2. **Member declaration order in `CcmApp` matters** — destruction is reverse, so a member that depends on another (e.g. `magicCollSvc_` depends on `magicRepo_` and `imgStore_`; `previewSvc_` depends on `http_` and is consumed by `ctx_`; `magicView_` depends on the typed `magicCollSvc_` and the shared services) must be declared **after** its deps. Do not reorder casually.
3. **Use `std::unique_ptr` for everything owned** by `CcmApp`. The `AppContext` then holds plain references into those owned objects, plus a vector of `IGameView*` raw pointers (the `unique_ptr<>`s for the views are the actual owners; the vector just describes the active set).
4. **Game-to-directory mapping** lives in `dirNameForGame(Game)` (anonymous namespace). When adding a new game, extend this function — it is wired into all three repositories (`JsonCollectionRepository`, `JsonSetRepository`, `LocalImageStore`).
4. **Game-to-directory mapping** lives in `dirNameForGame(Game)` (anonymous namespace). When adding a new game, extend this function — it is wired into all three repositories (`JsonCollectionRepository`, `JsonSetRepository`, `LocalImageStore`). Pokemon West (`Game::Pokemon`) and Asia (`Game::JapanesePokemon`) both map to `"pokemon"`; `JsonSetRepository` stores their set caches as `sets-west.json` / `sets-asia.json` in that directory (other games keep `sets.json`).
5. **`config.json` location** is the executable's parent directory, resolved via `wxStandardPaths::Get().GetExecutablePath()`. Do not change this — existing installations rely on that location.
6. **Image format handlers** must be registered via `wxImage::AddHandler(new wxPNGHandler)` and `new wxJPEGHandler` before any image is loaded. They are added in `OnInit()` first thing — keep it that way.
7. **Card preview source ownership** lives inside the `IGameModule`. The composition root never constructs an `<Name>CardPreviewSource` directly; it calls `previewSvc_->registerModule(*<name>Mod_)` and the service pulls the module's preview source via `IGameModule::cardPreviewSource()` (returning `nullptr` is silently skipped).
@@ -21,7 +21,8 @@ The `ccm` executable — composition root only. The single place where concrete
## Required follow-ups
- The **`POST_BUILD` copy of `ygo_card_back.png` / `digibattle99_card_back.png`** must stay in sync with `ui_wx/assets/`; if you relocate install layout or add more bundled assets, mirror the pattern (`make_directory` + `copy_if_different`) and document under `docs/assets-and-info-apis.md` / `ui_wx/AGENTS.md`.
- The **`POST_BUILD` copy of `ygo_card_back.png` / `digibattle99_card_back.png` / `pokemon_jp_en_catalog.json`** must stay in sync with `ui_wx/assets/`; if you relocate install layout or add more bundled assets, mirror the pattern (`make_directory` + `copy_if_different`) and document under `docs/assets-and-info-apis.md` / `ui_wx/AGENTS.md`.
- On MinGW-w64 Windows, POST_BUILD also copies `libstdc++-6.dll` / `libgcc_s_seh-1.dll` / `libwinpthread-1.dll` from the compiler directory into `$<TARGET_FILE_DIR:ccm>` so the exe does not load a mismatched runtime from `PATH`.
- After adding a new game module you **must**: (1) add a `unique_ptr<<Name>GameModule>` member in declaration-order-correct position, (2) construct it in `OnInit()`, (3) call `setSvc_->registerModule(<name>Mod_.get())`, (4) call `previewSvc_->registerModule(*<name>Mod_)` (no-op when the module has no preview source), (5) extend `dirNameForGame`, (6) add a typed `JsonCollectionRepository<<Name>Card>` + `CollectionService<<Name>Card>` if the game has a custom card type, (7) construct a `<Name>GameView` and append its raw pointer to the `AppContext::gameViews` vector, (8) make sure the view's `unique_ptr<>` member sits **after** all its deps (typed services + `IGameModule`).
- After adding a new core service you **must** add a `unique_ptr<...>` member, construct it in `OnInit()` after its deps, and add a reference field to `AppContext`.
- After adding a new dependency edge you **must** verify destruction order is still correct: deps **before** dependents in the member list.
+27 -3
View File
@@ -19,13 +19,37 @@ target_link_libraries(ccm
ccm_warnings
)
# Yu-Gi-Oh! / Digi-Battle preview fallback images (used when network card-back
# URLs fail or no public URL exists).
# Yu-Gi-Oh! / Digi-Battle preview fallback images and the Japanese Pokémon
# EN name catalog (used when network card-back URLs fail or no public URL
# exists / for JP English display names).
add_custom_command(TARGET ccm POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:ccm>/assets"
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:ccm>/assets/pokemon_jp_classic"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_SOURCE_DIR}/ui_wx/assets/ygo_card_back.png"
"$<TARGET_FILE_DIR:ccm>/assets/ygo_card_back.png"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_SOURCE_DIR}/ui_wx/assets/digibattle99_card_back.png"
"$<TARGET_FILE_DIR:ccm>/assets/digibattle99_card_back.png")
"$<TARGET_FILE_DIR:ccm>/assets/digibattle99_card_back.png"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_SOURCE_DIR}/ui_wx/assets/pokemon_jp_en_catalog.json"
"$<TARGET_FILE_DIR:ccm>/assets/pokemon_jp_en_catalog.json"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/ui_wx/assets/pokemon_jp_classic"
"$<TARGET_FILE_DIR:ccm>/assets/pokemon_jp_classic")
# MinGW-w64: ship the toolchain runtime next to ccm3.exe so Explorer / IDE
# launches do not pick a mismatched libstdc++ off PATH (symptoms: Entry Point
# Not Found for __emutls_v._ZSt11__once_call in libcpr.dll).
if(WIN32 AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
get_filename_component(_ccm_mingw_bin "${CMAKE_CXX_COMPILER}" DIRECTORY)
foreach(_ccm_rt_dll IN ITEMS libstdc++-6.dll libgcc_s_seh-1.dll libwinpthread-1.dll)
if(EXISTS "${_ccm_mingw_bin}/${_ccm_rt_dll}")
add_custom_command(TARGET ccm POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_ccm_mingw_bin}/${_ccm_rt_dll}"
"$<TARGET_FILE_DIR:ccm>/${_ccm_rt_dll}"
VERBATIM)
endif()
endforeach()
endif()
+27 -5
View File
@@ -9,6 +9,8 @@
#include "ccm/games/digibattle99/DigiBattle99GameModule.hpp"
#include "ccm/games/magic/MagicGameModule.hpp"
#include "ccm/games/pokemon/PokemonGameModule.hpp"
#include "ccm/games/pokemonjp/JapanesePokemonEnCatalog.hpp"
#include "ccm/games/pokemonjp/JapanesePokemonGameModule.hpp"
#include "ccm/games/yugioh/YuGiOhGameModule.hpp"
#include "ccm/infra/CprHttpClient.hpp"
#include "ccm/infra/JsonCollectionRepository.hpp"
@@ -45,10 +47,11 @@ namespace {
// need for the repositories to know about concrete game module classes.
std::string dirNameForGame(ccm::Game g) {
switch (g) {
case ccm::Game::Magic: return "magic";
case ccm::Game::Pokemon: return "pokemon";
case ccm::Game::YuGiOh: return "yugioh";
case ccm::Game::DigiBattle99: return "digibattle99";
case ccm::Game::Magic: return "magic";
case ccm::Game::Pokemon: return "pokemon";
case ccm::Game::YuGiOh: return "yugioh";
case ccm::Game::DigiBattle99: return "digibattle99";
case ccm::Game::JapanesePokemon: return "pokemon";
}
return "magic";
}
@@ -86,6 +89,17 @@ public:
ygoMod_ = std::make_unique<ccm::YuGiOhGameModule>(*http_);
digiBattle99Mod_ = std::make_unique<ccm::DigiBattle99GameModule>(*http_);
ccm::JapanesePokemonEnCatalog jpCatalog;
{
const auto catalogPath = exeDir / "assets" / "pokemon_jp_en_catalog.json";
if (auto text = fs_->readText(catalogPath); text) {
if (auto parsed = ccm::JapanesePokemonEnCatalog::parse(text.value()); parsed) {
jpCatalog = std::move(parsed).value();
}
}
}
jpPokeMod_ = std::make_unique<ccm::JapanesePokemonGameModule>(*http_, std::move(jpCatalog));
magicRepo_ = std::make_unique<ccm::JsonCollectionRepository<ccm::MagicCard>>(
*fs_, *config_, &dirNameForGame);
pokeRepo_ = std::make_unique<ccm::JsonCollectionRepository<ccm::PokemonCard>>(
@@ -113,6 +127,7 @@ public:
setSvc_->registerModule(pokeMod_.get());
setSvc_->registerModule(ygoMod_.get());
setSvc_->registerModule(digiBattle99Mod_.get());
setSvc_->registerModule(jpPokeMod_.get());
// Disk-backed preview cache lives next to the executable, in the same
// location scope as config.json - NOT inside the user's data-storage
@@ -128,11 +143,16 @@ public:
previewCache_ = std::make_unique<ccm::LocalPreviewByteCache>(
*fs_,
exeDir / ".cache" / "preview-cache");
previewSvc_ = std::make_unique<ccm::CardPreviewService>(*http_, previewCache_.get());
previewSvc_ = std::make_unique<ccm::CardPreviewService>(
*http_,
previewCache_.get(),
fs_.get(),
exeDir / "assets");
previewSvc_->registerModule(*magicMod_);
previewSvc_->registerModule(*pokeMod_);
previewSvc_->registerModule(*ygoMod_);
previewSvc_->registerModule(*digiBattle99Mod_);
previewSvc_->registerModule(*jpPokeMod_);
// Per-game UI bundles. Order here is the order shown in the Game menu.
magicView_ = std::make_unique<ccm::ui::MagicGameView>(
@@ -154,6 +174,7 @@ public:
*pokeMod_,
*ygoMod_,
*digiBattle99Mod_,
*jpPokeMod_,
{ magicView_.get(), pokeView_.get(), ygoView_.get(), digiBattle99View_.get() },
});
@@ -176,6 +197,7 @@ private:
std::unique_ptr<ccm::PokemonGameModule> pokeMod_;
std::unique_ptr<ccm::YuGiOhGameModule> ygoMod_;
std::unique_ptr<ccm::DigiBattle99GameModule> digiBattle99Mod_;
std::unique_ptr<ccm::JapanesePokemonGameModule> jpPokeMod_;
std::unique_ptr<ccm::JsonCollectionRepository<ccm::MagicCard>> magicRepo_;
std::unique_ptr<ccm::JsonCollectionRepository<ccm::PokemonCard>> pokeRepo_;
std::unique_ptr<ccm::JsonCollectionRepository<ccm::YuGiOhCard>> ygoRepo_;