major: initial release

* initial development

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* pipeline

* ci/cd

* ci/cd

* ci/cd

* ci/cd

* ci/cd

* ci/cd

* ci/cd

* pokemon

* pokemon

* pokemon

* pokemon

* pokemon

* pokemon

* improvements

* improvements

* ci/cd

* ci/cd

* improvements

* improvements

* improvements

* improvements

* improvements

* improvements

* improvements

* improvements

* improvements

---------

Co-authored-by: sdine <sdine@sdine.com>
This commit is contained in:
Sebastian Dine
2026-05-09 11:05:47 +02:00
committed by GitHub
parent 13262fa015
commit 55ace147bc
149 changed files with 12611 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
# AGENTS.md
Long-form contributor documentation that lives outside the source tree.
## Files
- `adding-a-new-game.md` — the **canonical, end-to-end procedure** for adding a brand-new game module to the project. Covers required external resources (sets API, optional card-preview API, icons), domain type with stable JSON, set source + tests, optional card preview source + tests, `IGameModule` glue, `Game` enum + `dirNameForGame` wiring, deriving the three UI panel templates (`BaseCardListPanel<TCard, TSortColumn>`, `BaseCardEditDialog<TCard>`, `BaseSelectedCardPanel<TCard>`), the `IGameView` adapter, composition-root wiring in `app/main.cpp`, CMake additions, AGENTS.md updates, and the test checklist.
- `ci-cd-guide.md` — user-facing CI/CD and release procedure: feature/master workflow split, orchestrator + reusable platform workflow structure, versioning conventions, PR title guard for `master`, artifact naming, and a concrete "create a new release" checklist.
- `versioning.md` — dedicated versioning reference: branch+sha scheme for feature builds, semantic version bump rules for `master`, tag format, and embedded app version behavior.
- `windows-installer.md` — how the NSIS-based Windows installer (`scripts/installer.nsi`) is built and configured: the `APP_VERSION` define, where the version is surfaced (window title, branding, Programs and Features), installed sections, registry layout, and CI vs. local invocation.
- `dow-doc-build-locally.md` — complete local build/setup reference for Windows and Linux, including dependency management and troubleshooting.
- `intro-to-new-developers.md` — onboarding map for new contributors: architecture, folder responsibilities, guardrails, anti-patterns, and links to deeper docs.
- `testing-and-test-code-of-conduct.md` — testing workflow plus expected standards for writing and maintaining deterministic, hermetic, behavior-focused tests.
- `assets-and-info-apis.md` — reference for the external info APIs (set metadata) and asset APIs (card preview images) used by the Magic and Pokemon modules, plus the runtime flow through `SetService` / `CardPreviewService` and the error-surface conventions.
- `README.md` — index page that clusters docs by area and links to all documents in this directory.
## Subdirectories
- `assets/images/` — static screenshots and other binary assets referenced from the documentation (currently `demo-mtg.png`, `demo-pkm.png`). Keep filenames stable so cross-doc links don't break, and prefer compressed PNG/JPEG over uncompressed formats.
## Conventions
- These docs are reference material for contributors, not user-facing release notes. Keep them precise and dated implicitly by the source state they describe.
- Code examples should be C++20 and quote real file paths from the repo.
- Do **not** introduce dependencies on a specific upcoming feature, hypothetical game, or unmerged branch. The procedure must always describe the current code as it is on `main`.
## Required follow-ups
- After changing per-game seams in `core/` (e.g. `IGameModule`, `ISetSource`, `ICardPreviewSource`, `CollectionService`, `SetService`, `CardPreviewService`, `ImageService`) you **must** update `adding-a-new-game.md` to keep the canonical procedure in sync. The same applies to the UI seams (`IGameView`, `BaseCardListPanel`, `BaseCardEditDialog`, `BaseSelectedCardPanel`) and the composition-root wiring in `app/main.cpp`.
- After changing the Magic or Pokemon set/preview adapters (`MagicSetSource`, `MagicCardPreviewSource`, `PokemonSetSource`, `PokemonCardPreviewSource`) — endpoints, response parsing, name/number normalization, or the info-vs-asset split — you **must** update `assets-and-info-apis.md` so the API reference matches the live behavior.
- After bumping a key dependency (`nlohmann/json`, `cpr`, `wxWidgets`, `doctest`) in a way that changes a public API used in the guide's examples, update those examples.
- After adding a new file under `docs/` (or a new entry under `docs/assets/images/`) you **must** add it to the file list above **and** to `README.md` so the index stays complete.
- Do **not** rename, move, or split this file without first updating every other `AGENTS.md` that points at it (root, `core/`, `ui_wx/`, `app/`, `tests/`).
## Anti-patterns
- Don't sneak hypothetical or in-progress games into the doc as concrete examples; the guide is meant to be game-agnostic and must read as such.
- Don't link to the original Rust/Tauri repo as the source of truth — it is a historical reference, not the spec. The C++ code under `core/`, `ui_wx/`, and `app/` is the spec.
- Don't duplicate the per-package `AGENTS.md` content here; cross-link instead.
+26
View File
@@ -0,0 +1,26 @@
#documentation #contributors #ccm3
# Documentation Index
This folder contains contributor documentation for Card Collection Manager 3. Start with [Intro for New Developers](intro-to-new-developers.md) if you are new to the repository, then use the category sections below to navigate to specific tasks.
## CI/CD And Release
- [ci-cd-guide.md](ci-cd-guide.md): CI workflows, merge guards, artifacts, and release execution.
- [versioning.md](versioning.md): feature-build version format, semantic release rules on `master`, and tag/app-version behavior.
- [windows-installer.md](windows-installer.md): how the NSIS-based Windows installer is built, configured, and versioned.
## Build And Local Setup
- [dow-doc-build-locally.md](dow-doc-build-locally.md): local build setup for Windows/Linux, dependency model, build options, and troubleshooting.
## Onboarding & Development Workflows
- [intro-to-new-developers.md](intro-to-new-developers.md): architecture orientation, boundaries, common pitfalls, and first-week workflow guidance.
- [testing-and-test-code-of-conduct.md](testing-and-test-code-of-conduct.md): test workflow plus repository rules for deterministic, behavior-focused tests.
- [adding-a-new-game.md](adding-a-new-game.md): canonical end-to-end procedure for adding a new game module across `core/`, `ui_wx/`, and `app/`.
- [assets-and-info-apis.md](assets-and-info-apis.md): external info and asset APIs used by Magic/Pokemon modules and their runtime purpose.
+473
View File
@@ -0,0 +1,473 @@
#documentation #architecture #game-modules
# Adding A New Game To Card Collection Manager
This is the canonical end-to-end walkthrough for adding support for a new TCG to Card Collection Manager. The guide covers external API selection, core and UI integration, composition-root wiring, and required tests so a full implementation can land cleanly on `main`.
**Quick Setup:** gather required external resources first, then follow sections in order and run the full verification checklist before opening a PR.
The guide is prescriptive about file locations and seam shapes but game-agnostic in naming. Replace `<Name>` with your game type name and `<name>` with the lowercase key used for on-disk directories and `dirName()`. Both Magic and Pokemon follow this structure; use their implementations as references when needed.
Read the root `AGENTS.md`, `core/AGENTS.md`, `ui_wx/AGENTS.md`, `app/AGENTS.md`, and `tests/AGENTS.md` before starting. They define the architecture rules this guide is built on top of.
---
## 1. Prerequisites: pick your external resources
Before you write any C++, gather the following. The further along you discover that something is missing, the more work you throw away.
### 1.1 Set list API (required)
A public HTTP endpoint that returns the canonical set / expansion list for the game, with at least:
- a stable identifier (`id`) — used as the on-disk and JSON key. Must be stable across API revisions.
- a human-readable name.
- a release date — used to sort the set picker chronologically.
The endpoint must be callable without authentication, or you must accept a hard-coded API key (we do not currently expose a way to ask the user for one). It must support HTTPS. Plan for the response body to be JSON; we do not have an XML or CSV path.
The release date may be in any format **as long as you can rewrite it to `YYYY/MM/DD` during parsing**, because the `Set` domain type stores it that way (see `core/include/ccm/domain/Set.hpp`) and the rest of the code assumes lexicographic comparison sorts chronologically.
### 1.2 Card preview API (optional)
A public HTTP endpoint that returns the URL of a card's preview image given some lookup key (typically `name`, `set id`, and possibly a printed collector number). If the game does not expose one, the UI will simply skip the remote preview and only show locally-stored images — the implementation is allowed to omit this seam entirely.
A few traps to plan around now, before you write code:
- **Lookup precision.** Some APIs return many ambiguous matches when you query by name only and require the set id (and sometimes the collector number) to disambiguate. Decide up front which fields make a search reliable enough to take the first result.
- **URL encoding.** All query strings must be RFC 3986 percent-encoded before they reach `IHttpClient::get` (`cpr::Url` does **not** re-encode). The Magic/Pokemon implementations have a private `urlEncode` helper you can copy.
- **Collector-number normalization.** Pokemon stores `4/102` but the API only accepts `4`. Whichever convention your domain type uses, normalize it inside `buildSearchUrl` so the wire format is whatever the API actually expects. Mismatches here produce empty result sets, which then look identical to "no preview available" and are very tedious to debug.
### 1.3 Flag icons
Identify any boolean flag columns the game needs (Magic: `Foil`, `Signed`, `Altered`; Pokemon: `Holo`, `1. Edition`, `Signed`, `Altered`). For each one that doesn't already exist, plan an SVG glyph. SVG art with a single fillable path works best — see `ui_wx/src/SvgIcons.cpp` for the established style. Re-use existing glyphs across games where the meaning is identical (`Signed` and `Altered` are shared between Magic and Pokemon).
### 1.4 Domain shape decision
Decide whether the new game can re-use an existing card type or needs its own. Re-use is allowed when **every** field has identical semantics; in practice, every game we have shipped has needed its own type because at least one flag or extra column differs (e.g. Pokemon adds `setNo`, `holo`, `firstEdition`).
If you create a new type, freeze the JSON layout now. The root `AGENTS.md` rule is unambiguous: **JSON layout must stay byte-for-byte stable** once you ship. Pick names that match any pre-existing on-disk format (this app may inherit data from a previous tool), and decide which C++ field names need a JSON alias (the canonical example: the C++ field `signed_` maps to the JSON key `"signed"` because `signed` is a C++ keyword).
---
## 2. Core: domain types and the `Game` enum
Everything below this point assumes you have already gathered the resources from §1.
### 2.1 Extend the `Game` enum
Edit `core/include/ccm/domain/Enums.hpp`:
- Add a new enumerator to `enum class Game`.
- Update the size of `allGames()` (`std::array<Game, N>`).
Edit `core/src/domain/Enums.cpp`:
- Add a `case` to `to_string(Game)`.
- Add a branch to `gameFromString(std::string_view)`.
- Add the new enumerator to the `allGames()` constexpr array.
The `Game` enum is the only place in `core/` that hardcodes which games exist. Adding a new entry here is what makes the rest of the registries (`SetService`, `CardPreviewService`, `AppContext::gameViews`, `dirNameForGame`) accept it.
### 2.2 Add the card domain type (only if needed)
If you decided in §1.4 that an existing card type fits, skip this section.
Otherwise, create `core/include/ccm/domain/<Name>Card.hpp`:
- A `struct <Name>Card` with `id`, `amount`, `name`, `set`, `note`, `images`, `language`, `condition` (these seven fields are required — the UI templates assume them) and any game-specific extras.
- Forward-declare `to_json` and `from_json` for `nlohmann::json`.
- Add a defaulted `friend bool operator==(const <Name>Card&, const <Name>Card&) = default;` so the JSON round-trip test can compare values.
Then create `core/src/domain/<Name>Card.cpp`:
- Hand-roll `to_json` and `from_json` using `nlohmann::json`. **Do not** use `NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE` — the explicit form keeps JSON aliases visible and makes future stability bugs easier to catch in code review.
A real example to cargo-cult from is `core/src/domain/PokemonCard.cpp`. Note how `signed` (the JSON key) maps to `signed_` (the C++ field), and how every JSON key is spelled out. If your game uses a printed collector number, follow Pokemon's lead and store it as `setNo` (string) so the format `"4/102"` can survive a round-trip even when the API only consumes `"4"`.
### 2.3 Update the JSON round-trip test
Open `tests/domain_json_tests.cpp`. Add a `TEST_CASE` that:
1. Constructs a `<Name>Card` with **every** field non-default (including `set.id`, `set.name`, `set.releaseDate`).
2. Serializes it to `nlohmann::json`.
3. Asserts the JSON contains the expected keys with the expected literal spellings (in particular, any C++/JSON aliases like `"signed"`).
4. Round-trips it back into a `<Name>Card` and `CHECK(roundTripped == original)`.
This is the **byte-for-byte stability gate**. If you skip it, the alias bugs only surface in production after you ship and someone's collection.json fails to parse.
---
## 3. Core: set source and (optional) card preview source
### 3.1 `<Name>SetSource`
Mirrors `core/include/ccm/games/pokemon/PokemonSetSource.hpp` and the matching `.cpp`. Put your files at:
- `core/include/ccm/games/<name>/<Name>SetSource.hpp`
- `core/src/games/<name>/<Name>SetSource.cpp`
The header should declare:
- `class <Name>SetSource final : public ISetSource`
- `static constexpr const char* kEndpoint = "<your full HTTPS URL>";`
- `explicit <Name>SetSource(IHttpClient& http);`
- `Result<std::vector<Set>> fetchAll() override;`
- `static Result<std::vector<Set>> parseResponse(const std::string& body);`
The static `parseResponse` is mandatory. It is the seam you unit-test (no HTTP, no fakes — just a string in, a `Result` out). `fetchAll()` is a thin wrapper that calls `http_.get(kEndpoint)` and forwards to `parseResponse` on success.
In `parseResponse`:
1. Parse the body with `nlohmann::json::parse(body)` inside a `try`/`catch (const std::exception&)` block. Throwing across the port boundary is forbidden; catch and return `Result<std::vector<Set>>::err(...)` with a useful message.
2. Walk the response, mapping each entry to a `Set { id, name, releaseDate }`. Rewrite the release date to `YYYY/MM/DD` if the API uses a different format.
3. `std::sort` ascending by release date.
4. Return `Result<std::vector<Set>>::ok(std::move(out))`.
A note on quirky APIs: some endpoints return a top-level array, some wrap it in `{ "data": [...] }`, and some put it under a different key. The two reference implementations diverge on exactly this: Magic walks the Scryfall-shaped response, Pokemon walks `data[]`. Do whatever your API requires; it is fine for `parseResponse` to be game-specific.
### 3.2 `<Name>CardPreviewSource` (optional)
Skip this section if the game has no remote preview API.
Mirror `core/include/ccm/games/pokemon/PokemonCardPreviewSource.hpp`. The header should declare:
- `class <Name>CardPreviewSource final : public ICardPreviewSource`
- `explicit <Name>CardPreviewSource(IHttpClient& http);`
- `Result<std::string> fetchImageUrl(std::string_view name, std::string_view setId, std::string_view setNo) override;`
- `static std::string buildSearchUrl(std::string_view name, std::string_view setId, std::string_view setNo);`
- `static Result<std::string> parseResponse(const std::string& body);`
Both `buildSearchUrl` and `parseResponse` are static and pure on purpose: every URL-encoding and JSON-shape rule is testable without HTTP. Common edge cases your tests must cover:
- Names with spaces, punctuation, or non-ASCII characters (percent-encoding correctness).
- An empty `setId` (don't append the `set.id:` clause).
- An empty `setNo`, and a `setNo` that needs normalization (strip everything after `/`, strip leading zeros, etc.).
- Response with the preferred image variant present.
- Response with only the fallback image variant present.
- Empty `data[]` array.
- Malformed JSON (parse error path).
`fetchImageUrl` is a thin wrapper: build URL → `http_.get(url)``parseResponse(body)`.
### 3.3 `<Name>GameModule`
Wire the two sources together. Create:
- `core/include/ccm/games/<name>/<Name>GameModule.hpp`
- `core/src/games/<name>/<Name>GameModule.cpp`
Header:
```cpp
#pragma once
#include "ccm/games/IGameModule.hpp"
#include "ccm/games/<name>/<Name>CardPreviewSource.hpp" // omit if no preview
#include "ccm/games/<name>/<Name>SetSource.hpp"
namespace ccm {
class <Name>GameModule final : public IGameModule {
public:
explicit <Name>GameModule(IHttpClient& http);
[[nodiscard]] Game id() const noexcept override { return Game::<Name>; }
[[nodiscard]] std::string dirName() const override { return "<name>"; }
[[nodiscard]] std::string displayName() const override { return "<Display>"; }
ISetSource& setSource() override { return setSource_; }
// Omit the override below if the game has no remote preview API.
ICardPreviewSource* cardPreviewSource() noexcept override { return &previewSource_; }
private:
<Name>SetSource setSource_;
<Name>CardPreviewSource previewSource_;
};
} // namespace ccm
```
Two subtle requirements:
- `dirName()` returns the **on-disk directory name**. Once you ship, this is forever — changing it later orphans every existing user's data. Pick something lowercase, ASCII, and short.
- `cardPreviewSource()` defaults to `nullptr` in `IGameModule`. Only override it if you actually have a preview source. Returning `nullptr` makes `CardPreviewService::registerModule(*module)` a silent no-op for that game; the UI gracefully falls back to "no preview available".
The `.cpp` is one line of constructor body — see `core/src/games/pokemon/PokemonGameModule.cpp`.
### 3.4 Register the new sources in `core/CMakeLists.txt`
There is no glob. Add the new `.cpp` files (set source, card preview source, game module, and the card domain `.cpp` if you added one) to the `add_library(ccm_core ...)` argument list. Configure the build before moving on; this catches any missing headers immediately.
---
## 4. Core: tests for the new sources
Writing the tests now, before the UI work, makes the next sections noticeably faster — every later UI debugging session benefits from already knowing the parser and the URL builder are correct.
### 4.1 `<name>_set_source_tests.cpp`
Create `tests/<name>_set_source_tests.cpp` modeled on `tests/pokemon_set_source_tests.cpp`. The required cases are:
- `parseResponse` happy path with two or three sets, including the date rewrite if your API uses a non-`YYYY/MM/DD` format.
- `parseResponse` already-sorted output (input out of order, output ascending by release date).
- `parseResponse` empty array → empty `Result::ok(...)`.
- `parseResponse` missing top-level container → `Result::err(...)`.
- `parseResponse` malformed JSON → `Result::err(...)`.
- `fetchAll` happy path through a `FixedHttpClient` fake (in-file, ~10 lines — see the existing tests). Assert the `lastUrl` equals `kEndpoint`.
- `fetchAll` HTTP error → `Result::err(...)` propagation.
### 4.2 `<name>_card_preview_source_tests.cpp` (if you have a preview source)
Create `tests/<name>_card_preview_source_tests.cpp` modeled on `tests/pokemon_card_preview_source_tests.cpp`. The required cases are:
- `buildSearchUrl` percent-encodes names with spaces and reserved characters.
- `buildSearchUrl` includes / omits the `setId` clause based on whether `setId` is empty.
- `buildSearchUrl` includes / omits / normalizes `setNo` according to your normalization rules.
- `parseResponse` returns the preferred image variant.
- `parseResponse` falls back to the secondary variant when the primary is absent.
- `parseResponse` errors on empty `data[]`, missing `images`, malformed JSON.
- `fetchImageUrl` round-trips through `FixedHttpClient` and asserts the URL was percent-encoded as expected.
- `fetchImageUrl` propagates HTTP errors.
### 4.3 Register the new test files
Add `<name>_set_source_tests.cpp` (and `<name>_card_preview_source_tests.cpp` if applicable) to `tests/CMakeLists.txt` `add_executable(ccm_core_tests ...)`. Build and run `ctest --test-dir build --output-on-failure`. **Do not** continue until these pass.
### 4.4 Sorter / filter tests
If you introduced a new card type in §2.2, add cases to `tests/card_sorter_tests.cpp` and `tests/card_filter_tests.cpp` covering the new sort columns and filter columns introduced by your domain type. The boolean-flag exclusion rule (filter only checks string/number columns; flag columns are skipped) must be covered explicitly so future refactors don't quietly break it.
### 4.5 Set-service routing test
Append a case to `tests/set_service_tests.cpp` that registers your new module alongside Magic and verifies that `updateSets(Game::<Name>)` does not perturb cached data for the other game. Routing isolation is what `SetService` exists for; one test per game keeps it honest.
---
## 5. UI: derive from the three base templates
The UI layer is built on three header-only class templates that own all the wxWidgets-specific machinery. Each has a small, well-documented set of virtual hooks; deriving for a new game is a hook-implementation exercise, not a wxWidgets exercise. Read `ui_wx/include/ccm/ui/Base*.hpp` once before starting.
### 5.1 SVG icons
If your game introduces flag columns whose glyphs do not already exist in `ui_wx/include/ccm/ui/SvgIcons.hpp`, add them now:
- Declare each new icon as `extern const char* const kSvg<Name>;` in the header.
- Define them in `ui_wx/src/SvgIcons.cpp`. Keep the `@FILL@` placeholder so the rasterizer can substitute the active palette text color at draw time. **Do not** bake a color into the SVG — that breaks dark mode.
- Re-use existing glyphs (`kSvgSigned`, `kSvgAltered`, `kSvgFoil`, `kSvgHolo`) when the meaning matches.
### 5.2 Sort and filter helpers
Add a `<Name>SortColumn` enum to `core/include/ccm/services/CardSorter.hpp`, plus the corresponding `sort<Name>Cards(std::vector<<Name>Card>&, <Name>SortColumn, bool)` declaration. Implement it in `core/src/services/CardSorter.cpp` mirroring the existing per-column dispatch (each enum entry maps to a comparator).
Add `[[nodiscard]] bool matches<Name>Filter(const <Name>Card&, std::string_view)` to `core/include/ccm/services/CardFilter.hpp` and implement it in `core/src/services/CardFilter.cpp`. Walk the same value-key columns that the list panel will display, lowercase both sides, and short-circuit on any substring hit. Boolean flag columns are intentionally excluded — only string/number columns participate in filtering.
These functions are also the targets of §4.4's tests; you'll have already written the tests if you followed the order.
### 5.3 `<Name>CardListPanel`
Create:
- `ui_wx/include/ccm/ui/<Name>CardListPanel.hpp`
- `ui_wx/src/<Name>CardListPanel.cpp`
The header declares a `final class` deriving from `BaseCardListPanel<<Name>Card, <Name>SortColumn>`, with overrides for:
- `declareTextColumns()` — return a `std::vector<TextColumnSpec>` of `{label, width, format, optional<sortColumn>}`. The order is left-to-right on screen. The **last** entry must be the `Note` column; the base reserves it.
- `declareIconColumns()` — return a `std::vector<IconColumnSpec>` of `{svg, width, optional<sortColumn>}`. These render between the leading text columns and the Note column.
- `renderTextCell(card, idx)` — return the cell text for the `idx`-th text column. The base passes index `0..textCols-1` for the leading rows and `textCols-1` for the Note row, so you usually `switch (idx)`.
- `isIconColumnSet(card, idx)` — return whether the `idx`-th icon column should render its glyph for this row. Index `0` is the first icon column declared in `declareIconColumns()`.
- `sortBy(column, ascending)` — call `sort<Name>Cards(mutableCards(), column, ascending)`. **Use `mutableCards()`**, not `cards()`, because `sortBy` writes through the underlying vector.
- `matchesFilter(card, filter)` — call `matches<Name>Filter(card, filter)`.
In the constructor body, call `buildLayout()` (from `BaseCardListPanel`) so the base wires up the `wxListCtrl`, the themed header row, and the image lists.
The reference implementation at `ui_wx/src/PokemonCardListPanel.cpp` is ~75 lines including the icon-column declaration and the `switch`-based renderer. Yours should land in the same ballpark.
### 5.4 `<Name>SelectedCardPanel`
Create:
- `ui_wx/include/ccm/ui/<Name>SelectedCardPanel.hpp`
- `ui_wx/src/<Name>SelectedCardPanel.cpp`
Inside the `.cpp`, define an unnamed-namespace `enum <Name>DetailKey : int { ... };` with one entry per detail row and one per flag column. Keep these names local — they're only used between this file's hook overrides.
Override:
- `declareDetailRows()` — return a `std::vector<DetailRowSpec>` of `{label, key, emptyLabel}`. The **first** row should typically be `Name`; its `emptyLabel` is what the panel shows when no card is selected.
- `declareFlagIcons()` — return a `std::vector<FlagIconSpec>` of `{svg, tooltip, key}`.
- `detailValueFor(card, key)``switch (key)` and return the appropriate string. **Also handle `kNoteKey`** (defined in `BaseSelectedCardPanel` as `-1`); the base calls `detailValueFor(card, kNoteKey)` to populate the bottom Note row.
- `isFlagSet(card, key)``switch (key)` and return the matching boolean.
- `previewKey(card)` — return `std::tuple<std::string, std::string, std::string>` of `(name, setId, setNo)`. Use empty `setNo` for games whose preview API does not need a collector number.
- `gameId()` — return `Game::<Name>`.
In the constructor body, call `buildLayout()` so the base wires up the preview area, detail grid, flag strip, and image list.
The reference implementation is `ui_wx/src/PokemonSelectedCardPanel.cpp`.
### 5.5 `<Name>CardEditDialog`
Create:
- `ui_wx/include/ccm/ui/<Name>CardEditDialog.hpp`
- `ui_wx/src/<Name>CardEditDialog.cpp`
Derive from `BaseCardEditDialog<<Name>Card>`. Override:
- `buildFlagsRow(wxBoxSizer* flagsBox)` — create your `wxCheckBox`es and `flagsBox->Add(...)` them. The base owns the surrounding `Flags` label and sizer.
- `appendExtraRows(wxFlexGridSizer* grid)` — only if your game has fields beyond the standard set. Use the inherited `appendRow(grid, label, ctrl)` helper. (Pokemon adds a `Set #` text input here.)
- `readExtraFromCard()` — copy fields from `constCard()` into your widgets.
- `writeExtraToCard()` — copy values from your widgets back into `mutableCard()`.
- `updateMenuName()` — return `"Update <Display>"`. This is what the dialog's "no sets cached" hint shows the user.
In the constructor:
1. Pass through to the `BaseCardEditDialog` constructor with the dialog title (e.g. `"Add <Display> Card"` or `"Edit <Display> Card"` based on `EditMode`), `imageService`, `setService`, `mode`, `std::move(initial)`, `Game::<Name>`, and the optional `preloadedSets` pointer.
2. Call `buildAndPopulate()` (from the base) to build the form, populate the choices, and call `readExtraFromCard()`.
The reference implementation is `ui_wx/src/PokemonCardEditDialog.cpp`.
### 5.6 `<Name>GameView`
This is the polymorphic glue between the new game's panels and the rest of the app. Create:
- `ui_wx/include/ccm/ui/<Name>GameView.hpp`
- `ui_wx/src/<Name>GameView.cpp`
Derive from `IGameView`. The constructor takes references to the shared services (`ConfigService`, `SetService`, `ImageService`, `CardPreviewService`), the typed `CollectionService<<Name>Card>&`, and the `IGameModule&`. Members:
- `<Name>CardListPanel* listPanel_{nullptr};`
- `<Name>SelectedCardPanel* selectedPanel_{nullptr};`
- `std::vector<Set> setsCache_;` — populated lazily by `setsForDialog()` so each Add/Edit open does not re-read from `SetService`.
Implement the virtuals:
- `gameId()` returns `Game::<Name>`.
- `displayName()` returns `"<Display>"`.
- `listPanel(parent)` — lazily allocates the list panel as a child of `parent`; on first allocation, also `Bind(EVT_CARD_SELECTED, ...)` to push `listPanel_->selected()` into `selectedPanel_`. **The binding must live here**, in the typed `IGameView`, not in `MainFrame``MainFrame` only sees `IGameView` and never `<Name>Card`.
- `selectedPanel(parent)` — lazily allocates the selected panel.
- `refreshCollection()` — calls `collection_.list(Game::<Name>)`, handles errors with `wxMessageBox`, and pushes the new vector into `listPanel_->setCards(...)`. Also re-syncs the selected panel.
- `onAddCard(parent)`, `onEditCard(parent)`, `onDeleteCard(parent)` — open the typed `<Name>CardEditDialog` (or pop a confirm dialog for delete), call the typed `CollectionService` to commit, and refresh on success.
- `onUpdateSets(parent)` — calls `sets_.updateSets(Game::<Name>)`, refreshes `setsCache_`, returns a status string.
- `setFilter(filter)` — forwards to `listPanel_->setFilter(filter)`.
- `applyTheme(palette)` — forwards to both panels' `applyTheme`.
- `updateSetsMenuLabel()` — returns `"Update <Display>"`. This is what the `Sets` menu entry shows.
The reference implementation is `ui_wx/src/PokemonGameView.cpp`. It's about 160 lines and is the same shape for every game.
### 5.7 Register the new UI sources
Add **all** new UI `.cpp` files to `ui_wx/CMakeLists.txt`:
- `<Name>CardListPanel.cpp`
- `<Name>SelectedCardPanel.cpp`
- `<Name>CardEditDialog.cpp`
- `<Name>GameView.cpp`
There is no glob.
---
## 6. Composition root
Edit `app/main.cpp` to wire the new game in. Read `app/AGENTS.md` first — destruction-order rules apply.
### 6.1 New members
Add `std::unique_ptr<...>` members to `CcmApp`. Order matters (destruction is reverse — deps before dependents):
```cpp
std::unique_ptr<ccm::<Name>GameModule> <name>Mod_;
std::unique_ptr<ccm::JsonCollectionRepository<ccm::<Name>Card>> <name>Repo_;
std::unique_ptr<ccm::CollectionService<ccm::<Name>Card>> <name>CollSvc_;
std::unique_ptr<ccm::ui::<Name>GameView> <name>View_;
```
Place them next to the existing Magic/Pokemon members in the matching position — game module after `http_`, repo after the module, collection service after the repo and the image store, view at the end before `ctx_`.
### 6.2 New constructions in `OnInit()`
```cpp
<name>Mod_ = std::make_unique<ccm::<Name>GameModule>(*http_);
<name>Repo_ = std::make_unique<ccm::JsonCollectionRepository<ccm::<Name>Card>>(
*fs_, *config_, &dirNameForGame);
<name>CollSvc_ = std::make_unique<ccm::CollectionService<ccm::<Name>Card>>(
*<name>Repo_, *imgStore_);
setSvc_->registerModule(<name>Mod_.get());
previewSvc_->registerModule(*<name>Mod_); // no-op when the module has no preview source
<name>View_ = std::make_unique<ccm::ui::<Name>GameView>(
*config_, *<name>CollSvc_, *setSvc_, *imgSvc_, *previewSvc_, *<name>Mod_);
```
### 6.3 `dirNameForGame`
Add a `case ccm::Game::<Name>: return "<name>";` arm. The string must match `<Name>GameModule::dirName()`.
### 6.4 `AppContext`
`AppContext` (`ui_wx/include/ccm/ui/AppContext.hpp`) currently holds explicit references to `magicModule` and `pokemonModule`. Add an `<Name>Module` reference field — keep the alphabetical / canonical order — and pass `*<name>Mod_` for it in the `AppContext{...}` brace-init in `OnInit()`. Also append `<name>View_.get()` to the `gameViews` vector.
The `Game` and `Sets` menus in `MainFrame` are built dynamically from `gameViews`, so once the new view is in the vector its menu entries (the `Game > <Display>` radio item and the `Sets > Update <Display>` action) appear automatically.
---
## 7. AGENTS.md and tests housekeeping
After all the above compiles and tests pass:
1. **`AGENTS.md`** (root) — the "After adding a new game module you must" required follow-up should already cover your work; read it and confirm. If you added a new domain type, the matching `tests/domain_json_tests.cpp` round-trip test is required (per `core/AGENTS.md`).
2. **`core/AGENTS.md`** — update only if your game required a new core seam shape (a new port, a new service, a new shared helper). Describing the new game itself is not required; the doc is meant to stay game-agnostic.
3. **`ui_wx/AGENTS.md`** — same: update only if you needed a new template hook or had to teach the `Base*` templates a new behaviour. Describing the new game's panel set is not required.
4. **`app/AGENTS.md`** — confirm the "Composition root is the only place" allowlist still mentions the concrete adapter types. If you added new ones (a new `<Name>GameView`, `<Name>GameModule`), append them.
5. **`tests/AGENTS.md`** — add the new test file names to the file map and the "Required follow-ups" list.
6. **This file** (`docs/adding-a-new-game.md`) — only edit when the procedure itself changes (new template hook, new service registration, new composition-root step). Do not insert your specific game's quirks here; capture those in code comments next to the relevant overrides.
---
## 8. Verification checklist
Run, in order, from the workspace root. Do not skip any step.
1. **Configure**: `cmake -S . -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release` (or your usual generator). Configuration must succeed without warnings about a missing source file.
2. **Build**: `cmake --build build --parallel`. Must succeed cleanly. Pay close attention to template-instantiation errors — those usually indicate one of the `Base*` hooks is missing or wrongly-typed.
3. **Tests**: `ctest --test-dir build --output-on-failure`. Every existing test plus the new `<name>_set_source_tests`, `<name>_card_preview_source_tests`, the extended `domain_json_tests`, the extended `card_sorter_tests`, the extended `card_filter_tests`, and the extended `set_service_tests` must pass.
4. **Smoke test**: launch `./build/bin/ccm3` (or `.\build\bin\ccm3.exe`). Note: the CMake target is `ccm` but the executable is renamed to `ccm3` via `set_target_properties(... OUTPUT_NAME ccm3)`.
- The `Game` menu shows your new game alongside Magic and Pokemon and switching is instantaneous (no panel re-creation cost on subsequent switches).
- The `Sets > Update <Display>` action fetches sets and reports a count.
- With sets cached, opening the new game's Add dialog populates the set picker and the dialog can be dismissed with `OK`.
- Adding, editing, and deleting a card all round-trip through disk: close and re-open the app and the card persists.
- Selecting a card kicks off a preview fetch (if the game has a preview source) and renders the image; the status line returns to `"Ready"` when the preview lands.
- The flag-icon strip shows / hides per card depending on which flags are set.
- Theme switching applies to all of the new game's panels (light → dark → light).
---
## 9. Common traps
These do not match a single seam in this guide but are worth calling out explicitly.
- **Stale set caches.** Each `IGameView` caches `std::vector<Set> setsCache_`. After `onUpdateSets` succeeds, refresh the cache (assign the new vector). The reference implementations do this.
- **`signed_` / `signed`.** The C++ field is `signed_`; the JSON key is `"signed"`. This is intentional and must not be changed. The same convention applies to any new field where the natural name collides with a C++ keyword — pick a trailing-underscore C++ name and an unaliased JSON key.
- **Spacer column index.** `BaseCardListPanel` reserves index `0` for a hidden zero-width spacer column (MSW comctl32 image-list gutter workaround). Real columns start at index `1`. If you ever need to call into `wxListCtrl` directly from a derived panel (you should not), remember this.
- **Preview-fetch threading.** The async preview fetch in `BaseSelectedCardPanel` uses a `shared_ptr<State>` + `std::atomic alive` + `std::atomic currentGen` triple. Do not capture `this` raw in any background work you add to a new game's selected panel; copy that pattern verbatim.
- **First-paint perf.** `MainFrame` defers initial collection load with `CallAfter(...)` and `BaseCardListPanel` defers the initial selection the same way. Don't move that work back into the constructor for "convenience" — it makes startup visibly slower.
- **`previewKey` for games without `setNo`.** If your preview API only needs `(name, setId)`, return an empty string for the third tuple element. The base will pass `""` through to the source, which is exactly what `MagicCardPreviewSource` is built to handle.
- **Filter exclusion.** The filter intentionally ignores boolean-flag columns. If you find yourself wanting `signed:true` style filters, that is a future feature, not a fix; don't smuggle it into `matches<Name>Filter` without a design discussion.
- **Theming dialogs.** Always `applyThemeToWindowTree(&dlg, palette, theme)` before `ShowModal()` for any dialog you open. The reference `<Name>GameView::onAddCard` / `onEditCard` show the canonical pattern.
---
## 10. Where to read first when something does not work
- The new game compiles but its menu entries do not appear — check that the view was appended to `AppContext::gameViews` in `app/main.cpp`.
- The list panel is empty even after `Sets > Update <Display>` succeeds — check `dirNameForGame`. The repository writes to `<dataStorage>/<dirName>/collection.json`, and a typo here makes the load silently return an empty list on next launch.
- The filter input does nothing on the new game — check that `<Name>GameView::setFilter(...)` forwards to the list panel and that `matches<Name>Filter` actually evaluates the active filter substring (the empty filter must match every row).
- The Add dialog shows `(no sets cached - use Sets > Update <Display>)` even after a successful update — `setsCache_` was not refreshed in `onUpdateSets`. The reference views assign `out.value()` into the cache.
- Preview never resolves — first add a unit test that hits `parseResponse` with a real captured response body. If that passes, log the URL `IHttpClient::get` is called with and try it in a browser or `curl`. Most "broken preview" bugs are URL encoding or a wrong shape in `buildSearchUrl`.
- Sort works but its arrow indicator is wrong — column `0` is the spacer, so the visual column index sort key cares about is one higher than you might expect. The base handles this; if it goes wrong, check that your `TextColumnSpec`/`IconColumnSpec` order matches `renderTextCell` / `isIconColumnSet` indexing exactly.
- Tests pass but the app crashes on shutdown — destruction order in `CcmApp` is wrong. Move `<name>View_` so it is declared **after** `<name>CollSvc_`, `setSvc_`, `imgSvc_`, `previewSvc_`, `<name>Mod_` — the view must be torn down before any of its referenced services.
+52
View File
@@ -0,0 +1,52 @@
#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](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
**Info API:** `https://api.pokemontcg.io/v2/sets`
Used by `PokemonSetSource` to fetch all sets. The parser maps `id`, `name`, and `releaseDate` directly into `Set`, then sorts ascending by release date.
**Asset API:** `https://api.pokemontcg.io/v2/cards?q=...`
Used by `PokemonCardPreviewSource` to search by `name` plus optional `set.id` and collector number. It extracts `data[0].images.large` first and falls back to `images.small` if needed.
The Pokemon source also normalizes collector numbers before request build. For example, `4/102` is reduced to `4` because the remote query expects only the printed number component.
## Runtime Flow In CCM3
The app uses the same flow for both games:
- `SetService` asks the game's `ISetSource` (info API) for the latest set list.
- `CardPreviewService` asks the game's `ICardPreviewSource` (asset API) for a preview image URL.
- `CardPreviewService` performs 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 fetches a per-game fallback card-back image URL through `CardPreviewService::fetchImageBytesByUrl(...)` and shows that image in the selected-card preview panel.
Current fallback image URLs (kept in `BaseSelectedCardPanel` for CCM2 parity):
- Magic: `https://gamepedia.cursecdn.com/mtgsalvation_gamepedia/f/f8/Magic_card_back.jpg`
- Pokemon: `https://archives.bulbagarden.net/media/upload/1/17/Cardback.jpg`
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
Both 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 (`data`, `image_uris`, `images.large`/`images.small`). If the fallback fetch succeeds, the panel intentionally shows the card-back image and the inline label `(image preview unavailable)`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

+90
View File
@@ -0,0 +1,90 @@
#documentation #ci-cd #github-actions
# CI/CD Guide
This guide defines the CI/CD behavior for Card Collection Manager 3. For the complete versioning policy, including prefix semantics and tag rules, see [Versioning Guide](versioning.md). For how the Windows installer artifact is produced and configured, see [Windows Installer Guide](windows-installer.md).
**Quick Setup:** protect `master` with the required check `Require semver prefix in PR title`, then keep release PR titles aligned with the accepted prefixes.
## Workflow Map
The repository uses GitHub Actions workflows split by branch intent, with one orchestrator per branch intent:
- `feature-ci.yml`: single workflow run for non-`master` pushes; orchestrates Linux and Windows feature builds.
- `feature-linux.yml`: reusable Linux build/test/package workflow invoked by `feature-ci.yml`.
- `feature-windows.yml`: reusable Windows build/test/package workflow invoked by `feature-ci.yml`.
- `master-pr-title-guard.yml`: validate PR title prefixes for PRs targeting `master`.
- `master-ci.yml`: single workflow run on merged PRs to `master`; computes semver, invokes Windows reusable build, then tags/publishes release assets.
- `master-windows.yml`: reusable Windows build/test/package workflow invoked by `master-ci.yml`.
## Version Flow
Feature branches and `master` use different version modes because they solve different problems: feature builds need traceability to a commit, while `master` builds need stable semantic releases.
### Feature Branch Builds
On pushes to non-`master` branches, `feature-ci.yml` fans out to Linux and Windows reusable workflows. Each platform workflow computes a version string in the format `<branch-name>-<short-sha>` via `scripts/compute_feature_version.sh` (for example `feature-dark-mode-a1b2c3d`).
That version is used in two places:
- artifact file names
- app embedded version via `-DCCM_APP_VERSION=...`
### Master Releases
When a PR is merged into `master`, `master-ci.yml` computes the next semantic version from the latest semver tag and the PR title prefix using `scripts/compute_master_semver.sh`.
Accepted prefix mapping:
- `major...` -> major bump
- `minor...` -> minor bump
- `fix...` -> patch bump
- `patch...` -> patch bump
- `path...` -> patch bump (accepted alias in current setup)
The resulting version is embedded in the app (`CCM_APP_VERSION`), used in artifact names, and tagged as `v<version>`.
## Master Merge Guard
PRs targeting `master` must start with one of the accepted prefixes:
- `major`
- `minor`
- `fix`
- `patch`
- `path`
If a title does not match, `master-pr-title-guard.yml` fails and the PR should not be merged.
## Artifact Naming
Feature workflow artifacts (produced by jobs inside `feature-ci.yml`):
- Windows: `ccm3-windows-<version>.zip`, `ccm3-windows-installer-<version>`
- Linux: `ccm3-linux-<version>.zip`
Master release artifacts (`master-ci.yml`, built via `master-windows.yml`):
- `ccm3-windows-<semver>.zip`
- `ccm3-windows-installer-<semver>.exe`
## Release Procedure
Follow this flow for every release:
1. Open a PR to `master`.
2. Use a valid semver prefix in the PR title (`major:`, `minor:`, `fix:`, `patch:`, or `path:`).
3. Wait for all required checks to pass.
4. Merge the PR.
5. Wait for `master-ci.yml` to complete semver computation, Windows build/test, tag creation, and release publishing.
6. Verify the `vX.Y.Z` tag and release assets in GitHub.
## Local Build Version Behavior
Outside CI, the app version defaults to `${PROJECT_VERSION} (localbuild)` through `CCM_APP_VERSION` in the top-level `CMakeLists.txt`. This keeps local binaries easy to distinguish from CI and release outputs.
## Troubleshooting
- **`msys2: command not found` in workflow logs:** ensure the MSYS2 setup step runs before jobs that use `shell: msys2 {0}`.
- **`Permission denied` while linking `ccm3.exe`:** the app is still running; close it and rebuild.
- **No release after merge:** verify the PR merged into `master` and used a valid prefix, then inspect `master-ci.yml` logs for version/tag/release failures.
+129
View File
@@ -0,0 +1,129 @@
#documentation #build #cmake
# Build Locally Guide
This guide explains how to build Card Collection Manager 3 on Windows and Linux, how dependencies are resolved, and how to run tests locally. For architecture orientation, see [Intro For New Developers](intro-to-new-developers.md).
**Quick Setup:** run CMake configure, build, launch `ccm3`, then run `ctest` from the same build directory.
## Build Model
The project uses CMake and builds one desktop executable:
- executable target: `ccm` (output binary: `ccm3` / `ccm3.exe`)
- language standard: C++20
- layered targets: `ccm_core` (logic), `ccm_ui_wx` (wx UI), `ccm` (composition root)
## Dependency Model
Dependencies are managed with CMake `FetchContent` in `cmake/Dependencies.cmake`.
Pinned versions:
- `nlohmann/json` `v3.11.3`
- `libcpr/cpr` `1.10.5`
- `wxWidgets` `v3.2.5`
- `doctest` `v2.4.11` (only when tests are enabled)
On first configure, CMake downloads sources. On first full build, heavy dependencies (especially wxWidgets and curl) build locally. Later builds reuse cached dependencies under `build/_deps`.
## Use System wxWidgets
By default, the build fetches wxWidgets. For faster local iteration with an installed wxWidgets, set `-DCCM_USE_SYSTEM_WX=ON`.
## Prerequisites
### Windows
Recommended: Clang + Ninja
- LLVM/Clang 14+ on `PATH`
- CMake 3.22+
- Ninja on `PATH`
Verified fallback: MSYS2 UCRT64 + MinGW-w64 GCC
- MSYS2 UCRT64 toolchain (`gcc`, `cmake`, `make` or `ninja`)
- CMake 3.22+
MSVC is intentionally not supported.
### Linux
- CMake 3.22+
- Clang or GCC with C++20 support
- Ninja recommended
- required system packages when using system wxWidgets (for example GTK/wx dev packages)
## Build Commands
Run from repository root.
### Windows (Clang + Ninja)
```powershell
cmake -S . -B build -G Ninja
cmake --build build --parallel
.\build\bin\ccm3.exe
```
### Windows (MinGW Makefiles)
```powershell
cmake -S . -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel 4
.\build\bin\ccm3.exe
```
### Linux (Ninja)
```bash
cmake -S . -B build -G Ninja
cmake --build build --parallel
./build/bin/ccm3
```
## Build Options
- `CCM_USE_SYSTEM_WX` (default `OFF`): use installed wxWidgets instead of fetched wxWidgets.
- `CCM_BUILD_TESTS` (default `ON`): build `ccm_core_tests`.
- `CMAKE_BUILD_TYPE` (commonly `Release`): standard CMake build type.
- `CCM_APP_VERSION` (default `${PROJECT_VERSION} (localbuild)`): app version string shown in About dialog.
Example for faster local iteration:
```bash
cmake -S . -B build -DCCM_USE_SYSTEM_WX=ON -DCCM_BUILD_TESTS=OFF
cmake --build build
```
## Run Tests Locally
From repository root:
```bash
cmake -S . -B build -DCCM_BUILD_TESTS=ON
cmake --build build --target ccm_core_tests
ctest --test-dir build --output-on-failure
```
Automated tests primarily cover `core/` and infrastructure adapters. UI testing is currently manual.
## Runtime Notes
### Windows Runtime DLLs
`cpr` builds as shared, so `build/bin` contains runtime DLLs (for example `libcpr.dll`, `libcurl.dll`, `libzlib.dll`) next to `ccm3.exe`.
For MinGW/MSYS2 builds, UCRT runtime DLLs must be available (typically via MSYS2 UCRT64 `bin` on `PATH`).
## Troubleshooting
- **First build is slow:** expected on cold dependency fetch/build, especially wxWidgets and curl.
- **`Permission denied` while linking `ccm3.exe`:** the app is still running; close it and rebuild.
- **Generator mismatch:** reuse the same generator for a build directory or create a new build directory.
- **Windows CI/local shell mismatch:** in CI jobs using `shell: msys2 {0}`, ensure MSYS2 setup runs before shell commands.
## Related Docs
- [CI/CD Guide](ci-cd-guide.md)
- [Versioning Guide](versioning.md)
- [Adding a new game to Card Collection Manager](adding-a-new-game.md)
+108
View File
@@ -0,0 +1,108 @@
#documentation #onboarding #architecture
# Intro For New Developers
This document orients new contributors to Card Collection Manager 3. For local setup and build commands, start with [Build Locally Guide](dow-doc-build-locally.md); then use this guide to understand architecture boundaries and daily workflows.
**Quick Setup:** read `AGENTS.md` files first, build once, run tests once, then make one small layer-scoped change to validate your environment.
## Toolchain Snapshot
Card Collection Manager 3 builds as a native C++ desktop binary with CMake. The project standard is C++20, with Clang as the preferred compiler on Windows and Linux, plus a verified MinGW-w64 fallback path on Windows.
- build system: CMake 3.22+ with `FetchContent`
- language/toolchain: C++20, Clang preferred, MinGW-w64 GCC fallback on Windows
- UI toolkit: wxWidgets (`ui_wx/` only)
- REST/HTTP client library: `cpr` (libcurl-based, used through `IHttpClient`/`CprHttpClient`)
- JSON library: `nlohmann/json`
- test framework: `doctest`
Use [Build Locally Guide](dow-doc-build-locally.md) for exact commands, generator options, runtime DLL notes, and troubleshooting details.
## Project Shape
Card Collection Manager 3 is a native desktop app written in C++20 with wxWidgets. The architecture is intentionally layered so core logic stays UI-agnostic.
- `core/`: domain logic, services, and infrastructure adapters
- `ui_wx/`: wxWidgets UI code only
- `app/`: composition root that wires adapters, services, and views
Dependency direction is strict: `app -> ui_wx -> core`.
## Architecture Rules
These rules are the baseline for all feature work:
- `core/` must never include or depend on wxWidgets.
- UI code consumes services through `ccm::ui::AppContext`.
- Concrete adapter wiring belongs in `app/main.cpp`.
- JSON keys and aliases are contract-sensitive and must stay stable.
## Repository Map
### `core/`
`core/` contains domain and non-UI behavior:
- `domain/`: value types and enums (`MagicCard`, `PokemonCard`, `Set`, `Configuration`)
- `ports/`: seam interfaces (`IHttpClient`, `IFileSystem`, repositories, game seams)
- `services/`: use-case logic (`CollectionService`, `SetService`, `ConfigService`)
- `infra/`: concrete adapters (`Json*Repository`, `StdFileSystem`, `CprHttpClient`, `LocalImageStore`)
- `games/`: per-game modules (`magic`, `pokemon`)
### `ui_wx/`
`ui_wx/` contains all presentation code:
- `MainFrame`: top-level shell and menu/split-view orchestration
- `BaseCardListPanel`, `BaseCardEditDialog`, `BaseSelectedCardPanel`: reusable UI templates
- `Magic*` and `Pokemon*` classes: game-specific view/panel implementations
- `Theme.cpp`, `SvgIcons.cpp`, `IconListCtrl.cpp`: theming and visual behavior
### `app/`
`app/main.cpp` is the composition root:
- instantiate adapters, services, and modules
- register game modules and game views
- build `AppContext`
- create and show `MainFrame`
Keep this file focused on wiring, not business logic.
### `tests/`
Tests target non-UI behavior with deterministic fakes and in-memory adapters. When a domain JSON contract or filesystem naming rule changes, update the matching tests in the same change.
## Common Workflows
### Add A Small Feature
1. Identify the correct layer (`core`, `ui_wx`, or both).
2. Make the smallest coherent change in that layer.
3. Rebuild the affected target.
4. Run tests when core behavior changes.
### Add A New Game
Use [Adding a new game to Card Collection Manager](adding-a-new-game.md). Do not bypass the existing seams or invent parallel architecture for a new game.
### Release-Oriented Changes
Use [CI/CD Guide](ci-cd-guide.md) and [Versioning Guide](versioning.md) for workflow and release policy decisions.
## Avoid These Pitfalls
- adding wx headers in `core/`
- putting business logic in `app/main.cpp`
- accessing concrete adapters directly from UI code instead of `AppContext`
- changing JSON key spellings casually
- using unpinned dependency versions
## First-Day Checklist
1. Read repository `AGENTS.md` files (`core/`, `ui_wx/`, `app/`, `tests/`).
2. Build locally with [Build Locally Guide](dow-doc-build-locally.md).
3. Run the test suite once.
4. Make one small layer-contained change.
5. Rebuild and rerun relevant tests.
+116
View File
@@ -0,0 +1,116 @@
#documentation #testing #quality
# Testing Guide And Test Code Of Conduct
This guide defines how testing works in Card Collection Manager 3 and which standards test code must meet. For local build setup and toolchain prerequisites, see [Build Locally Guide](dow-doc-build-locally.md).
**Quick Setup:** run `ccm_core_tests` from a clean build, keep tests hermetic, and update contract tests in the same change when contracts move.
## Testing Focus
The project prioritizes deterministic, fast, behavior-oriented testing. Most automated coverage intentionally targets `core/` logic and infrastructure/service behavior, while UI validation remains manual.
## Test Setup
- framework: `doctest`
- primary target: `ccm_core_tests`
- location: `tests/`
- default behavior: tests enabled via `CCM_BUILD_TESTS=ON`
## Run Tests
From repository root:
```bash
cmake -S . -B build -DCCM_BUILD_TESTS=ON
cmake --build build --target ccm_core_tests
ctest --test-dir build --output-on-failure
```
Windows and Linux use the same logical flow; only generator and compiler setup differ.
## Coverage Surface
Current automated tests cover non-UI behavior, including:
- filesystem naming and parsing behavior
- domain JSON round-trip behavior
- service behavior (`CollectionService`, `ConfigService`, `SetService`)
- repository behavior with in-memory filesystem fakes
- game set-source parsing behavior
## Manual UI Validation
Because there is no UI automation, UI-affecting changes require manual checks:
- theme switching (dark and light)
- dialog and popup behavior
- add/edit/delete card flows
- set update flows
- preview and image interactions
For theming work, rebuild and run the final app target (`ccm`) instead of validating only static library targets.
## Test Code Of Conduct
### Keep Tests Hermetic
- do not call real network services
- do not depend on local machine files
- use fakes and in-memory adapters where possible
### Test Behavior, Not Internals
- assert externally visible outcomes
- avoid brittle assertions tied to incidental implementation details
- prefer domain-level expectations over call-level trivia
### Keep Tests Deterministic
- no unseeded randomness
- no timing-sensitive assertions that can flap
- no ordering assumptions unless ordering is part of the contract
### Keep Tests Readable
- one intent per test case
- descriptive test names
- clear arrange/act/assert flow
- minimal abstraction for small tests
### Update Tests With Contract Changes
When contracts change, update tests in the same change:
- domain JSON schema or aliases -> round-trip tests
- filename formatting or parsing -> filesystem naming tests
- service semantics -> matching service tests
Behavior changes without aligned tests are incomplete.
### Avoid Over-Mocking
- prefer realistic fakes over mock-heavy tests
- mock at external boundaries only when needed
- preserve confidence in integration-shaped behavior paths
### Keep Runtime Practical
- keep suite runtime fast enough for frequent local execution
- avoid repeated expensive setup when shared setup works
- justify any expensive new suite and keep scope narrow
## Review Checklist
Before merging test changes, verify:
- tests pass locally
- no new flakiness risk
- no external dependency introduced
- assertions reflect intended behavior
- failure messages are clear and actionable
## Related Docs
- [Build Locally Guide](dow-doc-build-locally.md)
- [Intro For New Developers](intro-to-new-developers.md)
- [Adding a new game to Card Collection Manager](adding-a-new-game.md)
+89
View File
@@ -0,0 +1,89 @@
#documentation #versioning #releases
# Versioning Guide
This guide defines how Card Collection Manager 3 assigns versions in CI and release flows. For workflow wiring and release execution details, see [CI/CD Guide](ci-cd-guide.md).
**Quick Setup:** choose a valid PR title prefix before opening a `master` PR because the prefix determines the release bump.
## Versioning Model
The project uses two versioning modes:
- feature-build versioning for non-`master` branches
- semantic versioning for merged PRs into `master`
## Feature-Build Versioning
Non-`master` pushes use `<branch-name>-<short-sha>` (for example `feature-dark-theme-a1b2c3d` or `fix-sort-order-f91d2ab`).
Rules:
- branch names are sanitized and lowercased
- commit SHA is shortened to 7 characters
- computation runs in `scripts/compute_feature_version.sh`
Usage:
- artifact names
- app embedded version (`CCM_APP_VERSION`, visible in `Help -> About`)
## Master Semantic Versioning
Merged PRs into `master` use semantic versions in `MAJOR.MINOR.PATCH` format (for example `1.4.2`).
CI computes the next version from the latest semver tag and the PR title prefix:
- `major...` -> bump `MAJOR`, reset `MINOR` and `PATCH` to `0`
- `minor...` -> bump `MINOR`, reset `PATCH` to `0`
- `fix...` -> bump `PATCH`
- `patch...` -> bump `PATCH`
- `path...` -> bump `PATCH` (accepted alias in current setup)
Computation runs in `scripts/compute_master_semver.sh`.
## Validation Rules
If the PR title prefix is not accepted, two controls fail by design:
- PR title guard check for `master` PRs
- semantic-version script validation
This enforcement keeps release bumps deterministic and reviewable.
## Tag Format
Master releases create git tags in this format:
- `v<semantic-version>`
Examples:
- `v1.0.0`
- `v2.3.7`
## Embedded App Version
The app embeds a build-time version string through CMake variable `CCM_APP_VERSION`.
CI behavior:
- feature workflows set it to `<branch>-<sha>`
- master release workflow sets it to semantic version
Local/manual behavior:
- default is `${PROJECT_VERSION} (localbuild)` unless overridden
This default makes local binaries easy to distinguish from CI and release outputs.
## PR Title Conventions
Use explicit prefixes in this shape:
- `major: <summary>`
- `minor: <summary>`
- `fix: <summary>`
- `patch: <summary>`
Example: `minor: add custom themed confirmation dialogs`.
+128
View File
@@ -0,0 +1,128 @@
#documentation #installer #nsis #windows
# Windows Installer Guide
This guide explains how the Windows installer for Card Collection Manager 3 is built, how it is configured, and how it cooperates with the rest of the build/release pipeline. For CI flow and artifact naming, see [CI/CD Guide](ci-cd-guide.md). For where the version string comes from, see [Versioning Guide](versioning.md).
**Quick Setup:** install NSIS (`makensis` on `PATH`), build the app into `build/bin/`, then run `makensis -DAPP_VERSION="<version>" scripts/installer.nsi` from the repository root.
## Installer Model
The installer is a single NSIS script: `scripts/installer.nsi`. It produces one self-contained executable that ships the entire `build/bin/` directory plus an embedded uninstaller.
Key facts:
- installer technology: NSIS (`makensis`) with the Modern UI 2 (MUI2) library
- output file: `ccm3-windows-installer.exe`, written to the repository root (resolved as `..\ccm3-windows-installer.exe` from `scripts/`)
- payload source: every file under `build/bin/` (resolved as `..\build\bin\*.*` from `scripts/`)
- icon: `scripts/installer_icon.ico` (resolved relative to `scripts/`)
- default install location: `%PROGRAMFILES64%\Card Collection Manager 3`
- elevation: `RequestExecutionLevel admin`
Because the installer pulls from `build/bin/` directly, it must run **after** a successful release build that has been bundled with all required runtime DLLs (see [Build Locally Guide](dow-doc-build-locally.md) for what ends up in `build/bin/`).
## Configuration Inputs
Almost everything the installer needs is hard-coded in `scripts/installer.nsi`. The only configurable input is the version string, supplied at `makensis` time:
- `APP_VERSION` — passed via `-DAPP_VERSION="<value>"`. Falls back to `"localbuild"` if not provided, so manual local runs still work.
This single value is reused in three visible places, so the installer, the uninstaller, and the OS Programs and Features entry all advertise the same version:
- installer/uninstaller window title (NSIS `Name`): `Card Collection Manager 3 <version>`
- footer / branding text on every wizard page (NSIS `BrandingText`): `Card Collection Manager 3 <version>`
- Add/Remove Programs `DisplayVersion` registry value, so Windows shows the version in its own column
The MUI welcome page and the uninstall confirm page reference `$(^Name)` internally, so embedding the version into `Name` is enough to make those pages say "Welcome to the Card Collection Manager 3 \<version\> Setup Wizard" and "Card Collection Manager 3 \<version\> will be uninstalled..." respectively, without any extra wiring.
## Installed Sections
The installer presents three sections on the Components page:
- **Core files (required)** — read-only (`SectionIn RO`). Copies the full `build/bin/` payload into `$INSTDIR`, writes the uninstaller, and registers the Add/Remove Programs entry plus an `App Paths` entry so `ccm3` resolves from `Win+R`.
- **Start Menu shortcuts** — creates a shortcut at the top level of the Start Menu plus a `Card Collection Manager 3` folder containing both the app shortcut and an "Uninstall" shortcut. Uses `SetShellVarContext all` so shortcuts go to the all-users Start Menu.
- **Desktop shortcut** — creates a desktop shortcut for all users.
Shortcut names intentionally do **not** include the version, so installing a newer version overwrites the existing shortcuts cleanly instead of leaving orphaned per-version entries behind.
## Registry Layout
The installer writes two registry roots, both under `HKLM` so an uninstall removes them cleanly regardless of which user launches it:
- `Software\Microsoft\Windows\CurrentVersion\Uninstall\Card Collection Manager 3` (the Add/Remove Programs entry):
- `DisplayName` — product name
- `DisplayVersion` — value of `APP_VERSION`
- `DisplayIcon` — path to `ccm3.exe`
- `UninstallString` / `QuietUninstallString` — interactive and silent uninstall commands
- `InstallLocation``$INSTDIR`
- `NoModify` / `NoRepair` — both `1` (we do not implement modify/repair flows)
- `Software\Microsoft\Windows\CurrentVersion\App Paths\ccm3.exe`:
- default value — full path to `ccm3.exe`
- `Path``$INSTDIR` so child processes inherit DLL search rights
The uninstall section (`Section "Uninstall"`) deletes both roots and removes the install directory and all created shortcuts. It uses `RMDir /r "$INSTDIR"` because the install dir is owned by the app.
## Build Commands
Run from the repository root.
### Local manual build
```powershell
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
makensis -DAPP_VERSION="0.1.0-localbuild" scripts/installer.nsi
```
The output is `ccm3-windows-installer.exe` at the repository root. With no `-DAPP_VERSION`, the installer self-labels as `localbuild` instead.
### CI build
Both Windows workflows install NSIS via MSYS2 (`mingw-w64-ucrt-x86_64-nsis`) and invoke the same script:
```yaml
- name: Build Windows installer
run: makensis -DAPP_VERSION="${VERSION}" scripts/installer.nsi
```
The `${VERSION}` value comes from:
- `scripts/compute_master_semver.sh` for merged `master` PRs (semantic version, e.g. `1.2.3`)
- `scripts/compute_feature_version.sh` for non-`master` branches (e.g. `feature-dark-mode-a1b2c3d`)
The exact same `${VERSION}` is also passed to `cmake -DCCM_APP_VERSION=...`, so the installer/uninstaller, the Programs and Features entry, and the running app's About dialog always agree.
## Artifact Names
The CI artifacts produced from a single installer build are documented in [CI/CD Guide](ci-cd-guide.md), but for reference:
- raw build output: `ccm3-windows-installer.exe` (in repo root, regardless of version)
- feature workflow artifact: `ccm3-windows-installer-<version>` (folder containing the exe)
- master release asset: `ccm3-windows-installer-<semver>.exe` (renamed at release-asset packaging time)
Renaming happens in the workflow's release-assets step, not in `installer.nsi`, so the script's `OutFile` is intentionally fixed.
## Editing Rules
When changing the installer:
- edit `scripts/installer.nsi` and keep both `.github/workflows/feature-windows.yml` and `.github/workflows/master-windows.yml` invoking it the same way
- pass the version through `-DAPP_VERSION="${VERSION}"` so the installer, uninstaller, and Programs and Features stay in sync with `CCM_APP_VERSION`
- keep installer assets that should not be generated at runtime (such as `installer_icon.ico`) committed in `scripts/`
- keep shortcut display names version-agnostic so upgrades do not orphan old shortcuts
- if you add a new registry value to the uninstall key, mirror it in the `Section "Uninstall"` cleanup if it lives outside that key
## Troubleshooting
- **`makensis: command not found`:** install NSIS and ensure `makensis` is on `PATH`. In CI this is provided by the MSYS2 package `mingw-w64-ucrt-x86_64-nsis`.
- **`File: ... \build\bin\*.*` failures:** the build payload is missing. Run `cmake --build build --parallel` first and confirm `build/bin/ccm3.exe` plus the runtime DLLs exist (see [Build Locally Guide](dow-doc-build-locally.md)).
- **Installer self-labels as `localbuild` in CI:** `-DAPP_VERSION="${VERSION}"` was not passed, or `${VERSION}` was empty. Check that the workflow's version-computation step ran before the installer step and exported `VERSION`.
- **Programs and Features does not show a version:** the `DisplayVersion` registry write was skipped because the installer was built without `APP_VERSION` (or with an empty value). Rebuild with the define set.
- **Old shortcuts left behind after upgrade:** shortcut display names were changed (or were made version-specific). Restore the version-agnostic names so upgrades overwrite cleanly.
- **Uninstaller appears to leave files:** `RMDir /r "$INSTDIR"` does not remove files outside `$INSTDIR`. Anything written by the app at runtime under user profile paths is intentionally kept; the uninstaller only manages what the installer placed.
## Related Docs
- [CI/CD Guide](ci-cd-guide.md)
- [Versioning Guide](versioning.md)
- [Build Locally Guide](dow-doc-build-locally.md)