From 86f6098d43e1e824192435133ccc10d23ab08518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 11 Oct 2026 15:10:43 +0200 Subject: [PATCH] [Tutorial] Fix review blockers: null VDS guard, overlay QPointer, bounded fill, upgrade seeding --- .../src/game_graphics/deckview/deck_view_container.cpp | 5 ++++- .../widgets/general/tutorial/tutorial_controller.h | 2 +- .../widgets/general/tutorial/tutorial_overlay.h | 5 ++++- cockatrice/src/interface/widgets/tabs/tab_game.cpp | 7 ++++++- .../visual_deck_editor/visual_deck_editor_widget.cpp | 8 +++++++- .../libcockatrice/settings/interface_settings.cpp | 10 ++++++++++ 6 files changed, 32 insertions(+), 5 deletions(-) diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp index 36ee53af7..6095e40ec 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp @@ -135,7 +135,10 @@ TutorialSequence DeckViewContainer::generateTutorialSequence() {this, tr("There are multiple ways to select a deck:\n\n- From a local file" "\n- From the contents of your clipboard\nFrom an external online service")}); - visualDeckStorageWidget->generateTutorialSequence(deckViewContainerSequence); + // The VDS widget only exists when "show visual deck storage in game" is on. + if (visualDeckStorageWidget) { + visualDeckStorageWidget->generateTutorialSequence(deckViewContainerSequence); + } return deckViewContainerSequence; } diff --git a/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.h b/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.h index 567c5ea12..617a2972d 100644 --- a/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.h +++ b/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.h @@ -98,7 +98,7 @@ private: QWidget *tutorializedWidget; TutorialOverlay *tutorialOverlay; - QVector sequences; + QList sequences; bool tutorialCompleted = false; diff --git a/cockatrice/src/interface/widgets/general/tutorial/tutorial_overlay.h b/cockatrice/src/interface/widgets/general/tutorial/tutorial_overlay.h index 9bc183df9..f47467482 100644 --- a/cockatrice/src/interface/widgets/general/tutorial/tutorial_overlay.h +++ b/cockatrice/src/interface/widgets/general/tutorial/tutorial_overlay.h @@ -1,6 +1,7 @@ #ifndef TUTORIAL_OVERLAY_H #define TUTORIAL_OVERLAY_H +#include #include class QFrame; @@ -49,7 +50,9 @@ private: void recomputeLayout(); QRect computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const; - QWidget *targetWidget = nullptr; + // QPointer: the target can be deleted while the overlay lives on (e.g. toggling + // the in-game VDS setting deleteLater()s it), so never hold a raw pointer across steps. + QPointer targetWidget; QFrame *controlBar = nullptr; QLabel *titleLabel = nullptr; QPushButton *nextButton = nullptr; diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 1c5df9a4f..9a702a918 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -220,6 +220,11 @@ void TabGame::finishTutorialInitialization() tutorialInitialized = true; } + // No sequence building (and no widget access) once the tutorial is done. + if (SettingsCache::instance().userInterface().getTutorialCompleted()) { + return; + } + auto deckViewSequence = deckViewContainers.first()->generateTutorialSequence(); tutorialController->addSequence(deckViewSequence); @@ -376,7 +381,7 @@ void TabGame::finishTutorialInitialization() tutorialController->addSequence(gamePlaySequence); - if (tutorialStarted || SettingsCache::instance().userInterface().getTutorialCompleted()) { + if (tutorialStarted) { return; } // The tab may have been shown before the local player joined; start now that diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index e5efba3a7..f2f326924 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -508,8 +508,14 @@ TutorialSequence VisualDeckEditorWidget::addTutorialSteps() organizationStep.text = "Let's look at how cards are organized and displayed now.\n\nWe'll add some random cards " "from the database to your deck, so you can see it in action properly."; organizationStep.onExit = [this]() { - while (deckListModel->getDeckList()->getCardList().size() < 60) { + // getCardList() counts distinct card nodes, not copies, so once every random pick only bumps an + // existing node's count the size never grows. With fewer than ~60 distinct cards in the database + // this would loop forever, so bound the attempts to always terminate. + static constexpr int MaxFillAttempts = 600; + int attempts = 0; + while (deckListModel->getDeckList()->getCardList().size() < 60 && attempts < MaxFillAttempts) { deckListModel->addCard(CardDatabaseManager::query()->getRandomCard(), DECK_ZONE_MAIN); + ++attempts; } }; diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp index 88641df48..21ba1a1f7 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp @@ -5,6 +5,8 @@ #include "libcockatrice/utility/playmat_params.h" #include +#include +#include #include #include #include @@ -248,6 +250,14 @@ QStringList InterfaceSettings::getUserListExpandedSections() const bool InterfaceSettings::getTutorialCompleted() const { + // Upgrading users have settings files but no tutorial key yet: treat the tutorial as seen so they are + // not overlaid on upgrade. Genuinely new installs have no settings files at all, so they still get it. + if (!getSettings().contains("interface/tutorialCompleted")) { + QDir settingsDir = QFileInfo(settingPath).dir(); + if (!settingsDir.entryList({"*.ini"}, QDir::Files).isEmpty()) { + return true; + } + } return getValue("tutorialCompleted", QString(), QString(), false).toBool(); }