minor: yugioh support added

This commit is contained in:
Sebastian Dine
2026-05-09 19:32:18 +02:00
committed by GitHub
parent 6f575f4cec
commit 6ff4406638
68 changed files with 4994 additions and 134 deletions
+1
View File
@@ -25,6 +25,7 @@ struct AppContext {
CardPreviewService& cardPreview;
IGameModule& magicModule;
IGameModule& pokemonModule;
IGameModule& yuGiOhModule;
// Active per-game UI bundles. The order is the order shown in the
// Game menu; the composition root constructs them and hands raw
// pointers in. `MainFrame` does not own these — `app/main.cpp` does.
@@ -90,6 +90,7 @@ protected:
}
buildLayout();
populateChoices();
openingSnapshot_ = card_;
Thaw();
}
@@ -119,6 +120,11 @@ protected:
return "(no sets cached - use Sets > " + updateMenuName() + ")";
}
// Called when the card name field changes. Games that cache upstream print
// metadata keyed by `(name, set)` should clear it here so stale "Next"
// controls cannot outlive the lookup identity.
virtual void onCardLookupContextChanged() {}
// Common helpers ----------------------------------------------------------
void appendRow(wxFlexGridSizer* grid, const wxString& label, wxWindow* ctrl) {
@@ -129,12 +135,26 @@ protected:
[[nodiscard]] TCard& mutableCard() noexcept { return card_; }
[[nodiscard]] const TCard& constCard() const noexcept { return card_; }
void syncCardFromControls() { writeFromControls(); }
[[nodiscard]] wxComboBox* setComboControl() const noexcept { return setCombo_; }
[[nodiscard]] const Set* selectedSetFromControls() const {
const auto& available = availableSets();
if (!setCombo_ || !setCombo_->IsEnabled()) return nullptr;
const int sel = setCombo_->GetSelection();
if (sel < 0 || static_cast<std::size_t>(sel) >= available.size()) return nullptr;
return &available[static_cast<std::size_t>(sel)];
}
private:
void readSets() {
auto loaded = setService_.getSets(game_);
if (loaded.isOk()) {
sets_ = std::move(loaded).value();
std::sort(sets_.begin(), sets_.end(),
[](const Set& a, const Set& b) {
return a.releaseDate < b.releaseDate;
});
}
}
@@ -148,6 +168,10 @@ private:
grid->AddGrowableCol(1, 1);
nameCtrl_ = new wxTextCtrl(this, wxID_ANY, wxString::FromUTF8(card_.name.c_str()));
nameCtrl_->Bind(wxEVT_TEXT, [this](wxCommandEvent& ev) {
onCardLookupContextChanged();
ev.Skip();
});
appendRow(grid, "Name", nameCtrl_);
setCombo_ = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0,
@@ -393,6 +417,14 @@ private:
"Add card", wxOK | wxICON_INFORMATION);
return;
}
if (mode_ == EditMode::Edit && !(card_ == openingSnapshot_)) {
if (showThemedConfirmDialog(
this,
wxString::FromUTF8("Save changes to this card?"),
wxString::FromUTF8("Save changes")) != wxID_YES) {
return;
}
}
ev.Skip();
}
@@ -503,6 +535,7 @@ private:
SetService& setService_;
EditMode mode_;
TCard card_;
TCard openingSnapshot_{};
Game game_;
std::vector<Set> sets_;
+65 -11
View File
@@ -30,9 +30,12 @@
#include <wx/bitmap.h>
#include <wx/colour.h>
#include <wx/event.h>
#include <wx/filename.h>
#include <wx/image.h>
#include <wx/listbox.h>
#include <wx/log.h>
#include <wx/mstream.h>
#include <wx/stdpaths.h>
#include <wx/panel.h>
#include <wx/settings.h>
#include <wx/sizer.h>
@@ -43,8 +46,10 @@
#include <atomic>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <thread>
#include <tuple>
@@ -221,6 +226,9 @@ protected:
imageService_(imageService),
cardPreview_(cardPreview),
state_(std::make_shared<PreviewState>()) {
wxFileName exe(wxStandardPaths::Get().GetExecutablePath());
exe.SetFullName(wxEmptyString);
exeDirForBundledAssets_ = exe.GetPathWithSep().ToStdString(wxConvUTF8);
state_->panel = this;
}
@@ -284,6 +292,9 @@ private:
case Game::Pokemon:
// Mirrors CCM2's unresolved-preview fallback image.
return "https://archives.bulbagarden.net/media/upload/1/17/Cardback.jpg";
case Game::YuGiOh:
// Yugipedia English TCG backing (thumbnail — smaller than full scan).
return "https://ms.yugipedia.com/thumb/e/e5/Back-EN.png/250px-Back-EN.png";
default:
return {};
}
@@ -367,25 +378,58 @@ private:
CardPreviewService* svcPtr = &cardPreview_;
auto [name, setId, setNo] = previewKey(card);
const Game game = gameId();
const std::string exeDirCopy = exeDirForBundledAssets_;
std::thread([state, gen, svcPtr, name = std::move(name),
setId = std::move(setId), setNo = std::move(setNo), game]() {
setId = std::move(setId), setNo = std::move(setNo), game,
exeDirCopy]() {
auto bytes = svcPtr->fetchPreviewBytes(game, name, setId, setNo);
bool ok = bytes.isOk();
bool usedFallback = false;
std::string payload = ok ? std::move(bytes).value() : std::string{};
std::string err = ok ? std::string{} : bytes.error();
auto applyFallbackPayload = [&](std::string p) {
if (p.empty()) return;
payload = std::move(p);
ok = true;
usedFallback = true;
err.clear();
};
if (!ok || payload.empty()) {
const std::string fallbackUrl = fallbackImageUrlForGame(game);
if (!fallbackUrl.empty()) {
auto fallbackBytes = svcPtr->fetchImageBytesByUrl(fallbackUrl);
if (fallbackBytes.isOk()) {
payload = std::move(fallbackBytes).value();
ok = !payload.empty();
if (ok) {
usedFallback = true;
err.clear();
if (game == Game::YuGiOh) {
if (auto fb =
svcPtr->fetchImageBytesByUrl(
"https://ms.yugipedia.com/thumb/e/e5/Back-EN.png/"
"250px-Back-EN.png");
fb.isOk()) {
applyFallbackPayload(std::move(fb).value());
}
if (!ok || payload.empty()) {
if (auto fb = svcPtr->fetchImageBytesByUrl(
"https://ms.yugipedia.com/e/e5/Back-EN.png");
fb.isOk()) {
applyFallbackPayload(std::move(fb).value());
}
}
if (!ok || payload.empty()) {
namespace fs = std::filesystem;
const fs::path asset =
fs::path(exeDirCopy) / "assets" / "ygo_card_back.png";
std::ifstream in(asset, std::ios::binary);
if (in) {
std::ostringstream ss;
ss << in.rdbuf();
applyFallbackPayload(ss.str());
}
}
} else {
const std::string fallbackUrl = fallbackImageUrlForGame(game);
if (!fallbackUrl.empty()) {
auto fallbackBytes = svcPtr->fetchImageBytesByUrl(fallbackUrl);
if (fallbackBytes.isOk()) {
applyFallbackPayload(std::move(fallbackBytes).value());
}
}
}
@@ -419,7 +463,16 @@ private:
wxMemoryInputStream stream(bytes.data(), bytes.size());
wxImage img;
if (!img.LoadFile(stream, wxBITMAP_TYPE_ANY)) {
// libpng emits iCCP warnings for some upstream PNGs (e.g. Yugipedia
// scans with embedded sRGB chunks wx considers invalid). wx forwards
// those as wxLogWarning -> modal dialog. Suppress logging for decode
// only; the pixels load correctly either way.
bool decoded = false;
{
wxLogNull suppressPngWarnings;
decoded = img.LoadFile(stream, wxBITMAP_TYPE_ANY);
}
if (!decoded) {
previewStatus_->SetLabelText("(preview decode failed)");
clearPreview();
Layout();
@@ -477,6 +530,7 @@ private:
ImageService& imageService_;
CardPreviewService& cardPreview_;
std::string exeDirForBundledAssets_;
std::optional<TCard> card_;
wxStaticBitmap* previewBitmap_{nullptr};
@@ -0,0 +1,73 @@
#pragma once
#include "ccm/domain/YuGiOhCard.hpp"
#include "ccm/ports/ICardPreviewSource.hpp"
#include "ccm/services/CardPreviewService.hpp"
#include "ccm/ui/BaseCardEditDialog.hpp"
#include <wx/button.h>
#include <wx/stattext.h>
namespace ccm::ui {
class YuGiOhCardEditDialog final : public BaseCardEditDialog<YuGiOhCard> {
public:
YuGiOhCardEditDialog(wxWindow* parent,
ImageService& imageService,
SetService& setService,
CardPreviewService& cardPreview,
EditMode mode,
YuGiOhCard initial,
const std::vector<Set>* preloadedSets = nullptr);
protected:
void buildFlagsRow(wxBoxSizer* flagsBox) override;
void appendExtraRows(wxFlexGridSizer* grid) override;
void readExtraFromCard() override;
void writeExtraToCard() override;
[[nodiscard]] std::string updateMenuName() const override { return "Update Yu-Gi-Oh!"; }
void onCardLookupContextChanged() override;
private:
void onAutoDetectSetNo(wxCommandEvent&);
void onAutoDetectRarity(wxCommandEvent&);
void onNextSetNo(wxCommandEvent&);
void onNextRarity(wxCommandEvent&);
void onSetNoTextChanged(wxCommandEvent&);
void onSetSelectionChanged(wxCommandEvent&);
void autoDetectFromApi(bool fillSetNo, bool fillRarity);
void refreshSetNoFullPreview();
void clearCachedPrintVariants();
bool fetchAndCachePrintVariants();
void rebuildVariantRingsFromCache();
void filterCachedVariantsForCardLanguage();
void syncRingPositionsToControls();
void applyRarityStringToChoice(const std::string& rarity);
[[nodiscard]] std::string currentFullSetNoFromControls() const;
void refreshVariantNextControls();
[[nodiscard]] std::string extractSetNoNumeric(std::string_view fullSetNo) const;
[[nodiscard]] std::string composeFullSetNo(std::string_view numeric) const;
void scheduleDeferredVariantPrefetch();
void prefetchVariantsForCurrentCardSilent(unsigned capturedEpoch);
EditMode dialogMode_;
unsigned variantFetchEpoch_{0};
CardPreviewService& cardPreview_;
wxTextCtrl* setNoCtrl_{nullptr};
wxStaticText* setNoFullPreview_{nullptr};
wxChoice* rarityChoice_{nullptr};
wxButton* autoSetNoBtn_{nullptr};
wxButton* nextSetNoBtn_{nullptr};
wxButton* autoRarityBtn_{nullptr};
wxButton* nextRarityBtn_{nullptr};
wxCheckBox* firstEditionCheck_{nullptr};
wxCheckBox* signedCheck_{nullptr};
wxCheckBox* alteredCheck_{nullptr};
std::vector<AutoDetectedPrint> cachedVariants_;
std::vector<std::string> uniqueSetCodes_;
std::vector<std::string> raritiesForCurrentSetCode_;
std::size_t setCodeRingPos_{0};
std::size_t rarityRingPos_{0};
};
} // namespace ccm::ui
@@ -0,0 +1,22 @@
#pragma once
#include "ccm/domain/YuGiOhCard.hpp"
#include "ccm/services/CardSorter.hpp"
#include "ccm/ui/BaseCardListPanel.hpp"
namespace ccm::ui {
class YuGiOhCardListPanel final : public BaseCardListPanel<YuGiOhCard, YuGiOhSortColumn> {
public:
explicit YuGiOhCardListPanel(wxWindow* parent);
protected:
[[nodiscard]] std::vector<TextColumnSpec> declareTextColumns() const override;
[[nodiscard]] std::vector<IconColumnSpec> declareIconColumns() const override;
[[nodiscard]] std::string renderTextCell(const YuGiOhCard& card, std::size_t idx) const override;
[[nodiscard]] bool isIconColumnSet(const YuGiOhCard& card, std::size_t idx) const override;
void sortBy(YuGiOhSortColumn column, bool ascending) override;
[[nodiscard]] bool matchesFilter(const YuGiOhCard& card, std::string_view filter) const override;
};
} // namespace ccm::ui
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include "ccm/domain/YuGiOhCard.hpp"
#include "ccm/games/IGameModule.hpp"
#include "ccm/services/CardPreviewService.hpp"
#include "ccm/services/CollectionService.hpp"
#include "ccm/services/ConfigService.hpp"
#include "ccm/services/ImageService.hpp"
#include "ccm/services/SetService.hpp"
#include "ccm/ui/IGameView.hpp"
#include <string>
#include <string_view>
#include <vector>
namespace ccm::ui {
class YuGiOhCardListPanel;
class YuGiOhSelectedCardPanel;
class YuGiOhGameView final : public IGameView {
public:
YuGiOhGameView(ConfigService& config,
CollectionService<YuGiOhCard>& collection,
SetService& sets,
ImageService& images,
CardPreviewService& cardPreview,
IGameModule& module);
[[nodiscard]] Game gameId() const noexcept override { return Game::YuGiOh; }
[[nodiscard]] std::string displayName() const override { return "Yu-Gi-Oh!"; }
wxPanel* listPanel(wxWindow* parent) override;
wxPanel* selectedPanel(wxWindow* parent) override;
void refreshCollection() override;
void onAddCard(wxWindow* parentWindow) override;
void onEditCard(wxWindow* parentWindow) override;
void onDeleteCard(wxWindow* parentWindow) override;
std::string onUpdateSets(wxWindow* parentWindow) override;
void setFilter(std::string_view filter) override;
void applyTheme(const ThemePalette& palette) override;
[[nodiscard]] std::string updateSetsMenuLabel() const override { return "Update Yu-Gi-Oh!"; }
private:
void ensureSetsLoaded();
const std::vector<Set>& setsForDialog();
ConfigService& config_;
CollectionService<YuGiOhCard>& collection_;
SetService& sets_;
ImageService& images_;
CardPreviewService& cardPreview_;
IGameModule& module_;
YuGiOhCardListPanel* listPanel_{nullptr};
YuGiOhSelectedCardPanel* selectedPanel_{nullptr};
std::vector<Set> setsCache_;
bool attemptedInitialSetLoad_{false};
};
} // namespace ccm::ui
@@ -0,0 +1,24 @@
#pragma once
#include "ccm/domain/YuGiOhCard.hpp"
#include "ccm/ui/BaseSelectedCardPanel.hpp"
namespace ccm::ui {
class YuGiOhSelectedCardPanel final : public BaseSelectedCardPanel<YuGiOhCard> {
public:
YuGiOhSelectedCardPanel(wxWindow* parent,
ImageService& imageService,
CardPreviewService& cardPreview);
protected:
[[nodiscard]] std::vector<DetailRowSpec> declareDetailRows() const override;
[[nodiscard]] std::vector<FlagIconSpec> declareFlagIcons() const override;
[[nodiscard]] std::string detailValueFor(const YuGiOhCard& card, DetailKey key) const override;
[[nodiscard]] bool isFlagSet(const YuGiOhCard& card, DetailKey key) const override;
[[nodiscard]] std::tuple<std::string, std::string, std::string>
previewKey(const YuGiOhCard& card) const override;
[[nodiscard]] Game gameId() const noexcept override { return Game::YuGiOh; }
};
} // namespace ccm::ui