mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 17:01:02 +00:00
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:
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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 |
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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`.
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user