From 485e4d56aa993193479500ee27d26ea2599079a6 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Sat, 21 Feb 2026 21:38:05 +0100 Subject: [PATCH 001/116] Update CONTRIBUTING.md (#6615) I keep forgetting this so I'm making the warning bigger --- .github/CONTRIBUTING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ffde66d7b..b293b3937 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -461,7 +461,11 @@ revoke the tag by doing the following: git push --delete upstream $TAG_NAME git tag -d $TAG_NAME ``` -You can also do this on GitHub, you'll also want to delete the false release. +You can also do this on GitHub. + +> [!NOTE] +> If you want to push a new release to replace it immediately with the same +> name you have to delete the automatically created release first! In the first lines of [CMakeLists.txt]( https://github.com/Cockatrice/Cockatrice/blob/master/CMakeLists.txt) From f0da3cff405eb7ae05e7b514a48e8b67d9b3b102 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:12:28 -0800 Subject: [PATCH 002/116] [PictureLoader] Add hash of new gatherer card back image to blacklist (#6621) --- .../card_picture_loader/card_picture_loader_worker_work.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp index 4c676c704..6bd57c3d5 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp @@ -12,7 +12,10 @@ #include // Card back returned by gatherer when card is not found -static const QStringList MD5_BLACKLIST = {"db0c48db407a907c16ade38de048a441"}; +static const QStringList MD5_BLACKLIST = { + "db0c48db407a907c16ade38de048a441", // Old card back hash. Keep around just in case + "fbc7d763c08771c260b39e2115414eeb" // Current card back hash +}; CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad) : QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)), From 71cf3fabbffcffd6cf8061bfcad5f00f0d570e0f Mon Sep 17 00:00:00 2001 From: Bruno Alexandre Rosa <1791393+brunoalr@users.noreply.github.com> Date: Sun, 22 Feb 2026 14:52:07 -0300 Subject: [PATCH 003/116] build: use qt 6.10 on macOS and win10 (#6491) * build: use qt 6.8.* LTS in Windows 10 and macOS * bump msvc * bump to 6.10 * reset style to the default instead of the first key --------- Co-authored-by: ebbit1q --- .github/workflows/desktop-build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 5fab8f993..35e0c0055 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -260,7 +260,7 @@ jobs: make_package: 1 package_suffix: "-macOS13_Intel" artifact_name: macOS13_Intel-package - qt_version: 6.6.* + qt_version: 6.10.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets cmake_generator: Ninja @@ -275,7 +275,7 @@ jobs: make_package: 1 package_suffix: "-macOS14" artifact_name: macOS14-package - qt_version: 6.6.* + qt_version: 6.10.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets cmake_generator: Ninja @@ -290,7 +290,7 @@ jobs: make_package: 1 package_suffix: "-macOS15" artifact_name: macOS15-package - qt_version: 6.6.* + qt_version: 6.10.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets cmake_generator: Ninja @@ -302,7 +302,7 @@ jobs: soc: Apple xcode: "16.4" type: Debug - qt_version: 6.6.* + qt_version: 6.10.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets cmake_generator: Ninja @@ -327,8 +327,8 @@ jobs: make_package: 1 package_suffix: "-Win10" artifact_name: Windows10-installer - qt_version: 6.6.* - qt_arch: win64_msvc2019_64 + qt_version: 6.10.* + qt_arch: win64_msvc2022_64 qt_modules: qtimageformats qtmultimedia qtwebsockets cmake_generator: "Visual Studio 17 2022" cmake_generator_platform: x64 From 0f2899b5c7da0521ac389fad06c3d0f9eb8d185e Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:11:10 -0800 Subject: [PATCH 004/116] [DeckEditor] Alternate row colors in history list (#6626) --- .../widgets/deck_editor/deck_list_history_manager_widget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp index d2d748753..cef459752 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp @@ -36,6 +36,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de historyLabel = new QLabel(this); historyList = new QListWidget(this); + historyList->setAlternatingRowColors(true); historyButton->addSettingsWidget(historyLabel); historyButton->addSettingsWidget(historyList); From c6dc7eee64f4f776005ba9c81428fa43caa08373 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 22 Feb 2026 22:11:58 +0100 Subject: [PATCH 005/116] CI: Remove Windows 7 build (#6625) * Update release_template.md * Remove Win7 --- .ci/release_template.md | 1 - .github/workflows/desktop-build.yml | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/.ci/release_template.md b/.ci/release_template.md index 0207662b5..0e6c05165 100644 --- a/.ci/release_template.md +++ b/.ci/release_template.md @@ -10,7 +10,6 @@ Available pre-compiled binaries for installation: WindowsWindows 10+ - • Windows 7+ macOSmacOS 15+ Sequoia Apple M diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 35e0c0055..fbb0e2af3 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -308,18 +308,6 @@ jobs: cmake_generator: Ninja use_ccache: 1 - - os: Windows - target: 7 - runner: windows-2022 - type: Release - make_package: 1 - package_suffix: "-Win7" - artifact_name: Windows7-installer - qt_version: 5.15.* - qt_arch: win64_msvc2019_64 - cmake_generator: "Visual Studio 17 2022" - cmake_generator_platform: x64 - - os: Windows target: 10 runner: windows-2022 From a90997353bfa76435a91b78ede8e2e6189f84cf9 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:33:08 -0800 Subject: [PATCH 006/116] [TabGame] Fix concede removing player without waiting for server (#6622) --- cockatrice/src/interface/widgets/tabs/tab_game.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index e4a11f979..fae9dffe5 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -488,14 +488,12 @@ void TabGame::actConcede() QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return; emit game->getPlayerManager()->activeLocalPlayerConceded(); - player->setConceded(true); } else { if (QMessageBox::question(this, tr("Unconcede"), tr("You have already conceded. Do you want to return to this game?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return; emit game->getPlayerManager()->activeLocalPlayerUnconceded(); - player->setConceded(false); } } From 9f00c6f9554c4dc789ecafb9d134064fb10155b2 Mon Sep 17 00:00:00 2001 From: tooomm Date: Mon, 23 Feb 2026 01:44:14 +0100 Subject: [PATCH 007/116] Update vcpkg submodule (#6627) * Update vcpkg * Target 2025.10.17 --- vcpkg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcpkg b/vcpkg index 623240005..74e653621 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit 62324000504cdd27282f8275c99135cfb2bd1dc0 +Subproject commit 74e6536215718009aae747d86d84b78376bf9e09 From 2cb16c9fd03b67c2010c87f7349425d74e84167d Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:54:58 -0800 Subject: [PATCH 008/116] [CardDatabaseDisplay] Reduce width by using icons (#6603) * [CardDatabaseDisplay] Reduce width by using icons * use public domain filter svg icon --- cockatrice/cockatrice.qrc | 1 + cockatrice/resources/icons/filter.svg | 12 ++++ ...database_display_filter_toolbar_widget.cpp | 59 +++++++++++++------ ...l_database_display_filter_toolbar_widget.h | 2 + 4 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 cockatrice/resources/icons/filter.svg diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index f087bfe66..37fb145f0 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -24,6 +24,7 @@ resources/icons/dragon.svg resources/icons/dropdown_collapsed.svg resources/icons/dropdown_expanded.svg + resources/icons/filter.svg resources/icons/floppy_disk.svg resources/icons/forgot_password.svg resources/icons/gear.svg diff --git a/cockatrice/resources/icons/filter.svg b/cockatrice/resources/icons/filter.svg new file mode 100644 index 000000000..d53d9ba29 --- /dev/null +++ b/cockatrice/resources/icons/filter.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp index 324236ab7..3cc1bf23b 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp @@ -2,11 +2,14 @@ #include "visual_database_display_widget.h" +#include + VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *_parent) : QWidget(_parent), visualDatabaseDisplay(_parent) { filterContainerLayout = new QHBoxLayout(this); filterContainerLayout->setContentsMargins(11, 0, 11, 0); + filterContainerLayout->setSpacing(2); setLayout(filterContainerLayout); filterContainerLayout->setAlignment(Qt::AlignLeft); @@ -15,9 +18,17 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg connect(this, &VisualDatabaseDisplayFilterToolbarWidget::searchModelChanged, visualDatabaseDisplay, &VisualDatabaseDisplayWidget::onSearchModelChanged); - filterByLabel = new QLabel(this); + sortGroupBox = new QGroupBox(this); + filterGroupBox = new QGroupBox(this); + + auto scalePixmap = [](const QString &fileName) { return QIcon(QPixmap(fileName)).pixmap({20, 20}); }; sortByLabel = new QLabel(this); + sortByLabel->setPixmap(scalePixmap("theme:icons/sort_arrow_down")); + + filterByLabel = new QLabel(this); + filterByLabel->setPixmap(scalePixmap("theme:icons/filter")); + sortColumnCombo = new QComboBox(this); sortColumnCombo->setSizeAdjustPolicy(QComboBox::SizeAdjustPolicy::AdjustToContents); sortOrderCombo = new QComboBox(this); @@ -76,14 +87,20 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg void VisualDatabaseDisplayFilterToolbarWidget::initialize() { - sortByLabel->setVisible(true); - filterByLabel->setVisible(true); + // create groupbox layouts + auto sortLayout = new QHBoxLayout(this); + sortLayout->setContentsMargins(0, 0, 0, 0); + sortLayout->setSpacing(0); + sortGroupBox->setLayout(sortLayout); + sortLayout->setAlignment(Qt::AlignLeft); - quickFilterSaveLoadWidget->setVisible(true); - quickFilterNameWidget->setVisible(true); - quickFilterSubTypeWidget->setVisible(true); - quickFilterSetWidget->setVisible(true); + auto filterLayout = new QHBoxLayout(this); + filterLayout->setContentsMargins(0, 0, 0, 0); + filterLayout->setSpacing(2); + filterGroupBox->setLayout(filterLayout); + filterLayout->setAlignment(Qt::AlignLeft); + // create settings widgets auto filterModel = visualDatabaseDisplay->filterModel; saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel); @@ -101,23 +118,29 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize() quickFilterSetWidget->addSettingsWidget(setFilterWidget); quickFilterFormatLegalityWidget->addSettingsWidget(formatLegalityWidget); - filterContainerLayout->addWidget(sortByLabel); - filterContainerLayout->addWidget(sortColumnCombo); - filterContainerLayout->addWidget(sortOrderCombo); - filterContainerLayout->addWidget(filterByLabel); - filterContainerLayout->addWidget(quickFilterNameWidget); - filterContainerLayout->addWidget(quickFilterMainTypeWidget); - filterContainerLayout->addWidget(quickFilterSubTypeWidget); - filterContainerLayout->addWidget(quickFilterSetWidget); - filterContainerLayout->addWidget(quickFilterFormatLegalityWidget); + // fill groupbox layouts + sortLayout->addWidget(sortByLabel); + sortLayout->addWidget(sortColumnCombo); + sortLayout->addWidget(sortOrderCombo); + + filterLayout->addWidget(filterByLabel); + filterLayout->addWidget(quickFilterNameWidget); + filterLayout->addWidget(quickFilterMainTypeWidget); + filterLayout->addWidget(quickFilterSubTypeWidget); + filterLayout->addWidget(quickFilterSetWidget); + filterLayout->addWidget(quickFilterFormatLegalityWidget); + + // put everything into main layout + filterContainerLayout->addWidget(sortGroupBox); + filterContainerLayout->addWidget(filterGroupBox); filterContainerLayout->addStretch(); filterContainerLayout->addWidget(quickFilterSaveLoadWidget); } void VisualDatabaseDisplayFilterToolbarWidget::retranslateUi() { - sortByLabel->setText(tr("Sort by:")); - filterByLabel->setText(tr("Filter by:")); + sortByLabel->setToolTip(tr("Sort by")); + filterByLabel->setToolTip(tr("Filter by")); quickFilterSaveLoadWidget->setToolTip(tr("Save and load filters")); quickFilterNameWidget->setToolTip(tr("Filter by exact card name")); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h index a6c614656..5cca5187a 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h @@ -25,9 +25,11 @@ public: private: VisualDatabaseDisplayWidget *visualDatabaseDisplay; + QGroupBox *sortGroupBox; QLabel *sortByLabel; QComboBox *sortColumnCombo, *sortOrderCombo; + QGroupBox *filterGroupBox; QLabel *filterByLabel; QHBoxLayout *filterContainerLayout; From 12c667afd7f823f4532907543ebcf1b25e87aeed Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:34:23 -0800 Subject: [PATCH 009/116] [Search] Fix OR usage in examples (#6628) --- cockatrice/resources/help/deck_search.md | 2 +- cockatrice/resources/help/search.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cockatrice/resources/help/deck_search.md b/cockatrice/resources/help/deck_search.md index 3263daf04..163137335 100644 --- a/cockatrice/resources/help/deck_search.md +++ b/cockatrice/resources/help/deck_search.md @@ -47,6 +47,6 @@ searches are case insensitive.
[t:aggro OR o:control](#t:aggro OR o:control) (Any deck filename that contains either aggro or control)
Grouping:
-
red -([[]]:100 or aggro) (Any deck that has red in its filename but is not 100 cards or has aggro in its filename)
+
red -([[]]:100 OR aggro) (Any deck that has red in its filename but is not 100 cards or has aggro in its filename)
\ No newline at end of file diff --git a/cockatrice/resources/help/search.md b/cockatrice/resources/help/search.md index 63e6e74c3..0c8bdb450 100644 --- a/cockatrice/resources/help/search.md +++ b/cockatrice/resources/help/search.md @@ -51,16 +51,16 @@ In this list of examples below, each entry has an explanation and can be clicked
Edition:
[set:lea](#set:lea) (Cards that appear in Alpha, which has the set code LEA)
-
[e:lea or e:leb](#e:lea or e:leb) (Cards that appear in Alpha or Beta)
+
[e:lea OR e:leb](#e:lea OR e:leb) (Cards that appear in Alpha or Beta)
Negate:
[c:wu -c:m](#c:wu -c:m) (Any card that is white or blue, but not multicolored)
Branching:
-
[t:sliver or o:changeling](#t:sliver or o:changeling) (Any card that is either a sliver or has changeling)
+
[t:sliver OR o:changeling](#t:sliver OR o:changeling) (Any card that is either a sliver or has changeling)
Grouping:
-
t:angel -(angel or c:w) (Any angel that doesn't have angel in its name and isn't white)
+
t:angel -(angel OR c:w) (Any angel that doesn't have angel in its name and isn't white)
Regular Expression:
[/^fell/](#/^fell/) (Any card name that begins with "fell")
From 7ad2481e3d2f595631f9fa6091c66d4cee12c928 Mon Sep 17 00:00:00 2001 From: Rob Blanckaert Date: Wed, 25 Feb 2026 23:55:34 -0800 Subject: [PATCH 010/116] Update UnescapedStringListPart to include parentheses (#6631) * Update UnescapedStringListPart to include parentheses * also update deck_filter_string * add unit test --------- Co-authored-by: RickyRister --- cockatrice/src/filters/deck_filter_string.cpp | 2 +- libcockatrice_filters/libcockatrice/filters/filter_string.cpp | 2 +- tests/carddatabase/filter_string_test.cpp | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/filters/deck_filter_string.cpp b/cockatrice/src/filters/deck_filter_string.cpp index 42a77fe5a..6b671831d 100644 --- a/cockatrice/src/filters/deck_filter_string.cpp +++ b/cockatrice/src/filters/deck_filter_string.cpp @@ -31,7 +31,7 @@ GenericQuery <- String NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["]. NonSingleQuoteUnlessEscaped <- "\\\'". / ![']. -UnescapedStringListPart <- !['":<>=! ]. +UnescapedStringListPart <- !['":<>()=! ]. SingleApostropheString <- (UnescapedStringListPart+ ws*)* ['] (UnescapedStringListPart+ ws*)* String <- SingleApostropheString / UnescapedStringListPart+ / ["] ["] / ['] ['] diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp index 0d29f7897..704e8fadb 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp @@ -46,7 +46,7 @@ FieldQuery <- String [:] MatcherString / String ws? NumericExpression NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["]. NonSingleQuoteUnlessEscaped <- "\\\'". / ![']. -UnescapedStringListPart <- !['":<>=! ]. +UnescapedStringListPart <- !['":<>()=! ]. SingleApostropheString <- (UnescapedStringListPart+ ws*)* ['] (UnescapedStringListPart+ ws*)* String <- SingleApostropheString / UnescapedStringListPart+ / ["] ["] / ['] ['] StringValue <- String / [(] StringList [)] diff --git a/tests/carddatabase/filter_string_test.cpp b/tests/carddatabase/filter_string_test.cpp index c3e8c6fb7..c6d68be1f 100644 --- a/tests/carddatabase/filter_string_test.cpp +++ b/tests/carddatabase/filter_string_test.cpp @@ -71,6 +71,8 @@ QUERY(Color2, cat, "c:gw", true) QUERY(Color3, cat, "c!g", true) QUERY(Color4, cat, "c!gw", false) +QUERY(BracketNextToUnquotedString, cat, "(o:woof OR o:meow)", true) + } // namespace int main(int argc, char **argv) From 6f8f9f016a3b4f81ee269c403cc718dca51c575b Mon Sep 17 00:00:00 2001 From: tooomm Date: Thu, 26 Feb 2026 22:31:18 +0100 Subject: [PATCH 011/116] CI: `windows-2025` runner (#6632) * `windows-2025` runner * Install NSIS * fix naming --- .github/workflows/desktop-build.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index fbb0e2af3..a0878046e 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -310,7 +310,7 @@ jobs: - os: Windows target: 10 - runner: windows-2022 + runner: windows-2025 type: Release make_package: 1 package_suffix: "-Win10" @@ -410,6 +410,11 @@ jobs: modules: ${{matrix.qt_modules}} cache: true + - name: Install NSIS + if: matrix.os == 'Windows' + shell: bash + run: choco install nsis + - name: Setup vcpkg cache id: vcpkg-cache uses: TAServers/vcpkg-cache@v3 From 208ccc3a1a67ff94d71b520dc3aaaa1ca305e195 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Fri, 27 Feb 2026 00:53:39 -0500 Subject: [PATCH 012/116] fix(docs): correct typos in README (#6640) Fix 'projet' -> 'project' and 'invovled' -> 'involved'. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c08aee0e7..8ad9a092c 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Latest beta version: # Community Resources [![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](https://discord.gg/3Z9yzmA) -Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other projet contributors (`#dev` channel) or fellow users of the app. Come here to talk about the application, features, or just to hang out. +Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other project contributors (`#dev` channel) or fellow users of the app. Come here to talk about the application, features, or just to hang out. - [Official Website](https://cockatrice.github.io) - [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) - [Official Discord](https://discord.gg/3Z9yzmA) @@ -109,7 +109,7 @@ Cockatrice tries to use the [Google Developer Documentation Style Guide](https:/ Cockatrice uses Transifex to manage translations. You can help us bring Cockatrice, Oracle and Webatrice to your language and just adjust single wordings right from within your browser by visiting our [Transifex project page](https://explore.transifex.com/cockatrice/cockatrice/).
-Check out our [Translator FAQ](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ) for more information about getting invovled, and join a group of hundreds of others!
+Check out our [Translator FAQ](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ) for more information about getting involved, and join a group of hundreds of others!
# Build [![CI Desktop](https://github.com/Cockatrice/Cockatrice/actions/workflows/desktop-build.yml/badge.svg?branch=master&event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/desktop-build.yml?query=branch%3Amaster+event%3Apush) [![CI Docker](https://github.com/Cockatrice/Cockatrice/actions/workflows/docker-release.yml/badge.svg?branch=master&event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/docker-release.yml?query=branch%3Amaster+event%3Apush) [![CI Web](https://github.com/Cockatrice/Cockatrice/actions/workflows/web-build.yml/badge.svg?branch=master&event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/web-build.yml?query=branch%3Amaster+event%3Apush) From 59ce70afc516d428706d09748287a3a13db2e308 Mon Sep 17 00:00:00 2001 From: tooomm Date: Mon, 2 Mar 2026 01:59:18 +0100 Subject: [PATCH 013/116] Add Code Docs link to issue template (#6649) --- .github/ISSUE_TEMPLATE/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index c5354332b..f9abf643d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,9 +1,12 @@ blank_issues_enabled: false contact_links: - name: 💬 Discord Community (Get help with server issues, e.g. Login) - url: https://discord.gg/3Z9yzmA + url: https://discord.com/invite/3Z9yzmA about: Need help with using the client? Want to find some games? Try the Discord server! - name: 🌐 Translations (Help improve the localization of the app) url: https://explore.transifex.com/cockatrice/cockatrice/ # it is not possible to add a link to the wiki to this description about: For more information and guidance check our Translation FAQ on our wiki! + - name: 📖 Code Documentation + url: https://cockatrice.github.io/docs/ + about: From f978407a198c2127866e78635a68657da9997582 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:04:15 +0100 Subject: [PATCH 014/116] Bump actions/upload-artifact from 6 to 7 (#6652) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/desktop-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index a0878046e..e48cc7d3f 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -212,7 +212,7 @@ jobs: - name: Upload artifact id: upload_artifact if: matrix.package != 'skip' - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{matrix.distro}}${{matrix.version}}-package path: ${{steps.build.outputs.path}} @@ -499,7 +499,7 @@ jobs: - name: Upload artifact id: upload_artifact if: matrix.make_package - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{matrix.artifact_name}} path: ${{steps.build.outputs.path}} @@ -507,7 +507,7 @@ jobs: - name: Upload PDBs (Program Databases) if: matrix.os == 'Windows' - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: Windows${{matrix.target}}-PDBs path: | From e57ee8e9c9745a1ec20dfdc2439223fe3a0d5847 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Mon, 2 Mar 2026 23:06:05 +0100 Subject: [PATCH 015/116] fix set sorting (#6630) --- .../widgets/dialogs/dlg_manage_sets.cpp | 32 +++++++++---------- .../widgets/dialogs/dlg_manage_sets.h | 8 +++-- .../database/card_set/card_sets_model.cpp | 2 +- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp index 5e6a6a62d..a4f54564f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp @@ -20,8 +20,6 @@ #include #include -#define SORT_RESET -1 - WndSets::WndSets(QWidget *parent) : QMainWindow(parent) { setOrderIsSorted = false; @@ -97,7 +95,6 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) view->setDropIndicatorShown(true); view->setDragDropMode(QAbstractItemView::InternalMove); - view->sortByColumn(SetsModel::SortKeyCol, Qt::AscendingOrder); view->setColumnHidden(SetsModel::SortKeyCol, true); view->setColumnHidden(SetsModel::IsKnownCol, true); view->setColumnHidden(SetsModel::PriorityCol, true); @@ -196,10 +193,10 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) auto headerState = SettingsCache::instance().layouts().getSetsDialogHeaderState(); if (!headerState.isEmpty()) { view->header()->restoreState(headerState); - view->header()->setSortIndicator(SORT_RESET, Qt::DescendingOrder); } else { view->header()->resizeSections(QHeaderView::ResizeToContents); } + resetSort(); connect(view->header(), &QHeaderView::geometriesChanged, this, &WndSets::saveHeaderState); } @@ -239,6 +236,13 @@ void WndSets::rebuildMainLayout(int actionToTake) } } +void WndSets::resetSort() +{ + view->sortByColumn(SetsModel::SortKeyCol, Qt::AscendingOrder); + sortIndex = -1; + sortWarning->setVisible(false); +} + void WndSets::includeRebalancedCardsChanged(bool _includeRebalancedCards) { includeRebalancedCards = _includeRebalancedCards; @@ -260,9 +264,9 @@ void WndSets::actRestore() void WndSets::actRestoreOriginalOrder() { - view->header()->setSortIndicator(SORT_RESET, Qt::DescendingOrder); model->restoreOriginalOrder(); - sortWarning->setVisible(false); + view->selectionModel()->reset(); + resetSort(); } void WndSets::actDisableResetButton(const QString &filterString) @@ -288,11 +292,12 @@ void WndSets::actSort(int index) sortIndex = index; sortWarning->setVisible(true); } else { - view->header()->setSortIndicator(SORT_RESET, Qt::DescendingOrder); - sortIndex = -1; - sortWarning->setVisible(false); + resetSort(); } } + if (!view->selectionModel()->selection().empty()) { + view->scrollTo(view->selectionModel()->selectedRows().first()); + } } void WndSets::actIgnoreWarning() @@ -301,23 +306,18 @@ void WndSets::actIgnoreWarning() return; } model->sort(sortIndex, sortOrder); - view->header()->setSortIndicator(SORT_RESET, Qt::DescendingOrder); - sortIndex = -1; - sortWarning->setVisible(false); + resetSort(); } void WndSets::actDisableSortButtons(int index) { - if (index != SORT_RESET) { + if (index != SetsModel::SortKeyCol) { view->setDragEnabled(false); setOrderIsSorted = true; } else { setOrderIsSorted = false; view->setDragEnabled(true); } - if (!view->selectionModel()->selection().empty()) { - view->scrollTo(view->selectionModel()->selectedRows().first()); - } actToggleButtons(view->selectionModel()->selection(), QItemSelection()); } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.h b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.h index d7341caff..d9b77a76e 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.h @@ -45,9 +45,6 @@ private: QHBoxLayout *filterBox; int sortIndex; Qt::SortOrder sortOrder; - void closeEvent(QCloseEvent *ev) override; - void saveHeaderState(); - void rebuildMainLayout(int actionToTake); bool setOrderIsSorted; bool includeRebalancedCards; enum @@ -56,6 +53,11 @@ private: SOME_SETS_SELECTED }; + void closeEvent(QCloseEvent *ev) override; + void saveHeaderState(); + void rebuildMainLayout(int actionToTake); + void resetSort(); + public: explicit WndSets(QWidget *parent = nullptr); ~WndSets() override; diff --git a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp index b678e8276..f6dc4f9cf 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp @@ -45,7 +45,7 @@ QVariant SetsModel::data(const QModelIndex &index, int role) const switch (index.column()) { case SortKeyCol: - return QString("%1").arg(set->getSortKey(), 8, 10, QChar('0')); + return QString("%1").arg(index.row(), 8, 10, QChar('0')); case IsKnownCol: return set->getIsKnown(); case SetTypeCol: From 9794893b63ad1865d98913d601323e9bd58f3506 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:06:35 +0100 Subject: [PATCH 016/116] Bump actions/attest-build-provenance from 3 to 4 (#6653) Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 3 to 4. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/desktop-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index e48cc7d3f..e95898097 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -232,7 +232,7 @@ jobs: - name: Attest binary provenance id: attestation if: steps.upload_release.outcome == 'success' - uses: actions/attest-build-provenance@v3 + uses: actions/attest-build-provenance@v4 with: subject-name: ${{steps.build.outputs.name}} subject-path: ${{steps.build.outputs.path}} @@ -530,7 +530,7 @@ jobs: - name: Attest binary provenance id: attestation if: steps.upload_release.outcome == 'success' - uses: actions/attest-build-provenance@v3 + uses: actions/attest-build-provenance@v4 with: subject-name: ${{steps.build.outputs.name}} subject-path: ${{steps.build.outputs.path}} From 2828854d3203b0b85159309596c997820b34cb10 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:17:35 -0800 Subject: [PATCH 017/116] [Server] Support face-down cards in all public zones (#6539) * [Server] Support face-down cards in all public zones * add null check * Check using zone names instead * add comment --- .../remote/game/server_abstract_player.cpp | 34 +++++++++++++++++-- .../protocol/pb/command_move_card.proto | 4 ++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp index b08495bad..00561bef2 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -198,6 +198,35 @@ shouldDestroyOnMove(const Server_Card *card, const Server_CardZone *startZone, c return true; } +/** + * @brief Determines whether the moved card should be face-down + */ +static bool +shouldBeFaceDown(const MoveCardStruct &cardStruct, const Server_CardZone *startZone, const Server_CardZone *targetZone) +{ + if (!targetZone) { + return false; + } + + // being face-down only makes sense for public zones + if (targetZone->getType() != ServerInfo_Zone::PublicZone) { + return false; + } + + // face-down property in proto takes precedence + if (cardStruct.cardToMove->has_face_down()) { + return cardStruct.cardToMove->face_down(); + } + + // Default to keep face-down the same if zone didn't change. + // Compare using zone names because face-down is maintained when changing controllers. + if (startZone && startZone->getName() == targetZone->getName()) { + return cardStruct.card->getFaceDown(); + } + + return false; +} + Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, Server_CardZone *startzone, const QList &_cards, @@ -257,10 +286,7 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, for (auto cardStruct : cardsToMove) { Server_Card *card = cardStruct.card; - const CardToMove *thisCardProperties = cardStruct.cardToMove; int originalPosition = cardStruct.position; - bool faceDown = targetzone->hasCoords() && - (thisCardProperties->has_face_down() ? thisCardProperties->face_down() : card->getFaceDown()); bool sourceBeingLookedAt; int position = startzone->removeCard(card, sourceBeingLookedAt); @@ -315,6 +341,8 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, ++xIndex; int newX = isReversed ? targetzone->getCards().size() - xCoord + xIndex : xCoord + xIndex; + bool faceDown = shouldBeFaceDown(cardStruct, startzone, targetzone); + if (targetzone->hasCoords()) { newX = targetzone->getFreeGridColumn(newX, yCoord, card->getName(), faceDown); } else { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_move_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_move_card.proto index 6e9301d27..a5b96da2e 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_move_card.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_move_card.proto @@ -6,7 +6,9 @@ message CardToMove { // Id of the card in its current zone optional sint32 card_id = 1 [default = -1]; - // Places the card face down, hiding its name + // If true, places the card face down, hiding its name. + // If false, forcibly turns the card face up. + // If not set, defers the resulting face down state to the server. optional bool face_down = 2; // When moving add this value to the power/toughness field of the card From 2fba5dcd202c9a98381fbfbe6022199dcad0f230 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:18:21 -0800 Subject: [PATCH 018/116] [Game] Refactor CardDragItem faceDown logic (#6552) * Rename properties and only pass forceFaceDown * [Game] Refactor CardDragItem faceDown logic * revert refactor * leave face_down unset unless forced --- cockatrice/src/game/board/card_drag_item.cpp | 5 +++-- cockatrice/src/game/board/card_drag_item.h | 8 ++++---- cockatrice/src/game/board/card_item.cpp | 9 ++++----- cockatrice/src/game/board/card_item.h | 2 +- cockatrice/src/game/zones/pile_zone.cpp | 4 ++-- cockatrice/src/game/zones/table_zone.cpp | 6 ++++-- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/cockatrice/src/game/board/card_drag_item.cpp b/cockatrice/src/game/board/card_drag_item.cpp index 9eadadeda..5ae56ccba 100644 --- a/cockatrice/src/game/board/card_drag_item.cpp +++ b/cockatrice/src/game/board/card_drag_item.cpp @@ -13,9 +13,10 @@ CardDragItem::CardDragItem(CardItem *_item, int _id, const QPointF &_hotSpot, - bool _faceDown, + bool _forceFaceDown, AbstractCardDragItem *parentDrag) - : AbstractCardDragItem(_item, _hotSpot, parentDrag), id(_id), faceDown(_faceDown), occupied(false), currentZone(0) + : AbstractCardDragItem(_item, _hotSpot, parentDrag), id(_id), forceFaceDown(_forceFaceDown), occupied(false), + currentZone(0) { } diff --git a/cockatrice/src/game/board/card_drag_item.h b/cockatrice/src/game/board/card_drag_item.h index 257fde150..930c6be6f 100644 --- a/cockatrice/src/game/board/card_drag_item.h +++ b/cockatrice/src/game/board/card_drag_item.h @@ -16,7 +16,7 @@ class CardDragItem : public AbstractCardDragItem Q_OBJECT private: int id; - bool faceDown; + bool forceFaceDown; bool occupied; CardZone *currentZone; @@ -24,15 +24,15 @@ public: CardDragItem(CardItem *_item, int _id, const QPointF &_hotSpot, - bool _faceDown, + bool _forceFaceDown, AbstractCardDragItem *parentDrag = 0); int getId() const { return id; } - bool getFaceDown() const + bool isForceFaceDown() const { - return faceDown; + return forceFaceDown; } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void updatePosition(const QPointF &cursorScenePos) override; diff --git a/cockatrice/src/game/board/card_item.cpp b/cockatrice/src/game/board/card_item.cpp index 34c2f9a98..baf04e993 100644 --- a/cockatrice/src/game/board/card_item.cpp +++ b/cockatrice/src/game/board/card_item.cpp @@ -251,10 +251,10 @@ void CardItem::processCardInfo(const ServerInfo_Card &_info) setDoesntUntap(_info.doesnt_untap()); } -CardDragItem *CardItem::createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool faceDown) +CardDragItem *CardItem::createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown) { deleteDragItem(); - dragItem = new CardDragItem(this, _id, _pos, faceDown); + dragItem = new CardDragItem(this, _id, _pos, forceFaceDown); dragItem->setVisible(false); scene()->addItem(dragItem); dragItem->updatePosition(_scenePos); @@ -352,7 +352,7 @@ void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) // Use the buttonDownPos to align the hot spot with the position when // the user originally clicked - createDragItem(id, event->buttonDownPos(Qt::LeftButton), event->scenePos(), facedown || forceFaceDown); + createDragItem(id, event->buttonDownPos(Qt::LeftButton), event->scenePos(), forceFaceDown); dragItem->grabMouse(); int childIndex = 0; @@ -366,8 +366,7 @@ void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) childPos = card->pos() - pos(); else childPos = QPointF(childIndex * CARD_WIDTH / 2, 0); - CardDragItem *drag = - new CardDragItem(card, card->getId(), childPos, card->getFaceDown() || forceFaceDown, dragItem); + CardDragItem *drag = new CardDragItem(card, card->getId(), childPos, forceFaceDown, dragItem); drag->setPos(dragItem->pos() + childPos); scene()->addItem(drag); } diff --git a/cockatrice/src/game/board/card_item.h b/cockatrice/src/game/board/card_item.h index 86c4fe594..875c3f4d0 100644 --- a/cockatrice/src/game/board/card_item.h +++ b/cockatrice/src/game/board/card_item.h @@ -142,7 +142,7 @@ public: void processCardInfo(const ServerInfo_Card &_info); bool animationEvent(); - CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool faceDown); + CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown); void deleteDragItem(); void drawArrow(const QColor &arrowColor); void drawAttachArrow(); diff --git a/cockatrice/src/game/zones/pile_zone.cpp b/cockatrice/src/game/zones/pile_zone.cpp index 2d1390826..521d13b99 100644 --- a/cockatrice/src/game/zones/pile_zone.cpp +++ b/cockatrice/src/game/zones/pile_zone.cpp @@ -101,12 +101,12 @@ void PileZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event) if (getLogic()->getCards().isEmpty()) return; - bool faceDown = event->modifiers().testFlag(Qt::ShiftModifier); + bool forceFaceDown = event->modifiers().testFlag(Qt::ShiftModifier); bool bottomCard = event->modifiers().testFlag(Qt::ControlModifier); CardItem *card = bottomCard ? getLogic()->getCards().last() : getLogic()->getCards().first(); const int cardid = getLogic()->contentsKnown() ? card->getId() : (bottomCard ? getLogic()->getCards().size() - 1 : 0); - CardDragItem *drag = card->createDragItem(cardid, event->pos(), event->scenePos(), faceDown); + CardDragItem *drag = card->createDragItem(cardid, event->pos(), event->scenePos(), forceFaceDown); drag->grabMouse(); setCursor(Qt::OpenHandCursor); } diff --git a/cockatrice/src/game/zones/table_zone.cpp b/cockatrice/src/game/zones/table_zone.cpp index c76514350..ca1052d20 100644 --- a/cockatrice/src/game/zones/table_zone.cpp +++ b/cockatrice/src/game/zones/table_zone.cpp @@ -134,8 +134,10 @@ void TableZone::handleDropEventByGrid(const QList &dragItems, for (const auto &item : dragItems) { CardToMove *ctm = cmd.mutable_cards_to_move()->add_card(); ctm->set_card_id(item->getId()); - ctm->set_face_down(item->getFaceDown()); - if (startZone->getName() != getLogic()->getName() && !item->getFaceDown()) { + if (item->isForceFaceDown()) { + ctm->set_face_down(true); + } + if (startZone->getName() != getLogic()->getName() && !item->isForceFaceDown()) { const auto &card = item->getItem()->getCard(); if (card) { ctm->set_pt(card.getInfo().getPowTough().toStdString()); From 846ecb7e8daa078022434acaf25f368259f3f748 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:19:26 -0800 Subject: [PATCH 019/116] [Client] Support face-down cards in all public zones (#6602) * [Server] Support face-down cards in all public zones * add null check * Check using zone names instead * add comment * Rename properties and only pass forceFaceDown * [Game] Refactor CardDragItem faceDown logic * revert refactor * leave face_down unset unless forced * [Client] Support face-down cards in all public zones * leave face_down unset unless forced * log face down * update remaining logs --- cockatrice/src/game/board/card_item.cpp | 4 +++- .../src/game/log/message_log_widget.cpp | 24 +++++++++++++++---- .../src/game/zones/logic/card_zone_logic.cpp | 6 +++-- cockatrice/src/game/zones/pile_zone.cpp | 9 +++++-- cockatrice/src/game/zones/stack_zone.cpp | 6 ++++- cockatrice/src/game/zones/view_zone.cpp | 21 ++++++++++++---- 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/cockatrice/src/game/board/card_item.cpp b/cockatrice/src/game/board/card_item.cpp index baf04e993..8b9549f07 100644 --- a/cockatrice/src/game/board/card_item.cpp +++ b/cockatrice/src/game/board/card_item.cpp @@ -212,10 +212,12 @@ void CardItem::setAttachedTo(CardItem *_attachedTo) } } +/** + * @brief Resets the fields that should be reset after a zone transition + */ void CardItem::resetState(bool keepAnnotations) { attacking = false; - facedown = false; counters.clear(); pt.clear(); if (!keepAnnotations) { diff --git a/cockatrice/src/game/log/message_log_widget.cpp b/cockatrice/src/game/log/message_log_widget.cpp index 1ff3e32ff..645974994 100644 --- a/cockatrice/src/game/log/message_log_widget.cpp +++ b/cockatrice/src/game/log/message_log_widget.cpp @@ -314,9 +314,17 @@ void MessageLogWidget::logMoveCard(Player *player, finalStr = tr("%1 puts %2 into play%3."); } } else if (targetZoneName == GRAVE_ZONE_NAME) { - finalStr = tr("%1 puts %2%3 into their graveyard."); + if (card->getFaceDown()) { + finalStr = tr("%1 puts %2%3 into their graveyard face down."); + } else { + finalStr = tr("%1 puts %2%3 into their graveyard."); + } } else if (targetZoneName == EXILE_ZONE_NAME) { - finalStr = tr("%1 exiles %2%3."); + if (card->getFaceDown()) { + finalStr = tr("%1 exiles %2%3 face down."); + } else { + finalStr = tr("%1 exiles %2%3."); + } } else if (targetZoneName == HAND_ZONE_NAME) { finalStr = tr("%1 moves %2%3 to their hand."); } else if (targetZoneName == DECK_ZONE_NAME) { @@ -335,10 +343,18 @@ void MessageLogWidget::logMoveCard(Player *player, finalStr = tr("%1 moves %2%3 to sideboard."); } else if (targetZoneName == STACK_ZONE_NAME) { soundEngine->playSound("play_card"); - finalStr = tr("%1 plays %2%3."); + if (card->getFaceDown()) { + finalStr = tr("%1 plays %2%3 face down."); + } else { + finalStr = tr("%1 plays %2%3."); + } } else { fourthArg = targetZoneName; - finalStr = tr("%1 moves %2%3 to custom zone '%4'."); + if (card->getFaceDown()) { + finalStr = tr("%1 moves %2%3 to custom zone '%4' face down."); + } else { + finalStr = tr("%1 moves %2%3 to custom zone '%4'."); + } } QString message = finalStr.arg(sanitizeHtml(player->getPlayerInfo()->getName()), cardStr, nameFrom.second); diff --git a/cockatrice/src/game/zones/logic/card_zone_logic.cpp b/cockatrice/src/game/zones/logic/card_zone_logic.cpp index bd32eab3e..1872ba95e 100644 --- a/cockatrice/src/game/zones/logic/card_zone_logic.cpp +++ b/cockatrice/src/game/zones/logic/card_zone_logic.cpp @@ -43,8 +43,10 @@ void CardZoneLogic::addCard(CardItem *card, const bool reorganize, const int x, for (auto *view : views) { if (qobject_cast(view->getLogic())->prepareAddCard(x)) { - view->getLogic()->addCard(new CardItem(player, nullptr, card->getCardRef(), card->getId()), reorganize, x, - y); + auto copy = new CardItem(player, nullptr, card->getCardRef(), card->getId()); + copy->setFaceDown(card->getFaceDown()); + + view->getLogic()->addCard(copy, reorganize, x, y); } } diff --git a/cockatrice/src/game/zones/pile_zone.cpp b/cockatrice/src/game/zones/pile_zone.cpp index 521d13b99..87a60fc03 100644 --- a/cockatrice/src/game/zones/pile_zone.cpp +++ b/cockatrice/src/game/zones/pile_zone.cpp @@ -68,8 +68,13 @@ void PileZone::handleDropEvent(const QList &dragItems, CardZoneL cmd.set_x(0); cmd.set_y(0); - for (int i = 0; i < dragItems.size(); ++i) - cmd.mutable_cards_to_move()->add_card()->set_card_id(dragItems[i]->getId()); + for (int i = 0; i < dragItems.size(); ++i) { + auto cardToMove = cmd.mutable_cards_to_move()->add_card(); + cardToMove->set_card_id(dragItems[i]->getId()); + if (dragItems[i]->isForceFaceDown()) { + cardToMove->set_face_down(true); + } + } getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd); } diff --git a/cockatrice/src/game/zones/stack_zone.cpp b/cockatrice/src/game/zones/stack_zone.cpp index 820008b27..a9d649a4a 100644 --- a/cockatrice/src/game/zones/stack_zone.cpp +++ b/cockatrice/src/game/zones/stack_zone.cpp @@ -78,7 +78,11 @@ void StackZone::handleDropEvent(const QList &dragItems, for (CardDragItem *item : dragItems) { if (item) { - cmd.mutable_cards_to_move()->add_card()->set_card_id(item->getId()); + auto cardToMove = cmd.mutable_cards_to_move()->add_card(); + cardToMove->set_card_id(item->getId()); + if (item->isForceFaceDown()) { + cardToMove->set_face_down(true); + } } } diff --git a/cockatrice/src/game/zones/view_zone.cpp b/cockatrice/src/game/zones/view_zone.cpp index 4ae25989e..348ed6e87 100644 --- a/cockatrice/src/game/zones/view_zone.cpp +++ b/cockatrice/src/game/zones/view_zone.cpp @@ -73,7 +73,10 @@ void ZoneViewZone::initializeCards(const QList &cardLis for (int i = 0; i < cardList.size(); ++i) { auto card = cardList[i]; CardRef cardRef = {QString::fromStdString(card->name()), QString::fromStdString(card->provider_id())}; - getLogic()->addCard(new CardItem(getLogic()->getPlayer(), this, cardRef, card->id()), false, i); + auto copy = new CardItem(getLogic()->getPlayer(), this, cardRef, card->id()); + copy->setFaceDown(card->face_down()); + + getLogic()->addCard(copy, false, i); } reorganizeCards(); } else if (!qobject_cast(getLogic())->getOriginalZone()->contentsKnown()) { @@ -91,8 +94,10 @@ void ZoneViewZone::initializeCards(const QList &cardLis int number = numberCards == -1 ? c.size() : (numberCards < c.size() ? numberCards : c.size()); for (int i = 0; i < number; i++) { CardItem *card = c.at(i); - getLogic()->addCard(new CardItem(getLogic()->getPlayer(), this, card->getCardRef(), card->getId()), false, - i); + auto copy = new CardItem(getLogic()->getPlayer(), this, card->getCardRef(), card->getId()); + copy->setFaceDown(card->getFaceDown()); + + getLogic()->addCard(copy, false, i); } reorganizeCards(); } @@ -107,6 +112,7 @@ void ZoneViewZone::zoneDumpReceived(const Response &r) auto cardName = QString::fromStdString(cardInfo.name()); auto cardProviderId = QString::fromStdString(cardInfo.provider_id()); auto card = new CardItem(getLogic()->getPlayer(), this, {cardName, cardProviderId}, cardInfo.id(), getLogic()); + card->setFaceDown(cardInfo.face_down()); getLogic()->rawInsertCard(card, i); } @@ -279,8 +285,13 @@ void ZoneViewZone::handleDropEvent(const QList &dragItems, cmd.set_y(0); cmd.set_is_reversed(qobject_cast(getLogic())->getIsReversed()); - for (int i = 0; i < dragItems.size(); ++i) - cmd.mutable_cards_to_move()->add_card()->set_card_id(dragItems[i]->getId()); + for (int i = 0; i < dragItems.size(); ++i) { + auto cardToMove = cmd.mutable_cards_to_move()->add_card(); + cardToMove->set_card_id(dragItems[i]->getId()); + if (dragItems[i]->isForceFaceDown()) { + cardToMove->set_face_down(true); + } + } getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd); } From 43acac5f5db0a8ddf5221ececf35f57af6d6ea9e Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 4 Mar 2026 00:16:19 +0100 Subject: [PATCH 020/116] empty card info when switching back to theme background (#6657) --- cockatrice/src/interface/widgets/general/home_widget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index 269326257..5176dde83 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -63,6 +63,7 @@ void HomeWidget::initializeBackgroundFromSource() switch (backgroundSourceType) { case BackgroundSources::Theme: background = QPixmap("theme:backgrounds/home"); + backgroundSourceCard->setCard(ExactCard()); updateButtonsToBackgroundColor(); update(); break; From 566c876bdc7f716ee56cf11f08c1aa6626a5c4a4 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 4 Mar 2026 00:16:45 +0100 Subject: [PATCH 021/116] fix scaling for background images on home tab (#6656) --- .../card_info_picture_art_crop_widget.cpp | 12 ++++-------- .../cards/card_info_picture_art_crop_widget.h | 4 ++-- .../interface/widgets/general/home_widget.cpp | 19 +++++++++++-------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_art_crop_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_art_crop_widget.cpp index 451104d17..e5b0c4872 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_art_crop_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_art_crop_widget.cpp @@ -8,7 +8,7 @@ CardInfoPictureArtCropWidget::CardInfoPictureArtCropWidget(QWidget *parent) hide(); } -QPixmap CardInfoPictureArtCropWidget::getProcessedBackground(const QSize &targetSize) +QPixmap CardInfoPictureArtCropWidget::getBackground() { // Load the full-resolution card image, not a pre-scaled one QPixmap fullResPixmap; @@ -17,10 +17,8 @@ QPixmap CardInfoPictureArtCropWidget::getProcessedBackground(const QSize &target } else { CardPictureLoader::getCardBackPixmap(fullResPixmap, QSize(745, 1040)); } - - // Fail-safe if loading failed if (fullResPixmap.isNull()) { - return QPixmap(targetSize); + return QPixmap(); // return null qpixmap } const QSize sz = fullResPixmap.size(); @@ -33,9 +31,7 @@ QPixmap CardInfoPictureArtCropWidget::getProcessedBackground(const QSize &target foilRect = foilRect.intersected(fullResPixmap.rect()); // always clamp to source bounds - // Crop first, then scale for best quality + // return full resolution image crop QPixmap cropped = fullResPixmap.copy(foilRect); - QPixmap scaled = cropped.scaled(targetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); - - return scaled; + return cropped; } diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_art_crop_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_art_crop_widget.h index aed951cc6..0185f758c 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_art_crop_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_art_crop_widget.h @@ -16,8 +16,8 @@ class CardInfoPictureArtCropWidget : public CardInfoPictureWidget public: explicit CardInfoPictureArtCropWidget(QWidget *parent = nullptr); - // Returns a processed (cropped & scaled) version of the pixmap - QPixmap getProcessedBackground(const QSize &targetSize); + // Returns a cropped version of the pixmap + QPixmap getBackground(); }; #endif // CARD_INFO_PICTURE_ART_CROP_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index 5176dde83..7d0bad813 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -125,7 +125,7 @@ void HomeWidget::updateRandomCard() connect(newCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &HomeWidget::updateBackgroundProperties); backgroundSourceCard->setCard(newCard); - background = backgroundSourceCard->getProcessedBackground(size()); + background = backgroundSourceCard->getBackground(); if (SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() <= 0) { cardChangeTimer->stop(); @@ -143,7 +143,7 @@ void HomeWidget::onBackgroundShuffleFrequencyChanged() void HomeWidget::updateBackgroundProperties() { - background = backgroundSourceCard->getProcessedBackground(size()); + background = backgroundSourceCard->getBackground(); updateButtonsToBackgroundColor(); update(); // Triggers repaint } @@ -301,14 +301,17 @@ void HomeWidget::paintEvent(QPaintEvent *event) QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); - background = background.scaled(size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + if (!background.isNull()) { + QSize widgetSize = size() * devicePixelRatio(); + QPixmap toDraw = background.scaled(widgetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); - // Draw already-scaled background centered - QSize widgetSize = size(); - QSize bgSize = background.size(); - QPoint topLeft((widgetSize.width() - bgSize.width()) / 2, (widgetSize.height() - bgSize.height()) / 2); + // Draw scaled background centered + QSize bgSize = toDraw.size(); + QPoint topLeft((widgetSize.width() - bgSize.width()) / (devicePixelRatio() * 2), // undo scaling for painter + (widgetSize.height() - bgSize.height()) / (devicePixelRatio() * 2)); - painter.drawPixmap(topLeft, background); + painter.drawPixmap(topLeft, toDraw); + } // Draw translucent black overlay with rounded corners QRectF overlayRect(5, 5, width() - 10, height() - 10); From b36ab66583ec60f1dbb9366f3246d274daa11595 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 4 Mar 2026 00:49:50 +0100 Subject: [PATCH 022/116] nullcheck printing's set in home tab background art crop (#6646) * nullcheck printing's set in home tab background art crop * set warning and properly set timer * fix merge --- .../interface/widgets/general/home_widget.cpp | 46 ++++++++++++------- .../interface/widgets/general/home_widget.h | 1 + 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index 7d0bad813..750ddd000 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -58,21 +58,24 @@ void HomeWidget::initializeBackgroundFromSource() auto backgroundSourceType = BackgroundSources::fromId(SettingsCache::instance().getHomeTabBackgroundSource()); - cardChangeTimer->stop(); - switch (backgroundSourceType) { case BackgroundSources::Theme: + cardChangeTimer->stop(); background = QPixmap("theme:backgrounds/home"); + backgroundSourceDeck = DeckList(); backgroundSourceCard->setCard(ExactCard()); updateButtonsToBackgroundColor(); update(); break; case BackgroundSources::RandomCardArt: - cardChangeTimer->start(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() * 1000); + backgroundSourceDeck = DeckList(); + updateRandomCard(); + onBackgroundShuffleFrequencyChanged(); break; case BackgroundSources::DeckFileArt: loadBackgroundSourceDeck(); - cardChangeTimer->start(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() * 1000); + updateRandomCard(); + onBackgroundShuffleFrequencyChanged(); break; } } @@ -84,6 +87,20 @@ void HomeWidget::loadBackgroundSourceDeck() backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList(); } +void HomeWidget::setRandomCard(ExactCard &newCard) +{ + static constexpr int ATTEMPTS = 10; + for (int i = 0; i < ATTEMPTS; ++i) { + ExactCard tmpCard = CardDatabaseManager::query()->getRandomCard(); + if (tmpCard != backgroundSourceCard->getCard() && tmpCard.getCardPtr()->getProperty("layout") == "normal" && + tmpCard.getPrinting().getSet() != nullptr) { + newCard = tmpCard; + return; + } + } + qWarning() << "failed to set random card image after" << ATTEMPTS << "attempts"; +} + void HomeWidget::updateRandomCard() { auto backgroundSourceType = BackgroundSources::fromId(SettingsCache::instance().getHomeTabBackgroundSource()); @@ -94,10 +111,7 @@ void HomeWidget::updateRandomCard() case BackgroundSources::Theme: break; case BackgroundSources::RandomCardArt: - do { - newCard = CardDatabaseManager::query()->getRandomCard(); - } while (newCard == backgroundSourceCard->getCard() && - newCard.getCardPtr()->getProperty("layout") != "normal"); + setRandomCard(newCard); break; case BackgroundSources::DeckFileArt: QList cardRefs = backgroundSourceDeck.getCardRefList(); @@ -126,19 +140,14 @@ void HomeWidget::updateRandomCard() connect(newCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &HomeWidget::updateBackgroundProperties); backgroundSourceCard->setCard(newCard); background = backgroundSourceCard->getBackground(); - - if (SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() <= 0) { - cardChangeTimer->stop(); - } } void HomeWidget::onBackgroundShuffleFrequencyChanged() { cardChangeTimer->stop(); - if (SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() <= 0) { - return; + if (SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() > 0) { + cardChangeTimer->start(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() * 1000); } - cardChangeTimer->start(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() * 1000); } void HomeWidget::updateBackgroundProperties() @@ -325,8 +334,11 @@ void HomeWidget::paintEvent(QPaintEvent *event) QString cardName; ExactCard card = backgroundSourceCard->getCard(); if (card) { - cardName = card.getCardPtr()->getName() + " (" + card.getPrinting().getSet()->getCorrectedShortName() + ") " + - card.getPrinting().getProperty("num"); + cardName = card.getCardPtr()->getName(); + if (card.getPrinting().getSet() != nullptr) { + cardName += " (" + card.getPrinting().getSet()->getCorrectedShortName() + ") " + + card.getPrinting().getProperty("num"); + } } if (!cardName.isEmpty() && SettingsCache::instance().getHomeTabDisplayCardName()) { diff --git a/cockatrice/src/interface/widgets/general/home_widget.h b/cockatrice/src/interface/widgets/general/home_widget.h index 233fda543..b30bb5407 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.h +++ b/cockatrice/src/interface/widgets/general/home_widget.h @@ -45,6 +45,7 @@ private: QPair gradientColors; HomeStyledButton *connectButton; + void setRandomCard(ExactCard &newCard); void loadBackgroundSourceDeck(); }; From e7a3ad86eb8bc294da4b8d027392c4fb48a3bbb9 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 4 Mar 2026 06:00:18 -0800 Subject: [PATCH 023/116] [Game] Refactor move cards from library actions (#6658) * refactor move top/bottom cards actions * minor cleanup * translate zone display names --- cockatrice/src/game/player/player_actions.cpp | 84 +++++-------------- cockatrice/src/game/player/player_actions.h | 3 + 2 files changed, 25 insertions(+), 62 deletions(-) diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index ed77808b0..1a09836e4 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -424,37 +424,15 @@ void PlayerActions::actMoveTopCardToExile() void PlayerActions::actMoveTopCardsToGrave() { - const int maxCards = player->getDeckZone()->getCards().size(); - if (maxCards == 0) { - return; - } - - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move top cards to grave"), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1, - maxCards, 1, &ok); - if (!ok) { - return; - } else if (number > maxCards) { - number = maxCards; - } - defaultNumberTopCards = number; - - Command_MoveCard cmd; - cmd.set_start_zone("deck"); - cmd.set_target_player_id(player->getPlayerInfo()->getId()); - cmd.set_target_zone("grave"); - cmd.set_x(0); - cmd.set_y(0); - - for (int i = number - 1; i >= 0; --i) { - cmd.mutable_cards_to_move()->add_card()->set_card_id(i); - } - - sendGameCommand(cmd); + moveTopCardsTo("grave", tr("grave")); } void PlayerActions::actMoveTopCardsToExile() +{ + moveTopCardsTo("rfg", tr("exile")); +} + +void PlayerActions::moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName) { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { @@ -462,12 +440,14 @@ void PlayerActions::actMoveTopCardsToExile() } bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move top cards to exile"), + int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move top cards to %1").arg(zoneDisplayName), tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1, maxCards, 1, &ok); if (!ok) { return; - } else if (number > maxCards) { + } + + if (number > maxCards) { number = maxCards; } defaultNumberTopCards = number; @@ -475,7 +455,7 @@ void PlayerActions::actMoveTopCardsToExile() Command_MoveCard cmd; cmd.set_start_zone("deck"); cmd.set_target_player_id(player->getPlayerInfo()->getId()); - cmd.set_target_zone("rfg"); + cmd.set_target_zone(targetZone.toStdString()); cmd.set_x(0); cmd.set_y(0); @@ -628,37 +608,15 @@ void PlayerActions::actMoveBottomCardToExile() void PlayerActions::actMoveBottomCardsToGrave() { - const int maxCards = player->getDeckZone()->getCards().size(); - if (maxCards == 0) { - return; - } - - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move bottom cards to grave"), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, - maxCards, 1, &ok); - if (!ok) { - return; - } else if (number > maxCards) { - number = maxCards; - } - defaultNumberBottomCards = number; - - Command_MoveCard cmd; - cmd.set_start_zone("deck"); - cmd.set_target_player_id(player->getPlayerInfo()->getId()); - cmd.set_target_zone("grave"); - cmd.set_x(0); - cmd.set_y(0); - - for (int i = maxCards - number; i < maxCards; ++i) { - cmd.mutable_cards_to_move()->add_card()->set_card_id(i); - } - - sendGameCommand(cmd); + moveBottomCardsTo("grave", tr("grave")); } void PlayerActions::actMoveBottomCardsToExile() +{ + moveBottomCardsTo("rfg", tr("exile")); +} + +void PlayerActions::moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName) { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { @@ -666,12 +624,14 @@ void PlayerActions::actMoveBottomCardsToExile() } bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move bottom cards to exile"), + int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move bottom cards to %1").arg(zoneDisplayName), tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, maxCards, 1, &ok); if (!ok) { return; - } else if (number > maxCards) { + } + + if (number > maxCards) { number = maxCards; } defaultNumberBottomCards = number; @@ -679,7 +639,7 @@ void PlayerActions::actMoveBottomCardsToExile() Command_MoveCard cmd; cmd.set_start_zone("deck"); cmd.set_target_player_id(player->getPlayerInfo()->getId()); - cmd.set_target_zone("rfg"); + cmd.set_target_zone(targetZone.toStdString()); cmd.set_x(0); cmd.set_y(0); diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index b294b5946..beed2c241 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -180,6 +180,9 @@ private: FilterString movingCardsUntilFilter; int movingCardsUntilCounter = 0; + void moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName); + void moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName); + void createCard(const CardItem *sourceCard, const QString &dbCardName, CardRelationType attach = CardRelationType::DoesNotAttach, From 1bcea27a445c912b5d51462e5a35ab732942ded4 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:19:41 -0800 Subject: [PATCH 024/116] [Game] Add face down versions of move cards from library actions (#6661) * implement actions * add new actions to menu * update shortcuts --- .../src/client/settings/shortcuts_settings.h | 16 +++++++ .../src/game/player/menu/library_menu.cpp | 28 ++++++++++++ .../src/game/player/menu/library_menu.h | 4 ++ cockatrice/src/game/player/player_actions.cpp | 44 +++++++++++++++---- cockatrice/src/game/player/player_actions.h | 8 +++- 5 files changed, 90 insertions(+), 10 deletions(-) diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index 156ee82ea..47332b8c4 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -598,11 +598,19 @@ private: {"Player/aMoveTopCardsToGraveyard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Graveyard (Multiple)"), parseSequenceString("Alt+M"), ShortcutGroup::Move_top)}, + {"Player/aMoveTopCardsToGraveyardFaceDown", + ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Graveyard (Multiple), Face Down"), + parseSequenceString(""), + ShortcutGroup::Move_top)}, {"Player/aMoveTopCardToExile", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile"), parseSequenceString(""), ShortcutGroup::Move_top)}, {"Player/aMoveTopCardsToExile", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile (Multiple)"), parseSequenceString(""), ShortcutGroup::Move_top)}, + {"Player/aMoveTopCardsToExileFaceDown", + ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile (Multiple), Face Down"), + parseSequenceString(""), + ShortcutGroup::Move_top)}, {"Player/aMoveTopCardsUntil", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Stack Until Found"), parseSequenceString("Ctrl+Shift+Y"), ShortcutGroup::Move_top)}, @@ -620,11 +628,19 @@ private: {"Player/aMoveBottomCardsToGrave", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Graveyard (Multiple)"), parseSequenceString(""), ShortcutGroup::Move_bottom)}, + {"Player/aMoveBottomCardsToGraveFaceDown", + ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Graveyard (Multiple), Face Down"), + parseSequenceString(""), + ShortcutGroup::Move_bottom)}, {"Player/aMoveBottomCardToExile", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile"), parseSequenceString(""), ShortcutGroup::Move_bottom)}, {"Player/aMoveBottomCardsToExile", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile (Multiple)"), parseSequenceString(""), ShortcutGroup::Move_bottom)}, + {"Player/aMoveBottomCardsToExileFaceDown", + ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile (Multiple), Face Down"), + parseSequenceString(""), + ShortcutGroup::Move_bottom)}, {"Player/aMoveBottomCardToTop", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Top of Library"), parseSequenceString(""), ShortcutGroup::Move_bottom)}, diff --git a/cockatrice/src/game/player/menu/library_menu.cpp b/cockatrice/src/game/player/menu/library_menu.cpp index 749439558..1bb647d06 100644 --- a/cockatrice/src/game/player/menu/library_menu.cpp +++ b/cockatrice/src/game/player/menu/library_menu.cpp @@ -51,8 +51,10 @@ LibraryMenu::LibraryMenu(Player *_player, QWidget *parent) : TearOffMenu(parent) topLibraryMenu->addSeparator(); topLibraryMenu->addAction(aMoveTopCardToGraveyard); topLibraryMenu->addAction(aMoveTopCardsToGraveyard); + topLibraryMenu->addAction(aMoveTopCardsToGraveyardFaceDown); topLibraryMenu->addAction(aMoveTopCardToExile); topLibraryMenu->addAction(aMoveTopCardsToExile); + topLibraryMenu->addAction(aMoveTopCardsToExileFaceDown); topLibraryMenu->addAction(aMoveTopCardsUntil); topLibraryMenu->addSeparator(); topLibraryMenu->addAction(aShuffleTopCards); @@ -66,8 +68,10 @@ LibraryMenu::LibraryMenu(Player *_player, QWidget *parent) : TearOffMenu(parent) bottomLibraryMenu->addSeparator(); bottomLibraryMenu->addAction(aMoveBottomCardToGraveyard); bottomLibraryMenu->addAction(aMoveBottomCardsToGraveyard); + bottomLibraryMenu->addAction(aMoveBottomCardsToGraveyardFaceDown); bottomLibraryMenu->addAction(aMoveBottomCardToExile); bottomLibraryMenu->addAction(aMoveBottomCardsToExile); + bottomLibraryMenu->addAction(aMoveBottomCardsToExileFaceDown); bottomLibraryMenu->addSeparator(); bottomLibraryMenu->addAction(aShuffleBottomCards); @@ -136,8 +140,14 @@ void LibraryMenu::createMoveActions() connect(aMoveTopCardToExile, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardToExile); aMoveTopCardsToGraveyard = new QAction(this); connect(aMoveTopCardsToGraveyard, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardsToGrave); + aMoveTopCardsToGraveyardFaceDown = new QAction(this); + connect(aMoveTopCardsToGraveyardFaceDown, &QAction::triggered, playerActions, + &PlayerActions::actMoveTopCardsToGraveFaceDown); aMoveTopCardsToExile = new QAction(this); connect(aMoveTopCardsToExile, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardsToExile); + aMoveTopCardsToExileFaceDown = new QAction(this); + connect(aMoveTopCardsToExileFaceDown, &QAction::triggered, playerActions, + &PlayerActions::actMoveTopCardsToExileFaceDown); aMoveTopCardsUntil = new QAction(this); connect(aMoveTopCardsUntil, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardsUntil); aMoveTopCardToBottom = new QAction(this); @@ -156,8 +166,14 @@ void LibraryMenu::createMoveActions() aMoveBottomCardsToGraveyard = new QAction(this); connect(aMoveBottomCardsToGraveyard, &QAction::triggered, playerActions, &PlayerActions::actMoveBottomCardsToGrave); + aMoveBottomCardsToGraveyardFaceDown = new QAction(this); + connect(aMoveBottomCardsToGraveyardFaceDown, &QAction::triggered, playerActions, + &PlayerActions::actMoveBottomCardsToGraveFaceDown); aMoveBottomCardsToExile = new QAction(this); connect(aMoveBottomCardsToExile, &QAction::triggered, playerActions, &PlayerActions::actMoveBottomCardsToExile); + aMoveBottomCardsToExileFaceDown = new QAction(this); + connect(aMoveBottomCardsToExileFaceDown, &QAction::triggered, playerActions, + &PlayerActions::actMoveBottomCardsToExileFaceDown); aMoveBottomCardToTop = new QAction(this); connect(aMoveBottomCardToTop, &QAction::triggered, playerActions, &PlayerActions::actMoveBottomCardToTop); } @@ -216,7 +232,9 @@ void LibraryMenu::retranslateUi() aMoveTopCardToGraveyard->setText(tr("Move top card to grave&yard")); aMoveTopCardToExile->setText(tr("Move top card to e&xile")); aMoveTopCardsToGraveyard->setText(tr("Move top cards to &graveyard...")); + aMoveTopCardsToGraveyardFaceDown->setText(tr("Move top cards to graveyard face down...")); aMoveTopCardsToExile->setText(tr("Move top cards to &exile...")); + aMoveTopCardsToExileFaceDown->setText(tr("Move top cards to exile face down...")); aMoveTopCardsUntil->setText(tr("Put top cards on stack &until...")); aShuffleTopCards->setText(tr("Shuffle top cards...")); @@ -227,7 +245,9 @@ void LibraryMenu::retranslateUi() aMoveBottomCardToGraveyard->setText(tr("Move bottom card to grave&yard")); aMoveBottomCardToExile->setText(tr("Move bottom card to e&xile")); aMoveBottomCardsToGraveyard->setText(tr("Move bottom cards to &graveyard...")); + aMoveBottomCardsToGraveyardFaceDown->setText(tr("Move bottom cards to graveyard face down...")); aMoveBottomCardsToExile->setText(tr("Move bottom cards to &exile...")); + aMoveBottomCardsToExileFaceDown->setText(tr("Move bottom cards to exile face down...")); aMoveBottomCardToTop->setText(tr("Put bottom card on &top")); aShuffleBottomCards->setText(tr("Shuffle bottom cards...")); } @@ -335,8 +355,10 @@ void LibraryMenu::setShortcutsActive() aMoveTopToPlayFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveTopToPlayFaceDown")); aMoveTopCardToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardToGraveyard")); aMoveTopCardsToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardsToGraveyard")); + aMoveTopCardsToGraveyardFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardsToGraveyardFaceDown")); aMoveTopCardToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardToExile")); aMoveTopCardsToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardsToExile")); + aMoveTopCardsToExileFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardsToExileFaceDown")); aMoveTopCardsUntil->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardsUntil")); aMoveTopCardToBottom->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardToBottom")); aDrawBottomCard->setShortcuts(shortcuts.getShortcut("Player/aDrawBottomCard")); @@ -345,8 +367,10 @@ void LibraryMenu::setShortcutsActive() aMoveBottomToPlayFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomToPlayFaceDown")); aMoveBottomCardToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardToGrave")); aMoveBottomCardsToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardsToGrave")); + aMoveBottomCardsToGraveyardFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardsToGraveFaceDown")); aMoveBottomCardToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardToExile")); aMoveBottomCardsToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardsToExile")); + aMoveBottomCardsToExileFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardsToExileFaceDown")); aMoveBottomCardToTop->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardToTop")); } @@ -367,8 +391,10 @@ void LibraryMenu::setShortcutsInactive() aMoveTopToPlayFaceDown->setShortcut(QKeySequence()); aMoveTopCardToGraveyard->setShortcut(QKeySequence()); aMoveTopCardsToGraveyard->setShortcut(QKeySequence()); + aMoveTopCardsToGraveyardFaceDown->setShortcut(QKeySequence()); aMoveTopCardToExile->setShortcut(QKeySequence()); aMoveTopCardsToExile->setShortcut(QKeySequence()); + aMoveTopCardsToExileFaceDown->setShortcut(QKeySequence()); aMoveTopCardsUntil->setShortcut(QKeySequence()); aDrawBottomCard->setShortcut(QKeySequence()); aDrawBottomCards->setShortcut(QKeySequence()); @@ -376,6 +402,8 @@ void LibraryMenu::setShortcutsInactive() aMoveBottomToPlayFaceDown->setShortcut(QKeySequence()); aMoveBottomCardToGraveyard->setShortcut(QKeySequence()); aMoveBottomCardsToGraveyard->setShortcut(QKeySequence()); + aMoveBottomCardsToGraveyardFaceDown->setShortcut(QKeySequence()); aMoveBottomCardToExile->setShortcut(QKeySequence()); aMoveBottomCardsToExile->setShortcut(QKeySequence()); + aMoveBottomCardsToExileFaceDown->setShortcut(QKeySequence()); } diff --git a/cockatrice/src/game/player/menu/library_menu.h b/cockatrice/src/game/player/menu/library_menu.h index 96d4b82b1..c0883107c 100644 --- a/cockatrice/src/game/player/menu/library_menu.h +++ b/cockatrice/src/game/player/menu/library_menu.h @@ -88,7 +88,9 @@ public: QAction *aMoveTopCardToGraveyard = nullptr; QAction *aMoveTopCardToExile = nullptr; QAction *aMoveTopCardsToGraveyard = nullptr; + QAction *aMoveTopCardsToGraveyardFaceDown = nullptr; QAction *aMoveTopCardsToExile = nullptr; + QAction *aMoveTopCardsToExileFaceDown = nullptr; QAction *aMoveTopCardsUntil = nullptr; QAction *aShuffleTopCards = nullptr; @@ -100,7 +102,9 @@ public: QAction *aMoveBottomCardToGraveyard = nullptr; QAction *aMoveBottomCardToExile = nullptr; QAction *aMoveBottomCardsToGraveyard = nullptr; + QAction *aMoveBottomCardsToGraveyardFaceDown = nullptr; QAction *aMoveBottomCardsToExile = nullptr; + QAction *aMoveBottomCardsToExileFaceDown = nullptr; QAction *aShuffleBottomCards = nullptr; int defaultNumberTopCards = 1; diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 1a09836e4..514ac2e67 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -424,15 +424,25 @@ void PlayerActions::actMoveTopCardToExile() void PlayerActions::actMoveTopCardsToGrave() { - moveTopCardsTo("grave", tr("grave")); + moveTopCardsTo("grave", tr("grave"), false); +} + +void PlayerActions::actMoveTopCardsToGraveFaceDown() +{ + moveTopCardsTo("grave", tr("grave"), true); } void PlayerActions::actMoveTopCardsToExile() { - moveTopCardsTo("rfg", tr("exile")); + moveTopCardsTo("rfg", tr("exile"), false); } -void PlayerActions::moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName) +void PlayerActions::actMoveTopCardsToExileFaceDown() +{ + moveTopCardsTo("rfg", tr("exile"), true); +} + +void PlayerActions::moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown) { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { @@ -460,7 +470,11 @@ void PlayerActions::moveTopCardsTo(const QString &targetZone, const QString &zon cmd.set_y(0); for (int i = number - 1; i >= 0; --i) { - cmd.mutable_cards_to_move()->add_card()->set_card_id(i); + auto card = cmd.mutable_cards_to_move()->add_card(); + card->set_card_id(i); + if (faceDown) { + card->set_face_down(true); + } } sendGameCommand(cmd); @@ -608,15 +622,25 @@ void PlayerActions::actMoveBottomCardToExile() void PlayerActions::actMoveBottomCardsToGrave() { - moveBottomCardsTo("grave", tr("grave")); + moveBottomCardsTo("grave", tr("grave"), false); +} + +void PlayerActions::actMoveBottomCardsToGraveFaceDown() +{ + moveBottomCardsTo("grave", tr("grave"), true); } void PlayerActions::actMoveBottomCardsToExile() { - moveBottomCardsTo("rfg", tr("exile")); + moveBottomCardsTo("rfg", tr("exile"), false); } -void PlayerActions::moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName) +void PlayerActions::actMoveBottomCardsToExileFaceDown() +{ + moveBottomCardsTo("rfg", tr("exile"), true); +} + +void PlayerActions::moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown) { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { @@ -644,7 +668,11 @@ void PlayerActions::moveBottomCardsTo(const QString &targetZone, const QString & cmd.set_y(0); for (int i = maxCards - number; i < maxCards; ++i) { - cmd.mutable_cards_to_move()->add_card()->set_card_id(i); + auto card = cmd.mutable_cards_to_move()->add_card(); + card->set_card_id(i); + if (faceDown) { + card->set_face_down(true); + } } sendGameCommand(cmd); diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index beed2c241..1d695578d 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -98,7 +98,9 @@ public slots: void actMoveTopCardToGrave(); void actMoveTopCardToExile(); void actMoveTopCardsToGrave(); + void actMoveTopCardsToGraveFaceDown(); void actMoveTopCardsToExile(); + void actMoveTopCardsToExileFaceDown(); void actMoveTopCardsUntil(); void actMoveTopCardToBottom(); void actDrawBottomCard(); @@ -108,7 +110,9 @@ public slots: void actMoveBottomCardToGrave(); void actMoveBottomCardToExile(); void actMoveBottomCardsToGrave(); + void actMoveBottomCardsToGraveFaceDown(); void actMoveBottomCardsToExile(); + void actMoveBottomCardsToExileFaceDown(); void actMoveBottomCardToTop(); void actSelectAll(); @@ -180,8 +184,8 @@ private: FilterString movingCardsUntilFilter; int movingCardsUntilCounter = 0; - void moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName); - void moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName); + void moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); + void moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); void createCard(const CardItem *sourceCard, const QString &dbCardName, From 04f06206b7ae3ff3b4f21d50af3310965adac350 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Thu, 5 Mar 2026 14:47:14 -0500 Subject: [PATCH 025/116] Refactor/z value constants (#6651) * feat(z-values): add centralized Z-value constants infrastructure Magic numbers scattered across the codebase make Z-value layering hard to understand and maintain. Centralizing them provides: - Self-documenting layer hierarchy - Validation utilities for development - Single source of truth for Z-value ranges Two-tier header design: - z_value_layer_manager.h: Foundation with constants and validation - z_values.h: User-facing namespace with semantic constants * refactor(z-values): replace magic Z-value numbers with ZValues constants Magic numbers like 2000000007 are impossible to understand without context. Named constants (ZValues::DRAG_ITEM) are self-documenting. This intentionally renumbers overlay Z-values to use a cleaner offset sequence. The relative stacking order is preserved, which is what matters for correct rendering. Each consumer now includes z_values.h and uses semantic constants instead of magic numbers. * refactor(z-values): removed redundant inline with contexpr, Updated doc, magic numbers removed from TableZone. --- .../game/board/abstract_card_drag_item.cpp | 5 +- .../src/game/board/abstract_card_item.cpp | 3 +- cockatrice/src/game/board/arrow_item.cpp | 3 +- cockatrice/src/game/z_value_layer_manager.h | 136 ++++++++++++++++++ cockatrice/src/game/z_values.h | 83 +++++++++++ cockatrice/src/game/zones/table_zone.cpp | 5 +- .../src/game/zones/view_zone_widget.cpp | 9 +- 7 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 cockatrice/src/game/z_value_layer_manager.h create mode 100644 cockatrice/src/game/z_values.h diff --git a/cockatrice/src/game/board/abstract_card_drag_item.cpp b/cockatrice/src/game/board/abstract_card_drag_item.cpp index c961cbcb6..c9a964048 100644 --- a/cockatrice/src/game/board/abstract_card_drag_item.cpp +++ b/cockatrice/src/game/board/abstract_card_drag_item.cpp @@ -1,6 +1,7 @@ #include "abstract_card_drag_item.h" #include "../../client/settings/cache_settings.h" +#include "../z_values.h" #include #include @@ -18,13 +19,13 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item, { if (parentDrag) { parentDrag->addChildDrag(this); - setZValue(2000000007 + hotSpot.x() * 1000000 + hotSpot.y() * 1000 + 1000); + setZValue(ZValues::childDragZValue(hotSpot.x(), hotSpot.y())); connect(parentDrag, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater); } else { hotSpot = QPointF{qBound(0.0, hotSpot.x(), static_cast(CARD_WIDTH - 1)), qBound(0.0, hotSpot.y(), static_cast(CARD_HEIGHT - 1))}; setCursor(Qt::ClosedHandCursor); - setZValue(2000000007); + setZValue(ZValues::DRAG_ITEM); } if (item->getTapped()) setTransform(QTransform() diff --git a/cockatrice/src/game/board/abstract_card_item.cpp b/cockatrice/src/game/board/abstract_card_item.cpp index d5538011d..f8365c3d6 100644 --- a/cockatrice/src/game/board/abstract_card_item.cpp +++ b/cockatrice/src/game/board/abstract_card_item.cpp @@ -3,6 +3,7 @@ #include "../../client/settings/cache_settings.h" #include "../../interface/card_picture_loader/card_picture_loader.h" #include "../game_scene.h" +#include "../z_values.h" #include #include @@ -215,7 +216,7 @@ void AbstractCardItem::setHovered(bool _hovered) if (_hovered) processHoverEvent(); isHovered = _hovered; - setZValue(_hovered ? 2000000004 : realZValue); + setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue); setScale(_hovered && SettingsCache::instance().getScaleCards() ? 1.1 : 1); setTransformOriginPoint(_hovered ? CARD_WIDTH / 2 : 0, _hovered ? CARD_HEIGHT / 2 : 0); update(); diff --git a/cockatrice/src/game/board/arrow_item.cpp b/cockatrice/src/game/board/arrow_item.cpp index 1352b3a05..257d96f8a 100644 --- a/cockatrice/src/game/board/arrow_item.cpp +++ b/cockatrice/src/game/board/arrow_item.cpp @@ -5,6 +5,7 @@ #include "../player/player.h" #include "../player/player_actions.h" #include "../player/player_target.h" +#include "../z_values.h" #include "../zones/card_zone.h" #include "card_item.h" @@ -23,7 +24,7 @@ ArrowItem::ArrowItem(Player *_player, int _id, ArrowTarget *_startItem, ArrowTar : QGraphicsItem(), player(_player), id(_id), startItem(_startItem), targetItem(_targetItem), targetLocked(false), color(_color), fullColor(true) { - setZValue(2000000005); + setZValue(ZValues::ARROWS); if (startItem) startItem->addArrowFrom(this); diff --git a/cockatrice/src/game/z_value_layer_manager.h b/cockatrice/src/game/z_value_layer_manager.h new file mode 100644 index 000000000..303edb8a5 --- /dev/null +++ b/cockatrice/src/game/z_value_layer_manager.h @@ -0,0 +1,136 @@ +/** + * @file z_value_layer_manager.h + * @ingroup GameGraphics + * @brief Semantic Z-value layer management for game scene rendering. + * + * This file provides a structured approach to Z-value allocation in the game scene. + * Z-values in Qt determine stacking order - higher values render on top of lower values. + * + * ## Layer Architecture + * + * The game scene is organized into three conceptual layers: + * + * 1. **Zone Layer (0-999)**: Zone backgrounds, containers, and static elements + * - Zone backgrounds (0.5-1.0) + * - Cards within zones (1.0 base + index) + * + * 2. **Card Layer (1-40,000,000)**: Dynamic card rendering on the table zone + * - Cards use formula: (actualY + CARD_HEIGHT) * 100000 + (actualX + 1) * 100 + * - Maximum card Z-value: ~40,000,000 (with 3 rows, actualY <= ~289) + * + * 3. **Overlay Layer (2,000,000,000+)**: UI elements that must appear above all cards + * - Hovered cards (+1) + * - Arrows (+3) + * - Zone views (+4) + * - Drag items (+5, +6) + * - Top UI elements (+7) + * + * ## Design Rationale + * + * The large gap between card Z-values (max ~40M) and overlay base (2B) provides + * safety margin for future table zone expansions while ensuring overlays always + * render above cards regardless of table position. + * + * ## Usage + * + * Prefer using the semantic constants from ZValues namespace: + * @code + * card->setZValue(ZValues::HOVERED_CARD); + * arrow->setZValue(ZValues::ARROWS); + * @endcode + * + * Use validation functions to verify card Z-values during development: + * @code + * Q_ASSERT(ZValueLayerManager::isValidCardZValue(cardZ)); + * @endcode + */ + +#ifndef Z_VALUE_LAYER_MANAGER_H +#define Z_VALUE_LAYER_MANAGER_H + +#include + +/** + * @namespace ZValueLayerManager + * @brief Utilities for Z-value validation and layer management. + */ +namespace ZValueLayerManager +{ + +/** + * @enum Layer + * @brief Semantic layer identifiers for Z-value allocation. + * + * These represent conceptual rendering layers, not actual Z-values. + * Use the corresponding ZValues constants for actual rendering. + */ +enum class Layer +{ + /// Zone-level elements like backgrounds and containers + Zone, + /// Cards rendered in zones (uses sequential Z-values) + Card, + /// Temporary UI elements like hovered cards and drag items + Overlay +}; + +/** + * @brief Maximum Z-value a card can have on the table zone. + * + * Based on table zone formula: (actualY + CARD_HEIGHT) * 100000 + (actualX + 1) * 100 + * With maximum 3 rows and CARD_HEIGHT ~96, actualY <= ~289. + * Maximum: (289 + 96) * 100000 + 100 * 100 = 38,510,000 + * + * We use 40,000,000 as a safe upper bound with margin. + */ +constexpr qreal CARD_Z_VALUE_MAX = 40000000.0; + +/** + * @brief Base Z-value for overlay elements. + * + * Must exceed CARD_Z_VALUE_MAX to ensure overlays render above all cards. + * The 50x margin (2B vs 40M) provides safety for future expansion. + */ +constexpr qreal OVERLAY_BASE = 2000000000.0; + +/** + * @brief Validates that a Z-value is within the valid card range. + * + * Cards should have Z-values between CARD_BASE (1.0) and CARD_Z_VALUE_MAX. + * Values outside this range may interfere with overlay rendering. + * + * @param zValue The Z-value to validate + * @return true if the Z-value is valid for a card + */ +[[nodiscard]] constexpr bool isValidCardZValue(qreal zValue) +{ + return zValue >= 1.0 && zValue <= CARD_Z_VALUE_MAX; +} + +/** + * @brief Validates that a Z-value is in the overlay layer. + * + * Overlay elements should have Z-values at or above OVERLAY_BASE. + * + * @param zValue The Z-value to validate + * @return true if the Z-value is valid for an overlay element + */ +[[nodiscard]] constexpr bool isOverlayZValue(qreal zValue) +{ + return zValue >= OVERLAY_BASE; +} + +/** + * @brief Returns the Z-value for a specific overlay element. + * + * @param offset Offset from OVERLAY_BASE (0-7 for current elements) + * @return The absolute Z-value for the overlay element + */ +[[nodiscard]] constexpr qreal overlayZValue(qreal offset) +{ + return OVERLAY_BASE + offset; +} + +} // namespace ZValueLayerManager + +#endif // Z_VALUE_LAYER_MANAGER_H diff --git a/cockatrice/src/game/z_values.h b/cockatrice/src/game/z_values.h new file mode 100644 index 000000000..2c7fe9066 --- /dev/null +++ b/cockatrice/src/game/z_values.h @@ -0,0 +1,83 @@ +#ifndef Z_VALUES_H +#define Z_VALUES_H + +#include "z_value_layer_manager.h" + +/** + * @file z_values.h + * @ingroup GameGraphics + * @brief Centralized Z-value constants for rendering layer order. + * + * Z-values in Qt determine stacking order. Higher values render on top. + * These constants define the visual layering hierarchy for the game scene. + * + * ## Layer Architecture + * + * See z_value_layer_manager.h for detailed documentation on the three-layer + * architecture (Zone, Card, Overlay) and the rationale for Z-value choices. + * + * ## Quick Reference + * + * | Layer | Z-Value Range | Purpose | + * |----------|------------------|-----------------------------------| + * | Zone | 0.5 - 1.0 | Zone backgrounds, containers | + * | Card | 1.0 - 40,000,000 | Cards on table (position-based) | + * | Overlay | 2,000,000,000+ | UI elements above all cards | + */ + +namespace ZValues +{ + +// Expose base for callers that need it +constexpr qreal OVERLAY_BASE = ZValueLayerManager::OVERLAY_BASE; + +// Overlay layer Z-values for items that should appear above normal cards +constexpr qreal HOVERED_CARD = ZValueLayerManager::overlayZValue(1.0); +constexpr qreal ARROWS = ZValueLayerManager::overlayZValue(3.0); +constexpr qreal ZONE_VIEW_WIDGET = ZValueLayerManager::overlayZValue(4.0); +constexpr qreal DRAG_ITEM = ZValueLayerManager::overlayZValue(5.0); +constexpr qreal DRAG_ITEM_CHILD = ZValueLayerManager::overlayZValue(6.0); +constexpr qreal TOP_UI = ZValueLayerManager::overlayZValue(7.0); + +/** + * @brief Compute Z-value for child drag items based on hotspot position. + * + * When dragging multiple cards together, each child card needs a unique Z-value + * to prevent Z-fighting (flickering/flashing). The Z-values are derived from + * their position when grabbed to conserve original stacking. The formula encodes + * 2D coordinates into a single value where X has higher weight, ensuring + * deterministic visual stacking. + * + * @param hotSpotX The X coordinate of the grab position + * @param hotSpotY The Y coordinate of the grab position + * @return Unique Z-value for the child drag item + */ +[[nodiscard]] constexpr qreal childDragZValue(qreal hotSpotX, qreal hotSpotY) +{ + return DRAG_ITEM_CHILD + hotSpotX * 1000000 + hotSpotY * 1000 + 1000; +} + +/** + * @brief Compute Z-value for cards on the table zone based on position. + * + * Cards lower on the table (higher Y) render above cards higher up, + * and cards to the right (higher X) render above cards to the left. + * This creates natural visual stacking for overlapping cards. + * + * @param x The X coordinate of the card position + * @param y The Y coordinate of the card position + * @return Z-value for the card's table position + */ +[[nodiscard]] constexpr qreal tableCardZValue(qreal x, qreal y) +{ + constexpr qreal CARD_HEIGHT_FOR_Z = 102.0; + return (y + CARD_HEIGHT_FOR_Z) * 100000.0 + (x + 1) * 100.0; +} + +// Card layering (general architecture, not command-zone specific) +constexpr qreal CARD_BASE = 1.0; +constexpr qreal CARD_MAX = ZValueLayerManager::CARD_Z_VALUE_MAX; + +} // namespace ZValues + +#endif // Z_VALUES_H diff --git a/cockatrice/src/game/zones/table_zone.cpp b/cockatrice/src/game/zones/table_zone.cpp index ca1052d20..33c867c43 100644 --- a/cockatrice/src/game/zones/table_zone.cpp +++ b/cockatrice/src/game/zones/table_zone.cpp @@ -7,6 +7,7 @@ #include "../board/card_item.h" #include "../player/player.h" #include "../player/player_actions.h" +#include "../z_values.h" #include "logic/table_zone_logic.h" #include @@ -169,7 +170,7 @@ void TableZone::reorganizeCards() actualY += 15; getLogic()->getCards()[i]->setPos(actualX, actualY); - getLogic()->getCards()[i]->setRealZValue((actualY + CARD_HEIGHT) * 100000 + (actualX + 1) * 100); + getLogic()->getCards()[i]->setRealZValue(ZValues::tableCardZValue(actualX, actualY)); QListIterator attachedCardIterator(getLogic()->getCards()[i]->getAttachedCards()); int j = 0; @@ -179,7 +180,7 @@ void TableZone::reorganizeCards() qreal childX = actualX - j * STACKED_CARD_OFFSET_X; qreal childY = y + 5; attachedCard->setPos(childX, childY); - attachedCard->setRealZValue((childY + CARD_HEIGHT) * 100000 + (childX + 1) * 100); + attachedCard->setRealZValue(ZValues::tableCardZValue(childX, childY)); } } diff --git a/cockatrice/src/game/zones/view_zone_widget.cpp b/cockatrice/src/game/zones/view_zone_widget.cpp index 3ea4eb119..0ad3b10f1 100644 --- a/cockatrice/src/game/zones/view_zone_widget.cpp +++ b/cockatrice/src/game/zones/view_zone_widget.cpp @@ -7,6 +7,7 @@ #include "../game_scene.h" #include "../player/player.h" #include "../player/player_actions.h" +#include "../z_values.h" #include "view_zone.h" #include @@ -47,7 +48,7 @@ ZoneViewWidget::ZoneViewWidget(Player *_player, { setAcceptHoverEvents(true); setAttribute(Qt::WA_DeleteOnClose); - setZValue(2000000006); + setZValue(ZValues::ZONE_VIEW_WIDGET); setFlag(ItemIgnoresTransformations); QGraphicsLinearLayout *vbox = new QGraphicsLinearLayout(Qt::Vertical); @@ -71,7 +72,7 @@ ZoneViewWidget::ZoneViewWidget(Player *_player, QGraphicsProxyWidget *searchEditProxy = new QGraphicsProxyWidget; searchEditProxy->setWidget(&searchEdit); - searchEditProxy->setZValue(2000000007); + searchEditProxy->setZValue(ZValues::DRAG_ITEM); vbox->addItem(searchEditProxy); // top row @@ -80,13 +81,13 @@ ZoneViewWidget::ZoneViewWidget(Player *_player, // groupBy options QGraphicsProxyWidget *groupBySelectorProxy = new QGraphicsProxyWidget; groupBySelectorProxy->setWidget(&groupBySelector); - groupBySelectorProxy->setZValue(2000000008); + groupBySelectorProxy->setZValue(ZValues::TOP_UI); hTopRow->addItem(groupBySelectorProxy); // sortBy options QGraphicsProxyWidget *sortBySelectorProxy = new QGraphicsProxyWidget; sortBySelectorProxy->setWidget(&sortBySelector); - sortBySelectorProxy->setZValue(2000000007); + sortBySelectorProxy->setZValue(ZValues::DRAG_ITEM); hTopRow->addItem(sortBySelectorProxy); vbox->addItem(hTopRow); From e39bbd2b316753f254be8ae8b60c5c8808160b6b Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Fri, 6 Mar 2026 00:19:10 +0100 Subject: [PATCH 026/116] take default theme name from startup instead of using empty (#6663) --- cockatrice/src/interface/theme_manager.cpp | 4 +++- cockatrice/src/interface/theme_manager.h | 1 + cockatrice/src/interface/widgets/general/home_widget.cpp | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index abacb2d68..1dc61fdb7 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -89,6 +90,7 @@ struct PaletteColorInfo ThemeManager::ThemeManager(QObject *parent) : QObject(parent) { + defaultStyleName = qApp->style()->objectName(); ensureThemeDirectoryExists(); #if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)) connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, &ThemeManager::themeChangedSlot); @@ -346,7 +348,7 @@ void ThemeManager::themeChangedSlot() qApp->setStyle(QStyleFactory::create("Fusion")); qApp->setPalette(createDarkGreenFusionPalette()); } else { - qApp->setStyle(""); + qApp->setStyle(defaultStyleName); // setting the style also sets the palette } if (dirPath.isEmpty()) { diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index 594ede2bf..d942f0fef 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -40,6 +40,7 @@ public: }; private: + QString defaultStyleName; std::array brushes; QStringMap availableThemes; /* diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index 750ddd000..ea20ef6a0 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -46,6 +46,8 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) &HomeWidget::onBackgroundShuffleFrequencyChanged); // Lambda is cleaner to read than overloading this connect(&SettingsCache::instance(), &SettingsCache::homeTabDisplayCardNameChanged, this, [this] { repaint(); }); + connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, + &HomeWidget::updateButtonsToBackgroundColor); } void HomeWidget::initializeBackgroundFromSource() From 14f1925edc8a47045e771e71746e07290fef3679 Mon Sep 17 00:00:00 2001 From: tooomm Date: Fri, 6 Mar 2026 01:50:15 +0100 Subject: [PATCH 027/116] Add icon to exe (#6655) --- cmake/NSIS.template.in | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index a0d51fa2b..2fdc61fb9 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -26,6 +26,7 @@ Var PortableMode !define MUI_WELCOMEPAGE_TEXT "This wizard will guide you through the installation of Cockatrice.$\r$\n$\r$\nClick Next to continue." !define MUI_FINISHPAGE_RUN "$INSTDIR/cockatrice.exe" !define MUI_FINISHPAGE_RUN_TEXT "Run 'Cockatrice' now" +!define MUI_ICON "${NSIS_SOURCE_PATH}\cockatrice\resources\appicon.ico" !insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_LICENSE "${NSIS_SOURCE_PATH}\LICENSE" From bd5cbb89d47c2cdfac9eb59e550e737d51321775 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Thu, 5 Mar 2026 22:13:58 -0500 Subject: [PATCH 028/116] refactor: extract CARD_HEIGHT to shared CardDimensions header (#6668) * refactor: extract CARD_HEIGHT to shared CardDimensions header Move duplicated CARD_WIDTH/CARD_HEIGHT constants to card_dimensions.h. Fixed documentation in z_value_layer_manager.h. * WIDTH_F used directly instead of casting * Improved consistency and added missing newlines at end of files --- .../game/board/abstract_card_drag_item.cpp | 12 ++++---- .../src/game/board/abstract_card_drag_item.h | 2 +- .../src/game/board/abstract_card_item.cpp | 10 +++---- .../src/game/board/abstract_card_item.h | 4 +-- cockatrice/src/game/board/card_item.cpp | 9 +++--- cockatrice/src/game/board/card_item.h | 2 -- cockatrice/src/game/card_dimensions.h | 29 +++++++++++++++++++ cockatrice/src/game/deckview/deck_view.cpp | 22 +++++++------- .../src/game/player/player_graphics_item.cpp | 22 +++++++------- cockatrice/src/game/z_value_layer_manager.h | 8 ++--- cockatrice/src/game/z_values.h | 4 +-- cockatrice/src/game/zones/hand_zone.cpp | 2 +- cockatrice/src/game/zones/pile_zone.cpp | 12 ++++---- cockatrice/src/game/zones/table_zone.cpp | 22 +++++++------- cockatrice/src/game/zones/table_zone.h | 4 +-- cockatrice/src/game/zones/view_zone.cpp | 16 +++++----- .../src/game/zones/view_zone_widget.cpp | 4 +-- .../cards/card_info_display_widget.cpp | 2 +- 18 files changed, 108 insertions(+), 78 deletions(-) create mode 100644 cockatrice/src/game/card_dimensions.h diff --git a/cockatrice/src/game/board/abstract_card_drag_item.cpp b/cockatrice/src/game/board/abstract_card_drag_item.cpp index c9a964048..8e3def4ca 100644 --- a/cockatrice/src/game/board/abstract_card_drag_item.cpp +++ b/cockatrice/src/game/board/abstract_card_drag_item.cpp @@ -8,8 +8,6 @@ #include #include -static const float CARD_WIDTH_HALF = CARD_WIDTH / 2; -static const float CARD_HEIGHT_HALF = CARD_HEIGHT / 2; const QColor GHOST_MASK = QColor(255, 255, 255, 50); AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item, @@ -22,16 +20,16 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item, setZValue(ZValues::childDragZValue(hotSpot.x(), hotSpot.y())); connect(parentDrag, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater); } else { - hotSpot = QPointF{qBound(0.0, hotSpot.x(), static_cast(CARD_WIDTH - 1)), - qBound(0.0, hotSpot.y(), static_cast(CARD_HEIGHT - 1))}; + hotSpot = QPointF{qBound(0.0, hotSpot.x(), CardDimensions::WIDTH_F - 1), + qBound(0.0, hotSpot.y(), CardDimensions::HEIGHT_F - 1)}; setCursor(Qt::ClosedHandCursor); setZValue(ZValues::DRAG_ITEM); } if (item->getTapped()) setTransform(QTransform() - .translate(CARD_WIDTH_HALF, CARD_HEIGHT_HALF) + .translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F) .rotate(90) - .translate(-CARD_WIDTH_HALF, -CARD_HEIGHT_HALF)); + .translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F)); setCacheMode(DeviceCoordinateCache); @@ -48,7 +46,7 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item, QPainterPath AbstractCardDragItem::shape() const { QPainterPath shape; - qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CARD_WIDTH : 0.0; + qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0; shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius); return shape; } diff --git a/cockatrice/src/game/board/abstract_card_drag_item.h b/cockatrice/src/game/board/abstract_card_drag_item.h index 1d741eded..fe3b87983 100644 --- a/cockatrice/src/game/board/abstract_card_drag_item.h +++ b/cockatrice/src/game/board/abstract_card_drag_item.h @@ -34,7 +34,7 @@ public: AbstractCardDragItem(AbstractCardItem *_item, const QPointF &_hotSpot, AbstractCardDragItem *parentDrag = 0); [[nodiscard]] QRectF boundingRect() const override { - return QRectF(0, 0, CARD_WIDTH, CARD_HEIGHT); + return QRectF(0, 0, CardDimensions::WIDTH_F, CardDimensions::HEIGHT_F); } [[nodiscard]] QPainterPath shape() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; diff --git a/cockatrice/src/game/board/abstract_card_item.cpp b/cockatrice/src/game/board/abstract_card_item.cpp index f8365c3d6..9ec6ada9a 100644 --- a/cockatrice/src/game/board/abstract_card_item.cpp +++ b/cockatrice/src/game/board/abstract_card_item.cpp @@ -39,13 +39,13 @@ AbstractCardItem::~AbstractCardItem() QRectF AbstractCardItem::boundingRect() const { - return QRectF(0, 0, CARD_WIDTH, CARD_HEIGHT); + return QRectF(0, 0, CardDimensions::WIDTH_F, CardDimensions::HEIGHT_F); } QPainterPath AbstractCardItem::shape() const { QPainterPath shape; - qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CARD_WIDTH : 0.0; + qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0; shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius); return shape; } @@ -218,7 +218,7 @@ void AbstractCardItem::setHovered(bool _hovered) isHovered = _hovered; setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue); setScale(_hovered && SettingsCache::instance().getScaleCards() ? 1.1 : 1); - setTransformOriginPoint(_hovered ? CARD_WIDTH / 2 : 0, _hovered ? CARD_HEIGHT / 2 : 0); + setTransformOriginPoint(_hovered ? CardDimensions::WIDTH_HALF_F : 0, _hovered ? CardDimensions::HEIGHT_HALF_F : 0); update(); } @@ -274,9 +274,9 @@ void AbstractCardItem::setTapped(bool _tapped, bool canAnimate) else { tapAngle = tapped ? 90 : 0; setTransform(QTransform() - .translate((float)CARD_WIDTH / 2, (float)CARD_HEIGHT / 2) + .translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F) .rotate(tapAngle) - .translate((float)-CARD_WIDTH / 2, (float)-CARD_HEIGHT / 2)); + .translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F)); update(); } } diff --git a/cockatrice/src/game/board/abstract_card_item.h b/cockatrice/src/game/board/abstract_card_item.h index dad73dc14..7d2c29cae 100644 --- a/cockatrice/src/game/board/abstract_card_item.h +++ b/cockatrice/src/game/board/abstract_card_item.h @@ -8,6 +8,7 @@ #define ABSTRACTCARDITEM_H #include "../../game_graphics/board/graphics_item_type.h" +#include "../card_dimensions.h" #include "arrow_target.h" #include @@ -15,9 +16,6 @@ class Player; -const int CARD_WIDTH = 72; -const int CARD_HEIGHT = 102; - class AbstractCardItem : public ArrowTarget { Q_OBJECT diff --git a/cockatrice/src/game/board/card_item.cpp b/cockatrice/src/game/board/card_item.cpp index 8b9549f07..62de4a02e 100644 --- a/cockatrice/src/game/board/card_item.cpp +++ b/cockatrice/src/game/board/card_item.cpp @@ -367,8 +367,9 @@ void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) if (zone->getHasCardAttr()) childPos = card->pos() - pos(); else - childPos = QPointF(childIndex * CARD_WIDTH / 2, 0); - CardDragItem *drag = new CardDragItem(card, card->getId(), childPos, forceFaceDown, dragItem); + childPos = QPointF(childIndex * CardDimensions::WIDTH_HALF_F, 0); + CardDragItem *drag = + new CardDragItem(card, card->getId(), childPos, card->getFaceDown() || forceFaceDown, dragItem); drag->setPos(dragItem->pos() + childPos); scene()->addItem(drag); } @@ -475,9 +476,9 @@ bool CardItem::animationEvent() } setTransform(QTransform() - .translate(CARD_WIDTH_HALF, CARD_HEIGHT_HALF) + .translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F) .rotate(tapAngle) - .translate(-CARD_WIDTH_HALF, -CARD_HEIGHT_HALF)); + .translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F)); setHovered(false); update(); diff --git a/cockatrice/src/game/board/card_item.h b/cockatrice/src/game/board/card_item.h index 875c3f4d0..da2097a2c 100644 --- a/cockatrice/src/game/board/card_item.h +++ b/cockatrice/src/game/board/card_item.h @@ -21,8 +21,6 @@ class QAction; class QColor; const int MAX_COUNTERS_ON_CARD = 999; -const float CARD_WIDTH_HALF = CARD_WIDTH / 2; -const float CARD_HEIGHT_HALF = CARD_HEIGHT / 2; const int ROTATION_DEGREES_PER_FRAME = 10; class CardItem : public AbstractCardItem diff --git a/cockatrice/src/game/card_dimensions.h b/cockatrice/src/game/card_dimensions.h new file mode 100644 index 000000000..255d0bc04 --- /dev/null +++ b/cockatrice/src/game/card_dimensions.h @@ -0,0 +1,29 @@ +#ifndef CARD_DIMENSIONS_H +#define CARD_DIMENSIONS_H + +#include + +/** + * @file card_dimensions.h + * @brief Canonical card dimension constants for layout and Z-value calculations. + * + * These values represent the logical pixel dimensions of a standard card graphic. + * They are used throughout the game scene for layout, rendering, and Z-value computation. + */ +namespace CardDimensions +{ +/// Card width in pixels +constexpr int WIDTH = 72; +/// Card height in pixels +constexpr int HEIGHT = 102; + +/// Pre-converted for floating-point contexts (Z-value calculations) +constexpr qreal WIDTH_F = static_cast(WIDTH); +constexpr qreal HEIGHT_F = static_cast(HEIGHT); + +/// Half-dimensions for centering and rotation transforms +constexpr qreal WIDTH_HALF_F = WIDTH_F / 2; +constexpr qreal HEIGHT_HALF_F = HEIGHT_F / 2; +} // namespace CardDimensions + +#endif // CARD_DIMENSIONS_H diff --git a/cockatrice/src/game/deckview/deck_view.cpp b/cockatrice/src/game/deckview/deck_view.cpp index 383545cf3..620dfaa5f 100644 --- a/cockatrice/src/game/deckview/deck_view.cpp +++ b/cockatrice/src/game/deckview/deck_view.cpp @@ -95,8 +95,9 @@ void DeckViewCard::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti pen.setJoinStyle(Qt::MiterJoin); pen.setColor(originZone == DECK_ZONE_MAIN ? Qt::green : Qt::red); painter->setPen(pen); - qreal cardRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * (CARD_WIDTH - 3) : 0.0; - painter->drawRoundedRect(QRectF(1.5, 1.5, CARD_WIDTH - 3., CARD_HEIGHT - 3.), cardRadius, cardRadius); + qreal cardRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * (CardDimensions::WIDTH_F - 3) : 0.0; + painter->drawRoundedRect(QRectF(1.5, 1.5, CardDimensions::WIDTH_F - 3, CardDimensions::HEIGHT_F - 3), cardRadius, + cardRadius); painter->restore(); } @@ -122,7 +123,7 @@ void DeckViewCard::mouseMoveEvent(QGraphicsSceneMouseEvent *event) if (c == this) continue; ++j; - auto childPos = QPointF(j * CARD_WIDTH / 2, 0); + auto childPos = QPointF(j * CardDimensions::WIDTH_HALF_F, 0); auto *drag = new DeckViewCardDragItem(c, childPos, dragItem); drag->setPos(dragItem->pos() + childPos); scene()->addItem(drag); @@ -204,7 +205,7 @@ void DeckViewCardContainer::paint(QPainter *painter, const QStyleOptionGraphicsI painter->setPen(QColor(255, 255, 255, 100)); painter->drawLine(QPointF(0, yUntilNow - paddingY / 2), QPointF(width, yUntilNow - paddingY / 2)); } - qreal thisRowHeight = CARD_HEIGHT * currentRowsAndCols[i].first; + qreal thisRowHeight = CardDimensions::HEIGHT_F * currentRowsAndCols[i].first; QRectF textRect(0, yUntilNow, totalTextWidth, thisRowHeight); yUntilNow += thisRowHeight + paddingY; @@ -260,9 +261,9 @@ QSizeF DeckViewCardContainer::calculateBoundingRect(const QList> // Calculate space needed for cards for (int i = 0; i < rowsAndCols.size(); ++i) { - totalHeight += CARD_HEIGHT * rowsAndCols[i].first + paddingY; - if (CARD_WIDTH * rowsAndCols[i].second > totalWidth) - totalWidth = CARD_WIDTH * rowsAndCols[i].second; + totalHeight += CardDimensions::HEIGHT_F * rowsAndCols[i].first + paddingY; + if (CardDimensions::WIDTH_F * rowsAndCols[i].second > totalWidth) + totalWidth = CardDimensions::WIDTH_F * rowsAndCols[i].second; } return QSizeF(getCardTypeTextWidth() + totalWidth, totalHeight + separatorY + paddingY); @@ -289,9 +290,10 @@ void DeckViewCardContainer::rearrangeItems(const QList> &rowsAnd std::sort(row.begin(), row.end(), DeckViewCardContainer::sortCardsByName); for (int j = 0; j < row.size(); ++j) { DeckViewCard *card = row[j]; - card->setPos(x + (j % tempCols) * CARD_WIDTH, yUntilNow + (j / tempCols) * CARD_HEIGHT); + card->setPos(x + (j % tempCols) * CardDimensions::WIDTH_F, + yUntilNow + (j / tempCols) * CardDimensions::HEIGHT_F); } - yUntilNow += tempRows * CARD_HEIGHT + paddingY; + yUntilNow += tempRows * CardDimensions::HEIGHT_F + paddingY; } prepareGeometryChange(); @@ -392,7 +394,7 @@ void DeckViewScene::applySideboardPlan(const QList &plan) void DeckViewScene::rearrangeItems() { - const int spacing = CARD_HEIGHT / 3; + const int spacing = CardDimensions::HEIGHT / 3; QList contList = cardContainers.values(); // Initialize space requirements diff --git a/cockatrice/src/game/player/player_graphics_item.cpp b/cockatrice/src/game/player/player_graphics_item.cpp index 30648c926..bcc4b7f72 100644 --- a/cockatrice/src/game/player/player_graphics_item.cpp +++ b/cockatrice/src/game/player/player_graphics_item.cpp @@ -19,7 +19,8 @@ PlayerGraphicsItem::PlayerGraphicsItem(Player *_player) : player(_player) playerArea = new PlayerArea(this); playerTarget = new PlayerTarget(player, playerArea); - qreal avatarMargin = (counterAreaWidth + CARD_HEIGHT + 15 - playerTarget->boundingRect().width()) / 2.0; + qreal avatarMargin = + (counterAreaWidth + CardDimensions::HEIGHT_F + 15 - playerTarget->boundingRect().width()) / 2.0; playerTarget->setPos(QPointF(avatarMargin, avatarMargin)); initializeZones(); @@ -55,8 +56,9 @@ void PlayerGraphicsItem::onPlayerActiveChanged(bool _active) void PlayerGraphicsItem::initializeZones() { deckZoneGraphicsItem = new PileZone(player->getDeckZone(), this); - auto base = QPointF(counterAreaWidth + (CARD_HEIGHT - CARD_WIDTH + 15) / 2.0, - 10 + playerTarget->boundingRect().height() + 5 - (CARD_HEIGHT - CARD_WIDTH) / 2.0); + auto base = QPointF(counterAreaWidth + (CardDimensions::HEIGHT_F - CardDimensions::WIDTH_F + 15) / 2.0, + 10 + playerTarget->boundingRect().height() + 5 - + (CardDimensions::HEIGHT_F - CardDimensions::WIDTH_F) / 2.0); deckZoneGraphicsItem->setPos(base); qreal h = deckZoneGraphicsItem->boundingRect().width() + 5; @@ -95,7 +97,7 @@ QRectF PlayerGraphicsItem::boundingRect() const qreal PlayerGraphicsItem::getMinimumWidth() const { - qreal result = tableZoneGraphicsItem->getMinimumWidth() + CARD_HEIGHT + 15 + counterAreaWidth + + qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width(); if (!SettingsCache::instance().getHorizontalHand()) { result += handZoneGraphicsItem->boundingRect().width(); @@ -112,8 +114,8 @@ void PlayerGraphicsItem::paint(QPainter * /*painter*/, void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth) { // Extend table (and hand, if horizontal) to accommodate the new player width. - qreal tableWidth = - newPlayerWidth - CARD_HEIGHT - 15 - counterAreaWidth - stackZoneGraphicsItem->boundingRect().width(); + qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth - + stackZoneGraphicsItem->boundingRect().width(); if (!SettingsCache::instance().getHorizontalHand()) { tableWidth -= handZoneGraphicsItem->boundingRect().width(); } @@ -152,7 +154,7 @@ void PlayerGraphicsItem::rearrangeCounters() void PlayerGraphicsItem::rearrangeZones() { - auto base = QPointF(CARD_HEIGHT + counterAreaWidth + 15, 0); + auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0); if (SettingsCache::instance().getHorizontalHand()) { if (mirrored) { if (player->getHandZone()->contentsKnown()) { @@ -203,7 +205,7 @@ void PlayerGraphicsItem::rearrangeZones() void PlayerGraphicsItem::updateBoundingRect() { prepareGeometryChange(); - qreal width = CARD_HEIGHT + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width(); + qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width(); if (SettingsCache::instance().getHorizontalHand()) { qreal handHeight = player->getPlayerInfo()->getHandVisible() ? handZoneGraphicsItem->boundingRect().height() : 0; @@ -214,7 +216,7 @@ void PlayerGraphicsItem::updateBoundingRect() 0, 0, width + handZoneGraphicsItem->boundingRect().width() + tableZoneGraphicsItem->boundingRect().width(), tableZoneGraphicsItem->boundingRect().height()); } - playerArea->setSize(CARD_HEIGHT + counterAreaWidth + 15, bRect.height()); + playerArea->setSize(CardDimensions::HEIGHT_F + counterAreaWidth + 15, bRect.height()); emit sizeChanged(); -} \ No newline at end of file +} diff --git a/cockatrice/src/game/z_value_layer_manager.h b/cockatrice/src/game/z_value_layer_manager.h index 303edb8a5..4eb864486 100644 --- a/cockatrice/src/game/z_value_layer_manager.h +++ b/cockatrice/src/game/z_value_layer_manager.h @@ -15,7 +15,7 @@ * - Cards within zones (1.0 base + index) * * 2. **Card Layer (1-40,000,000)**: Dynamic card rendering on the table zone - * - Cards use formula: (actualY + CARD_HEIGHT) * 100000 + (actualX + 1) * 100 + * - Cards use formula: (actualY + CardDimensions::HEIGHT) * 100000 + (actualX + 1) * 100 * - Maximum card Z-value: ~40,000,000 (with 3 rows, actualY <= ~289) * * 3. **Overlay Layer (2,000,000,000+)**: UI elements that must appear above all cards @@ -77,9 +77,9 @@ enum class Layer /** * @brief Maximum Z-value a card can have on the table zone. * - * Based on table zone formula: (actualY + CARD_HEIGHT) * 100000 + (actualX + 1) * 100 - * With maximum 3 rows and CARD_HEIGHT ~96, actualY <= ~289. - * Maximum: (289 + 96) * 100000 + 100 * 100 = 38,510,000 + * Based on table zone formula: (actualY + CardDimensions::HEIGHT) * 100000 + (actualX + 1) * 100 + * With maximum 3 rows and CardDimensions::HEIGHT = 102, actualY <= ~289. + * Maximum: (289 + 102) * 100000 + 100 * 100 = 39,110,000 * * We use 40,000,000 as a safe upper bound with margin. */ diff --git a/cockatrice/src/game/z_values.h b/cockatrice/src/game/z_values.h index 2c7fe9066..c6e7f2c8a 100644 --- a/cockatrice/src/game/z_values.h +++ b/cockatrice/src/game/z_values.h @@ -1,6 +1,7 @@ #ifndef Z_VALUES_H #define Z_VALUES_H +#include "card_dimensions.h" #include "z_value_layer_manager.h" /** @@ -70,8 +71,7 @@ constexpr qreal TOP_UI = ZValueLayerManager::overlayZValue(7.0); */ [[nodiscard]] constexpr qreal tableCardZValue(qreal x, qreal y) { - constexpr qreal CARD_HEIGHT_FOR_Z = 102.0; - return (y + CARD_HEIGHT_FOR_Z) * 100000.0 + (x + 1) * 100.0; + return (y + CardDimensions::HEIGHT_F) * 100000.0 + (x + 1) * 100.0; } // Card layering (general architecture, not command-zone specific) diff --git a/cockatrice/src/game/zones/hand_zone.cpp b/cockatrice/src/game/zones/hand_zone.cpp index cc3b44910..7badfcca4 100644 --- a/cockatrice/src/game/zones/hand_zone.cpp +++ b/cockatrice/src/game/zones/hand_zone.cpp @@ -56,7 +56,7 @@ void HandZone::handleDropEvent(const QList &dragItems, QRectF HandZone::boundingRect() const { if (SettingsCache::instance().getHorizontalHand()) - return QRectF(0, 0, width, CARD_HEIGHT + 10); + return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10); else return QRectF(0, 0, 100, zoneHeight); } diff --git a/cockatrice/src/game/zones/pile_zone.cpp b/cockatrice/src/game/zones/pile_zone.cpp index 87a60fc03..d85b5f4e2 100644 --- a/cockatrice/src/game/zones/pile_zone.cpp +++ b/cockatrice/src/game/zones/pile_zone.cpp @@ -19,9 +19,9 @@ PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_log setCursor(Qt::OpenHandCursor); setTransform(QTransform() - .translate((float)CARD_WIDTH / 2, (float)CARD_HEIGHT / 2) + .translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F) .rotate(90) - .translate((float)-CARD_WIDTH / 2, (float)-CARD_HEIGHT / 2)); + .translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F)); connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) { Q_UNUSED(_roundCardCorners); @@ -33,13 +33,13 @@ PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_log QRectF PileZone::boundingRect() const { - return QRectF(0, 0, CARD_WIDTH, CARD_HEIGHT); + return QRectF(0, 0, CardDimensions::WIDTH_F, CardDimensions::HEIGHT_F); } QPainterPath PileZone::shape() const { QPainterPath shape; - qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CARD_WIDTH : 0.0; + qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0; shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius); return shape; } @@ -52,9 +52,9 @@ void PileZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*optio getLogic()->getCards().at(0)->paintPicture(painter, getLogic()->getCards().at(0)->getTranslatedSize(painter), 90); - painter->translate((float)CARD_WIDTH / 2, (float)CARD_HEIGHT / 2); + painter->translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F); painter->rotate(-90); - painter->translate((float)-CARD_WIDTH / 2, (float)-CARD_HEIGHT / 2); + painter->translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F); paintNumberEllipse(getLogic()->getCards().size(), 28, Qt::white, -1, -1, painter); } diff --git a/cockatrice/src/game/zones/table_zone.cpp b/cockatrice/src/game/zones/table_zone.cpp index 33c867c43..a020e4255 100644 --- a/cockatrice/src/game/zones/table_zone.cpp +++ b/cockatrice/src/game/zones/table_zone.cpp @@ -31,7 +31,7 @@ TableZone::TableZone(TableZoneLogic *_logic, QGraphicsItem *parent) : SelectZone updateBg(); - height = MARGIN_TOP + MARGIN_BOTTOM + TABLEROWS * CARD_HEIGHT + (TABLEROWS - 1) * PADDING_Y; + height = MARGIN_TOP + MARGIN_BOTTOM + TABLEROWS * CardDimensions::HEIGHT + (TABLEROWS - 1) * PADDING_Y; width = MIN_WIDTH; currentMinimumWidth = width; @@ -106,7 +106,7 @@ void TableZone::paintLandDivider(QPainter *painter) { // Place the line 2 grid heights down then back it off just enough to allow // some space between a 3-card stack and the land area. - qreal separatorY = MARGIN_TOP + 2 * (CARD_HEIGHT + PADDING_Y) - STACKED_CARD_OFFSET_Y / 2; + qreal separatorY = MARGIN_TOP + 2 * (CardDimensions::HEIGHT + PADDING_Y) - STACKED_CARD_OFFSET_Y / 2; if (isInverted()) separatorY = height - separatorY; painter->setPen(QColor(255, 255, 255, 40)); @@ -232,7 +232,7 @@ void TableZone::resizeToContents() // Minimum width is the rightmost card position plus enough room for // another card with padding, then margin. - currentMinimumWidth = xMax + (2 * CARD_WIDTH) + PADDING_X + MARGIN_RIGHT; + currentMinimumWidth = xMax + (2 * CardDimensions::WIDTH) + PADDING_X + MARGIN_RIGHT; if (currentMinimumWidth < MIN_WIDTH) currentMinimumWidth = MIN_WIDTH; @@ -282,10 +282,10 @@ void TableZone::computeCardStackWidths() const int key = getCardStackMapKey(gridPoint.x() / 3, gridPoint.y()); const int stackCount = cardStackCount.value(key, 0); if (stackCount == 1) - cardStackWidth.insert(key, CARD_WIDTH + getLogic()->getCards()[i]->getAttachedCards().size() * - STACKED_CARD_OFFSET_X); + cardStackWidth.insert(key, CardDimensions::WIDTH + getLogic()->getCards()[i]->getAttachedCards().size() * + STACKED_CARD_OFFSET_X); else - cardStackWidth.insert(key, CARD_WIDTH + (stackCount - 1) * STACKED_CARD_OFFSET_X); + cardStackWidth.insert(key, CardDimensions::WIDTH + (stackCount - 1) * STACKED_CARD_OFFSET_X); } } @@ -299,7 +299,7 @@ QPointF TableZone::mapFromGrid(QPoint gridPoint) const // Add in width of card stack plus padding for each column for (int i = 0; i < gridPoint.x() / 3; ++i) { const int key = getCardStackMapKey(i, gridPoint.y()); - x += cardStackWidth.value(key, CARD_WIDTH) + PADDING_X; + x += cardStackWidth.value(key, CardDimensions::WIDTH) + PADDING_X; } if (isInverted()) @@ -310,7 +310,7 @@ QPointF TableZone::mapFromGrid(QPoint gridPoint) const // Add in card size and padding for each row for (int i = 0; i < gridPoint.y(); ++i) - y += CARD_HEIGHT + PADDING_Y; + y += CardDimensions::HEIGHT + PADDING_Y; return QPointF(x, y); } @@ -324,7 +324,7 @@ QPoint TableZone::mapToGrid(const QPointF &mapPoint) const int y = mapPoint.y() - MARGIN_TOP; // Below calculation effectively rounds to the nearest grid point. - const int gridPointHeight = CARD_HEIGHT + PADDING_Y; + const int gridPointHeight = CardDimensions::HEIGHT + PADDING_Y; int gridPointY = (y + PADDING_Y / 2) / gridPointHeight; gridPointY = clampValidTableRow(gridPointY); @@ -340,7 +340,7 @@ QPoint TableZone::mapToGrid(const QPointF &mapPoint) const // Maximum value is a card width from the right margin, referenced to the // grid area. - const int xMax = width - MARGIN_LEFT - MARGIN_RIGHT - CARD_WIDTH; + const int xMax = width - MARGIN_LEFT - MARGIN_RIGHT - CardDimensions::WIDTH; int xStack = 0; int xNextStack = 0; @@ -348,7 +348,7 @@ QPoint TableZone::mapToGrid(const QPointF &mapPoint) const while ((xNextStack <= x) && (xNextStack <= xMax)) { xStack = xNextStack; const int key = getCardStackMapKey(nextStackCol, gridPointY); - xNextStack += cardStackWidth.value(key, CARD_WIDTH) + PADDING_X; + xNextStack += cardStackWidth.value(key, CardDimensions::WIDTH) + PADDING_X; nextStackCol++; } int stackCol = qMax(nextStackCol - 1, 0); diff --git a/cockatrice/src/game/zones/table_zone.h b/cockatrice/src/game/zones/table_zone.h index 3b353e76c..61eb48d7b 100644 --- a/cockatrice/src/game/zones/table_zone.h +++ b/cockatrice/src/game/zones/table_zone.h @@ -41,12 +41,12 @@ private: /* Minimum width of the table zone including margins. */ - static const int MIN_WIDTH = MARGIN_LEFT + (5 * CARD_WIDTH) + MARGIN_RIGHT; + static const int MIN_WIDTH = MARGIN_LEFT + (5 * CardDimensions::WIDTH) + MARGIN_RIGHT; /* Offset sizes when cards are stacked on each other in the grid */ - static const int STACKED_CARD_OFFSET_X = CARD_WIDTH / 3; + static const int STACKED_CARD_OFFSET_X = CardDimensions::WIDTH / 3; static const int STACKED_CARD_OFFSET_Y = PADDING_Y / 3; /* diff --git a/cockatrice/src/game/zones/view_zone.cpp b/cockatrice/src/game/zones/view_zone.cpp index 348ed6e87..d2fd1e971 100644 --- a/cockatrice/src/game/zones/view_zone.cpp +++ b/cockatrice/src/game/zones/view_zone.cpp @@ -118,7 +118,9 @@ void ZoneViewZone::zoneDumpReceived(const Response &r) qobject_cast(getLogic())->updateCardIds(ZoneViewZoneLogic::INITIALIZE); reorganizeCards(); - emit getLogic()->cardCountChanged(); + // clang-format off + emit getLogic()->cardCountChanged(); // emit keyword causes spurious spacing around -> + // clang-format on } // Because of boundingRect(), this function must not be called before the zone was added to a scene. @@ -166,8 +168,8 @@ void ZoneViewZone::reorganizeCards() // determine bounding rect qreal aleft = 0; qreal atop = 0; - qreal awidth = gridSize.cols * CARD_WIDTH + (CARD_WIDTH / 2) + HORIZONTAL_PADDING; - qreal aheight = (gridSize.rows * CARD_HEIGHT) / 3 + CARD_HEIGHT * 1.3; + qreal awidth = gridSize.cols * CardDimensions::WIDTH_F + CardDimensions::WIDTH_HALF_F + HORIZONTAL_PADDING; + qreal aheight = (gridSize.rows * CardDimensions::HEIGHT_F) / 3 + CardDimensions::HEIGHT_F * 1.3; optimumRect = QRectF(aleft, atop, awidth, aheight); updateGeometry(); @@ -210,8 +212,8 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca } lastColumnProp = columnProp; - qreal x = col * CARD_WIDTH; - qreal y = row * CARD_HEIGHT / 3; + qreal x = col * CardDimensions::WIDTH_F; + qreal y = row * CardDimensions::HEIGHT_F / 3; c->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y); c->setRealZValue(i); longestRow = qMax(row, longestRow); @@ -238,8 +240,8 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca for (int i = 0; i < cardCount; i++) { CardItem *c = cards.at(i); - qreal x = (i / rows) * CARD_WIDTH; - qreal y = (i % rows) * CARD_HEIGHT / 3; + qreal x = (i / rows) * CardDimensions::WIDTH_F; + qreal y = (i % rows) * CardDimensions::HEIGHT_F / 3; c->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y); c->setRealZValue(i); } diff --git a/cockatrice/src/game/zones/view_zone_widget.cpp b/cockatrice/src/game/zones/view_zone_widget.cpp index 0ad3b10f1..23d7d6a19 100644 --- a/cockatrice/src/game/zones/view_zone_widget.cpp +++ b/cockatrice/src/game/zones/view_zone_widget.cpp @@ -458,7 +458,7 @@ void ZoneViewWidget::resizeScrollbar(const qreal newZoneHeight) */ static qreal rowsToHeight(int rows) { - const qreal cardsHeight = (rows + 1) * (CARD_HEIGHT / 3); + const qreal cardsHeight = (rows + 1) * (CardDimensions::HEIGHT_F / 3); return cardsHeight + 5; // +5 padding to make the cutoff look nicer } @@ -574,4 +574,4 @@ void ZoneViewWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) if (event->pos().y() <= 0) { expandWindow(); } -} \ No newline at end of file +} diff --git a/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp index f8b581e33..4930fbbfd 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp @@ -10,7 +10,7 @@ #include CardInfoDisplayWidget::CardInfoDisplayWidget(const CardRef &cardRef, QWidget *parent, Qt::WindowFlags flags) - : QFrame(parent, flags), aspectRatio((qreal)CARD_HEIGHT / (qreal)CARD_WIDTH) + : QFrame(parent, flags), aspectRatio(CardDimensions::HEIGHT_F / CardDimensions::WIDTH_F) { setContentsMargins(3, 3, 3, 3); pic = new CardInfoPictureWidget(); From dead99363909f1b010ce4d468dc311d0a84218a1 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:39:04 -0800 Subject: [PATCH 029/116] [DeckList] Refactor load from plaintext to take normalizer as param (#6664) * [DeckList] Refactor load from plaintext to take normalizer as param * update usages * weaken unit test * weaken unit test more * revert unit test * move CardNameNormalizer to libcockatrice_card * update unit test * formatting --- .../parsers/interface_json_deck_parser.h | 7 +-- .../src/interface/deck_loader/deck_loader.cpp | 5 ++- .../dialogs/dlg_load_deck_from_clipboard.cpp | 3 +- .../dialogs/dlg_load_deck_from_website.cpp | 3 +- .../dialogs/dlg_load_deck_from_website.h | 1 + ...idekt_api_response_deck_display_widget.cpp | 5 +-- .../average_deck/edhrec_deck_api_response.cpp | 5 +-- libcockatrice_card/CMakeLists.txt | 2 + .../card/import/card_name_normalizer.cpp | 45 +++++++++++++++++++ .../card/import/card_name_normalizer.h | 15 +++++++ .../libcockatrice/deck_list/deck_list.cpp | 43 ++++-------------- .../libcockatrice/deck_list/deck_list.h | 6 ++- tests/loading_from_clipboard/CMakeLists.txt | 3 +- .../clipboard_testing.cpp | 3 +- 14 files changed, 94 insertions(+), 52 deletions(-) create mode 100644 libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp create mode 100644 libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h diff --git a/cockatrice/src/client/network/parsers/interface_json_deck_parser.h b/cockatrice/src/client/network/parsers/interface_json_deck_parser.h index e19e4f4e4..1818aa35c 100644 --- a/cockatrice/src/client/network/parsers/interface_json_deck_parser.h +++ b/cockatrice/src/client/network/parsers/interface_json_deck_parser.h @@ -8,10 +8,11 @@ #define INTERFACE_JSON_DECK_PARSER_H #include "../../../interface/deck_loader/card_node_function.h" -#include "../../../interface/deck_loader/deck_loader.h" #include #include +#include +#include class IJsonDeckParser { @@ -49,7 +50,7 @@ public: outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n'; } - deckList.loadFromStream_Plain(outStream, false); + deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer()); deckList.forEachCard(CardNodeFunction::ResolveProviderId()); return deckList; @@ -96,7 +97,7 @@ public: outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n'; } - deckList.loadFromStream_Plain(outStream, false); + deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer()); deckList.forEachCard(CardNodeFunction::ResolveProviderId()); QJsonObject commandersObj = obj.value("commanders").toObject(); diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp index 3481e245b..d71dca24a 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.cpp +++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -42,7 +43,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo DeckList deckList; switch (fmt) { case DeckFileFormat::PlainText: - result = deckList.loadFromFile_Plain(&file); + result = deckList.loadFromFile_Plain(&file, CardNameNormalizer()); break; case DeckFileFormat::Cockatrice: { result = deckList.loadFromFile_Native(&file); @@ -50,7 +51,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo qCInfo(DeckLoaderLog) << "Failed to load " << fileName << "as cockatrice format; retrying as plain format"; file.seek(0); - result = deckList.loadFromFile_Plain(&file); + result = deckList.loadFromFile_Plain(&file, CardNameNormalizer()); fmt = DeckFileFormat::PlainText; } break; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp index b72130c81..9ceb35d78 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp @@ -14,6 +14,7 @@ #include #include #include +#include /** * Creates the main layout and connects the signals that are common to all versions of this window @@ -81,7 +82,7 @@ bool AbstractDlgDeckTextEdit::loadIntoDeck(DeckList &deckList) const QTextStream stream(&buffer); - if (deckList.loadFromStream_Plain(stream, true)) { + if (deckList.loadFromStream_Plain(stream, true, CardNameNormalizer())) { if (loadSetNameAndNumberCheckBox->isChecked()) { deckList.forEachCard(CardNodeFunction::ResolveProviderId()); } else { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp index da4135246..ba00873d9 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include DlgLoadDeckFromWebsite::DlgLoadDeckFromWebsite(QWidget *parent) : QDialog(parent) @@ -99,7 +100,7 @@ void DlgLoadDeckFromWebsite::accept() // Parse the plain text deck here DeckList deckList; QTextStream stream(&deckText); - deckList.loadFromStream_Plain(stream, false); + deckList.loadFromStream_Plain(stream, false, CardNameNormalizer()); deckList.forEachCard(CardNodeFunction::ResolveProviderId()); deck = deckList; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.h b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.h index fe6f0a7e9..1ac98d206 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp index f40b9094f..8b17cd49e 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -2,7 +2,6 @@ #include "../../../../../deck_loader/card_node_function.h" #include "../../../../../deck_loader/deck_loader.h" -#include "../../../../cards/card_info_picture_with_text_overlay_widget.h" #include "../../../../cards/card_size_widget.h" #include "../../../../cards/deck_card_zone_display_widget.h" #include "../../../../visual_deck_editor/visual_deck_display_options_widget.h" @@ -10,7 +9,7 @@ #include "../api_response/deck/archidekt_api_response_deck.h" #include -#include +#include ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent, ArchidektApiResponseDeck _response, @@ -80,7 +79,7 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset); auto decklist = QSharedPointer(new DeckList); - decklist->loadFromStream_Plain(deckStream, false); + decklist->loadFromStream_Plain(deckStream, false, CardNameNormalizer()); model->setDeckList(decklist); model->forEachCard(CardNodeFunction::ResolveProviderId()); diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp index a744a9b14..1cb070c30 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp @@ -1,11 +1,10 @@ #include "edhrec_deck_api_response.h" -#include "../../../../../../deck_loader/deck_loader.h" - #include #include #include #include +#include void EdhrecDeckApiResponse::fromJson(const QJsonArray &json) { @@ -15,7 +14,7 @@ void EdhrecDeckApiResponse::fromJson(const QJsonArray &json) } QTextStream stream(&deckList); - deck.loadFromStream_Plain(stream, true); + deck.loadFromStream_Plain(stream, true, CardNameNormalizer()); } void EdhrecDeckApiResponse::debugPrint() const diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index f516cde00..dd3799e33 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -12,6 +12,7 @@ set(HEADERS libcockatrice/card/database/parser/card_database_parser.h libcockatrice/card/database/parser/cockatrice_xml_3.h libcockatrice/card/database/parser/cockatrice_xml_4.h + libcockatrice/card/import/card_name_normalizer.h libcockatrice/card/printing/exact_card.h libcockatrice/card/printing/printing_info.h libcockatrice/card/set/card_set.h @@ -36,6 +37,7 @@ add_library( libcockatrice/card/database/parser/card_database_parser.cpp libcockatrice/card/database/parser/cockatrice_xml_3.cpp libcockatrice/card/database/parser/cockatrice_xml_4.cpp + libcockatrice/card/import/card_name_normalizer.cpp libcockatrice/card/printing/exact_card.cpp libcockatrice/card/printing/printing_info.cpp libcockatrice/card/relation/card_relation.cpp diff --git a/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp new file mode 100644 index 000000000..2349dc3e8 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp @@ -0,0 +1,45 @@ +#include "card_name_normalizer.h" + +#include + +QString CardNameNormalizer::operator()(const QString &cardNameString) const +{ + QString cardName = cardNameString; + + // Regex for advanced card parsing + static const QRegularExpression reSplitCard(R"( ?\/\/ ?)"); + static const QRegularExpression reBrace(R"( ?[\[\{][^\]\}]*[\]\}] ?)"); // not nested + static const QRegularExpression reRoundBrace(R"(^\([^\)]*\) ?)"); // () are only matched at start of string + static const QRegularExpression reDigitBrace(R"( ?\(\d*\) ?)"); // () are matched if containing digits + static const QRegularExpression reBraceDigit( + R"( ?\([\dA-Z]+\) *\d+$)"); // () are matched if containing setcode then a number + static const QRegularExpression reDoubleFacedMarker(R"( ?\(Transform\) ?)"); + + static const QHash differences{{QRegularExpression("’"), "'"}, + {QRegularExpression("Æ"), "Ae"}, + {QRegularExpression("æ"), "ae"}, + {QRegularExpression(" ?[|/]+ ?"), " // "}}; + + // Handle advanced card types + if (cardName.contains(reSplitCard)) { + cardName = cardName.split(reSplitCard).join(" // "); + } + + if (cardName.contains(reDoubleFacedMarker)) { + QStringList faces = cardName.split(reDoubleFacedMarker); + cardName = faces.first().trimmed(); + } + + // Remove unnecessary characters + cardName.remove(reBrace); + cardName.remove(reRoundBrace); // I'll be entirely honest here, these are split to accommodate just three cards + cardName.remove(reDigitBrace); // from un-sets that have a word in between round braces at the end + cardName.remove(reBraceDigit); // very specific format with the set code in () and collectors number after + + // Normalize characters + for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) { + cardName.replace(diff.key(), diff.value()); + } + + return cardName; +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h new file mode 100644 index 000000000..716e7da80 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h @@ -0,0 +1,15 @@ +#ifndef COCKATRICE_CARD_NAME_NORMALIZER_H +#define COCKATRICE_CARD_NAME_NORMALIZER_H + +#include + +/** + * Functor that normalizes the raw card name parsed during a plaintext deck import into the card name that Cockatrice + * uses. + */ +struct CardNameNormalizer +{ + QString operator()(const QString &cardNameString) const; +}; + +#endif // COCKATRICE_CARD_NAME_NORMALIZER_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index 3d1070f15..e3e7b41c0 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -199,9 +199,12 @@ bool DeckList::saveToFile_Native(QIODevice *device) const * * @param in The text to load * @param preserveMetadata If true, don't clear the existing metadata + * @param cardNameNormalizer Function that takes the parsed card name string in the text and * @return False if the input was empty, true otherwise. */ -bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata) +bool DeckList::loadFromStream_Plain(QTextStream &in, + bool preserveMetadata, + const std::function &cardNameNormalizer) { const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); const QRegularExpression reEmpty("^\\s*$"); @@ -213,23 +216,11 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata) // Regex for advanced card parsing const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))"); - const QRegularExpression reSplitCard(R"( ?\/\/ ?)"); - const QRegularExpression reBrace(R"( ?[\[\{][^\]\}]*[\]\}] ?)"); // not nested - const QRegularExpression reRoundBrace(R"(^\([^\)]*\) ?)"); // () are only matched at start of string - const QRegularExpression reDigitBrace(R"( ?\(\d*\) ?)"); // () are matched if containing digits - const QRegularExpression reBraceDigit( - R"( ?\([\dA-Z]+\) *\d+$)"); // () are matched if containing setcode then a number - const QRegularExpression reDoubleFacedMarker(R"( ?\(Transform\) ?)"); // Regex for extracting set code and collector number with attached symbols const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))"); const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))"); - const QHash differences{{QRegularExpression("’"), QString("'")}, - {QRegularExpression("Æ"), QString("Ae")}, - {QRegularExpression("æ"), QString("ae")}, - {QRegularExpression(" ?[|/]+ ?"), QString(" // ")}}; - cleanList(preserveMetadata); auto inputs = in.readAll().trimmed().split('\n'); @@ -355,26 +346,8 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata) cardName = match.captured(2); } - // Handle advanced card types - if (cardName.contains(reSplitCard)) { - cardName = cardName.split(reSplitCard).join(" // "); - } - - if (cardName.contains(reDoubleFacedMarker)) { - QStringList faces = cardName.split(reDoubleFacedMarker); - cardName = faces.first().trimmed(); - } - - // Remove unnecessary characters - cardName.remove(reBrace); - cardName.remove(reRoundBrace); // I'll be entirely honest here, these are split to accommodate just three cards - cardName.remove(reDigitBrace); // from un-sets that have a word in between round braces at the end - cardName.remove(reBraceDigit); // very specific format with the set code in () and collectors number after - - // Normalize names - for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) { - cardName.replace(diff.key(), diff.value()); - } + // Normalize the card name + cardName = cardNameNormalizer(cardName); // Determine the zone (mainboard/sideboard) QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; @@ -387,10 +360,10 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata) return true; } -bool DeckList::loadFromFile_Plain(QIODevice *device) +bool DeckList::loadFromFile_Plain(QIODevice *device, const std::function &cardNameNormalizer) { QTextStream in(device); - return loadFromStream_Plain(in, false); + return loadFromStream_Plain(in, false, cardNameNormalizer); } bool DeckList::saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h index 91e56fc9f..a96adeb38 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -206,8 +206,10 @@ public: /// @name Serialization (Plain text) ///@{ - bool loadFromStream_Plain(QTextStream &stream, bool preserveMetadata); - bool loadFromFile_Plain(QIODevice *device); + bool loadFromStream_Plain(QTextStream &stream, + bool preserveMetadata, + const std::function &cardNameNormalizer); + bool loadFromFile_Plain(QIODevice *device, const std::function &cardNameNormalizer); bool saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const; bool saveToFile_Plain(QIODevice *device, bool prefixSideboardCards = true, bool slashTappedOutSplitCards = false) const; diff --git a/tests/loading_from_clipboard/CMakeLists.txt b/tests/loading_from_clipboard/CMakeLists.txt index 40e85c66e..eecaafcbb 100644 --- a/tests/loading_from_clipboard/CMakeLists.txt +++ b/tests/loading_from_clipboard/CMakeLists.txt @@ -10,6 +10,7 @@ set(TEST_QT_MODULES ${COCKATRICE_QT_VERSION_NAME}::Concurrent ${COCKATRICE_QT_VE ) target_link_libraries( - loading_from_clipboard_test libcockatrice_deck_list Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} + loading_from_clipboard_test libcockatrice_deck_list libcockatrice_card Threads::Threads ${GTEST_BOTH_LIBRARIES} + ${TEST_QT_MODULES} ) add_test(NAME loading_from_clipboard_test COMMAND loading_from_clipboard_test) diff --git a/tests/loading_from_clipboard/clipboard_testing.cpp b/tests/loading_from_clipboard/clipboard_testing.cpp index 29535c1dc..a9977b800 100644 --- a/tests/loading_from_clipboard/clipboard_testing.cpp +++ b/tests/loading_from_clipboard/clipboard_testing.cpp @@ -1,6 +1,7 @@ #include "clipboard_testing.h" #include +#include #include DeckList getDeckList(const QString &clipboard) @@ -8,7 +9,7 @@ DeckList getDeckList(const QString &clipboard) DeckList deckList; QString cp(clipboard); QTextStream stream(&cp); // text stream requires local copy - deckList.loadFromStream_Plain(stream, false); + deckList.loadFromStream_Plain(stream, false, CardNameNormalizer()); return deckList; } From 2f10634ca2478521f02a85dc4bc4cfe010940077 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:48:17 -0800 Subject: [PATCH 030/116] [DeckList] Fix double-faced cards not importing correctly (#6665) * [DeckList] Fix double-faced cards not importing correctly * make tests compile --- .../card/import/card_name_normalizer.cpp | 21 +++++++++++++++++++ tests/loading_from_clipboard/CMakeLists.txt | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp index 2349dc3e8..91ebd6647 100644 --- a/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp +++ b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp @@ -1,7 +1,25 @@ #include "card_name_normalizer.h" +#include "../database/card_database_manager.h" +#include "../printing/exact_card.h" + #include +/** + * @brief Resolves the complete display name of a card. + * @param cardName Base name. + * @return Full display name, or the cardName unchanged if a display name is not found. + */ +static QString getCompleteCardName(const QString &cardName) +{ + ExactCard temp = CardDatabaseManager::query()->guessCard({cardName}); + if (temp) { + return temp.getName(); + } + + return cardName; +} + QString CardNameNormalizer::operator()(const QString &cardNameString) const { QString cardName = cardNameString; @@ -41,5 +59,8 @@ QString CardNameNormalizer::operator()(const QString &cardNameString) const cardName.replace(diff.key(), diff.value()); } + // Resolve complete card name + cardName = getCompleteCardName(cardName); + return cardName; } \ No newline at end of file diff --git a/tests/loading_from_clipboard/CMakeLists.txt b/tests/loading_from_clipboard/CMakeLists.txt index eecaafcbb..85133a8e8 100644 --- a/tests/loading_from_clipboard/CMakeLists.txt +++ b/tests/loading_from_clipboard/CMakeLists.txt @@ -1,5 +1,5 @@ add_definitions("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"") -add_executable(loading_from_clipboard_test clipboard_testing.cpp loading_from_clipboard_test.cpp) +add_executable(loading_from_clipboard_test ${VERSION_STRING_CPP} clipboard_testing.cpp loading_from_clipboard_test.cpp) if(NOT GTEST_FOUND) add_dependencies(loading_from_clipboard_test gtest) From f15b70e4aef3994ab2fa9e371ee4cbbcb9b41a15 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 8 Mar 2026 17:53:16 +0100 Subject: [PATCH 031/116] Use new attest action (#6671) --- .github/workflows/desktop-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index e95898097..2cd25fc32 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -232,7 +232,7 @@ jobs: - name: Attest binary provenance id: attestation if: steps.upload_release.outcome == 'success' - uses: actions/attest-build-provenance@v4 + uses: actions/attest@v4 with: subject-name: ${{steps.build.outputs.name}} subject-path: ${{steps.build.outputs.path}} @@ -530,7 +530,7 @@ jobs: - name: Attest binary provenance id: attestation if: steps.upload_release.outcome == 'success' - uses: actions/attest-build-provenance@v4 + uses: actions/attest@v4 with: subject-name: ${{steps.build.outputs.name}} subject-path: ${{steps.build.outputs.path}} From c375cdbb1ae84faedc142eadf11ca5a63332bdfe Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 8 Mar 2026 18:03:01 +0100 Subject: [PATCH 032/116] CI: Upload artifact files directly (#6654) * Upload artifact files directly * match pdbs name * Update name variable --- .ci/name_build.sh | 10 +++++----- .github/workflows/desktop-build.yml | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.ci/name_build.sh b/.ci/name_build.sh index 060f8a68e..7b964e21c 100755 --- a/.ci/name_build.sh +++ b/.ci/name_build.sh @@ -35,15 +35,15 @@ if ! cd "$path"; then fi # set filename -name="${file%.*}" # remove all after last . -new_name="$name$SUFFIX." +name="${file%.*}" # remove file extension +new_name="$name$SUFFIX" if [[ $MAKE_ZIP ]]; then - filename="${new_name}zip" + filename="${new_name}.zip" echo "creating zip '$filename' from '$file'" zip "$filename" "$file" else extension="${file##*.}" # remove all before last . - filename="$new_name$extension" + filename="$new_name.$extension" echo "renaming '$file' to '$filename'" mv "$file" "$filename" fi @@ -52,4 +52,4 @@ cd "$oldpwd" relative_path="$path/$filename" ls -l "$relative_path" echo "path=$relative_path" >>"$GITHUB_OUTPUT" -echo "name=$filename" >>"$GITHUB_OUTPUT" +echo "name=$new_name" >>"$GITHUB_OUTPUT" diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 2cd25fc32..bc49db2da 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -214,8 +214,8 @@ jobs: if: matrix.package != 'skip' uses: actions/upload-artifact@v7 with: - name: ${{matrix.distro}}${{matrix.version}}-package path: ${{steps.build.outputs.path}} + archive: false if-no-files-found: error - name: Upload to release @@ -501,15 +501,15 @@ jobs: if: matrix.make_package uses: actions/upload-artifact@v7 with: - name: ${{matrix.artifact_name}} path: ${{steps.build.outputs.path}} + archive: false if-no-files-found: error - name: Upload PDBs (Program Databases) if: matrix.os == 'Windows' uses: actions/upload-artifact@v7 with: - name: Windows${{matrix.target}}-PDBs + name: ${{steps.build.outputs.name}}-PDBs path: | build/cockatrice/Release/*.pdb build/oracle/Release/*.pdb From 4606fcdbd59ed762b8cedb8472cfda771b90642b Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 8 Mar 2026 23:27:15 +0100 Subject: [PATCH 033/116] Add description for code docs (#6679) --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f9abf643d..02bdc148a 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -9,4 +9,4 @@ contact_links: about: For more information and guidance check our Translation FAQ on our wiki! - name: 📖 Code Documentation url: https://cockatrice.github.io/docs/ - about: + about: Helpful source focusing on developers, but there are also references for users! From 852429f24861ae6381dd4b18d6576e74621b980d Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 8 Mar 2026 23:48:30 +0100 Subject: [PATCH 034/116] remove unused zip command (#6676) --- .ci/name_build.sh | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/.ci/name_build.sh b/.ci/name_build.sh index 7b964e21c..b111aded6 100755 --- a/.ci/name_build.sh +++ b/.ci/name_build.sh @@ -2,7 +2,6 @@ # used by the ci to rename build artifacts # renames the file to [original name][SUFFIX].[original extension] # where SUFFIX is either available in the environment or as the first arg -# if MAKE_ZIP is set instead a zip is made # expected to be run in the build directory unless BUILD_DIR is set # adds output to GITHUB_OUTPUT builddir="${BUILD_DIR:=.}" @@ -22,8 +21,8 @@ set -e # find file found="$(find "$builddir" -maxdepth 1 -type f -name "$findrx" -print -quit)" -path="${found%/*}" # remove all after last / -file="${found##*/}" # remove all before last / +path="${found%/*}" # remove all including first "/" from right side +file="${found##*/}" # remove all including last "/" from left side if [[ ! $file ]]; then echo "::error file=$0::could not find package" exit 1 @@ -35,18 +34,12 @@ if ! cd "$path"; then fi # set filename -name="${file%.*}" # remove file extension +name="${file%.*}" # remove all including first "." from right side new_name="$name$SUFFIX" -if [[ $MAKE_ZIP ]]; then - filename="${new_name}.zip" - echo "creating zip '$filename' from '$file'" - zip "$filename" "$file" -else - extension="${file##*.}" # remove all before last . - filename="$new_name.$extension" - echo "renaming '$file' to '$filename'" - mv "$file" "$filename" -fi +extension="${file##*.}" # remove all including last "." from left side +filename="$new_name.$extension" +echo "renaming '$file' to '$filename'" +mv "$file" "$filename" cd "$oldpwd" relative_path="$path/$filename" From fd293444c5a1700638fbbdb3b70d9f9677fd1821 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 8 Mar 2026 23:50:33 +0100 Subject: [PATCH 035/116] tweaks (#6674) --- .github/workflows/docker-release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 1846766bf..2fe646374 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -62,5 +62,6 @@ jobs: push: ${{ github.ref_type == 'tag' }} tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + annotations: ${{ steps.metadata.outputs.annotations }} + cache-from: type=gha,scope=servatrice + cache-to: type=gha,mode=max,scope=servatrice From 15a1d5440bc8aa794a2ae157fbd0e9b624ca3de4 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:50:54 -0700 Subject: [PATCH 036/116] [DeckEditor] Fix undo/redo resetting deck sorting (#6673) --- .../src/interface/widgets/deck_editor/deck_state_manager.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp index 8afbfaaa2..8da27b63c 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -320,6 +320,7 @@ void DeckStateManager::undo(int steps) deckListModel->rebuildTree(); emit deckListModel->layoutChanged(); + emit deckReplaced(); } void DeckStateManager::redo(int steps) @@ -338,6 +339,7 @@ void DeckStateManager::redo(int steps) deckListModel->rebuildTree(); emit deckListModel->layoutChanged(); + emit deckReplaced(); } void DeckStateManager::requestHistorySave(const QString &reason) From e79bbc67b9be76d31f149b04fb7cadeb2ef6c224 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:57:39 -0700 Subject: [PATCH 037/116] [DeckEditor] Fix undo/redo clearing legality (#6675) --- .../libcockatrice/models/deck_list/deck_list_model.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 8cbf23be4..e43c612f0 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -85,6 +85,8 @@ void DeckListModel::rebuildTree() } endResetModel(); + + refreshCardFormatLegalities(); } int DeckListModel::rowCount(const QModelIndex &parent) const @@ -649,7 +651,6 @@ void DeckListModel::setDeckList(const QSharedPointer &_deck) deckList = _deck; } rebuildTree(); - refreshCardFormatLegalities(); emit deckReplaced(); } From 413b4b637b8949e2b52c5109d02daed0f8deeab5 Mon Sep 17 00:00:00 2001 From: tooomm Date: Mon, 9 Mar 2026 21:20:38 +0100 Subject: [PATCH 038/116] sign+notarize only releases (#6678) --- .github/workflows/desktop-build.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index bc49db2da..d627906fd 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -220,7 +220,7 @@ jobs: - name: Upload to release id: upload_release - if: needs.configure.outputs.tag != null && matrix.package != 'skip' + if: matrix.package != 'skip' && needs.configure.outputs.tag != null shell: bash env: GH_TOKEN: ${{github.token}} @@ -453,7 +453,8 @@ jobs: key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}} - name: Sign app bundle - if: matrix.os == 'macOS' && matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null) + if: matrix.os == 'macOS' && matrix.make_package && needs.configure.outputs.tag != null + id: sign_macos env: MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} @@ -465,7 +466,7 @@ jobs: fi - name: Notarize app bundle - if: matrix.os == 'macOS' && matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null) + if: steps.sign_macos.outcome == 'success' env: MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} @@ -497,8 +498,8 @@ jobs: fi - name: Upload artifact - id: upload_artifact if: matrix.make_package + id: upload_artifact uses: actions/upload-artifact@v7 with: path: ${{steps.build.outputs.path}} @@ -506,7 +507,7 @@ jobs: if-no-files-found: error - name: Upload PDBs (Program Databases) - if: matrix.os == 'Windows' + if: matrix.os == 'Windows' && github.ref_type != 'tag' uses: actions/upload-artifact@v7 with: name: ${{steps.build.outputs.name}}-PDBs @@ -517,8 +518,8 @@ jobs: if-no-files-found: error - name: Upload to release + if: needs.configure.outputs.tag != null && matrix.make_package == '1' id: upload_release - if: needs.configure.outputs.tag != null shell: bash env: GH_TOKEN: ${{github.token}} @@ -528,8 +529,8 @@ jobs: run: gh release upload "$tag_name" "$asset_path#$asset_name" - name: Attest binary provenance - id: attestation if: steps.upload_release.outcome == 'success' + id: attestation uses: actions/attest@v4 with: subject-name: ${{steps.build.outputs.name}} From 8268311fab199a86e000e7b753027afac75996dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:22:26 +0100 Subject: [PATCH 039/116] Bump docker/setup-qemu-action from 3 to 4 (#6683) Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 2fe646374..02a06e592 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -41,7 +41,7 @@ jobs: org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker buildx uses: docker/setup-buildx-action@v3 From 95d7e027fc8f3f4ed44b3d76b8144a07a1421058 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:30:47 +0100 Subject: [PATCH 040/116] Bump docker/setup-buildx-action from 3 to 4 (#6682) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 02a06e592..65ccd1322 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -44,7 +44,7 @@ jobs: uses: docker/setup-qemu-action@v4 - name: Set up Docker buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to GitHub Container Registry if: github.ref_type == 'tag' From 42cec1045725a29b72b1278ace7d037c5662f9d4 Mon Sep 17 00:00:00 2001 From: tooomm Date: Mon, 9 Mar 2026 21:32:47 +0100 Subject: [PATCH 041/116] CI: Cleanup (#6677) * Cleanup vcpkg matrix * Add filename with extension as output * fix name -> fullname, cleanup --- .ci/name_build.sh | 1 + .github/workflows/desktop-build.yml | 10 ++-------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.ci/name_build.sh b/.ci/name_build.sh index b111aded6..85818bbd9 100755 --- a/.ci/name_build.sh +++ b/.ci/name_build.sh @@ -46,3 +46,4 @@ relative_path="$path/$filename" ls -l "$relative_path" echo "path=$relative_path" >>"$GITHUB_OUTPUT" echo "name=$new_name" >>"$GITHUB_OUTPUT" +echo "fullname=$filename" >>"$GITHUB_OUTPUT" diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index d627906fd..233d9e488 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -225,7 +225,7 @@ jobs: env: GH_TOKEN: ${{github.token}} tag_name: ${{needs.configure.outputs.tag}} - asset_name: ${{steps.build.outputs.name}} + asset_name: ${{steps.build.outputs.fullname}} asset_path: ${{steps.build.outputs.path}} run: gh release upload "$tag_name" "$asset_path#$asset_name" @@ -234,7 +234,6 @@ jobs: if: steps.upload_release.outcome == 'success' uses: actions/attest@v4 with: - subject-name: ${{steps.build.outputs.name}} subject-path: ${{steps.build.outputs.path}} show-summary: false @@ -259,7 +258,6 @@ jobs: override_target: 13 make_package: 1 package_suffix: "-macOS13_Intel" - artifact_name: macOS13_Intel-package qt_version: 6.10.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets @@ -274,7 +272,6 @@ jobs: type: Release make_package: 1 package_suffix: "-macOS14" - artifact_name: macOS14-package qt_version: 6.10.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets @@ -289,7 +286,6 @@ jobs: type: Release make_package: 1 package_suffix: "-macOS15" - artifact_name: macOS15-package qt_version: 6.10.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets @@ -314,7 +310,6 @@ jobs: type: Release make_package: 1 package_suffix: "-Win10" - artifact_name: Windows10-installer qt_version: 6.10.* qt_arch: win64_msvc2022_64 qt_modules: qtimageformats qtmultimedia qtwebsockets @@ -524,7 +519,7 @@ jobs: env: GH_TOKEN: ${{github.token}} tag_name: ${{needs.configure.outputs.tag}} - asset_name: ${{steps.build.outputs.name}} + asset_name: ${{steps.build.outputs.fullname}} asset_path: ${{steps.build.outputs.path}} run: gh release upload "$tag_name" "$asset_path#$asset_name" @@ -533,7 +528,6 @@ jobs: id: attestation uses: actions/attest@v4 with: - subject-name: ${{steps.build.outputs.name}} subject-path: ${{steps.build.outputs.path}} show-summary: false From 9e2276a59fa0df05b16bb4e9dbd0a2a377dd572a Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Wed, 11 Mar 2026 19:34:05 -0400 Subject: [PATCH 042/116] Refactor zone names (#6686) * Add ZoneNames constants for protocol zone identifiers. Introduce a centralized ZoneNames namespace providing constexpr constants for zone identifiers used in the client-server protocol. This establishes a single source of truth for zone names like TABLE, GRAVE, EXILE, HAND, DECK, SIDEBOARD, and STACK. The protocol values remain unchanged (e.g., EXILE maps to rfg for backwards compatibility) while providing meaningful constant names. * refactor(server): use ZoneNames constants in server game logic Replace hardcoded zone name strings with ZoneNames:: constants in: - server_player.cpp: zone setup, draw, shuffle, mulligan operations - server_abstract_player.cpp: card movement and token destruction - server_game.cpp: returning cards when players leave No functional changes - purely mechanical string literal replacement. * refactor(client): use ZoneNames constants in core player/zone logic Update the foundational player and zone classes to use ZoneNames:: constants instead of string literals. Changes include: - player.h/cpp: zone initialization and builtinZones set - card_zone_logic.cpp: zone name translation for UI display - table_zone.cpp: table zone operations No functional changes - purely mechanical string literal replacement. * refactor(client): use ZoneNames constants in player actions and events Replace zone name strings with ZoneNames:: constants in the player action and event handling code. player_actions.cpp contains the most extensive changes (~90+ replacements) covering all card movement commands. No functional changes - purely mechanical string literal replacement. * refactor(client): use ZoneNames constants in zone menu handlers Update all zone-specific menu files to use ZoneNames:: constants for QAction data values and zone targeting. This covers context menus for cards, graveyard, hand, and exile (RFG) zones. No functional changes - purely mechanical string literal replacement. * refactor(client): use ZoneNames constants in game scene components Update remaining game scene components to use ZoneNames:: constants: - arrow_item.cpp: arrow drawing between cards - game_scene.cpp: zone view positioning - message_log_widget.cpp: removes duplicate local static constants that were previously defining zone names redundantly - phases_toolbar.cpp: phase actions (untap all) Notable: message_log_widget.cpp previously had its own local constants (TABLE_ZONE_NAME, GRAVE_ZONE_NAME, etc.) which are now removed in favor of the centralized ZoneNames:: constants. * formatting fix --- cockatrice/src/game/board/arrow_item.cpp | 19 +-- cockatrice/src/game/game_scene.cpp | 5 +- .../src/game/log/message_log_widget.cpp | 43 +++--- cockatrice/src/game/phases_toolbar.cpp | 3 +- cockatrice/src/game/player/menu/card_menu.cpp | 14 +- .../src/game/player/menu/grave_menu.cpp | 9 +- cockatrice/src/game/player/menu/hand_menu.cpp | 9 +- cockatrice/src/game/player/menu/rfg_menu.cpp | 10 +- cockatrice/src/game/player/player.cpp | 22 +-- cockatrice/src/game/player/player.h | 15 +- cockatrice/src/game/player/player_actions.cpp | 137 +++++++++--------- .../src/game/player/player_event_handler.cpp | 5 +- .../src/game/zones/logic/card_zone_logic.cpp | 11 +- cockatrice/src/game/zones/table_zone.cpp | 3 +- .../remote/game/server_abstract_player.cpp | 9 +- .../server/remote/game/server_game.cpp | 5 +- .../server/remote/game/server_player.cpp | 33 +++-- libcockatrice_utility/CMakeLists.txt | 9 +- .../libcockatrice/utility/zone_names.h | 19 +++ 19 files changed, 207 insertions(+), 173 deletions(-) create mode 100644 libcockatrice_utility/libcockatrice/utility/zone_names.h diff --git a/cockatrice/src/game/board/arrow_item.cpp b/cockatrice/src/game/board/arrow_item.cpp index 257d96f8a..60585a774 100644 --- a/cockatrice/src/game/board/arrow_item.cpp +++ b/cockatrice/src/game/board/arrow_item.cpp @@ -19,6 +19,7 @@ #include #include #include +#include ArrowItem::ArrowItem(Player *_player, int _id, ArrowTarget *_startItem, ArrowTarget *_targetItem, const QColor &_color) : QGraphicsItem(), player(_player), id(_id), startItem(_startItem), targetItem(_targetItem), targetLocked(false), @@ -239,16 +240,16 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) } // if the card is in hand then we will move the card to stack or table as part of drawing the arrow - if (startZone->getName() == "hand") { + if (startZone->getName() == ZoneNames::HAND) { startCard->playCard(false); CardInfoPtr ci = startCard->getCard().getCardPtr(); bool playToStack = SettingsCache::instance().getPlayToStack(); - if (ci && - ((!playToStack && ci->getUiAttributes().tableRow == 3) || - (playToStack && ci->getUiAttributes().tableRow != 0 && startCard->getZone()->getName() != "stack"))) - cmd.set_start_zone("stack"); + if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) || + (playToStack && ci->getUiAttributes().tableRow != 0 && + startCard->getZone()->getName() != ZoneNames::STACK))) + cmd.set_start_zone(ZoneNames::STACK); else - cmd.set_start_zone(playToStack ? "stack" : "table"); + cmd.set_start_zone(playToStack ? ZoneNames::STACK : ZoneNames::TABLE); } if (deleteInPhase != 0) { @@ -318,7 +319,7 @@ void ArrowAttachItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) void ArrowAttachItem::attachCards(CardItem *startCard, const CardItem *targetCard) { // do nothing if target is already attached to another card or is not in play - if (targetCard->getAttachedTo() || targetCard->getZone()->getName() != "table") { + if (targetCard->getAttachedTo() || targetCard->getZone()->getName() != ZoneNames::TABLE) { return; } @@ -326,12 +327,12 @@ void ArrowAttachItem::attachCards(CardItem *startCard, const CardItem *targetCar CardZoneLogic *targetZone = targetCard->getZone(); // move card onto table first if attaching from some other zone - if (startZone->getName() != "table") { + if (startZone->getName() != ZoneNames::TABLE) { player->getPlayerActions()->playCardToTable(startCard, false); } Command_AttachCard cmd; - cmd.set_start_zone("table"); + cmd.set_start_zone(ZoneNames::TABLE); cmd.set_card_id(startCard->getId()); cmd.set_target_player_id(targetZone->getPlayer()->getPlayerInfo()->getId()); cmd.set_target_zone(targetZone->getName().toStdString()); diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game/game_scene.cpp index 77037cd6e..5dc3b48f7 100644 --- a/cockatrice/src/game/game_scene.cpp +++ b/cockatrice/src/game/game_scene.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include /** @@ -410,9 +411,9 @@ void GameScene::toggleZoneView(Player *player, const QString &zoneName, int numb connect(item, &ZoneViewWidget::closePressed, this, &GameScene::removeZoneView); addItem(item); - if (zoneName == "grave") + if (zoneName == ZoneNames::GRAVE) item->setPos(360, 100); - else if (zoneName == "rfg") + else if (zoneName == ZoneNames::EXILE) item->setPos(380, 120); else item->setPos(340, 80); diff --git a/cockatrice/src/game/log/message_log_widget.cpp b/cockatrice/src/game/log/message_log_widget.cpp index 645974994..c38e433eb 100644 --- a/cockatrice/src/game/log/message_log_widget.cpp +++ b/cockatrice/src/game/log/message_log_widget.cpp @@ -10,16 +10,9 @@ #include <../../client/settings/card_counter_settings.h> #include #include +#include #include -static const QString TABLE_ZONE_NAME = "table"; -static const QString GRAVE_ZONE_NAME = "grave"; -static const QString EXILE_ZONE_NAME = "rfg"; -static const QString HAND_ZONE_NAME = "hand"; -static const QString DECK_ZONE_NAME = "deck"; -static const QString SIDEBOARD_ZONE_NAME = "sb"; -static const QString STACK_ZONE_NAME = "stack"; - static QString sanitizeHtml(QString dirty) { return dirty.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); @@ -37,15 +30,15 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position QString fromStr; QString zoneName = zone->getName(); - if (zoneName == TABLE_ZONE_NAME) { + if (zoneName == ZoneNames::TABLE) { fromStr = tr(" from play"); - } else if (zoneName == GRAVE_ZONE_NAME) { + } else if (zoneName == ZoneNames::GRAVE) { fromStr = tr(" from their graveyard"); - } else if (zoneName == EXILE_ZONE_NAME) { + } else if (zoneName == ZoneNames::EXILE) { fromStr = tr(" from exile"); - } else if (zoneName == HAND_ZONE_NAME) { + } else if (zoneName == ZoneNames::HAND) { fromStr = tr(" from their hand"); - } else if (zoneName == DECK_ZONE_NAME) { + } else if (zoneName == ZoneNames::DECK) { if (position == 0) { if (cardName.isEmpty()) { if (ownerChange) { @@ -83,9 +76,9 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position fromStr = tr(" from their library"); } } - } else if (zoneName == SIDEBOARD_ZONE_NAME) { + } else if (zoneName == ZoneNames::SIDEBOARD) { fromStr = tr(" from sideboard"); - } else if (zoneName == STACK_ZONE_NAME) { + } else if (zoneName == ZoneNames::STACK) { fromStr = tr(" from the stack"); } else { fromStr = tr(" from custom zone '%1'").arg(zoneName); @@ -275,9 +268,9 @@ void MessageLogWidget::logMoveCard(Player *player, bool ownerChanged = startZone->getPlayer() != targetZone->getPlayer(); // do not log if moved within the same zone - if ((startZoneName == TABLE_ZONE_NAME && targetZoneName == TABLE_ZONE_NAME && !ownerChanged) || - (startZoneName == HAND_ZONE_NAME && targetZoneName == HAND_ZONE_NAME) || - (startZoneName == EXILE_ZONE_NAME && targetZoneName == EXILE_ZONE_NAME)) { + if ((startZoneName == ZoneNames::TABLE && targetZoneName == ZoneNames::TABLE && !ownerChanged) || + (startZoneName == ZoneNames::HAND && targetZoneName == ZoneNames::HAND) || + (startZoneName == ZoneNames::EXILE && targetZoneName == ZoneNames::EXILE)) { return; } @@ -306,28 +299,28 @@ void MessageLogWidget::logMoveCard(Player *player, QString finalStr; std::optional fourthArg; - if (targetZoneName == TABLE_ZONE_NAME) { + if (targetZoneName == ZoneNames::TABLE) { soundEngine->playSound("play_card"); if (card->getFaceDown()) { finalStr = tr("%1 puts %2 into play%3 face down."); } else { finalStr = tr("%1 puts %2 into play%3."); } - } else if (targetZoneName == GRAVE_ZONE_NAME) { + } else if (targetZoneName == ZoneNames::GRAVE) { if (card->getFaceDown()) { finalStr = tr("%1 puts %2%3 into their graveyard face down."); } else { finalStr = tr("%1 puts %2%3 into their graveyard."); } - } else if (targetZoneName == EXILE_ZONE_NAME) { + } else if (targetZoneName == ZoneNames::EXILE) { if (card->getFaceDown()) { finalStr = tr("%1 exiles %2%3 face down."); } else { finalStr = tr("%1 exiles %2%3."); } - } else if (targetZoneName == HAND_ZONE_NAME) { + } else if (targetZoneName == ZoneNames::HAND) { finalStr = tr("%1 moves %2%3 to their hand."); - } else if (targetZoneName == DECK_ZONE_NAME) { + } else if (targetZoneName == ZoneNames::DECK) { if (newX == -1) { finalStr = tr("%1 puts %2%3 into their library."); } else if (newX >= targetZone->getCards().size()) { @@ -339,9 +332,9 @@ void MessageLogWidget::logMoveCard(Player *player, fourthArg = QString::number(newX); finalStr = tr("%1 puts %2%3 into their library %4 cards from the top."); } - } else if (targetZoneName == SIDEBOARD_ZONE_NAME) { + } else if (targetZoneName == ZoneNames::SIDEBOARD) { finalStr = tr("%1 moves %2%3 to sideboard."); - } else if (targetZoneName == STACK_ZONE_NAME) { + } else if (targetZoneName == ZoneNames::STACK) { soundEngine->playSound("play_card"); if (card->getFaceDown()) { finalStr = tr("%1 plays %2%3 face down."); diff --git a/cockatrice/src/game/phases_toolbar.cpp b/cockatrice/src/game/phases_toolbar.cpp index 5106e40de..2341a1d7f 100644 --- a/cockatrice/src/game/phases_toolbar.cpp +++ b/cockatrice/src/game/phases_toolbar.cpp @@ -11,6 +11,7 @@ #include #include #include +#include PhaseButton::PhaseButton(const QString &_name, QGraphicsItem *parent, QAction *_doubleClickAction, bool _highlightable) : QObject(), QGraphicsItem(parent), name(_name), active(false), highlightable(_highlightable), @@ -259,7 +260,7 @@ void PhasesToolbar::actNextTurn() void PhasesToolbar::actUntapAll() { Command_SetCardAttr cmd; - cmd.set_zone("table"); + cmd.set_zone(ZoneNames::TABLE); cmd.set_attribute(AttrTapped); cmd.set_attr_value("0"); diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp index ddf0a55f3..ef95f052e 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game/player/menu/card_menu.cpp @@ -12,6 +12,7 @@ #include #include +#include CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive) : player(_player), card(_card), shortcutsActive(_shortcutsActive) @@ -115,11 +116,12 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive } else if (writeableCard) { if (card->getZone()) { - if (card->getZone()->getName() == "table") { + if (card->getZone()->getName() == ZoneNames::TABLE) { createTableMenu(); - } else if (card->getZone()->getName() == "stack") { + } else if (card->getZone()->getName() == ZoneNames::STACK) { createStackMenu(); - } else if (card->getZone()->getName() == "rfg" || card->getZone()->getName() == "grave") { + } else if (card->getZone()->getName() == ZoneNames::EXILE || + card->getZone()->getName() == ZoneNames::GRAVE) { createGraveyardOrExileMenu(); } else { createHandOrCustomZoneMenu(); @@ -128,7 +130,7 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive addMenu(new MoveMenu(player)); } } else { - if (card->getZone() && card->getZone()->getName() != "hand") { + if (card->getZone() && card->getZone()->getName() != ZoneNames::HAND) { addAction(aDrawArrow); addSeparator(); addRelatedCardView(); @@ -285,7 +287,7 @@ void CardMenu::createHandOrCustomZoneMenu() addMenu(new MoveMenu(player)); // actions that are really wonky when done from deck or sideboard - if (card->getZone()->getName() == "hand") { + if (card->getZone()->getName() == ZoneNames::HAND) { addSeparator(); addAction(aAttach); addAction(aDrawArrow); @@ -298,7 +300,7 @@ void CardMenu::createHandOrCustomZoneMenu() } addRelatedCardView(); - if (card->getZone()->getName() == "hand") { + if (card->getZone()->getName() == ZoneNames::HAND) { addRelatedCardActions(); } } diff --git a/cockatrice/src/game/player/menu/grave_menu.cpp b/cockatrice/src/game/player/menu/grave_menu.cpp index d4faca42b..2af62c08a 100644 --- a/cockatrice/src/game/player/menu/grave_menu.cpp +++ b/cockatrice/src/game/player/menu/grave_menu.cpp @@ -6,6 +6,7 @@ #include #include +#include GraveyardMenu::GraveyardMenu(Player *_player, QWidget *parent) : TearOffMenu(parent), player(_player) { @@ -39,16 +40,16 @@ void GraveyardMenu::createMoveActions() if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { aMoveGraveToTopLibrary = new QAction(this); - aMoveGraveToTopLibrary->setData(QList() << "deck" << 0); + aMoveGraveToTopLibrary->setData(QList() << ZoneNames::DECK << 0); aMoveGraveToBottomLibrary = new QAction(this); - aMoveGraveToBottomLibrary->setData(QList() << "deck" << -1); + aMoveGraveToBottomLibrary->setData(QList() << ZoneNames::DECK << -1); aMoveGraveToHand = new QAction(this); - aMoveGraveToHand->setData(QList() << "hand" << 0); + aMoveGraveToHand->setData(QList() << ZoneNames::HAND << 0); aMoveGraveToRfg = new QAction(this); - aMoveGraveToRfg->setData(QList() << "rfg" << 0); + aMoveGraveToRfg->setData(QList() << ZoneNames::EXILE << 0); connect(aMoveGraveToTopLibrary, &QAction::triggered, grave, &PileZoneLogic::moveAllToZone); connect(aMoveGraveToBottomLibrary, &QAction::triggered, grave, &PileZoneLogic::moveAllToZone); diff --git a/cockatrice/src/game/player/menu/hand_menu.cpp b/cockatrice/src/game/player/menu/hand_menu.cpp index 019ab925c..d65c136bf 100644 --- a/cockatrice/src/game/player/menu/hand_menu.cpp +++ b/cockatrice/src/game/player/menu/hand_menu.cpp @@ -9,6 +9,7 @@ #include #include +#include HandMenu::HandMenu(Player *_player, PlayerActions *actions, QWidget *parent) : TearOffMenu(parent), player(_player) { @@ -76,13 +77,13 @@ HandMenu::HandMenu(Player *_player, PlayerActions *actions, QWidget *parent) : T if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { aMoveHandToTopLibrary = new QAction(this); - aMoveHandToTopLibrary->setData(QList() << "deck" << 0); + aMoveHandToTopLibrary->setData(QList() << ZoneNames::DECK << 0); aMoveHandToBottomLibrary = new QAction(this); - aMoveHandToBottomLibrary->setData(QList() << "deck" << -1); + aMoveHandToBottomLibrary->setData(QList() << ZoneNames::DECK << -1); aMoveHandToGrave = new QAction(this); - aMoveHandToGrave->setData(QList() << "grave" << 0); + aMoveHandToGrave->setData(QList() << ZoneNames::GRAVE << 0); aMoveHandToRfg = new QAction(this); - aMoveHandToRfg->setData(QList() << "rfg" << 0); + aMoveHandToRfg->setData(QList() << ZoneNames::EXILE << 0); auto hand = player->getHandZone(); diff --git a/cockatrice/src/game/player/menu/rfg_menu.cpp b/cockatrice/src/game/player/menu/rfg_menu.cpp index 25e162581..8a101c04c 100644 --- a/cockatrice/src/game/player/menu/rfg_menu.cpp +++ b/cockatrice/src/game/player/menu/rfg_menu.cpp @@ -3,6 +3,8 @@ #include "../player.h" #include "../player_actions.h" +#include + RfgMenu::RfgMenu(Player *_player, QWidget *parent) : TearOffMenu(parent), player(_player) { createMoveActions(); @@ -30,13 +32,13 @@ void RfgMenu::createMoveActions() auto rfg = player->getRfgZone(); aMoveRfgToTopLibrary = new QAction(this); - aMoveRfgToTopLibrary->setData(QList() << "deck" << 0); + aMoveRfgToTopLibrary->setData(QList() << ZoneNames::DECK << 0); aMoveRfgToBottomLibrary = new QAction(this); - aMoveRfgToBottomLibrary->setData(QList() << "deck" << -1); + aMoveRfgToBottomLibrary->setData(QList() << ZoneNames::DECK << -1); aMoveRfgToHand = new QAction(this); - aMoveRfgToHand->setData(QList() << "hand" << 0); + aMoveRfgToHand->setData(QList() << ZoneNames::HAND << 0); aMoveRfgToGrave = new QAction(this); - aMoveRfgToGrave->setData(QList() << "grave" << 0); + aMoveRfgToGrave->setData(QList() << ZoneNames::GRAVE << 0); connect(aMoveRfgToTopLibrary, &QAction::triggered, rfg, &PileZoneLogic::moveAllToZone); connect(aMoveRfgToBottomLibrary, &QAction::triggered, rfg, &PileZoneLogic::moveAllToZone); diff --git a/cockatrice/src/game/player/player.cpp b/cockatrice/src/game/player/player.cpp index 0723ae6bc..ac4149f0e 100644 --- a/cockatrice/src/game/player/player.cpp +++ b/cockatrice/src/game/player/player.cpp @@ -61,15 +61,15 @@ void Player::forwardActionSignalsToEventHandler() void Player::initializeZones() { - addZone(new PileZoneLogic(this, "deck", false, true, false, this)); - addZone(new PileZoneLogic(this, "grave", false, false, true, this)); - addZone(new PileZoneLogic(this, "rfg", false, false, true, this)); - addZone(new PileZoneLogic(this, "sb", false, false, false, this)); - addZone(new TableZoneLogic(this, "table", true, false, true, this)); - addZone(new StackZoneLogic(this, "stack", true, false, true, this)); + addZone(new PileZoneLogic(this, ZoneNames::DECK, false, true, false, this)); + addZone(new PileZoneLogic(this, ZoneNames::GRAVE, false, false, true, this)); + addZone(new PileZoneLogic(this, ZoneNames::EXILE, false, false, true, this)); + addZone(new PileZoneLogic(this, ZoneNames::SIDEBOARD, false, false, false, this)); + addZone(new TableZoneLogic(this, ZoneNames::TABLE, true, false, true, this)); + addZone(new StackZoneLogic(this, ZoneNames::STACK, true, false, true, this)); bool visibleHand = playerInfo->getLocalOrJudge() || (game->getPlayerManager()->isSpectator() && game->getGameMetaInfo()->spectatorsOmniscient()); - addZone(new HandZoneLogic(this, "hand", false, false, visibleHand, this)); + addZone(new HandZoneLogic(this, ZoneNames::HAND, false, false, visibleHand, this)); } Player::~Player() @@ -119,13 +119,13 @@ void Player::setZoneId(int _zoneId) void Player::processPlayerInfo(const ServerInfo_Player &info) { static QSet builtinZones{/* PileZones */ - "deck", "grave", "rfg", "sb", + ZoneNames::DECK, ZoneNames::GRAVE, ZoneNames::EXILE, ZoneNames::SIDEBOARD, /* TableZone */ - "table", + ZoneNames::TABLE, /* StackZone */ - "stack", + ZoneNames::STACK, /* HandZone */ - "hand"}; + ZoneNames::HAND}; clearCounters(); clearArrows(); diff --git a/cockatrice/src/game/player/player.h b/cockatrice/src/game/player/player.h index 0a03b3abe..e9c008821 100644 --- a/cockatrice/src/game/player/player.h +++ b/cockatrice/src/game/player/player.h @@ -27,6 +27,7 @@ #include #include #include +#include inline Q_LOGGING_CATEGORY(PlayerLog, "player"); @@ -155,37 +156,37 @@ public: PileZoneLogic *getDeckZone() { - return qobject_cast(zones.value("deck")); + return qobject_cast(zones.value(ZoneNames::DECK)); } PileZoneLogic *getGraveZone() { - return qobject_cast(zones.value("grave")); + return qobject_cast(zones.value(ZoneNames::GRAVE)); } PileZoneLogic *getRfgZone() { - return qobject_cast(zones.value("rfg")); + return qobject_cast(zones.value(ZoneNames::EXILE)); } PileZoneLogic *getSideboardZone() { - return qobject_cast(zones.value("sb")); + return qobject_cast(zones.value(ZoneNames::SIDEBOARD)); } TableZoneLogic *getTableZone() { - return qobject_cast(zones.value("table")); + return qobject_cast(zones.value(ZoneNames::TABLE)); } StackZoneLogic *getStackZone() { - return qobject_cast(zones.value("stack")); + return qobject_cast(zones.value(ZoneNames::STACK)); } HandZoneLogic *getHandZone() { - return qobject_cast(zones.value("hand")); + return qobject_cast(zones.value(ZoneNames::HAND)); } AbstractCounter *addCounter(const ServerInfo_Counter &counter); diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 514ac2e67..20e526727 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -28,6 +28,7 @@ #include #include #include +#include // milliseconds in between triggers of the move top cards until action static constexpr int MOVE_TOP_CARD_UNTIL_INTERVAL = 100; @@ -63,13 +64,13 @@ void PlayerActions::playCard(CardItem *card, bool faceDown) int tableRow = info.getUiAttributes().tableRow; bool playToStack = SettingsCache::instance().getPlayToStack(); QString currentZone = card->getZone()->getName(); - if (currentZone == "stack" && tableRow == 3) { - cmd.set_target_zone("grave"); + if (currentZone == ZoneNames::STACK && tableRow == 3) { + cmd.set_target_zone(ZoneNames::GRAVE); cmd.set_x(0); cmd.set_y(0); - } else if (!faceDown && - ((!playToStack && tableRow == 3) || ((playToStack && tableRow != 0) && currentZone != "stack"))) { - cmd.set_target_zone("stack"); + } else if (!faceDown && ((!playToStack && tableRow == 3) || + ((playToStack && tableRow != 0) && currentZone != ZoneNames::STACK))) { + cmd.set_target_zone(ZoneNames::STACK); cmd.set_x(-1); cmd.set_y(0); } else { @@ -81,7 +82,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown) } cardToMove->set_tapped(!faceDown && info.getUiAttributes().cipt); if (tableRow != 3) - cmd.set_target_zone("table"); + cmd.set_target_zone(ZoneNames::TABLE); cmd.set_x(gridPoint.x()); cmd.set_y(gridPoint.y()); } @@ -124,7 +125,7 @@ void PlayerActions::playCardToTable(const CardItem *card, bool faceDown) cardToMove->set_pt(info.getPowTough().toStdString()); } cardToMove->set_tapped(!faceDown && info.getUiAttributes().cipt); - cmd.set_target_zone("table"); + cmd.set_target_zone(ZoneNames::TABLE); cmd.set_x(gridPoint.x()); cmd.set_y(gridPoint.y()); sendGameCommand(cmd); @@ -132,12 +133,12 @@ void PlayerActions::playCardToTable(const CardItem *card, bool faceDown) void PlayerActions::actViewLibrary() { - player->getGameScene()->toggleZoneView(player, "deck", -1); + player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, -1); } void PlayerActions::actViewHand() { - player->getGameScene()->toggleZoneView(player, "hand", -1); + player->getGameScene()->toggleZoneView(player, ZoneNames::HAND, -1); } /** @@ -181,7 +182,7 @@ void PlayerActions::actViewTopCards() deckSize, 1, &ok); if (ok) { defaultNumberTopCards = number; - player->getGameScene()->toggleZoneView(player, "deck", number); + player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, number); } } @@ -194,14 +195,14 @@ void PlayerActions::actViewBottomCards() deckSize, 1, &ok); if (ok) { defaultNumberBottomCards = number; - player->getGameScene()->toggleZoneView(player, "deck", number, true); + player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, number, true); } } void PlayerActions::actAlwaysRevealTopCard() { Command_ChangeZoneProperties cmd; - cmd.set_zone_name("deck"); + cmd.set_zone_name(ZoneNames::DECK); cmd.set_always_reveal_top_card(player->getPlayerMenu()->getLibraryMenu()->isAlwaysRevealTopCardChecked()); sendGameCommand(cmd); @@ -210,7 +211,7 @@ void PlayerActions::actAlwaysRevealTopCard() void PlayerActions::actAlwaysLookAtTopCard() { Command_ChangeZoneProperties cmd; - cmd.set_zone_name("deck"); + cmd.set_zone_name(ZoneNames::DECK); cmd.set_always_look_at_top_card(player->getPlayerMenu()->getLibraryMenu()->isAlwaysLookAtTopCardChecked()); sendGameCommand(cmd); @@ -223,17 +224,17 @@ void PlayerActions::actOpenDeckInDeckEditor() void PlayerActions::actViewGraveyard() { - player->getGameScene()->toggleZoneView(player, "grave", -1); + player->getGameScene()->toggleZoneView(player, ZoneNames::GRAVE, -1); } void PlayerActions::actViewRfg() { - player->getGameScene()->toggleZoneView(player, "rfg", -1); + player->getGameScene()->toggleZoneView(player, ZoneNames::EXILE, -1); } void PlayerActions::actViewSideboard() { - player->getGameScene()->toggleZoneView(player, "sb", -1); + player->getGameScene()->toggleZoneView(player, ZoneNames::SIDEBOARD, -1); } void PlayerActions::actShuffle() @@ -263,7 +264,7 @@ void PlayerActions::actShuffleTop() defaultNumberTopCards = number; Command_Shuffle cmd; - cmd.set_zone_name("deck"); + cmd.set_zone_name(ZoneNames::DECK); cmd.set_start(0); cmd.set_end(number - 1); // inclusive, the indexed card at end will be shuffled @@ -292,7 +293,7 @@ void PlayerActions::actShuffleBottom() defaultNumberBottomCards = number; Command_Shuffle cmd; - cmd.set_zone_name("deck"); + cmd.set_zone_name(ZoneNames::DECK); cmd.set_start(-number); cmd.set_end(-1); @@ -376,7 +377,7 @@ void PlayerActions::actUndoDraw() void PlayerActions::cmdSetTopCard(Command_MoveCard &cmd) { - cmd.set_start_zone("deck"); + cmd.set_start_zone(ZoneNames::DECK); auto *cardToMove = cmd.mutable_cards_to_move()->add_card(); cardToMove->set_card_id(0); cmd.set_target_player_id(player->getPlayerInfo()->getId()); @@ -386,7 +387,7 @@ void PlayerActions::cmdSetBottomCard(Command_MoveCard &cmd) { CardZoneLogic *zone = player->getDeckZone(); int lastCard = zone->getCards().size() - 1; - cmd.set_start_zone("deck"); + cmd.set_start_zone(ZoneNames::DECK); auto *cardToMove = cmd.mutable_cards_to_move()->add_card(); cardToMove->set_card_id(lastCard); cmd.set_target_player_id(player->getPlayerInfo()->getId()); @@ -400,7 +401,7 @@ void PlayerActions::actMoveTopCardToGrave() Command_MoveCard cmd; cmdSetTopCard(cmd); - cmd.set_target_zone("grave"); + cmd.set_target_zone(ZoneNames::GRAVE); cmd.set_x(0); cmd.set_y(0); @@ -415,7 +416,7 @@ void PlayerActions::actMoveTopCardToExile() Command_MoveCard cmd; cmdSetTopCard(cmd); - cmd.set_target_zone("rfg"); + cmd.set_target_zone(ZoneNames::EXILE); cmd.set_x(0); cmd.set_y(0); @@ -424,22 +425,22 @@ void PlayerActions::actMoveTopCardToExile() void PlayerActions::actMoveTopCardsToGrave() { - moveTopCardsTo("grave", tr("grave"), false); + moveTopCardsTo(ZoneNames::GRAVE, tr("grave"), false); } void PlayerActions::actMoveTopCardsToGraveFaceDown() { - moveTopCardsTo("grave", tr("grave"), true); + moveTopCardsTo(ZoneNames::GRAVE, tr("grave"), true); } void PlayerActions::actMoveTopCardsToExile() { - moveTopCardsTo("rfg", tr("exile"), false); + moveTopCardsTo(ZoneNames::EXILE, tr("exile"), false); } void PlayerActions::actMoveTopCardsToExileFaceDown() { - moveTopCardsTo("rfg", tr("exile"), true); + moveTopCardsTo(ZoneNames::EXILE, tr("exile"), true); } void PlayerActions::moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown) @@ -463,7 +464,7 @@ void PlayerActions::moveTopCardsTo(const QString &targetZone, const QString &zon defaultNumberTopCards = number; Command_MoveCard cmd; - cmd.set_start_zone("deck"); + cmd.set_start_zone(ZoneNames::DECK); cmd.set_target_player_id(player->getPlayerInfo()->getId()); cmd.set_target_zone(targetZone.toStdString()); cmd.set_x(0); @@ -549,7 +550,7 @@ void PlayerActions::actMoveTopCardToBottom() Command_MoveCard cmd; cmdSetTopCard(cmd); - cmd.set_target_zone("deck"); + cmd.set_target_zone(ZoneNames::DECK); cmd.set_x(-1); // bottom of deck cmd.set_y(0); @@ -564,7 +565,7 @@ void PlayerActions::actMoveTopCardToPlay() Command_MoveCard cmd; cmdSetTopCard(cmd); - cmd.set_target_zone("stack"); + cmd.set_target_zone(ZoneNames::STACK); cmd.set_x(-1); cmd.set_y(0); @@ -578,12 +579,12 @@ void PlayerActions::actMoveTopCardToPlayFaceDown() } Command_MoveCard cmd; - cmd.set_start_zone("deck"); + cmd.set_start_zone(ZoneNames::DECK); CardToMove *cardToMove = cmd.mutable_cards_to_move()->add_card(); cardToMove->set_card_id(0); cardToMove->set_face_down(true); cmd.set_target_player_id(player->getPlayerInfo()->getId()); - cmd.set_target_zone("table"); + cmd.set_target_zone(ZoneNames::TABLE); cmd.set_x(-1); cmd.set_y(0); @@ -598,7 +599,7 @@ void PlayerActions::actMoveBottomCardToGrave() Command_MoveCard cmd; cmdSetBottomCard(cmd); - cmd.set_target_zone("grave"); + cmd.set_target_zone(ZoneNames::GRAVE); cmd.set_x(0); cmd.set_y(0); @@ -613,7 +614,7 @@ void PlayerActions::actMoveBottomCardToExile() Command_MoveCard cmd; cmdSetBottomCard(cmd); - cmd.set_target_zone("rfg"); + cmd.set_target_zone(ZoneNames::EXILE); cmd.set_x(0); cmd.set_y(0); @@ -622,22 +623,22 @@ void PlayerActions::actMoveBottomCardToExile() void PlayerActions::actMoveBottomCardsToGrave() { - moveBottomCardsTo("grave", tr("grave"), false); + moveBottomCardsTo(ZoneNames::GRAVE, tr("grave"), false); } void PlayerActions::actMoveBottomCardsToGraveFaceDown() { - moveBottomCardsTo("grave", tr("grave"), true); + moveBottomCardsTo(ZoneNames::GRAVE, tr("grave"), true); } void PlayerActions::actMoveBottomCardsToExile() { - moveBottomCardsTo("rfg", tr("exile"), false); + moveBottomCardsTo(ZoneNames::EXILE, tr("exile"), false); } void PlayerActions::actMoveBottomCardsToExileFaceDown() { - moveBottomCardsTo("rfg", tr("exile"), true); + moveBottomCardsTo(ZoneNames::EXILE, tr("exile"), true); } void PlayerActions::moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown) @@ -661,7 +662,7 @@ void PlayerActions::moveBottomCardsTo(const QString &targetZone, const QString & defaultNumberBottomCards = number; Command_MoveCard cmd; - cmd.set_start_zone("deck"); + cmd.set_start_zone(ZoneNames::DECK); cmd.set_target_player_id(player->getPlayerInfo()->getId()); cmd.set_target_zone(targetZone.toStdString()); cmd.set_x(0); @@ -686,7 +687,7 @@ void PlayerActions::actMoveBottomCardToTop() Command_MoveCard cmd; cmdSetBottomCard(cmd); - cmd.set_target_zone("deck"); + cmd.set_target_zone(ZoneNames::DECK); cmd.set_x(0); // top of deck cmd.set_y(0); @@ -756,7 +757,7 @@ void PlayerActions::actDrawBottomCard() Command_MoveCard cmd; cmdSetBottomCard(cmd); - cmd.set_target_zone("hand"); + cmd.set_target_zone(ZoneNames::HAND); cmd.set_x(0); cmd.set_y(0); @@ -782,9 +783,9 @@ void PlayerActions::actDrawBottomCards() defaultNumberBottomCards = number; Command_MoveCard cmd; - cmd.set_start_zone("deck"); + cmd.set_start_zone(ZoneNames::DECK); cmd.set_target_player_id(player->getPlayerInfo()->getId()); - cmd.set_target_zone("hand"); + cmd.set_target_zone(ZoneNames::HAND); cmd.set_x(0); cmd.set_y(0); @@ -803,7 +804,7 @@ void PlayerActions::actMoveBottomCardToPlay() Command_MoveCard cmd; cmdSetBottomCard(cmd); - cmd.set_target_zone("stack"); + cmd.set_target_zone(ZoneNames::STACK); cmd.set_x(-1); cmd.set_y(0); @@ -820,13 +821,13 @@ void PlayerActions::actMoveBottomCardToPlayFaceDown() int lastCard = zone->getCards().size() - 1; Command_MoveCard cmd; - cmd.set_start_zone("deck"); + cmd.set_start_zone(ZoneNames::DECK); auto *cardToMove = cmd.mutable_cards_to_move()->add_card(); cardToMove->set_card_id(lastCard); cardToMove->set_face_down(true); cmd.set_target_player_id(player->getPlayerInfo()->getId()); - cmd.set_target_zone("table"); + cmd.set_target_zone(ZoneNames::TABLE); cmd.set_x(-1); cmd.set_y(0); @@ -836,7 +837,7 @@ void PlayerActions::actMoveBottomCardToPlayFaceDown() void PlayerActions::actUntapAll() { Command_SetCardAttr cmd; - cmd.set_zone("table"); + cmd.set_zone(ZoneNames::TABLE); cmd.set_attribute(AttrTapped); cmd.set_attr_value("0"); @@ -886,7 +887,7 @@ void PlayerActions::actCreateAnotherToken() } Command_CreateToken cmd; - cmd.set_zone("table"); + cmd.set_zone(ZoneNames::TABLE); cmd.set_card_name(lastTokenInfo.name.toStdString()); cmd.set_card_provider_id(lastTokenInfo.providerId.toStdString()); cmd.set_color(lastTokenInfo.color.toStdString()); @@ -1067,7 +1068,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, const // move card onto table first if attaching from some other zone // we only do this for AttachTo because cross-zone TransformInto is already handled server-side - if (attachType == CardRelationType::AttachTo && sourceCard->getZone()->getName() != "table") { + if (attachType == CardRelationType::AttachTo && sourceCard->getZone()->getName() != ZoneNames::TABLE) { playCardToTable(sourceCard, false); } @@ -1093,7 +1094,7 @@ void PlayerActions::createCard(const CardItem *sourceCard, // create the token for the related card Command_CreateToken cmd; - cmd.set_zone("table"); + cmd.set_zone(ZoneNames::TABLE); cmd.set_card_name(cardInfo->getName().toStdString()); switch (cardInfo->getColors().size()) { case 0: @@ -1122,12 +1123,12 @@ void PlayerActions::createCard(const CardItem *sourceCard, switch (attachType) { case CardRelationType::DoesNotAttach: - cmd.set_target_zone("table"); + cmd.set_target_zone(ZoneNames::TABLE); cmd.set_card_provider_id(relatedCard.getPrinting().getUuid().toStdString()); break; case CardRelationType::AttachTo: - cmd.set_target_zone("table"); // We currently only support creating tokens on the table + cmd.set_target_zone(ZoneNames::TABLE); // We currently only support creating tokens on the table cmd.set_card_provider_id(relatedCard.getPrinting().getUuid().toStdString()); cmd.set_target_card_id(sourceCard->getId()); cmd.set_target_mode(Command_CreateToken::ATTACH_TO); @@ -1135,7 +1136,7 @@ void PlayerActions::createCard(const CardItem *sourceCard, case CardRelationType::TransformInto: // allow cards to directly transform on stack - cmd.set_zone(sourceCard->getZone()->getName() == "stack" ? "stack" : "table"); + cmd.set_zone(sourceCard->getZone()->getName() == ZoneNames::STACK ? ZoneNames::STACK : ZoneNames::TABLE); // Transform card zone changes are handled server-side cmd.set_target_zone(sourceCard->getZone()->getName().toStdString()); cmd.set_target_card_id(sourceCard->getId()); @@ -1250,7 +1251,7 @@ void PlayerActions::actMoveCardXCardsFromTop() cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); cmd->set_target_player_id(player->getPlayerInfo()->getId()); - cmd->set_target_zone("deck"); + cmd->set_target_zone(ZoneNames::DECK); cmd->set_x(number); cmd->set_y(0); commandList.append(cmd); @@ -1639,7 +1640,7 @@ void PlayerActions::playSelectedCards(const bool faceDown) [](const auto &card1, const auto &card2) { return card1->getId() > card2->getId(); }); for (auto &card : selectedCards) { - if (card && !isUnwritableRevealZone(card->getZone()) && card->getZone()->getName() != "table") { + if (card && !isUnwritableRevealZone(card->getZone()) && card->getZone()->getName() != ZoneNames::TABLE) { playCard(card, faceDown); } } @@ -1692,7 +1693,7 @@ void PlayerActions::actRevealHand(int revealToPlayerId) if (revealToPlayerId != -1) { cmd.set_player_id(revealToPlayerId); } - cmd.set_zone_name("hand"); + cmd.set_zone_name(ZoneNames::HAND); sendGameCommand(cmd); } @@ -1703,7 +1704,7 @@ void PlayerActions::actRevealRandomHandCard(int revealToPlayerId) if (revealToPlayerId != -1) { cmd.set_player_id(revealToPlayerId); } - cmd.set_zone_name("hand"); + cmd.set_zone_name(ZoneNames::HAND); cmd.add_card_id(RANDOM_CARD_FROM_ZONE); sendGameCommand(cmd); @@ -1715,7 +1716,7 @@ void PlayerActions::actRevealLibrary(int revealToPlayerId) if (revealToPlayerId != -1) { cmd.set_player_id(revealToPlayerId); } - cmd.set_zone_name("deck"); + cmd.set_zone_name(ZoneNames::DECK); sendGameCommand(cmd); } @@ -1726,7 +1727,7 @@ void PlayerActions::actLendLibrary(int lendToPlayerId) if (lendToPlayerId != -1) { cmd.set_player_id(lendToPlayerId); } - cmd.set_zone_name("deck"); + cmd.set_zone_name(ZoneNames::DECK); cmd.set_grant_write_access(true); sendGameCommand(cmd); @@ -1739,7 +1740,7 @@ void PlayerActions::actRevealTopCards(int revealToPlayerId, int amount) cmd.set_player_id(revealToPlayerId); } - cmd.set_zone_name("deck"); + cmd.set_zone_name(ZoneNames::DECK); cmd.set_top_cards(amount); // backward compatibility: servers before #1051 only permits to reveal the first card cmd.add_card_id(0); @@ -1753,7 +1754,7 @@ void PlayerActions::actRevealRandomGraveyardCard(int revealToPlayerId) if (revealToPlayerId != -1) { cmd.set_player_id(revealToPlayerId); } - cmd.set_zone_name("grave"); + cmd.set_zone_name(ZoneNames::GRAVE); cmd.add_card_id(RANDOM_CARD_FROM_ZONE); sendGameCommand(cmd); } @@ -1816,7 +1817,7 @@ void PlayerActions::cardMenuAction() } case cmClone: { auto *cmd = new Command_CreateToken; - cmd->set_zone("table"); + cmd->set_zone(ZoneNames::TABLE); cmd->set_card_name(card->getName().toStdString()); cmd->set_card_provider_id(card->getProviderId().toStdString()); cmd->set_color(card->getColor().toStdString()); @@ -1858,13 +1859,13 @@ void PlayerActions::cardMenuAction() cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); cmd->set_target_player_id(player->getPlayerInfo()->getId()); - cmd->set_target_zone("deck"); + cmd->set_target_zone(ZoneNames::DECK); cmd->set_x(0); cmd->set_y(0); if (idList.card_size() > 1) { auto *scmd = new Command_Shuffle; - scmd->set_zone_name("deck"); + scmd->set_zone_name(ZoneNames::DECK); scmd->set_start(0); scmd->set_end(idList.card_size() - 1); // inclusive, the indexed card at end will be shuffled // Server process events backwards, so... @@ -1880,13 +1881,13 @@ void PlayerActions::cardMenuAction() cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); cmd->set_target_player_id(player->getPlayerInfo()->getId()); - cmd->set_target_zone("deck"); + cmd->set_target_zone(ZoneNames::DECK); cmd->set_x(-1); cmd->set_y(0); if (idList.card_size() > 1) { auto *scmd = new Command_Shuffle; - scmd->set_zone_name("deck"); + scmd->set_zone_name(ZoneNames::DECK); scmd->set_start(-idList.card_size()); scmd->set_end(-1); // Server process events backwards, so... @@ -1902,7 +1903,7 @@ void PlayerActions::cardMenuAction() cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); cmd->set_target_player_id(player->getPlayerInfo()->getId()); - cmd->set_target_zone("hand"); + cmd->set_target_zone(ZoneNames::HAND); cmd->set_x(0); cmd->set_y(0); commandList.append(cmd); @@ -1914,7 +1915,7 @@ void PlayerActions::cardMenuAction() cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); cmd->set_target_player_id(player->getPlayerInfo()->getId()); - cmd->set_target_zone("grave"); + cmd->set_target_zone(ZoneNames::GRAVE); cmd->set_x(0); cmd->set_y(0); commandList.append(cmd); @@ -1926,7 +1927,7 @@ void PlayerActions::cardMenuAction() cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); cmd->set_target_player_id(player->getPlayerInfo()->getId()); - cmd->set_target_zone("rfg"); + cmd->set_target_zone(ZoneNames::EXILE); cmd->set_x(0); cmd->set_y(0); commandList.append(cmd); diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp index 331605918..f4c3840e0 100644 --- a/cockatrice/src/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -30,6 +30,7 @@ #include #include #include +#include PlayerEventHandler::PlayerEventHandler(Player *_player) : player(_player) { @@ -321,8 +322,8 @@ void PlayerEventHandler::eventMoveCard(const Event_MoveCard &event, const GameEv } player->getPlayerMenu()->updateCardMenu(card); - if (player->getPlayerActions()->isMovingCardsUntil() && startZoneString == "deck" && - targetZone->getName() == "stack") { + if (player->getPlayerActions()->isMovingCardsUntil() && startZoneString == ZoneNames::DECK && + targetZone->getName() == ZoneNames::STACK) { player->getPlayerActions()->moveOneCardUntil(card); } } diff --git a/cockatrice/src/game/zones/logic/card_zone_logic.cpp b/cockatrice/src/game/zones/logic/card_zone_logic.cpp index 1872ba95e..e917e4ad7 100644 --- a/cockatrice/src/game/zones/logic/card_zone_logic.cpp +++ b/cockatrice/src/game/zones/logic/card_zone_logic.cpp @@ -10,6 +10,7 @@ #include #include #include +#include /** * @param _player the player that the zone belongs to @@ -174,9 +175,9 @@ void CardZoneLogic::clearContents() QString CardZoneLogic::getTranslatedName(bool theirOwn, GrammaticalCase gc) const { QString ownerName = player->getPlayerInfo()->getName(); - if (name == "hand") + if (name == ZoneNames::HAND) return (theirOwn ? tr("their hand", "nominative") : tr("%1's hand", "nominative").arg(ownerName)); - else if (name == "deck") + else if (name == ZoneNames::DECK) switch (gc) { case CaseLookAtZone: return (theirOwn ? tr("their library", "look at zone") @@ -192,11 +193,11 @@ QString CardZoneLogic::getTranslatedName(bool theirOwn, GrammaticalCase gc) cons default: return (theirOwn ? tr("their library", "nominative") : tr("%1's library", "nominative").arg(ownerName)); } - else if (name == "grave") + else if (name == ZoneNames::GRAVE) return (theirOwn ? tr("their graveyard", "nominative") : tr("%1's graveyard", "nominative").arg(ownerName)); - else if (name == "rfg") + else if (name == ZoneNames::EXILE) return (theirOwn ? tr("their exile", "nominative") : tr("%1's exile", "nominative").arg(ownerName)); - else if (name == "sb") + else if (name == ZoneNames::SIDEBOARD) switch (gc) { case CaseLookAtZone: return (theirOwn ? tr("their sideboard", "look at zone") diff --git a/cockatrice/src/game/zones/table_zone.cpp b/cockatrice/src/game/zones/table_zone.cpp index a020e4255..b6ac2150b 100644 --- a/cockatrice/src/game/zones/table_zone.cpp +++ b/cockatrice/src/game/zones/table_zone.cpp @@ -15,6 +15,7 @@ #include #include #include +#include const QColor TableZone::BACKGROUND_COLOR = QColor(100, 100, 100); const QColor TableZone::FADE_MASK = QColor(0, 0, 0, 80); @@ -195,7 +196,7 @@ void TableZone::toggleTapped() auto isCardOnTable = [](const QGraphicsItem *item) { if (auto card = qgraphicsitem_cast(item)) { - return card->getZone()->getName() == "table"; + return card->getZone()->getName() == ZoneNames::TABLE; } return false; }; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp index 00561bef2..f04bcc849 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -48,6 +48,7 @@ #include #include #include +#include Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game, int _playerId, @@ -190,8 +191,8 @@ shouldDestroyOnMove(const Server_Card *card, const Server_CardZone *startZone, c } // Allow tokens on the stack - if ((startZone->getName() == "table" || startZone->getName() == "stack") && - (targetZone->getName() == "table" || targetZone->getName() == "stack")) { + if ((startZone->getName() == ZoneNames::TABLE || startZone->getName() == ZoneNames::STACK) && + (targetZone->getName() == ZoneNames::TABLE || targetZone->getName() == ZoneNames::STACK)) { return false; } @@ -264,7 +265,7 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, } // do not allow attached cards to move around on the table - if (card->getParentCard() && targetzone->getName() == "table") { + if (card->getParentCard() && targetzone->getName() == ZoneNames::TABLE) { continue; } @@ -347,7 +348,7 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, newX = targetzone->getFreeGridColumn(newX, yCoord, card->getName(), faceDown); } else { yCoord = 0; - card->resetState(targetzone->getName() == "stack"); + card->resetState(targetzone->getName() == ZoneNames::STACK); } targetzone->insertCard(card, newX, yCoord); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index eae23dacf..2224ddb13 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -51,6 +51,7 @@ #include #include #include +#include Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, int _gameId, @@ -832,7 +833,7 @@ void Server_Game::returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPl QMutexLocker locker(&gameMutex); // Return cards to their rightful owners before conceding the game static const QRegularExpression ownerRegex{"Owner: ?([^\n]+)"}; - const auto &playerTable = player->getZones().value("table"); + const auto &playerTable = player->getZones().value(ZoneNames::TABLE); for (const auto &card : playerTable->getCards()) { if (card == nullptr) { continue; @@ -858,7 +859,7 @@ void Server_Game::returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPl continue; } - const auto &targetZone = otherPlayer->getZones().value("table"); + const auto &targetZone = otherPlayer->getZones().value(ZoneNames::TABLE); if (playerTable == nullptr || targetZone == nullptr) { continue; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp index e62f861a9..1175e4b57 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -47,6 +47,7 @@ #include #include #include +#include Server_Player::Server_Player(Server_Game *_game, int _playerId, @@ -80,15 +81,15 @@ void Server_Player::setupZones() // ------------------------------------------------------------------ // Create zones - auto *deckZone = new Server_CardZone(this, "deck", false, ServerInfo_Zone::HiddenZone); + auto *deckZone = new Server_CardZone(this, ZoneNames::DECK, false, ServerInfo_Zone::HiddenZone); addZone(deckZone); - auto *sbZone = new Server_CardZone(this, "sb", false, ServerInfo_Zone::HiddenZone); + auto *sbZone = new Server_CardZone(this, ZoneNames::SIDEBOARD, false, ServerInfo_Zone::HiddenZone); addZone(sbZone); - addZone(new Server_CardZone(this, "table", true, ServerInfo_Zone::PublicZone)); - addZone(new Server_CardZone(this, "hand", false, ServerInfo_Zone::PrivateZone)); - addZone(new Server_CardZone(this, "stack", false, ServerInfo_Zone::PublicZone)); - addZone(new Server_CardZone(this, "grave", false, ServerInfo_Zone::PublicZone)); - addZone(new Server_CardZone(this, "rfg", false, ServerInfo_Zone::PublicZone)); + addZone(new Server_CardZone(this, ZoneNames::TABLE, true, ServerInfo_Zone::PublicZone)); + addZone(new Server_CardZone(this, ZoneNames::HAND, false, ServerInfo_Zone::PrivateZone)); + addZone(new Server_CardZone(this, ZoneNames::STACK, false, ServerInfo_Zone::PublicZone)); + addZone(new Server_CardZone(this, ZoneNames::GRAVE, false, ServerInfo_Zone::PublicZone)); + addZone(new Server_CardZone(this, ZoneNames::EXILE, false, ServerInfo_Zone::PublicZone)); addCounter(new Server_Counter(0, "life", makeColor(255, 255, 255), 25, game->getStartingLifeTotal())); addCounter(new Server_Counter(1, "w", makeColor(255, 255, 150), 20, 0)); @@ -164,8 +165,8 @@ void Server_Player::addCounter(Server_Counter *counter) Response::ResponseCode Server_Player::drawCards(GameEventStorage &ges, int number) { - Server_CardZone *deckZone = zones.value("deck"); - Server_CardZone *handZone = zones.value("hand"); + Server_CardZone *deckZone = zones.value(ZoneNames::DECK); + Server_CardZone *handZone = zones.value(ZoneNames::HAND); if (deckZone->getCards().size() < number) { number = deckZone->getCards().size(); } @@ -210,7 +211,7 @@ void Server_Player::onCardBeingMoved(GameEventStorage &ges, // "Undo draw" should only remain valid if the just-drawn card stays within the user's hand (e.g., they only // reorder their hand). If a just-drawn card leaves the hand then remove cards before it from the list // (Ignore the case where the card is currently being un-drawn.) - if (startzone->getName() == "hand" && targetzone->getName() != "hand" && !undoingDraw) { + if (startzone->getName() == ZoneNames::HAND && targetzone->getName() != ZoneNames::HAND && !undoingDraw) { int index = lastDrawList.lastIndexOf(card->getId()); if (index != -1) { lastDrawList.erase(lastDrawList.begin(), lastDrawList.begin() + index); @@ -326,11 +327,11 @@ Server_Player::cmdShuffle(const Command_Shuffle &cmd, ResponseContainer & /*rc*/ return Response::RespContextError; } - if (cmd.has_zone_name() && cmd.zone_name() != "deck") { + if (cmd.has_zone_name() && cmd.zone_name() != ZoneNames::DECK) { return Response::RespFunctionNotAllowed; } - Server_CardZone *zone = zones.value("deck"); + Server_CardZone *zone = zones.value(ZoneNames::DECK); if (!zone) { return Response::RespNameNotFound; } @@ -357,8 +358,8 @@ Server_Player::cmdMulligan(const Command_Mulligan &cmd, ResponseContainer & /*rc return Response::RespContextError; } - Server_CardZone *hand = zones.value("hand"); - Server_CardZone *_deck = zones.value("deck"); + Server_CardZone *hand = zones.value(ZoneNames::HAND); + Server_CardZone *_deck = zones.value(ZoneNames::DECK); int number = cmd.number(); if (!hand->getCards().isEmpty()) { @@ -414,8 +415,8 @@ Server_Player::cmdUndoDraw(const Command_UndoDraw & /*cmd*/, ResponseContainer & Response::ResponseCode retVal; auto *cardToMove = new CardToMove; cardToMove->set_card_id(lastDrawList.takeLast()); - retVal = moveCard(ges, zones.value("hand"), QList() << cardToMove, zones.value("deck"), 0, 0, - false, true); + retVal = moveCard(ges, zones.value(ZoneNames::HAND), QList() << cardToMove, + zones.value(ZoneNames::DECK), 0, 0, false, true); delete cardToMove; return retVal; diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 0575c260f..c0c7d8cc9 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -10,8 +10,13 @@ set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/l ) set(UTILITY_HEADERS - libcockatrice/utility/color.h libcockatrice/utility/expression.h libcockatrice/utility/levenshtein.h - libcockatrice/utility/macros.h libcockatrice/utility/passwordhasher.h libcockatrice/utility/trice_limits.h + libcockatrice/utility/color.h + libcockatrice/utility/expression.h + libcockatrice/utility/levenshtein.h + libcockatrice/utility/macros.h + libcockatrice/utility/passwordhasher.h + libcockatrice/utility/trice_limits.h + libcockatrice/utility/zone_names.h ) add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS}) diff --git a/libcockatrice_utility/libcockatrice/utility/zone_names.h b/libcockatrice_utility/libcockatrice/utility/zone_names.h new file mode 100644 index 000000000..d1463de6a --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/zone_names.h @@ -0,0 +1,19 @@ +#ifndef ZONE_NAMES_H +#define ZONE_NAMES_H + +namespace ZoneNames +{ +// Protocol-level zone identifiers shared between client and server. +// These must match exactly across all components. + +constexpr const char *TABLE = "table"; +constexpr const char *GRAVE = "grave"; +constexpr const char *EXILE = "rfg"; // "removed from game" +constexpr const char *HAND = "hand"; +constexpr const char *DECK = "deck"; +constexpr const char *SIDEBOARD = "sb"; +constexpr const char *STACK = "stack"; + +} // namespace ZoneNames + +#endif // ZONE_NAMES_H From 20ad9af989ffd2caace45170fc01af46fd43d818 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:00:32 -0700 Subject: [PATCH 043/116] [CacheSettings] Refactor country list creation (#6687) --- .../src/client/settings/cache_settings.cpp | 266 +----------------- 1 file changed, 15 insertions(+), 251 deletions(-) diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 30093fd52..e7a5d114e 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -1115,257 +1115,21 @@ void SettingsCache::setClientVersion(const QString &_clientVersion) QStringList SettingsCache::getCountries() const { - static QStringList countries = QStringList() << "ad" - << "ae" - << "af" - << "ag" - << "ai" - << "al" - << "am" - << "ao" - << "aq" - << "ar" - << "as" - << "at" - << "au" - << "aw" - << "ax" - << "az" - << "ba" - << "bb" - << "bd" - << "be" - << "bf" - << "bg" - << "bh" - << "bi" - << "bj" - << "bl" - << "bm" - << "bn" - << "bo" - << "bq" - << "br" - << "bs" - << "bt" - << "bv" - << "bw" - << "by" - << "bz" - << "ca" - << "cc" - << "cd" - << "cf" - << "cg" - << "ch" - << "ci" - << "ck" - << "cl" - << "cm" - << "cn" - << "co" - << "cr" - << "cu" - << "cv" - << "cw" - << "cx" - << "cy" - << "cz" - << "de" - << "dj" - << "dk" - << "dm" - << "do" - << "dz" - << "ec" - << "ee" - << "eg" - << "eh" - << "er" - << "es" - << "et" - << "eu" - << "fi" - << "fj" - << "fk" - << "fm" - << "fo" - << "fr" - << "ga" - << "gb" - << "gd" - << "ge" - << "gf" - << "gg" - << "gh" - << "gi" - << "gl" - << "gm" - << "gn" - << "gp" - << "gq" - << "gr" - << "gs" - << "gt" - << "gu" - << "gw" - << "gy" - << "hk" - << "hm" - << "hn" - << "hr" - << "ht" - << "hu" - << "id" - << "ie" - << "il" - << "im" - << "in" - << "io" - << "iq" - << "ir" - << "is" - << "it" - << "je" - << "jm" - << "jo" - << "jp" - << "ke" - << "kg" - << "kh" - << "ki" - << "km" - << "kn" - << "kp" - << "kr" - << "kw" - << "ky" - << "kz" - << "la" - << "lb" - << "lc" - << "li" - << "lk" - << "lr" - << "ls" - << "lt" - << "lu" - << "lv" - << "ly" - << "ma" - << "mc" - << "md" - << "me" - << "mf" - << "mg" - << "mh" - << "mk" - << "ml" - << "mm" - << "mn" - << "mo" - << "mp" - << "mq" - << "mr" - << "ms" - << "mt" - << "mu" - << "mv" - << "mw" - << "mx" - << "my" - << "mz" - << "na" - << "nc" - << "ne" - << "nf" - << "ng" - << "ni" - << "nl" - << "no" - << "np" - << "nr" - << "nu" - << "nz" - << "om" - << "pa" - << "pe" - << "pf" - << "pg" - << "ph" - << "pk" - << "pl" - << "pm" - << "pn" - << "pr" - << "ps" - << "pt" - << "pw" - << "py" - << "qa" - << "re" - << "ro" - << "rs" - << "ru" - << "rw" - << "sa" - << "sb" - << "sc" - << "sd" - << "se" - << "sg" - << "sh" - << "si" - << "sj" - << "sk" - << "sl" - << "sm" - << "sn" - << "so" - << "sr" - << "ss" - << "st" - << "sv" - << "sx" - << "sy" - << "sz" - << "tc" - << "td" - << "tf" - << "tg" - << "th" - << "tj" - << "tk" - << "tl" - << "tm" - << "tn" - << "to" - << "tr" - << "tt" - << "tv" - << "tw" - << "tz" - << "ua" - << "ug" - << "um" - << "us" - << "uy" - << "uz" - << "va" - << "vc" - << "ve" - << "vg" - << "vi" - << "vn" - << "vu" - << "wf" - << "ws" - << "xk" - << "ye" - << "yt" - << "za" - << "zm" - << "zw"; + static const QStringList countries = { + "ad", "ae", "af", "ag", "ai", "al", "am", "ao", "aq", "ar", "as", "at", "au", "aw", "ax", "az", "ba", "bb", + "bd", "be", "bf", "bg", "bh", "bi", "bj", "bl", "bm", "bn", "bo", "bq", "br", "bs", "bt", "bv", "bw", "by", + "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", "cu", "cv", "cw", "cx", + "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "eu", "fi", "fj", + "fk", "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", + "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", + "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", "kz", + "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "me", "mf", "mg", "mh", + "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", + "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", + "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", "rw", "sa", "sb", "sc", "sd", "se", + "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "ss", "st", "sv", "sx", "sy", "sz", "tc", "td", + "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "um", "us", + "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu", "wf", "ws", "xk", "ye", "yt", "za", "zm", "zw"}; return countries; } From aa4592dc9e26c2ea0d08ae68ab7d3b09b77797a8 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Thu, 12 Mar 2026 17:30:01 -0400 Subject: [PATCH 044/116] Add local game options (#6669) * Add local game options dialog. Introduces LocalGameOptions struct and DlgLocalGameOptions dialog to replace the previous QInputDialog for starting local games. Encapsulates game configuration with a simple interface that prevents parameter explosion as options are added. The dialog provides UI with settings persistence via SettingsCache * integrate local game options into main window. Replaces QInputDialog with DlgLocalGameOptions in actSinglePlayer(). The startLocalGame() function now accepts LocalGameOptions, enabling configuration of starting life total and spectator visibility in addition to player count. Also adds user documentation for the local game options flow. * Removed superfluous documentation file * removed spectator option and moved structure definition * Now remember settings separately and & shortcuts removed * re-run checks --- cockatrice/CMakeLists.txt | 1 + .../src/client/settings/cache_settings.cpp | 25 ++++++ .../src/client/settings/cache_settings.h | 21 +++++ .../dialogs/dlg_local_game_options.cpp | 83 +++++++++++++++++++ .../widgets/dialogs/dlg_local_game_options.h | 53 ++++++++++++ cockatrice/src/interface/window_main.cpp | 22 ++--- cockatrice/src/interface/window_main.h | 4 +- .../extra-pages/user_documentation/index.md | 2 +- 8 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp create mode 100644 cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index d675b809d..1ca3c77c2 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -39,6 +39,7 @@ set(cockatrice_SOURCES src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp src/interface/widgets/dialogs/dlg_load_remote_deck.cpp + src/interface/widgets/dialogs/dlg_local_game_options.cpp src/interface/widgets/dialogs/dlg_manage_sets.cpp src/interface/widgets/dialogs/dlg_register.cpp src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index e7a5d114e..1d8121b19 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -385,6 +385,13 @@ SettingsCache::SettingsCache() defaultStartingLifeTotal = settings->value("game/defaultstartinglifetotal", 20).toInt(); shareDecklistsOnLoad = settings->value("game/sharedecklistsonload", false).toBool(); rememberGameSettings = settings->value("game/remembergamesettings", true).toBool(); + + // Local game settings use "localgameoptions/" prefix to keep them separate + // from server game settings which use "game/" prefix + localGameRememberSettings = settings->value("localgameoptions/remembersettings", false).toBool(); + localGameMaxPlayers = settings->value("localgameoptions/maxplayers", 1).toInt(); + localGameStartingLifeTotal = settings->value("localgameoptions/startinglifetotal", 20).toInt(); + clientID = settings->value("personal/clientid", CLIENT_INFO_NOT_SET).toString(); clientVersion = settings->value("personal/clientversion", CLIENT_INFO_NOT_SET).toString(); } @@ -1242,6 +1249,24 @@ void SettingsCache::setRememberGameSettings(const bool _rememberGameSettings) settings->setValue("game/remembergamesettings", rememberGameSettings); } +void SettingsCache::setLocalGameRememberSettings(bool value) +{ + localGameRememberSettings = value; + settings->setValue("localgameoptions/remembersettings", value); +} + +void SettingsCache::setLocalGameMaxPlayers(int value) +{ + localGameMaxPlayers = value; + settings->setValue("localgameoptions/maxplayers", value); +} + +void SettingsCache::setLocalGameStartingLifeTotal(int value) +{ + localGameStartingLifeTotal = value; + settings->setValue("localgameoptions/startinglifetotal", value); +} + void SettingsCache::setNotifyAboutUpdate(QT_STATE_CHANGED_T _notifyaboutupdate) { notifyAboutUpdates = static_cast(_notifyaboutupdate); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index 5c5054105..2bbf85352 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -330,6 +330,12 @@ private: [[nodiscard]] QString getSafeConfigFilePath(QString configEntry, QString defaultPath) const; void loadPaths(); bool rememberGameSettings; + + // Local game settings (separate from server game settings in game/*) + bool localGameRememberSettings; + int localGameMaxPlayers; + int localGameStartingLifeTotal; + QList releaseChannels; bool isPortableBuild; bool roundCardCorners; @@ -862,6 +868,18 @@ public: { return rememberGameSettings; } + [[nodiscard]] bool getLocalGameRememberSettings() const + { + return localGameRememberSettings; + } + [[nodiscard]] int getLocalGameMaxPlayers() const + { + return localGameMaxPlayers; + } + [[nodiscard]] int getLocalGameStartingLifeTotal() const + { + return localGameStartingLifeTotal; + } [[nodiscard]] int getKeepAlive() const override { return keepalive; @@ -1089,6 +1107,9 @@ public slots: void setDefaultStartingLifeTotal(const int _defaultStartingLifeTotal); void setShareDecklistsOnLoad(const bool _shareDecklistsOnLoad); void setRememberGameSettings(const bool _rememberGameSettings); + void setLocalGameRememberSettings(bool value); + void setLocalGameMaxPlayers(int value); + void setLocalGameStartingLifeTotal(int value); void setCheckUpdatesOnStartup(QT_STATE_CHANGED_T value); void setStartupCardUpdateCheckPromptForUpdate(bool value); void setStartupCardUpdateCheckAlwaysUpdate(bool value); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp new file mode 100644 index 000000000..52466ff10 --- /dev/null +++ b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp @@ -0,0 +1,83 @@ +#include "dlg_local_game_options.h" + +#include "../../../client/settings/cache_settings.h" + +#include +#include +#include +#include +#include +#include +#include + +DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent) +{ + numberPlayersLabel = new QLabel(tr("Players:"), this); + numberPlayersEdit = new QSpinBox(this); + numberPlayersEdit->setMinimum(1); + numberPlayersEdit->setMaximum(8); + numberPlayersEdit->setValue(1); + numberPlayersLabel->setBuddy(numberPlayersEdit); + + auto *generalGrid = new QGridLayout; + generalGrid->addWidget(numberPlayersLabel, 0, 0); + generalGrid->addWidget(numberPlayersEdit, 0, 1); + generalGroupBox = new QGroupBox(tr("General"), this); + generalGroupBox->setLayout(generalGrid); + + startingLifeTotalLabel = new QLabel(tr("Starting life total:"), this); + startingLifeTotalEdit = new QSpinBox(this); + startingLifeTotalEdit->setMinimum(1); + startingLifeTotalEdit->setMaximum(99999); + startingLifeTotalEdit->setValue(20); + startingLifeTotalLabel->setBuddy(startingLifeTotalEdit); + + auto *gameSetupGrid = new QGridLayout; + gameSetupGrid->addWidget(startingLifeTotalLabel, 0, 0); + gameSetupGrid->addWidget(startingLifeTotalEdit, 0, 1); + gameSetupOptionsGroupBox = new QGroupBox(tr("Game setup options"), this); + gameSetupOptionsGroupBox->setLayout(gameSetupGrid); + + rememberSettingsCheckBox = new QCheckBox(tr("Remember settings"), this); + + buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgLocalGameOptions::actOK); + connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgLocalGameOptions::reject); + + auto *mainLayout = new QVBoxLayout; + mainLayout->addWidget(generalGroupBox); + mainLayout->addWidget(gameSetupOptionsGroupBox); + mainLayout->addWidget(rememberSettingsCheckBox); + mainLayout->addWidget(buttonBox); + setLayout(mainLayout); + + rememberSettingsCheckBox->setChecked(SettingsCache::instance().getLocalGameRememberSettings()); + if (rememberSettingsCheckBox->isChecked()) { + numberPlayersEdit->setValue(SettingsCache::instance().getLocalGameMaxPlayers()); + startingLifeTotalEdit->setValue(SettingsCache::instance().getLocalGameStartingLifeTotal()); + } + + setWindowTitle(tr("Local game options")); + setFixedHeight(sizeHint().height()); + + numberPlayersEdit->setFocus(); +} + +void DlgLocalGameOptions::actOK() +{ + SettingsCache::instance().setLocalGameRememberSettings(rememberSettingsCheckBox->isChecked()); + if (rememberSettingsCheckBox->isChecked()) { + SettingsCache::instance().setLocalGameMaxPlayers(numberPlayersEdit->value()); + SettingsCache::instance().setLocalGameStartingLifeTotal(startingLifeTotalEdit->value()); + } + + accept(); +} + +LocalGameOptions DlgLocalGameOptions::getOptions() const +{ + return LocalGameOptions{ + .numberPlayers = numberPlayersEdit->value(), + .startingLifeTotal = startingLifeTotalEdit->value(), + }; +} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h new file mode 100644 index 000000000..4307581a4 --- /dev/null +++ b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h @@ -0,0 +1,53 @@ +/** + * @file dlg_local_game_options.h + * @ingroup RoomDialogs + * @brief Dialog for configuring local game options. + * + * Provides a user interface for setting up local games with configurable + * number of players and starting life total. + */ + +#ifndef DLG_LOCAL_GAME_OPTIONS_H +#define DLG_LOCAL_GAME_OPTIONS_H + +#include + +struct LocalGameOptions +{ + int numberPlayers = 1; + int startingLifeTotal = 20; +}; + +class QCheckBox; +class QDialogButtonBox; +class QGroupBox; +class QLabel; +class QSpinBox; + +class DlgLocalGameOptions : public QDialog +{ + Q_OBJECT +public: + explicit DlgLocalGameOptions(QWidget *parent = nullptr); + + [[nodiscard]] LocalGameOptions getOptions() const; + +private slots: + void actOK(); + +private: + QGroupBox *generalGroupBox; + QGroupBox *gameSetupOptionsGroupBox; + + QLabel *numberPlayersLabel; + QSpinBox *numberPlayersEdit; + + QLabel *startingLifeTotalLabel; + QSpinBox *startingLifeTotalEdit; + + QCheckBox *rememberSettingsCheckBox; + + QDialogButtonBox *buttonBox; +}; + +#endif // DLG_LOCAL_GAME_OPTIONS_H diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 3724ff29d..fbba6c3f5 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -27,6 +27,7 @@ #include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h" #include "../interface/widgets/dialogs/dlg_forgot_password_request.h" #include "../interface/widgets/dialogs/dlg_forgot_password_reset.h" +#include "../interface/widgets/dialogs/dlg_local_game_options.h" #include "../interface/widgets/dialogs/dlg_manage_sets.h" #include "../interface/widgets/dialogs/dlg_register.h" #include "../interface/widgets/dialogs/dlg_settings.h" @@ -49,7 +50,6 @@ #include #include #include -#include #include #include #include @@ -225,16 +225,15 @@ void MainWindow::actDisconnect() void MainWindow::actSinglePlayer() { - bool ok; - int numberPlayers = - QInputDialog::getInt(this, tr("Number of players"), tr("Please enter the number of players."), 1, 1, 8, 1, &ok); - if (!ok) + DlgLocalGameOptions dlg(this); + if (dlg.exec() != QDialog::Accepted) { return; + } - startLocalGame(numberPlayers); + startLocalGame(dlg.getOptions()); } -void MainWindow::startLocalGame(int numberPlayers) +void MainWindow::startLocalGame(const LocalGameOptions &options) { aConnect->setEnabled(false); aRegister->setEnabled(false); @@ -248,7 +247,7 @@ void MainWindow::startLocalGame(int numberPlayers) QList localClients; localClients.append(mainClient); - for (int i = 0; i < numberPlayers - 1; ++i) { + for (int i = 0; i < options.numberPlayers - 1; ++i) { LocalServerInterface *slaveLsi = localServer->newConnection(); LocalClient *slaveClient = new LocalClient(slaveLsi, tr("Player %1").arg(i + 2), SettingsCache::instance().getClientID(), this); @@ -257,7 +256,8 @@ void MainWindow::startLocalGame(int numberPlayers) tabSupervisor->startLocal(localClients); Command_CreateGame createCommand; - createCommand.set_max_players(static_cast(numberPlayers)); + createCommand.set_max_players(static_cast(options.numberPlayers)); + createCommand.set_starting_life_total(options.startingLifeTotal); mainClient->sendCommand(LocalClient::prepareRoomCommand(createCommand, 0)); } @@ -913,7 +913,9 @@ MainWindow::MainWindow(QWidget *parent) void MainWindow::startupConfigCheck() { if (SettingsCache::instance().debug().getLocalGameOnStartup()) { - startLocalGame(SettingsCache::instance().debug().getLocalGamePlayerCount()); + LocalGameOptions options; + options.numberPlayers = SettingsCache::instance().debug().getLocalGamePlayerCount(); + startLocalGame(options); } if (SettingsCache::instance().getCheckUpdatesOnStartup()) { diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 75b0f5062..ed6de5b0d 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -25,6 +25,8 @@ #ifndef WINDOW_H #define WINDOW_H +#include "widgets/dialogs/dlg_local_game_options.h" + #include #include #include @@ -137,7 +139,7 @@ private: void createCardUpdateProcess(bool background = false); void exitCardDatabaseUpdate(); - void startLocalGame(int numberPlayers); + void startLocalGame(const LocalGameOptions &options); QList tabMenus; QMenu *cockatriceMenu, *dbMenu, *tabsMenu, *helpMenu, *trayIconMenu; diff --git a/doc/doxygen/extra-pages/user_documentation/index.md b/doc/doxygen/extra-pages/user_documentation/index.md index 3f10a0ac6..d7e9d529d 100644 --- a/doc/doxygen/extra-pages/user_documentation/index.md +++ b/doc/doxygen/extra-pages/user_documentation/index.md @@ -1,5 +1,5 @@ @page user_reference User Reference - + ## Deck Management - @subpage creating_decks From 2b2a6db0810937ffaf935170d1001cbbe3305cac Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 21:22:48 +0100 Subject: [PATCH 045/116] Translate oracle/oracle_en@source.ts in it (#6692) 100% translated source file: 'oracle/oracle_en@source.ts' on 'it'. Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com> --- oracle/translations/oracle_it.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/oracle/translations/oracle_it.ts b/oracle/translations/oracle_it.ts index 1765bbbca..bea5af275 100644 --- a/oracle/translations/oracle_it.ts +++ b/oracle/translations/oracle_it.ts @@ -173,7 +173,7 @@ e pedine che verranno usate da Cockatrice. spoiler - + spoiler @@ -193,7 +193,7 @@ e pedine che verranno usate da Cockatrice. Local file: - + File nel pc: @@ -203,7 +203,7 @@ e pedine che verranno usate da Cockatrice. Choose file... - + Scegli file... @@ -231,7 +231,7 @@ e pedine che verranno usate da Cockatrice. tokens - + pedine @@ -251,7 +251,7 @@ e pedine che verranno usate da Cockatrice. Local file: - + File nel pc: @@ -261,7 +261,7 @@ e pedine che verranno usate da Cockatrice. Choose file... - + Scegli file... @@ -392,12 +392,12 @@ e pedine che verranno usate da Cockatrice. Load %1 file - + Carica %1 file %1 file (%1) - + %1 file (%1) @@ -421,12 +421,12 @@ e pedine che verranno usate da Cockatrice. Please choose a file. - + Seleziona un file. Cannot open file '%1'. - + Impossibile aprire il file '%1'. From 33d5721490f04c2697c8153a560c0300edf6ea06 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sat, 14 Mar 2026 03:41:38 -0700 Subject: [PATCH 046/116] [Game] Fix not using zone-specific card menu for opponent's cards (#6695) --- cockatrice/src/game/player/menu/card_menu.cpp | 57 ++++++++++--------- cockatrice/src/game/player/menu/card_menu.h | 9 +-- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp index ef95f052e..cd77c2968 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game/player/menu/card_menu.cpp @@ -113,32 +113,20 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive addAction(aSelectAll); addAction(aSelectColumn); addRelatedCardView(); - } else if (writeableCard) { - + } else { if (card->getZone()) { if (card->getZone()->getName() == ZoneNames::TABLE) { - createTableMenu(); + createTableMenu(writeableCard); } else if (card->getZone()->getName() == ZoneNames::STACK) { - createStackMenu(); + createStackMenu(writeableCard); } else if (card->getZone()->getName() == ZoneNames::EXILE || card->getZone()->getName() == ZoneNames::GRAVE) { - createGraveyardOrExileMenu(); + createGraveyardOrExileMenu(writeableCard); } else { - createHandOrCustomZoneMenu(); + createHandOrCustomZoneMenu(writeableCard); } } else { - addMenu(new MoveMenu(player)); - } - } else { - if (card->getZone() && card->getZone()->getName() != ZoneNames::HAND) { - addAction(aDrawArrow); - addSeparator(); - addRelatedCardView(); - addRelatedCardActions(); - addSeparator(); - addAction(aClone); - addSeparator(); - addAction(aSelectAll); + createZonelessMenu(writeableCard); } } } @@ -154,11 +142,9 @@ void CardMenu::removePlayer(Player *playerToRemove) } } -void CardMenu::createTableMenu() +void CardMenu::createTableMenu(bool canModifyCard) { // Card is on the battlefield - bool canModifyCard = player->getPlayerInfo()->judge || card->getOwner() == player; - if (!canModifyCard) { addRelatedCardView(); addRelatedCardActions(); @@ -213,10 +199,8 @@ void CardMenu::createTableMenu() addMenu(mCardCounters); } -void CardMenu::createStackMenu() +void CardMenu::createStackMenu(bool canModifyCard) { - bool canModifyCard = player->getPlayerInfo()->judge || card->getOwner() == player; - // Card is on the stack if (canModifyCard) { addAction(aAttach); @@ -238,10 +222,8 @@ void CardMenu::createStackMenu() addRelatedCardActions(); } -void CardMenu::createGraveyardOrExileMenu() +void CardMenu::createGraveyardOrExileMenu(bool canModifyCard) { - bool canModifyCard = player->getPlayerInfo()->judge || card->getOwner() == player; - // Card is in the graveyard or exile if (canModifyCard) { addAction(aPlay); @@ -270,8 +252,20 @@ void CardMenu::createGraveyardOrExileMenu() addRelatedCardActions(); } -void CardMenu::createHandOrCustomZoneMenu() +void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard) { + if (!canModifyCard) { + addAction(aDrawArrow); + addSeparator(); + addRelatedCardView(); + addRelatedCardActions(); + addSeparator(); + addAction(aClone); + addSeparator(); + addAction(aSelectAll); + return; + } + // Card is in hand or a custom zone specified by server addAction(aPlay); addAction(aPlayFacedown); @@ -305,6 +299,13 @@ void CardMenu::createHandOrCustomZoneMenu() } } +void CardMenu::createZonelessMenu(bool canModifyCard) +{ + if (canModifyCard) { + addMenu(new MoveMenu(player)); + } +} + /** * @brief Populates the menu with an action for each active player. * diff --git a/cockatrice/src/game/player/menu/card_menu.h b/cockatrice/src/game/player/menu/card_menu.h index afe24da62..b7f2f8241 100644 --- a/cockatrice/src/game/player/menu/card_menu.h +++ b/cockatrice/src/game/player/menu/card_menu.h @@ -18,10 +18,11 @@ class CardMenu : public QMenu public: explicit CardMenu(Player *player, const CardItem *card, bool shortcutsActive); void removePlayer(Player *playerToRemove); - void createTableMenu(); - void createStackMenu(); - void createGraveyardOrExileMenu(); - void createHandOrCustomZoneMenu(); + void createTableMenu(bool canModifyCard); + void createStackMenu(bool canModifyCard); + void createGraveyardOrExileMenu(bool canModifyCard); + void createHandOrCustomZoneMenu(bool canModifyCard); + void createZonelessMenu(bool canModifyCard); QMenu *mCardCounters; From 8180d2e3b07d45f8c9d65d9c9602bc26d9fe9396 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sat, 14 Mar 2026 15:12:10 +0100 Subject: [PATCH 047/116] CI: Update compiler cache on hit (#6691) * update ccache * Update desktop-build.yml * Update desktop-build.yml --- .github/workflows/desktop-build.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 233d9e488..f99c91abb 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -202,6 +202,13 @@ jobs: --ccache "$CCACHE_SIZE" $NO_CLIENT .ci/name_build.sh + # Delete used cache to emulate a cache update. See https://github.com/actions/cache/issues/342. + - name: Delete old compiler cache (ccache) + if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit && steps.build.outcome == 'success' + env: + GH_TOKEN: ${{ github.token }} + run: gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }} + - name: Save compiler cache (ccache) if: github.ref == 'refs/heads/master' uses: actions/cache/save@v5 @@ -438,6 +445,13 @@ jobs: TARGET_MACOS_VERSION: ${{ matrix.override_target }} run: .ci/compile.sh --server --test --vcpkg + # Delete used cache to emulate a cache update. See https://github.com/actions/cache/issues/342. + - name: Delete old compiler cache (ccache) + if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 && steps.ccache_restore.outputs.cache-hit && steps.build.outcome == 'success' + env: + GH_TOKEN: ${{ github.token }} + run: gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }} + - name: Save compiler cache (ccache) if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 uses: actions/cache/save@v5 From 9bb399606ced58ed15cbb85dfb9737c130854fcb Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Sun, 15 Mar 2026 03:39:44 -0400 Subject: [PATCH 048/116] refactor: extract shared card insertion algorithm from hand/stack zones (#6701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand and stack zones had near-identical addCardImpl() implementations, differing only in whether resetState() preserves annotations. Extract the shared pattern into a template function (CardZoneAlgorithms::addCardToList) to eliminate duplication and enable isolated testing without Qt dependencies. Pile, table, and zone-view logic are intentionally excluded — their post-add behavior (signals, coordinate placement, hidden cards) is materially different. --- .../game/zones/logic/card_zone_algorithms.h | 45 +++++ .../src/game/zones/logic/hand_zone_logic.cpp | 16 +- .../src/game/zones/logic/stack_zone_logic.cpp | 16 +- tests/CMakeLists.txt | 1 + tests/card_zone_algorithms/CMakeLists.txt | 15 ++ .../card_zone_algorithms_test.cpp | 159 ++++++++++++++++++ 6 files changed, 226 insertions(+), 26 deletions(-) create mode 100644 cockatrice/src/game/zones/logic/card_zone_algorithms.h create mode 100644 tests/card_zone_algorithms/CMakeLists.txt create mode 100644 tests/card_zone_algorithms/card_zone_algorithms_test.cpp diff --git a/cockatrice/src/game/zones/logic/card_zone_algorithms.h b/cockatrice/src/game/zones/logic/card_zone_algorithms.h new file mode 100644 index 000000000..118a7d29d --- /dev/null +++ b/cockatrice/src/game/zones/logic/card_zone_algorithms.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_CARD_ZONE_ALGORITHMS_H +#define COCKATRICE_CARD_ZONE_ALGORITHMS_H + +namespace CardZoneAlgorithms +{ + +/** + * Shared insertion logic for zones where cards become visible on add and follow + * the standard pattern: clamp index, insert, clear identity if contents unknown, + * reset state, show card. + * + * Zones with different post-add behavior (signal connections, positional resets, + * hidden cards, or coordinate-based placement) should NOT use this — implement + * addCardImpl directly instead. + * + * Template parameters allow testing with lightweight mocks that avoid Qt graphics + * dependencies. + * + * @tparam CardList Must provide: size() -> int, insert(int, CardType*), + * getContentsKnown() -> bool + * @tparam CardType Must provide: setId(int), setCardRef(CardRefType), + * resetState(bool), setVisible(bool) + * @param keepAnnotations Forwarded to card->resetState(). Stack-like zones preserve + * annotations across zone transitions; hand-like zones clear them. + */ +template +void addCardToList(CardList &cards, CardType *card, int x, bool keepAnnotations) +{ + if (x < 0 || x >= cards.size()) { + x = static_cast(cards.size()); + } + cards.insert(x, card); + + if (!cards.getContentsKnown()) { + card->setId(-1); + card->setCardRef({}); + } + + card->resetState(keepAnnotations); + card->setVisible(true); +} + +} // namespace CardZoneAlgorithms + +#endif // COCKATRICE_CARD_ZONE_ALGORITHMS_H diff --git a/cockatrice/src/game/zones/logic/hand_zone_logic.cpp b/cockatrice/src/game/zones/logic/hand_zone_logic.cpp index 292b9f7e3..efa85d649 100644 --- a/cockatrice/src/game/zones/logic/hand_zone_logic.cpp +++ b/cockatrice/src/game/zones/logic/hand_zone_logic.cpp @@ -1,6 +1,7 @@ #include "hand_zone_logic.h" #include "../../board/card_item.h" +#include "card_zone_algorithms.h" HandZoneLogic::HandZoneLogic(Player *_player, const QString &_name, @@ -14,16 +15,5 @@ HandZoneLogic::HandZoneLogic(Player *_player, void HandZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/) { - // if x is negative set it to add at end - if (x < 0 || x >= cards.size()) { - x = cards.size(); - } - cards.insert(x, card); - - if (!cards.getContentsKnown()) { - card->setId(-1); - card->setCardRef({}); - } - card->resetState(); - card->setVisible(true); -} \ No newline at end of file + CardZoneAlgorithms::addCardToList(cards, card, x, false); +} diff --git a/cockatrice/src/game/zones/logic/stack_zone_logic.cpp b/cockatrice/src/game/zones/logic/stack_zone_logic.cpp index a477a8025..6f3cb6e99 100644 --- a/cockatrice/src/game/zones/logic/stack_zone_logic.cpp +++ b/cockatrice/src/game/zones/logic/stack_zone_logic.cpp @@ -1,6 +1,7 @@ #include "stack_zone_logic.h" #include "../../board/card_item.h" +#include "card_zone_algorithms.h" StackZoneLogic::StackZoneLogic(Player *_player, const QString &_name, @@ -14,16 +15,5 @@ StackZoneLogic::StackZoneLogic(Player *_player, void StackZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/) { - // if x is negative set it to add at end - if (x < 0 || x >= cards.size()) { - x = static_cast(cards.size()); - } - cards.insert(x, card); - - if (!cards.getContentsKnown()) { - card->setId(-1); - card->setCardRef({}); - } - card->resetState(true); - card->setVisible(true); -} \ No newline at end of file + CardZoneAlgorithms::addCardToList(cards, card, x, true); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d80ccce4f..3a9eb55cf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -60,6 +60,7 @@ target_link_libraries( ${TEST_QT_MODULES} ) +add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) add_subdirectory(loading_from_clipboard) add_subdirectory(oracle) diff --git a/tests/card_zone_algorithms/CMakeLists.txt b/tests/card_zone_algorithms/CMakeLists.txt new file mode 100644 index 000000000..889e92eaa --- /dev/null +++ b/tests/card_zone_algorithms/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(card_zone_algorithms_test card_zone_algorithms_test.cpp) + +target_include_directories(card_zone_algorithms_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src/game/zones/logic) + +target_link_libraries( + card_zone_algorithms_test + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} +) + +add_test(NAME card_zone_algorithms_test COMMAND card_zone_algorithms_test) + +if(NOT GTEST_FOUND) + add_dependencies(card_zone_algorithms_test gtest) +endif() diff --git a/tests/card_zone_algorithms/card_zone_algorithms_test.cpp b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp new file mode 100644 index 000000000..cc098cae9 --- /dev/null +++ b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp @@ -0,0 +1,159 @@ +#include "card_zone_algorithms.h" + +#include +#include + +struct MockCardRef +{ +}; + +struct MockCard +{ + int idSet = 0; + bool idWasCalled = false; + MockCardRef cardRefSet{}; + bool cardRefWasCalled = false; + bool resetStateCalled = false; + bool resetStateKeepAnnotations = false; + bool visibleSet = false; + + void setId(int id) + { + idSet = id; + idWasCalled = true; + } + + void setCardRef(MockCardRef ref) + { + cardRefSet = ref; + cardRefWasCalled = true; + } + + void resetState(bool keepAnnotations) + { + resetStateCalled = true; + resetStateKeepAnnotations = keepAnnotations; + } + + void setVisible(bool visible) + { + visibleSet = visible; + } +}; + +class MockCardList +{ + std::vector cards; + bool contentsKnown; + +public: + explicit MockCardList(bool _contentsKnown) : contentsKnown(_contentsKnown) + { + } + + int size() const + { + return static_cast(cards.size()); + } + + void insert(int index, MockCard *card) + { + cards.insert(cards.begin() + index, card); + } + + bool getContentsKnown() const + { + return contentsKnown; + } + + MockCard *at(int index) const + { + return cards.at(index); + } +}; + +class AddCardAlgorithmTest : public ::testing::Test +{ +protected: + MockCardList knownList{true}; + MockCardList unknownList{false}; +}; + +TEST_F(AddCardAlgorithmTest, NegativeIndexClampsToEnd) +{ + MockCard a, b; + CardZoneAlgorithms::addCardToList(knownList, &a, 0, false); + CardZoneAlgorithms::addCardToList(knownList, &b, -1, false); + + EXPECT_EQ(knownList.at(0), &a); + EXPECT_EQ(knownList.at(1), &b); + EXPECT_EQ(knownList.size(), 2); +} + +TEST_F(AddCardAlgorithmTest, IndexBeyondSizeClampsToEnd) +{ + MockCard a, b; + CardZoneAlgorithms::addCardToList(knownList, &a, 0, false); + CardZoneAlgorithms::addCardToList(knownList, &b, 999, false); + + EXPECT_EQ(knownList.at(0), &a); + EXPECT_EQ(knownList.at(1), &b); + EXPECT_EQ(knownList.size(), 2); +} + +TEST_F(AddCardAlgorithmTest, ContentsKnownPreservesIdentity) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(knownList, &card, 0, false); + + EXPECT_FALSE(card.idWasCalled); + EXPECT_FALSE(card.cardRefWasCalled); + EXPECT_TRUE(card.visibleSet); +} + +TEST_F(AddCardAlgorithmTest, ContentsUnknownClearsIdentity) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(unknownList, &card, 0, false); + + EXPECT_TRUE(card.idWasCalled); + EXPECT_EQ(card.idSet, -1); + EXPECT_TRUE(card.cardRefWasCalled); +} + +TEST_F(AddCardAlgorithmTest, MidListInsertionPreservesOrder) +{ + MockCard a, b, c; + CardZoneAlgorithms::addCardToList(knownList, &a, 0, false); + CardZoneAlgorithms::addCardToList(knownList, &b, 1, false); + CardZoneAlgorithms::addCardToList(knownList, &c, 1, false); + + EXPECT_EQ(knownList.size(), 3); + EXPECT_EQ(knownList.at(0), &a); + EXPECT_EQ(knownList.at(1), &c); + EXPECT_EQ(knownList.at(2), &b); +} + +TEST_F(AddCardAlgorithmTest, KeepAnnotationsFalsePassedThrough) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(knownList, &card, 0, false); + + EXPECT_TRUE(card.resetStateCalled); + EXPECT_FALSE(card.resetStateKeepAnnotations); +} + +TEST_F(AddCardAlgorithmTest, KeepAnnotationsTruePassedThrough) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(knownList, &card, 0, true); + + EXPECT_TRUE(card.resetStateCalled); + EXPECT_TRUE(card.resetStateKeepAnnotations); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From ce652de272f816d88fdd475fd00025965bd1b5d5 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Sun, 15 Mar 2026 12:24:50 -0400 Subject: [PATCH 049/116] Fix/cmake test only build (#6709) * fix(cmake): guard filter_string_test behind WITH_ORACLE or WITH_CLIENT. filter_string_test links against libcockatrice_filters, which is only built when WITH_ORACLE or WITH_CLIENT is enabled. Without this guard, test-only builds fail at configure time because the target doesn't exist. The guard condition mirrors the one in the root CMakeLists.txt that controls whether libcockatrice_filters is built. * fix(cmake): centralize TEST_QT_MODULES in FindQtRuntime.cmake Each test CMakeLists.txt was independently defining TEST_QT_MODULES with its own subset of Qt modules. This duplicated knowledge that already lives in FindQtRuntime.cmake (which handles module discovery for all other targets: SERVATRICE, COCKATRICE, ORACLE). Consolidate into a single definition using the union of all test requirements (Concurrent Network Svg Widgets), matching the existing pattern for application-target modules. This ensures test-only builds (-DTEST=ON without application targets) discover all necessary Qt components. * fix(cmake): guard libcockatrice_network behind application targets. libcockatrice_network is only needed by the client, server, and oracle targets. Other application-specific libraries (settings, models, filters) already have similar guards. This was an oversight that caused test-only builds to fail when network dependencies weren't available. --- CMakeLists.txt | 11 +++++-- cmake/FindQtRuntime.cmake | 4 ++- tests/CMakeLists.txt | 2 ++ tests/carddatabase/CMakeLists.txt | 33 ++++++++++----------- tests/loading_from_clipboard/CMakeLists.txt | 4 --- tests/oracle/CMakeLists.txt | 2 -- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fb4f3bd2d..fe808a652 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -254,7 +254,9 @@ endif() set(CPACK_PACKAGE_CONTACT "Zach Halpern ") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_NAME}") set(CPACK_PACKAGE_VENDOR "Cockatrice Development Team") -set(CPACK_PACKAGE_DESCRIPTION "Cockatrice is an open-source, multiplatform application for playing tabletop card games over a network. The program's server design prevents users from manipulating the game for unfair advantage. The client also provides a single-player mode, which allows users to brew while offline.") +set(CPACK_PACKAGE_DESCRIPTION + "Cockatrice is an open-source, multiplatform application for playing tabletop card games over a network. The program's server design prevents users from manipulating the game for unfair advantage. The client also provides a single-player mode, which allows users to brew while offline." +) set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") set(CPACK_PACKAGE_VERSION_MAJOR "${PROJECT_VERSION_MAJOR}") set(CPACK_PACKAGE_VERSION_MINOR "${PROJECT_VERSION_MINOR}") @@ -328,7 +330,12 @@ include(CPack) add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_interfaces ${CMAKE_BINARY_DIR}/libcockatrice_interfaces) add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_protocol ${CMAKE_BINARY_DIR}/libcockatrice_protocol) -add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_network ${CMAKE_BINARY_DIR}/libcockatrice_network) +if(WITH_CLIENT + OR WITH_SERVER + OR WITH_ORACLE +) + add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_network ${CMAKE_BINARY_DIR}/libcockatrice_network) +endif() add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_deck_list ${CMAKE_BINARY_DIR}/libcockatrice_deck_list) add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_rng ${CMAKE_BINARY_DIR}/libcockatrice_rng) add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_card ${CMAKE_BINARY_DIR}/libcockatrice_card) diff --git a/cmake/FindQtRuntime.cmake b/cmake/FindQtRuntime.cmake index c205ebdcf..42060c835 100644 --- a/cmake/FindQtRuntime.cmake +++ b/cmake/FindQtRuntime.cmake @@ -29,7 +29,9 @@ if(WITH_ORACLE) set(_ORACLE_NEEDED Concurrent Network Svg Widgets) endif() if(TEST) - set(_TEST_NEEDED Widgets) + # Union of Qt modules required across all test targets (independent of application targets). + # When adding a new test that needs additional Qt modules, add them here rather than in the test's CMakeLists.txt. + set(_TEST_NEEDED Concurrent Network Svg Widgets) endif() set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_COCKATRICE_NEEDED} ${_ORACLE_NEEDED} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3a9eb55cf..c5346e59f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,5 @@ +# NOTE: Qt modules for tests are defined centrally in cmake/FindQtRuntime.cmake (the _TEST_NEEDED variable). +# If a new test needs additional Qt modules, add them there — not in individual test CMakeLists.txt files. enable_testing() add_test(NAME dummy_test COMMAND dummy_test) diff --git a/tests/carddatabase/CMakeLists.txt b/tests/carddatabase/CMakeLists.txt index 4eeffc363..987e23cd5 100644 --- a/tests/carddatabase/CMakeLists.txt +++ b/tests/carddatabase/CMakeLists.txt @@ -6,13 +6,6 @@ project(CardDatabaseTests VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MI # ------------------------ add_definitions("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"") -# ------------------------ -# Qt modules -# ------------------------ -set(TEST_QT_MODULES ${COCKATRICE_QT_VERSION_NAME}::Concurrent ${COCKATRICE_QT_VERSION_NAME}::Network - ${COCKATRICE_QT_VERSION_NAME}::Widgets ${COCKATRICE_QT_VERSION_NAME}::Svg -) - # ------------------------ # Card Database Test # ------------------------ @@ -30,23 +23,29 @@ add_test(NAME carddatabase_test COMMAND carddatabase_test) # ------------------------ # Filter String Test +# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt) # ------------------------ -add_executable(filter_string_test ${MOCKS_SOURCES} ${VERSION_STRING_CPP} filter_string_test.cpp mocks.cpp) +if(WITH_ORACLE OR WITH_CLIENT) + add_executable(filter_string_test ${MOCKS_SOURCES} ${VERSION_STRING_CPP} filter_string_test.cpp mocks.cpp) -target_link_libraries( - filter_string_test - PRIVATE libcockatrice_filters - PRIVATE Threads::Threads - PRIVATE ${GTEST_BOTH_LIBRARIES} - PRIVATE ${TEST_QT_MODULES} -) + target_link_libraries( + filter_string_test + PRIVATE libcockatrice_filters + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} + ) -add_test(NAME filter_string_test COMMAND filter_string_test) + add_test(NAME filter_string_test COMMAND filter_string_test) + + if(NOT GTEST_FOUND) + add_dependencies(filter_string_test gtest) + endif() +endif() # ------------------------ # Dependencies on gtest # ------------------------ if(NOT GTEST_FOUND) add_dependencies(carddatabase_test gtest) - add_dependencies(filter_string_test gtest) endif() diff --git a/tests/loading_from_clipboard/CMakeLists.txt b/tests/loading_from_clipboard/CMakeLists.txt index 85133a8e8..719d62f45 100644 --- a/tests/loading_from_clipboard/CMakeLists.txt +++ b/tests/loading_from_clipboard/CMakeLists.txt @@ -5,10 +5,6 @@ if(NOT GTEST_FOUND) add_dependencies(loading_from_clipboard_test gtest) endif() -set(TEST_QT_MODULES ${COCKATRICE_QT_VERSION_NAME}::Concurrent ${COCKATRICE_QT_VERSION_NAME}::Network - ${COCKATRICE_QT_VERSION_NAME}::Widgets -) - target_link_libraries( loading_from_clipboard_test libcockatrice_deck_list libcockatrice_card Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} diff --git a/tests/oracle/CMakeLists.txt b/tests/oracle/CMakeLists.txt index b89663d6f..c5c1e9097 100644 --- a/tests/oracle/CMakeLists.txt +++ b/tests/oracle/CMakeLists.txt @@ -4,8 +4,6 @@ if(NOT GTEST_FOUND) add_dependencies(parse_cipt_test gtest) endif() -set(TEST_QT_MODULES ${COCKATRICE_QT_VERSION_NAME}::Widgets) - target_link_libraries(parse_cipt_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) add_test(NAME parse_cipt_test COMMAND parse_cipt_test) From cf01dc770b349abaca1b88f573fd0bfee1686416 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 15 Mar 2026 17:41:17 +0100 Subject: [PATCH 050/116] CI: Adjustments to ccache usage (#6703) * Increase ccache size * Fix Arch ccache usage * more cleanup * harmonize names --- .github/workflows/desktop-build.yml | 30 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index f99c91abb..021bf9e81 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -153,10 +153,10 @@ jobs: continue-on-error: ${{matrix.allow-failure == 'yes'}} env: NAME: ${{matrix.distro}}${{matrix.version}} - CACHE: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}} # directory for caching docker image and ccache + CACHE_DIR: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}} # directory for caching docker image and ccache # Cache size over the entire repo is 10Gi: # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy - CCACHE_SIZE: 500M + CCACHE_SIZE: 550M CMAKE_GENERATOR: 'Ninja' steps: @@ -169,7 +169,7 @@ jobs: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: - path: ${{env.CACHE}} + path: ${{env.CACHE_DIR}} key: ccache-${{matrix.distro}}${{matrix.version}}-${{env.BRANCH_NAME}} restore-keys: ccache-${{matrix.distro}}${{matrix.version}}- @@ -202,18 +202,18 @@ jobs: --ccache "$CCACHE_SIZE" $NO_CLIENT .ci/name_build.sh - # Delete used cache to emulate a cache update. See https://github.com/actions/cache/issues/342. - - name: Delete old compiler cache (ccache) - if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit && steps.build.outcome == 'success' + # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342. + - name: Delete remote compiler cache (ccache) + if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit env: GH_TOKEN: ${{ github.token }} run: gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }} - - name: Save compiler cache (ccache) + - name: Save updated compiler cache (ccache) if: github.ref == 'refs/heads/master' uses: actions/cache/save@v5 with: - path: ${{env.CACHE}} + path: ${{env.CACHE_DIR}} key: ${{ steps.ccache_restore.outputs.cache-primary-key }} - name: Upload artifact @@ -330,7 +330,7 @@ jobs: CCACHE_DIR: ${{github.workspace}}/.cache/ # Cache size over the entire repo is 10Gi: # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy - CCACHE_SIZE: 500M + CCACHE_SIZE: 550M steps: - name: Checkout @@ -445,21 +445,19 @@ jobs: TARGET_MACOS_VERSION: ${{ matrix.override_target }} run: .ci/compile.sh --server --test --vcpkg - # Delete used cache to emulate a cache update. See https://github.com/actions/cache/issues/342. - - name: Delete old compiler cache (ccache) - if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 && steps.ccache_restore.outputs.cache-hit && steps.build.outcome == 'success' + # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342. + - name: Delete remote compiler cache (ccache) + if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 && steps.ccache_restore.outputs.cache-hit env: GH_TOKEN: ${{ github.token }} run: gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }} - - name: Save compiler cache (ccache) + - name: Save updated compiler cache (ccache) if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 uses: actions/cache/save@v5 - env: - BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: path: ${{env.CCACHE_DIR}} - key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}} + key: ${{ steps.ccache_restore.outputs.cache-primary-key }} - name: Sign app bundle if: matrix.os == 'macOS' && matrix.make_package && needs.configure.outputs.tag != null From 96f436b65e1f1bd63a7012af6ce692e42a04e340 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 15 Mar 2026 17:47:48 +0100 Subject: [PATCH 051/116] CI: Resolve to latest Qt version in range for Windows as well (#6700) * CI: Resolve to latest Qt version in range for Windows as well * install aqt * Check dedicated win version --- .ci/resolve_latest_aqt_qt_version.sh | 11 ++++++++++- .github/workflows/desktop-build.yml | 9 +++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.ci/resolve_latest_aqt_qt_version.sh b/.ci/resolve_latest_aqt_qt_version.sh index 154c15321..066140ac9 100755 --- a/.ci/resolve_latest_aqt_qt_version.sh +++ b/.ci/resolve_latest_aqt_qt_version.sh @@ -27,7 +27,16 @@ if ! hash aqt; then fi # Resolve latest patch -if ! qt_resolved=$(aqt list-qt mac desktop --spec "$qt_spec" --latest-version); then +if [[ $RUNNER_OS == macOS ]]; then + if ! qt_resolved=$(aqt list-qt mac desktop --spec "$qt_spec" --latest-version); then + exit 1 + fi +elif [[ $RUNNER_OS == Windows ]]; then + if ! qt_resolved=$(aqt list-qt windows desktop --spec "$qt_spec" --latest-version); then + exit 1 + fi +else + echo "aqt command for $RUNNER_OS not defined." exit 1 fi diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 021bf9e81..e176ae527 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -361,15 +361,12 @@ jobs: restore-keys: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}- - name: Install aqtinstall - if: matrix.os == 'macOS' run: pipx install aqtinstall - # Checking if there's a newer, uncached version of Qt available to install via aqtinstall + # Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases - name: Resolve latest Qt patch version - if: matrix.os == 'macOS' id: resolve_qt_version shell: bash - # Ouputs the version of Qt to install via aqtinstall run: .ci/resolve_latest_aqt_qt_version.sh "${{matrix.qt_version}}" - name: Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries (${{ matrix.soc }} macOS) @@ -386,10 +383,10 @@ jobs: if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' uses: jurplel/install-qt-action@v4 with: - cache: false version: ${{ steps.resolve_qt_version.outputs.version }} arch: ${{matrix.qt_arch}} modules: ${{matrix.qt_modules}} + cache: false dir: ${{github.workspace}} - name: Thin Qt libraries (${{ matrix.soc }} macOS) @@ -407,7 +404,7 @@ jobs: if: matrix.os == 'Windows' uses: jurplel/install-qt-action@v4 with: - version: ${{matrix.qt_version}} + version: ${{ steps.resolve_qt_version.outputs.version }} arch: ${{matrix.qt_arch}} modules: ${{matrix.qt_modules}} cache: true From 4a3d79d00b16041f1ac91da81641716d5d165178 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:10:22 +0100 Subject: [PATCH 052/116] Bump docker/build-push-action from 6 to 7 (#6714) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6...v7) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 65ccd1322..e1f2795b1 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -55,7 +55,7 @@ jobs: password: ${{ github.token }} - name: Build and push Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64 From b4a5c863d75f5e4b3f6ca72b8fbcefcb0afa124f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:11:32 +0100 Subject: [PATCH 053/116] Bump docker/metadata-action from 5 to 6 (#6713) Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/v5...v6) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index e1f2795b1..2a4aab1ea 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -27,7 +27,7 @@ jobs: - name: Docker metadata id: metadata - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | ghcr.io/cockatrice/servatrice From dd8164611b7899e545f43dfd8aa17212afb1b4b9 Mon Sep 17 00:00:00 2001 From: tooomm Date: Mon, 16 Mar 2026 23:38:04 +0100 Subject: [PATCH 054/116] revert env name (#6711) --- .github/workflows/desktop-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index e176ae527..02c3f7aec 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -153,7 +153,7 @@ jobs: continue-on-error: ${{matrix.allow-failure == 'yes'}} env: NAME: ${{matrix.distro}}${{matrix.version}} - CACHE_DIR: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}} # directory for caching docker image and ccache + CACHE: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}} # directory for caching docker image and ccache # Cache size over the entire repo is 10Gi: # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy CCACHE_SIZE: 550M @@ -169,7 +169,7 @@ jobs: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: - path: ${{env.CACHE_DIR}} + path: ${{env.CACHE}} key: ccache-${{matrix.distro}}${{matrix.version}}-${{env.BRANCH_NAME}} restore-keys: ccache-${{matrix.distro}}${{matrix.version}}- @@ -213,7 +213,7 @@ jobs: if: github.ref == 'refs/heads/master' uses: actions/cache/save@v5 with: - path: ${{env.CACHE_DIR}} + path: ${{env.CACHE}} key: ${{ steps.ccache_restore.outputs.cache-primary-key }} - name: Upload artifact From 69c046cca426339caec5023ea2de18df85d0d697 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Mon, 16 Mar 2026 23:38:47 +0100 Subject: [PATCH 055/116] remove all separate storing of widget sizes, rely on geometry (#6712) --- .../widgets/tabs/abstract_tab_deck_editor.h | 1 - .../widgets/tabs/tab_deck_editor.cpp | 26 -------- .../interface/widgets/tabs/tab_deck_editor.h | 3 - .../src/interface/widgets/tabs/tab_game.cpp | 60 +++---------------- .../src/interface/widgets/tabs/tab_game.h | 1 - .../tab_deck_editor_visual.cpp | 24 -------- .../tab_deck_editor_visual.h | 7 +-- .../settings/layouts_settings.cpp | 44 -------------- .../libcockatrice/settings/layouts_settings.h | 8 --- 9 files changed, 8 insertions(+), 166 deletions(-) diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h index 3730a7cdd..477c3f973 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -237,7 +237,6 @@ protected slots: // UI Layout Management virtual void loadLayout() = 0; virtual void restartLayout() = 0; - virtual void freeDocksSize() = 0; virtual void refreshShortcuts() = 0; /** @brief Handles dock close events. */ diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp index cf258d454..59ee6bdeb 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp @@ -143,17 +143,6 @@ void TabDeckEditor::loadLayout() restoreState(layoutState); restoreGeometry(layouts.getDeckEditorGeometry()); } - - for (auto it = dockToActions.constKeyValueBegin(); it != dockToActions.constKeyValueEnd(); ++it) { - auto dockWidget = it->first; - auto actions = it->second; - - QSize size = layouts.getDeckEditorWidgetSize(dockWidget->objectName(), actions.defaultSize); - dockWidget->setMinimumSize(size); - dockWidget->setMaximumSize(size); - } - - QTimer::singleShot(100, this, &TabDeckEditor::freeDocksSize); } /** @@ -177,17 +166,6 @@ void TabDeckEditor::restartLayout() splitDockWidget(printingSelectorDockWidget, deckDockWidget, Qt::Horizontal); splitDockWidget(cardInfoDockWidget, printingSelectorDockWidget, Qt::Horizontal); splitDockWidget(cardInfoDockWidget, filterDockWidget, Qt::Vertical); - - QTimer::singleShot(100, this, &TabDeckEditor::freeDocksSize); -} - -/** @brief Frees dock sizes to allow flexible resizing. */ -void TabDeckEditor::freeDocksSize() -{ - for (auto dockWidget : dockToActions.keys()) { - dockWidget->setMinimumSize(100, 100); - dockWidget->setMaximumSize(5000, 5000); - } } /** @@ -202,10 +180,6 @@ bool TabDeckEditor::eventFilter(QObject *o, QEvent *e) LayoutsSettings &layouts = SettingsCache::instance().layouts(); layouts.setDeckEditorLayoutState(saveState()); layouts.setDeckEditorGeometry(saveGeometry()); - - for (auto dockWidget : dockToActions.keys()) { - layouts.setDeckEditorWidgetSize(dockWidget->objectName(), dockWidget->size()); - } } return false; } diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h index 285d241d2..ab7a0bfc5 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h @@ -62,9 +62,6 @@ protected slots: /** @brief Resets the layout to default positions and dock states. */ void restartLayout() override; - /** @brief Frees the dock sizes for resizing flexibility. */ - void freeDocksSize() override; - /** @brief Refreshes shortcuts for this tab from settings. */ void refreshShortcuts() override; diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index fae9dffe5..cf8269069 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -1085,38 +1085,9 @@ void TabGame::loadLayout() if (replayDock) { restoreGeometry(layouts.getReplayPlayAreaGeometry()); restoreState(layouts.getReplayPlayAreaLayoutState()); - - for (auto it = dockToActions.constKeyValueBegin(); it != dockToActions.constKeyValueEnd(); ++it) { - auto dockWidget = it->first; - auto actions = it->second; - - QSize size = layouts.getReplayPlayAreaWidgetSize(dockWidget->objectName(), actions.defaultSize); - dockWidget->setMinimumSize(size); - dockWidget->setMaximumSize(size); - } - } else { restoreGeometry(layouts.getGamePlayAreaGeometry()); restoreState(layouts.getGamePlayAreaLayoutState()); - - for (auto it = dockToActions.constKeyValueBegin(); it != dockToActions.constKeyValueEnd(); ++it) { - auto dockWidget = it->first; - auto actions = it->second; - - QSize size = layouts.getGamePlayAreaWidgetSize(dockWidget->objectName(), actions.defaultSize); - dockWidget->setMinimumSize(size); - dockWidget->setMaximumSize(size); - } - } - - QTimer::singleShot(100, this, &TabGame::freeDocksSize); -} - -void TabGame::freeDocksSize() -{ - for (auto dockWidget : dockToActions.keys()) { - dockWidget->setMinimumSize(100, 100); - dockWidget->setMaximumSize(5000, 5000); } } @@ -1139,24 +1110,15 @@ void TabGame::actResetLayout() replayDock->setFloating(false); addDockWidget(Qt::BottomDockWidgetArea, replayDock); - cardInfoDock->setMinimumSize(250, 360); - cardInfoDock->setMaximumSize(250, 360); - messageLayoutDock->setMinimumSize(250, 200); - messageLayoutDock->setMaximumSize(250, 200); - playerListDock->setMinimumSize(250, 50); - playerListDock->setMaximumSize(250, 50); - replayDock->setMinimumSize(900, 100); - replayDock->setMaximumSize(900, 100); + cardInfoDock->resize(250, 360); + messageLayoutDock->resize(250, 200); + playerListDock->resize(250, 50); + replayDock->resize(900, 100); } else { - cardInfoDock->setMinimumSize(250, 360); - cardInfoDock->setMaximumSize(250, 360); - messageLayoutDock->setMinimumSize(250, 250); - messageLayoutDock->setMaximumSize(250, 250); - playerListDock->setMinimumSize(250, 50); - playerListDock->setMaximumSize(250, 50); + cardInfoDock->resize(250, 360); + messageLayoutDock->resize(250, 250); + playerListDock->resize(250, 50); } - - QTimer::singleShot(100, this, &TabGame::freeDocksSize); } void TabGame::createPlayAreaWidget(bool bReplay) @@ -1334,17 +1296,9 @@ void TabGame::hideEvent(QHideEvent *event) if (replayDock) { layouts.setReplayPlayAreaState(saveState()); layouts.setReplayPlayAreaGeometry(saveGeometry()); - - for (auto dockWidget : dockToActions.keys()) { - layouts.setReplayPlayAreaWidgetSize(dockWidget->objectName(), dockWidget->size()); - } } else { layouts.setGamePlayAreaState(saveState()); layouts.setGamePlayAreaGeometry(saveGeometry()); - - for (auto dockWidget : dockToActions.keys()) { - layouts.setGamePlayAreaWidgetSize(dockWidget->objectName(), dockWidget->size()); - } } Tab::hideEvent(event); diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index c35557743..d8746ccc9 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -165,7 +165,6 @@ private slots: void notifyPlayerKicked(); void processPlayerLeave(Player *leavingPlayer); void actResetLayout(); - void freeDocksSize(); void hideEvent(QHideEvent *event) override; diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index 29f871823..d03ac483b 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -246,15 +246,6 @@ void TabDeckEditorVisual::showPrintingSelector() printingSelectorDockWidget->setVisible(true); } -/** @brief Set size restrictions for free floating dock widgets. */ -void TabDeckEditorVisual::freeDocksSize() -{ - for (auto dockWidget : dockToActions.keys()) { - dockWidget->setMinimumSize(100, 100); - dockWidget->setMaximumSize(5000, 5000); - } -} - /** @brief Refreshes keyboard shortcuts for this tab from settings. */ void TabDeckEditorVisual::refreshShortcuts() { @@ -273,17 +264,6 @@ void TabDeckEditorVisual::loadLayout() restoreState(layoutState); restoreGeometry(layouts.getVisualDeckEditorGeometry()); } - - for (auto it = dockToActions.constKeyValueBegin(); it != dockToActions.constKeyValueEnd(); ++it) { - auto dockWidget = it->first; - auto actions = it->second; - - QSize size = layouts.getVisualDeckEditorWidgetSize(dockWidget->objectName(), actions.defaultSize); - dockWidget->setMinimumSize(size); - dockWidget->setMaximumSize(size); - } - - QTimer::singleShot(100, this, &TabDeckEditorVisual::freeDocksSize); } /** @brief Resets the layout to default positions and dock states. */ @@ -346,10 +326,6 @@ bool TabDeckEditorVisual::eventFilter(QObject *o, QEvent *e) LayoutsSettings &layouts = SettingsCache::instance().layouts(); layouts.setVisualDeckEditorLayoutState(saveState()); layouts.setVisualDeckEditorGeometry(saveGeometry()); - - for (auto dockWidget : dockToActions.keys()) { - layouts.setVisualDeckEditorWidgetSize(dockWidget->objectName(), dockWidget->size()); - } } return false; } diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h index 371699c4d..8a0677c9d 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h @@ -66,11 +66,6 @@ protected slots: */ void restartLayout() override; - /** - * @brief Set size restrictions for free floating dock widgets. - */ - void freeDocksSize() override; - /** * @brief Refresh keyboard shortcuts for this tab. */ @@ -178,4 +173,4 @@ public slots: bool actSaveDeckAs() override; }; -#endif \ No newline at end of file +#endif diff --git a/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp b/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp index 0ec03ba07..f7704cbc7 100644 --- a/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp @@ -48,17 +48,6 @@ void LayoutsSettings::setDeckEditorGeometry(const QByteArray &value) setValue(value, GEOMETRY_PROP, GROUP_DECK_EDITOR); } -void LayoutsSettings::setDeckEditorWidgetSize(const QString &widgetName, const QSize &value) -{ - setValue(value, widgetName, GROUP_DECK_EDITOR, SIZE_PROP); -} - -QSize LayoutsSettings::getDeckEditorWidgetSize(const QString &widgetName, const QSize &defaultValue) -{ - QVariant previous = getValue(widgetName, GROUP_DECK_EDITOR, SIZE_PROP); - return previous == QVariant() ? defaultValue : previous.toSize(); -} - QByteArray LayoutsSettings::getVisualDeckEditorLayoutState() { return getValue(STATE_PROP, GROUP_VISUAL_DECK_EDITOR).toByteArray(); @@ -79,17 +68,6 @@ void LayoutsSettings::setVisualDeckEditorGeometry(const QByteArray &value) setValue(value, GEOMETRY_PROP, GROUP_VISUAL_DECK_EDITOR); } -void LayoutsSettings::setVisualDeckEditorWidgetSize(const QString &widgetName, const QSize &value) -{ - setValue(value, widgetName, GROUP_VISUAL_DECK_EDITOR, SIZE_PROP); -} - -QSize LayoutsSettings::getVisualDeckEditorWidgetSize(const QString &widgetName, const QSize &defaultValue) -{ - QVariant previous = getValue(widgetName, GROUP_VISUAL_DECK_EDITOR, SIZE_PROP); - return previous == QVariant() ? defaultValue : previous.toSize(); -} - QByteArray LayoutsSettings::getDeckEditorDbHeaderState() { return getValue(STATE_PROP, GROUP_DECK_EDITOR_DB, "header").toByteArray(); @@ -150,17 +128,6 @@ QByteArray LayoutsSettings::getGamePlayAreaGeometry() return getValue(GEOMETRY_PROP, GROUP_GAME_PLAY_AREA).toByteArray(); } -void LayoutsSettings::setGamePlayAreaWidgetSize(const QString &widgetName, const QSize &value) -{ - setValue(value, widgetName, GROUP_GAME_PLAY_AREA, SIZE_PROP); -} - -QSize LayoutsSettings::getGamePlayAreaWidgetSize(const QString &widgetName, const QSize &defaultValue) -{ - QVariant previous = getValue(widgetName, GROUP_GAME_PLAY_AREA, SIZE_PROP); - return previous == QVariant() ? defaultValue : previous.toSize(); -} - void LayoutsSettings::setReplayPlayAreaGeometry(const QByteArray &value) { setValue(value, GEOMETRY_PROP, GROUP_REPLAY_PLAY_AREA); @@ -180,14 +147,3 @@ QByteArray LayoutsSettings::getReplayPlayAreaGeometry() { return getValue(GEOMETRY_PROP, GROUP_REPLAY_PLAY_AREA).toByteArray(); } - -void LayoutsSettings::setReplayPlayAreaWidgetSize(const QString &widgetName, const QSize &value) -{ - setValue(value, widgetName, GROUP_REPLAY_PLAY_AREA, SIZE_PROP); -} - -QSize LayoutsSettings::getReplayPlayAreaWidgetSize(const QString &widgetName, const QSize &defaultValue) -{ - QVariant previous = getValue(widgetName, GROUP_REPLAY_PLAY_AREA, SIZE_PROP); - return previous == QVariant() ? defaultValue : previous.toSize(); -} \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/layouts_settings.h b/libcockatrice_settings/libcockatrice/settings/layouts_settings.h index 620753815..cab9a456e 100644 --- a/libcockatrice_settings/libcockatrice/settings/layouts_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/layouts_settings.h @@ -21,11 +21,9 @@ public: void setDeckEditorLayoutState(const QByteArray &value); void setDeckEditorGeometry(const QByteArray &value); - void setDeckEditorWidgetSize(const QString &widgetName, const QSize &value); void setVisualDeckEditorLayoutState(const QByteArray &value); void setVisualDeckEditorGeometry(const QByteArray &value); - void setVisualDeckEditorWidgetSize(const QString &widgetName, const QSize &value); void setDeckEditorDbHeaderState(const QByteArray &value); void setSetsDialogHeaderState(const QByteArray &value); @@ -34,21 +32,17 @@ public: void setGamePlayAreaGeometry(const QByteArray &value); void setGamePlayAreaState(const QByteArray &value); - void setGamePlayAreaWidgetSize(const QString &widgetName, const QSize &value); void setReplayPlayAreaGeometry(const QByteArray &value); void setReplayPlayAreaState(const QByteArray &value); - void setReplayPlayAreaWidgetSize(const QString &widgetName, const QSize &value); QByteArray getMainWindowGeometry(); QByteArray getDeckEditorLayoutState(); QByteArray getDeckEditorGeometry(); - QSize getDeckEditorWidgetSize(const QString &widgetName, const QSize &defaultValue = {}); QByteArray getVisualDeckEditorLayoutState(); QByteArray getVisualDeckEditorGeometry(); - QSize getVisualDeckEditorWidgetSize(const QString &widgetName, const QSize &defaultValue = {}); QByteArray getDeckEditorDbHeaderState(); QByteArray getSetsDialogHeaderState(); @@ -57,11 +51,9 @@ public: QByteArray getGamePlayAreaLayoutState(); QByteArray getGamePlayAreaGeometry(); - QSize getGamePlayAreaWidgetSize(const QString &widgetName, const QSize &defaultValue = {}); QByteArray getReplayPlayAreaLayoutState(); QByteArray getReplayPlayAreaGeometry(); - QSize getReplayPlayAreaWidgetSize(const QString &widgetName, const QSize &defaultValue = {}); signals: public slots: From fc453c68a70d374632977882f35d188775f64359 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Mon, 16 Mar 2026 18:44:29 -0400 Subject: [PATCH 056/116] Add card selection counter (#6685) * feat(game): add drag selection counter overlay Display count of selected cards inside the lasso during drag selection. Count appears near cursor, repositioning to stay within selection bounds. Includes SelectionRubberBand subclass to allow label to appear above band. QRubberBand calls raise() in showEvent/changeEvent to stay on top - this subclass suppresses that behavior so dragCountLabel can be visible. Adds user setting to enable/disable the drag count overlay. * feat(game): add persistent selection counter overlay. Display total count of selected cards in bottom-right corner when multiple cards are selected. Updates on selection changes and window resize. The counter connects to QGraphicsScene::selectionChanged to stay up-to-date without requiring manual refresh. Adds user setting to enable/disable the total count overlay. --------- Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com> --- .../src/client/settings/cache_settings.cpp | 15 +++ .../src/client/settings/cache_settings.h | 12 ++ cockatrice/src/game/game_scene.cpp | 4 +- cockatrice/src/game/game_scene.h | 4 +- cockatrice/src/game/game_view.cpp | 121 +++++++++++++++++- cockatrice/src/game/game_view.h | 6 +- cockatrice/src/game/zones/select_zone.cpp | 3 +- .../widgets/dialogs/dlg_settings.cpp | 14 +- .../interface/widgets/dialogs/dlg_settings.h | 2 + 9 files changed, 170 insertions(+), 11 deletions(-) diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 1d8121b19..a66897b4a 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -284,6 +284,9 @@ SettingsCache::SettingsCache() closeEmptyCardView = settings->value("interface/closeEmptyCardView", true).toBool(); focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool(); + showDragSelectionCount = settings->value("interface/showlassoselectioncount", true).toBool(); + showTotalSelectionCount = settings->value("interface/showpersistentselectioncount", true).toBool(); + showShortcuts = settings->value("menu/showshortcuts", true).toBool(); showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool(); displayCardNames = settings->value("cards/displaycardnames", true).toBool(); @@ -1308,6 +1311,18 @@ void SettingsCache::setRoundCardCorners(bool _roundCardCorners) emit roundCardCornersChanged(roundCardCorners); } +void SettingsCache::setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount) +{ + showDragSelectionCount = static_cast(_showDragSelectionCount); + settings->setValue("interface/showlassoselectioncount", showDragSelectionCount); +} + +void SettingsCache::setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSelectionCount) +{ + showTotalSelectionCount = static_cast(_showTotalSelectionCount); + settings->setValue("interface/showpersistentselectioncount", showTotalSelectionCount); +} + void SettingsCache::loadPaths() { QString dataPath = getDataPath(); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index 2bbf85352..ece61487f 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -340,6 +340,8 @@ private: bool isPortableBuild; bool roundCardCorners; bool showStatusBar; + bool showDragSelectionCount; + bool showTotalSelectionCount; public: SettingsCache(); @@ -455,6 +457,14 @@ public: { return showStatusBar; } + [[nodiscard]] bool getShowDragSelectionCount() const + { + return showDragSelectionCount; + } + [[nodiscard]] bool getShowTotalSelectionCount() const + { + return showTotalSelectionCount; + } [[nodiscard]] bool getNotificationsEnabled() const { return notificationsEnabled; @@ -1120,5 +1130,7 @@ public slots: void setUpdateReleaseChannelIndex(int value); void setMaxFontSize(int _max); void setRoundCardCorners(bool _roundCardCorners); + void setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount); + void setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSelectionCount); }; #endif diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game/game_scene.cpp index 5dc3b48f7..034ff6947 100644 --- a/cockatrice/src/game/game_scene.cpp +++ b/cockatrice/src/game/game_scene.cpp @@ -521,9 +521,9 @@ void GameScene::startRubberBand(const QPointF &selectionOrigin) emit sigStartRubberBand(selectionOrigin); } -void GameScene::resizeRubberBand(const QPointF &cursorPoint) +void GameScene::resizeRubberBand(const QPointF &cursorPoint, int selectedCount) { - emit sigResizeRubberBand(cursorPoint); + emit sigResizeRubberBand(cursorPoint, selectedCount); } void GameScene::stopRubberBand() diff --git a/cockatrice/src/game/game_scene.h b/cockatrice/src/game/game_scene.h index 86fa0795a..f08e83aa4 100644 --- a/cockatrice/src/game/game_scene.h +++ b/cockatrice/src/game/game_scene.h @@ -163,7 +163,7 @@ public: /** Unregisters a card from animation updates. */ void unregisterAnimationItem(AbstractCardItem *card); void startRubberBand(const QPointF &selectionOrigin); - void resizeRubberBand(const QPointF &cursorPoint); + void resizeRubberBand(const QPointF &cursorPoint, int selectedCount); void stopRubberBand(); public slots: @@ -196,7 +196,7 @@ protected: signals: void sigStartRubberBand(const QPointF &selectionOrigin); - void sigResizeRubberBand(const QPointF &cursorPoint); + void sigResizeRubberBand(const QPointF &cursorPoint, int selectedCount); void sigStopRubberBand(); }; diff --git a/cockatrice/src/game/game_view.cpp b/cockatrice/src/game/game_view.cpp index dd5cc70c1..ce53828a7 100644 --- a/cockatrice/src/game/game_view.cpp +++ b/cockatrice/src/game/game_view.cpp @@ -4,9 +4,32 @@ #include "game_scene.h" #include +#include #include #include +// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings. +// This subclass disables that behavior so dragCountLabel can appear above it. +class SelectionRubberBand : public QRubberBand +{ +public: + using QRubberBand::QRubberBand; + +protected: + void showEvent(QShowEvent *event) override + { + QWidget::showEvent(event); // Skip QRubberBand's raise() + } + + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::ZOrderChange) { + return; // Skip QRubberBand's raise() on z-order changes + } + QRubberBand::changeEvent(event); + } +}; + GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, parent), rubberBand(0) { setBackgroundBrush(QBrush(QColor(0, 0, 0))); @@ -19,6 +42,7 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par connect(scene, &GameScene::sigStartRubberBand, this, &GameView::startRubberBand); connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand); connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand); + connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); }); aCloseMostRecentZoneView = new QAction(this); @@ -27,7 +51,23 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &GameView::refreshShortcuts); refreshShortcuts(); - rubberBand = new QRubberBand(QRubberBand::Rectangle, this); + rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this); + + const QString countLabelStyle = "color: white; " + "font-size: 14px; " + "font-weight: bold; " + "background-color: rgba(0, 0, 0, 160); " + "border-radius: 3px; " + "padding: 1px 2px;"; + + dragCountLabel = new QLabel(this); + dragCountLabel->setStyleSheet(countLabelStyle); + dragCountLabel->hide(); + dragCountLabel->raise(); + + totalCountLabel = new QLabel(this); + totalCountLabel->setStyleSheet(countLabelStyle); + totalCountLabel->hide(); } void GameView::resizeEvent(QResizeEvent *event) @@ -39,6 +79,7 @@ void GameView::resizeEvent(QResizeEvent *event) s->processViewSizeChange(event->size()); updateSceneRect(scene()->sceneRect()); + updateTotalSelectionCount(event->size()); } void GameView::updateSceneRect(const QRectF &rect) @@ -48,20 +89,67 @@ void GameView::updateSceneRect(const QRectF &rect) void GameView::startRubberBand(const QPointF &_selectionOrigin) { + if (!rubberBand) + return; + selectionOrigin = _selectionOrigin; rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0))); rubberBand->show(); } -void GameView::resizeRubberBand(const QPointF &cursorPoint) +void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount) { - if (rubberBand) - rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), cursorPoint.toPoint()).normalized()); + if (!rubberBand) + return; + + constexpr int kLabelPaddingInPixels = 4; + + QPoint cursor = cursorPoint.toPoint(); + QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized(); + rubberBand->setGeometry(rect); + + if (!SettingsCache::instance().getShowDragSelectionCount()) { + dragCountLabel->hide(); + return; + } + + if (selectedCount > 0) { + dragCountLabel->setText(QString::number(selectedCount)); + dragCountLabel->adjustSize(); + QSize labelSize = dragCountLabel->size(); + + if (rect.width() < labelSize.width() + 2 * kLabelPaddingInPixels || + rect.height() < labelSize.height() + 2 * kLabelPaddingInPixels) { + dragCountLabel->hide(); + return; + } + + const int minX = rect.left() + kLabelPaddingInPixels; + const int minY = rect.top() + kLabelPaddingInPixels; + + int x = qMax(minX, cursor.x() - labelSize.width() - kLabelPaddingInPixels); + int y = qMax(minY, cursor.y() - labelSize.height() - kLabelPaddingInPixels); + + bool isAtTopLeftCorner = (x == minX) && (y == minY); + if (isAtTopLeftCorner) { + constexpr int kCursorClearanceInPixels = 16; + x = qMin(cursor.x() + kCursorClearanceInPixels, rect.right() - labelSize.width() - kLabelPaddingInPixels); + } + + dragCountLabel->move(x, y); + dragCountLabel->show(); + } else { + dragCountLabel->hide(); + } } void GameView::stopRubberBand() { + if (!rubberBand) + return; + rubberBand->hide(); + dragCountLabel->hide(); } void GameView::refreshShortcuts() @@ -69,3 +157,28 @@ void GameView::refreshShortcuts() aCloseMostRecentZoneView->setShortcuts( SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView")); } + +void GameView::updateTotalSelectionCount(const QSize &viewSize) +{ + if (!SettingsCache::instance().getShowTotalSelectionCount()) { + totalCountLabel->hide(); + return; + } + + int count = scene()->selectedItems().count(); + + if (count > 1) { + totalCountLabel->setText(QString::number(count)); + totalCountLabel->adjustSize(); + + constexpr int kMarginInPixels = 10; + int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width(); + int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height(); + int x = availableWidth - totalCountLabel->width() - kMarginInPixels; + int y = availableHeight - totalCountLabel->height() - kMarginInPixels; + totalCountLabel->move(x, y); + totalCountLabel->show(); + } else { + totalCountLabel->hide(); + } +} diff --git a/cockatrice/src/game/game_view.h b/cockatrice/src/game/game_view.h index 72df9cd08..a77ab9257 100644 --- a/cockatrice/src/game/game_view.h +++ b/cockatrice/src/game/game_view.h @@ -10,6 +10,7 @@ #include class GameScene; +class QLabel; class QRubberBand; class GameView : public QGraphicsView @@ -18,15 +19,18 @@ class GameView : public QGraphicsView private: QAction *aCloseMostRecentZoneView; QRubberBand *rubberBand; + QLabel *dragCountLabel; + QLabel *totalCountLabel; QPointF selectionOrigin; protected: void resizeEvent(QResizeEvent *event) override; private slots: void startRubberBand(const QPointF &selectionOrigin); - void resizeRubberBand(const QPointF &cursorPoint); + void resizeRubberBand(const QPointF &cursorPoint, int selectedCount); void stopRubberBand(); void refreshShortcuts(); + void updateTotalSelectionCount(const QSize &viewSize = QSize()); public slots: void updateSceneRect(const QRectF &rect); diff --git a/cockatrice/src/game/zones/select_zone.cpp b/cockatrice/src/game/zones/select_zone.cpp index 719eec148..9bf5f9faf 100644 --- a/cockatrice/src/game/zones/select_zone.cpp +++ b/cockatrice/src/game/zones/select_zone.cpp @@ -68,7 +68,8 @@ void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event) } } static_cast(scene())->resizeRubberBand( - deviceTransform(static_cast(scene())->getViewportTransform()).map(pos)); + deviceTransform(static_cast(scene())->getViewportTransform()).map(pos), + cardsInSelectionRect.size()); event->accept(); } } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index e7286f078..e3bf209dc 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -821,6 +821,14 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&annotateTokensCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), &SettingsCache::setAnnotateTokens); + showDragSelectionCountCheckBox.setChecked(SettingsCache::instance().getShowDragSelectionCount()); + connect(&showDragSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setShowDragSelectionCount); + + showTotalSelectionCountCheckBox.setChecked(SettingsCache::instance().getShowTotalSelectionCount()); + connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setShowTotalSelectionCount); + useTearOffMenusCheckBox.setChecked(SettingsCache::instance().getUseTearOffMenus()); connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), [](const QT_STATE_CHANGED_T state) { SettingsCache::instance().setUseTearOffMenus(state == Qt::Checked); }); @@ -833,7 +841,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() generalGrid->addWidget(&closeEmptyCardViewCheckBox, 4, 0); generalGrid->addWidget(&focusCardViewSearchBarCheckBox, 5, 0); generalGrid->addWidget(&annotateTokensCheckBox, 6, 0); - generalGrid->addWidget(&useTearOffMenusCheckBox, 7, 0); + generalGrid->addWidget(&showDragSelectionCountCheckBox, 7, 0); + generalGrid->addWidget(&showTotalSelectionCountCheckBox, 8, 0); + generalGrid->addWidget(&useTearOffMenusCheckBox, 9, 0); generalGroupBox = new QGroupBox; generalGroupBox->setLayout(generalGrid); @@ -955,6 +965,8 @@ void UserInterfaceSettingsPage::retranslateUi() closeEmptyCardViewCheckBox.setText(tr("Close card view window when last card is removed")); focusCardViewSearchBarCheckBox.setText(tr("Auto focus search bar when card view window is opened")); annotateTokensCheckBox.setText(tr("Annotate card text on tokens")); + showDragSelectionCountCheckBox.setText(tr("Show selection counter during drag selection")); + showTotalSelectionCountCheckBox.setText(tr("Show total selection counter")); useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen")); notificationsGroupBox->setTitle(tr("Notifications settings")); notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar")); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index db107c6e2..b655a30bc 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -171,6 +171,8 @@ private: QCheckBox closeEmptyCardViewCheckBox; QCheckBox focusCardViewSearchBarCheckBox; QCheckBox annotateTokensCheckBox; + QCheckBox showDragSelectionCountCheckBox; + QCheckBox showTotalSelectionCountCheckBox; QCheckBox useTearOffMenusCheckBox; QCheckBox tapAnimationCheckBox; QCheckBox openDeckInNewTabCheckBox; From c5cd7d87001ed5738c428dadb38f73999c033a87 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:13:36 -0700 Subject: [PATCH 057/116] [ShortcutsSettings] Fix duplicate aPlay shortcut; change play shortcut's group (#6716) * [ShortcutsSettings] Fix duplicate aPlay shortcut; change play shortcut's group * update description --- cockatrice/src/client/settings/shortcuts_settings.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index 47332b8c4..51615745b 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -513,6 +513,9 @@ private: {"Player/aPlay", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Play Card"), parseSequenceString(""), ShortcutGroup::Playing_Area)}, + {"Player/aPlayFacedown", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Play Card, Face Down"), + parseSequenceString(""), + ShortcutGroup::Playing_Area)}, {"Player/aAttach", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Attach Card..."), parseSequenceString("Ctrl+Alt+A"), ShortcutGroup::Playing_Area)}, @@ -560,12 +563,6 @@ private: {"Player/aMoveToTopLibrary", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Top of Library"), parseSequenceString(""), ShortcutGroup::Move_selected)}, - {"Player/aPlayFacedown", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Battlefield, Face Down"), - parseSequenceString(""), - ShortcutGroup::Move_selected)}, - {"Player/aPlay", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Battlefield"), - parseSequenceString(""), - ShortcutGroup::Move_selected)}, {"Player/aViewHand", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Hand"), parseSequenceString(""), ShortcutGroup::View)}, {"Player/aViewGraveyard", From 38c85e6db16dbc692dbc908c1a9693a11761788c Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:24:34 -0700 Subject: [PATCH 058/116] [Dialog] Reduce spacing in create local game dialog (#6719) --- .../src/interface/widgets/dialogs/dlg_local_game_options.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp index 52466ff10..a9adfd907 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp @@ -20,6 +20,7 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent) numberPlayersLabel->setBuddy(numberPlayersEdit); auto *generalGrid = new QGridLayout; + generalGrid->setContentsMargins(5, 5, 5, 5); generalGrid->addWidget(numberPlayersLabel, 0, 0); generalGrid->addWidget(numberPlayersEdit, 0, 1); generalGroupBox = new QGroupBox(tr("General"), this); @@ -33,6 +34,7 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent) startingLifeTotalLabel->setBuddy(startingLifeTotalEdit); auto *gameSetupGrid = new QGridLayout; + gameSetupGrid->setContentsMargins(5, 5, 5, 5); gameSetupGrid->addWidget(startingLifeTotalLabel, 0, 0); gameSetupGrid->addWidget(startingLifeTotalEdit, 0, 1); gameSetupOptionsGroupBox = new QGroupBox(tr("Game setup options"), this); From 067fe9b5349a673f2d61f0cf5e2e92e0873013b4 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:13:43 +0100 Subject: [PATCH 059/116] Translate cockatrice/cockatrice_en@source.ts in it (#6724) 100% translated source file: 'cockatrice/cockatrice_en@source.ts' on 'it'. Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com> --- cockatrice/translations/cockatrice_it.ts | 348 ++++++++++++----------- 1 file changed, 180 insertions(+), 168 deletions(-) diff --git a/cockatrice/translations/cockatrice_it.ts b/cockatrice/translations/cockatrice_it.ts index a0765fbfb..5893d4121 100644 --- a/cockatrice/translations/cockatrice_it.ts +++ b/cockatrice/translations/cockatrice_it.ts @@ -101,7 +101,7 @@ Controlla se la cartella è valida e prova ancora. Add Analytics Panel - + Aggiungi pannello di analisi @@ -156,7 +156,13 @@ You will not be able to manage printing preferences on a per-deck basis, or see You will have to use the Set Manager, available through Card Database -> Manage Sets. Are you sure you would like to enable this feature? - + Abilitare questa funzione disattiverà il Selettore di stampa. + +Non potrai gestire le preferenze delle stampe per i singoli mazzi, o vedere le stampe scelte dagli altri giocatori per i loro mazzi. + +Dovrai usare il Gestore dei set, raggiungibile tramite Database carte -> Organizza set. + +Sicuro di voler abilitare questa funzione? @@ -167,12 +173,18 @@ You can now choose printings on a per-deck basis in the Deck Editor and configur You can also use the Set Manager to adjust custom sort order for printings in the Printing Selector (other sort orders like alphabetical or release date are available). Are you sure you would like to disable this feature? - + Disabilitare questa funzione attiverà il Selettore di stampa. + +Potrai gestire le preferenze delle stampe per i singoli mazzi nell'Editor, e configurare quale stampa viene aggiunta di default a un mazzo fissandola nel selettore. + +Potrai anche usare il Gestore dei set per personalizzare l'ordine delle stampe nel Selettore (sono disponibili anche ordinamenti predefiniti come alfabetico o per data di uscita). + +Sicuro di voler disabilitare questa funzione? Confirm Change - + Conferma modifica @@ -217,7 +229,7 @@ Are you sure you would like to disable this feature? Show game filter toolbar above list in room tab - + Mostra barra di filtro sopra la lista delle partite @@ -237,7 +249,7 @@ Are you sure you would like to disable this feature? Override all card art with personal set preference (Pre-ProviderID change behavior) - + Determina l'illustrazione della carta attraverso la priorità dei set (come prima dell'implementazione del ProviderID) @@ -326,7 +338,7 @@ Are you sure you would like to disable this feature? Open Deck in Deck Editor - + Apri mazzo nell'editor dei mazzi @@ -550,7 +562,7 @@ Questa è visibile solo ai moderatori e non alla persona bannata. Name (Exact) - + Nome (esatto) @@ -674,12 +686,12 @@ Questa è visibile solo ai moderatori e non alla persona bannata. Set: - + Set: Collector Number: - + Numero di collezione: @@ -822,7 +834,7 @@ Questa è visibile solo ai moderatori e non alla persona bannata. Card Size - Dimensioni della carta + Dimensioni carte @@ -995,22 +1007,22 @@ Questa è visibile solo ai moderatori e non alla persona bannata. Add Panel - + Aggiungi pannello Remove Panel - + Rimuovi pannello Save Layout - + Salva disposizione Load Layout - + Carica disposizione @@ -1018,7 +1030,7 @@ Questa è visibile solo ai moderatori e non alla persona bannata. Card Database - + Database carte @@ -1082,7 +1094,7 @@ Questa è visibile solo ai moderatori e non alla persona bannata. Loading Database... - + Caricamento database... @@ -1147,7 +1159,7 @@ Questa è visibile solo ai moderatori e non alla persona bannata. Format: - + Formato: @@ -1488,32 +1500,32 @@ Questa è visibile solo ai moderatori e non alla persona bannata. Undo - + Annulla Redo - + Ripeti Undo/Redo history - + Annulla/Ripeti Storico Click on an entry to revert to that point in the history. - + Clicca su una riga per ritornare a quel punto nello storico delle modifiche. [redo] - + [ripeti] [undo] - + [annulla] @@ -1746,57 +1758,57 @@ Questa è visibile solo ai moderatori e non alla persona bannata. Rename deck to "%1" from "%2" - + Rinominato mazzo in "%1" da "%2" Updated comments (was %1 chars, now %2 chars) - + Commenti aggiornati (prima %1 caratteri, ora %2 caratteri) Set banner card to %1 (%2) - + Impostata carta copertina a %1 (%2) Tags changed - + Etichette modificate Set format to %1 - + Formato impostato a %1 Added (%1): %2 (%3) %4 - + Aggiunto (%1): %2 (%3) %4 Moved to %1 1 × "%2" (%3) - + Spostato in %1 1 × "%2" (%3) Removed "%1" (all copies) - + Rimosso "%1" (tutte le copie) %1 1 × "%2" (%3) - + %1 1 × "%2" (%3) Added - + Aggiunto Removed - + Rimosso @@ -2150,7 +2162,7 @@ Vuoi convertire il mazzo al formato .cod? Create game as judge - + Crea partita come arbitro @@ -2526,7 +2538,7 @@ Per rimuovere il tuo avatar attuale, conferma senza scegliere una nuova immagine The chosen name conflicts with an existing card or token. Make sure to enable the 'Token' set in the "Manage sets" dialog to display them correctly. Il nome scelto è in conflitto con una carta o pedina esistente. -Assicurati di abilitare il set "Pedine" nella finestra "Organizza set" per visualizzarle correttamente. +Assicurati di abilitare il set "Pedine" nella finestra "Gestisci Espansioni" per visualizzarle correttamente. @@ -2618,7 +2630,7 @@ Assicurati di abilitare il set "Pedine" nella finestra "Organizza Hide games not created by buddies Hide games not created by buddy - + Nascondi partite non create dagli amici @@ -3150,17 +3162,17 @@ La tua email verrà utilizzata per verificare il tuo account. Bulk modified printings. - + Stampe modificate massivamente. Cleared all printing information. - + Informazioni sulle stampe eliminate. Set all printings to preferred. - + Imposta tutte le stampe come preferite. @@ -3556,63 +3568,63 @@ Dovrai scaricare la nuova versione manualmente. Draw Probability Settings - + Impostazioni probabilità di pescare Criteria: - + Criterio: Card Name - + Nome carta Type - + Tipo Subtype - + Sottotipo Mana Value - + Valore di mana Exactness: - + Precisione: At least - + Almeno Exactly - + Esattamente Quantity (N): - + Quantità (N): Cards drawn (M): - + Carte pescate (M): cards - + carte @@ -3620,67 +3632,67 @@ Dovrai scaricare la nuova versione manualmente. Draw Probability - + Probabilità di pescare Probability of drawing - + Probabilità di pescare Card Name - + Nome carta Type - + Tipo Subtype - + Sottotipo Mana Value - + Valore di mana At least - + Almeno Exactly - + Esattamente card(s) having drawn at least - + carta/e avendo pescato almeno cards - + carte Category - + Categoria Qty - + Quantità Odds (%) - + Probabilità (%) @@ -3757,7 +3769,7 @@ Dovrai scaricare la nuova versione manualmente. Game Changers - + Carte decisive @@ -3765,7 +3777,7 @@ Dovrai scaricare la nuova versione manualmente. Budget - + Budget @@ -3916,12 +3928,12 @@ Dovrai scaricare la nuova versione manualmente. Join Game as Judge - + Unisciti a una partita come arbitro Spectate Game as Judge - + Osserva partita come arbitro @@ -3961,7 +3973,7 @@ Dovrai scaricare la nuova versione manualmente. Join as judge - + Unisciti come arbitro @@ -3971,7 +3983,7 @@ Dovrai scaricare la nuova versione manualmente. Join as judge spectator - + Unisciti come arbitro spettatore @@ -3989,32 +4001,32 @@ Dovrai scaricare la nuova versione manualmente. All types - + Tutti i tipi Filter by game name... - + Filtra per nome della partita... Filter by game type/format - + Filtra per tipo / formato di partita Hide games not created by buddies - + Nascondi le partite non create da amici Hide full games - + Nascondi partite al completo Hide started games - + Nascondi partite iniziate @@ -4297,7 +4309,7 @@ Dovrai scaricare la nuova versione manualmente. &All players - + &Tutti i giocatori @@ -4330,37 +4342,37 @@ Dovrai scaricare la nuova versione manualmente. Sort hand by... - + Ordina mano per... Name - + Nome Type - + Tipo Mana Value - + Valore di mana Take &mulligan (Choose hand size) - + Effettua un &mulligan (Scegli numero di carte) Take mulligan (Same hand size) - + Effettua un mulligan (Stessa dimensione della mano) Take mulligan (Hand size - 1) - + Effettua un mulligan (Dimensione della mano - 1) @@ -4396,7 +4408,7 @@ Dovrai scaricare la nuova versione manualmente. All players - + Tutti i giocatori @@ -4429,7 +4441,7 @@ Dovrai scaricare la nuova versione manualmente. Browse Archidekt - + Esplora Archidekt @@ -4638,17 +4650,17 @@ Dovrai scaricare la nuova versione manualmente. &All players - + &Tutti i giocatori Reveal top cards of library - + Rivela le carte in cima al grimorio Number of cards: (max. %1) - + Numero di carte: (max. %1) @@ -5186,7 +5198,7 @@ La tua versione è la %1, la versione online è la %2. &Manage sets... - &Organizza set... + &Gestisci Espansioni... @@ -5358,7 +5370,7 @@ All the sets in the card database have been enabled. Read more about changing the set order or disabling specific sets and consequent effects in the "Manage Sets" dialog. Ciao! Sembra che tu stia usando questa versione di Cockatrice per la prima volta Tutti i set nell'archivio delle carte sono stati abilitati. -Scopri metodi alternativi per visualizzare i set o disabilitare set ed effetti nella finestra "Organizza set". +Scopri metodi alternativi per visualizzare i set o disabilitare set ed effetti nella finestra "Gestisci Espansioni". @@ -5488,42 +5500,42 @@ Il database delle carte verrà ricaricato. Mana Base Configuration - + Configurazione Base di mana Display type: - + Modalità di visualizzazione: pie - + a torta bar - + a barre combinedBar - + a barre combinate Filter Colors (optional): - + Filtra per colori (opzionale): OK - + OK Cancel - + Annulla @@ -5539,47 +5551,47 @@ Il database delle carte verrà ricaricato. Group By: - + Raggruppa per: type - + tipo color - + colore subtype - + sottotipo power - + forza toughness - + costituzione Filters (optional): - + Filtri (opzionali): Show main bar row - + Mostra il grafico principale Show per-category rows - + Mostra i grafici per categorie @@ -5595,27 +5607,27 @@ Il database delle carte verrà ricaricato. Display type: - + Modalità di visualizzazione: pie - + A torta bar - + A barre combinedBar - + A barra combinata Filter Colors (optional): - + Filtri per colore (opzionali): @@ -5631,27 +5643,27 @@ Il database delle carte verrà ricaricato. Top display type: - + Modalità di visualizzazione principale: pie - + A torta bar - + A barre Colors: - + Colori: Show per-color rows - + Mostra grafici per colore @@ -5659,12 +5671,12 @@ Il database delle carte verrà ricaricato. %1 pips (%2 cards) - + %1 simboli (%2 carte) %1 mana (%2 cards) - + %1 mana (%2 carte) @@ -5672,12 +5684,12 @@ Il database delle carte verrà ricaricato. Mana Production + Devotion - + Produzione + Devozione di mana Mana Distribution Settings - + Impostazioni distribuzione di mana @@ -7637,58 +7649,58 @@ Controlla le impostazioni! Desc. - + Decrescente Asc. - + Crescente Any Bracket - + Qualsiasi fascia Deck name contains... - + Nome del mazzo contiene... Owner name contains... - + Nome del proprietario contiene... Disabled - + Disabilitato Search - + Cerca Formats - + Formati Min. # of Cards: - + Num. minimo di carte: Page: - + Pagina: Archidekt: - + Archidekt: @@ -7716,7 +7728,7 @@ Controlla le impostazioni! Card Database - + Database carte @@ -7819,12 +7831,12 @@ Controlla le impostazioni! Visual Deck View - Galleria mazzo + Mazzo visuale Visual Database Display - Galleria database + Database visuale @@ -8729,7 +8741,7 @@ Più informazioni inserisci, più specifici saranno i risultati. Archidekt - + Archidekt @@ -8857,7 +8869,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Database Display - + Visualizzazione Database @@ -9353,7 +9365,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Do not delete &arrows inside of subphases - + Non eliminare le &freccie al cambio di sottofase @@ -9555,7 +9567,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Search filter... - + Filtro di ricerca... @@ -9578,22 +9590,22 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Do not display formats with less than this amount of cards in the database - + Non mostrare i formati con meno di questo ammontare di carte nel database Filter mode (AND/OR/NOT conjunctions of filters) - + Modalità di filtraggio (utilizzabile con connettivi logici AND/OR/NOT) Mode: Exact Match - + Modalità: Corrispondenza esatta Mode: Includes - + Modalità: Include @@ -9624,7 +9636,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Filter by name... (Exact match) - + Filtra per nome... (Corrispondenza esatta) @@ -9714,7 +9726,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Visual - + Visuale @@ -9729,12 +9741,12 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Sort by: - + Ordina per: Filter by: - + Filtra per: @@ -9759,7 +9771,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Table - + Tabella @@ -9767,43 +9779,43 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Group by: - + Raggruppa per: Change how cards are divided into categories/groups. - + Modifica come le carte sono divise in categorie / gruppi. Sort by: - + Ordina per: Click and drag to change the sort order within the groups - + Clicca e trascina per modificare l'ordinamento interno ai gruppi Configure how cards are sorted within their groups - + Configura come le carte sono ordinate all'interno dei loro gruppi Toggle Layout: Overlap - + Disposizione: Sovrapposta Change how cards are displayed within zones (i.e. overlapped or fully visible.) - + Modifica come le carte vengono mostrate nelle zone (es. sovrapposte o completamente visibili.) Toggle Layout: Flat - + Disposizione: Piatta @@ -9824,7 +9836,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Type a card name here for suggestions from the database... - + Scrivi un nome di carta qui per avere suggerimenti dal database... @@ -10948,7 +10960,7 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Reveal Selected Cards to All Players - + Rivela Carta Selezionata a Tutti i Giocatori @@ -11103,12 +11115,12 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Mulligan (Same hand size) - + Mulligan (Come la mano attuale) Mulligan (Hand size - 1) - + Mulligan (Dimensione della mano - 1) @@ -11138,27 +11150,27 @@ Se pregato di evitare di continuare questa attività o potrebbero venire presi u Sort Hand by Name - + Ordina mano per Nome Sort Hand by Type - + Ordina mano per Tipo Sort Hand by Mana Value - + Ordina mano per Valore di Mana Reveal Hand to All Players - + Rivela Mano a Tutti i Giocatori Reveal Random Card to All Players - + Rivela Carta Casuale a Tutti i Giocatori From 652c8464a7c391884b1295fca96a82c08881cbb8 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 22 Mar 2026 10:28:24 +0100 Subject: [PATCH 060/116] Docker compose: Link to our GHCR hosted `servatrice` image (#6728) * Point to our own image * Point to our own image * Readd build * Readd build --- docker-compose.yml | 2 +- docker-compose.yml.windows | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3d9f9f38f..6cbac61f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: build: context: . dockerfile: Dockerfile - image: servatrice + image: ghcr.io/cockatrice/servatrice:latest depends_on: - mysql ports: diff --git a/docker-compose.yml.windows b/docker-compose.yml.windows index 6663c90fd..3d29b1e5f 100644 --- a/docker-compose.yml.windows +++ b/docker-compose.yml.windows @@ -16,7 +16,7 @@ services: build: context: . dockerfile: Dockerfile - image: servatrice + image: ghcr.io/cockatrice/servatrice:latest depends_on: - mysql ports: From bc219191dbdcf5e870c36470b9c633558f483de9 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 22 Mar 2026 04:32:42 -0700 Subject: [PATCH 061/116] [Game] Refactor options in DlgMoveTopCardsUntil into struct (#6718) --- .../game/dialogs/dlg_move_top_cards_until.cpp | 28 ++++++++----------- .../game/dialogs/dlg_move_top_cards_until.h | 20 +++++++------ cockatrice/src/game/player/player_actions.cpp | 11 +++----- cockatrice/src/game/player/player_actions.h | 5 ++-- 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/cockatrice/src/game/dialogs/dlg_move_top_cards_until.cpp b/cockatrice/src/game/dialogs/dlg_move_top_cards_until.cpp index a9db4cef6..2b54452ac 100644 --- a/cockatrice/src/game/dialogs/dlg_move_top_cards_until.cpp +++ b/cockatrice/src/game/dialogs/dlg_move_top_cards_until.cpp @@ -12,8 +12,7 @@ #include #include -DlgMoveTopCardsUntil::DlgMoveTopCardsUntil(QWidget *parent, QStringList exprs, uint _numberOfHits, bool autoPlay) - : QDialog(parent) +DlgMoveTopCardsUntil::DlgMoveTopCardsUntil(QWidget *parent, const MoveTopCardsUntilOptions &options) : QDialog(parent) { exprLabel = new QLabel(tr("Card name (or search expressions):")); @@ -21,13 +20,13 @@ DlgMoveTopCardsUntil::DlgMoveTopCardsUntil(QWidget *parent, QStringList exprs, u exprComboBox->setFocus(); exprComboBox->setEditable(true); exprComboBox->setInsertPolicy(QComboBox::InsertAtTop); - exprComboBox->insertItems(0, exprs); + exprComboBox->insertItems(0, options.exprs); exprLabel->setBuddy(exprComboBox); numberOfHitsLabel = new QLabel(tr("Number of hits:")); numberOfHitsEdit = new QSpinBox(this); numberOfHitsEdit->setRange(1, 99); - numberOfHitsEdit->setValue(_numberOfHits); + numberOfHitsEdit->setValue(options.numberOfHits); numberOfHitsLabel->setBuddy(numberOfHitsEdit); auto *grid = new QGridLayout; @@ -35,7 +34,7 @@ DlgMoveTopCardsUntil::DlgMoveTopCardsUntil(QWidget *parent, QStringList exprs, u grid->addWidget(numberOfHitsEdit, 0, 1); autoPlayCheckBox = new QCheckBox(tr("Auto play hits")); - autoPlayCheckBox->setChecked(autoPlay); + autoPlayCheckBox->setChecked(options.autoPlay); buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgMoveTopCardsUntil::validateAndAccept); @@ -118,6 +117,13 @@ QString DlgMoveTopCardsUntil::getExpr() const return exprComboBox->currentText(); } +MoveTopCardsUntilOptions DlgMoveTopCardsUntil::getOptions() const +{ + return {.exprs = getExprs(), + .numberOfHits = numberOfHitsEdit->text().toInt(), + .autoPlay = autoPlayCheckBox->isChecked()}; +} + QStringList DlgMoveTopCardsUntil::getExprs() const { QStringList exprs; @@ -125,14 +131,4 @@ QStringList DlgMoveTopCardsUntil::getExprs() const exprs.append(exprComboBox->itemText(i)); } return exprs; -} - -uint DlgMoveTopCardsUntil::getNumberOfHits() const -{ - return numberOfHitsEdit->text().toUInt(); -} - -bool DlgMoveTopCardsUntil::isAutoPlay() const -{ - return autoPlayCheckBox->isChecked(); -} +} \ No newline at end of file diff --git a/cockatrice/src/game/dialogs/dlg_move_top_cards_until.h b/cockatrice/src/game/dialogs/dlg_move_top_cards_until.h index 9a9dc91e7..20ba11c5c 100644 --- a/cockatrice/src/game/dialogs/dlg_move_top_cards_until.h +++ b/cockatrice/src/game/dialogs/dlg_move_top_cards_until.h @@ -16,6 +16,13 @@ class FilterString; +struct MoveTopCardsUntilOptions +{ + QStringList exprs = {}; + int numberOfHits = 1; + bool autoPlay = false; +}; + class DlgMoveTopCardsUntil : public QDialog { Q_OBJECT @@ -29,15 +36,12 @@ class DlgMoveTopCardsUntil : public QDialog void validateAndAccept(); bool validateMatchExists(const FilterString &filterString); -public: - explicit DlgMoveTopCardsUntil(QWidget *parent = nullptr, - QStringList exprs = QStringList(), - uint numberOfHits = 1, - bool autoPlay = false); - [[nodiscard]] QString getExpr() const; [[nodiscard]] QStringList getExprs() const; - [[nodiscard]] uint getNumberOfHits() const; - [[nodiscard]] bool isAutoPlay() const; + +public: + explicit DlgMoveTopCardsUntil(QWidget *parent = nullptr, const MoveTopCardsUntilOptions &options = {}); + [[nodiscard]] QString getExpr() const; + [[nodiscard]] MoveTopCardsUntilOptions getOptions() const; }; #endif // DLG_MOVE_TOP_CARDS_UNTIL_H diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 20e526727..287231402 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -485,22 +485,19 @@ void PlayerActions::actMoveTopCardsUntil() { stopMoveTopCardsUntil(); - DlgMoveTopCardsUntil dlg(player->getGame()->getTab(), movingCardsUntilExprs, movingCardsUntilNumberOfHits, - movingCardsUntilAutoPlay); + DlgMoveTopCardsUntil dlg(player->getGame()->getTab(), movingCardsUntilOptions); if (!dlg.exec()) { return; } auto expr = dlg.getExpr(); - movingCardsUntilExprs = dlg.getExprs(); - movingCardsUntilNumberOfHits = dlg.getNumberOfHits(); - movingCardsUntilAutoPlay = dlg.isAutoPlay(); + movingCardsUntilOptions = dlg.getOptions(); if (player->getDeckZone()->getCards().empty()) { stopMoveTopCardsUntil(); } else { movingCardsUntilFilter = FilterString(expr); - movingCardsUntilCounter = movingCardsUntilNumberOfHits; + movingCardsUntilCounter = movingCardsUntilOptions.numberOfHits; movingCardsUntil = true; actMoveTopCardToPlay(); } @@ -512,7 +509,7 @@ void PlayerActions::moveOneCardUntil(CardItem *card) const bool isMatch = card && movingCardsUntilFilter.check(card->getCard().getCardPtr()); - if (isMatch && movingCardsUntilAutoPlay) { + if (isMatch && movingCardsUntilOptions.autoPlay) { // Directly calling playCard will deadlock, since we are already in the middle of processing an event. // Use QTimer::singleShot to queue up the playCard on the event loop. QTimer::singleShot(0, this, [card, this] { playCard(card, false); }); diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index 1d695578d..d4c6daacf 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -8,6 +8,7 @@ #ifndef COCKATRICE_PLAYER_ACTIONS_H #define COCKATRICE_PLAYER_ACTIONS_H #include "../dialogs/dlg_create_token.h" +#include "../dialogs/dlg_move_top_cards_until.h" #include "event_processing_options.h" #include "player.h" @@ -178,11 +179,9 @@ private: bool movingCardsUntil; QTimer *moveTopCardTimer; - QStringList movingCardsUntilExprs = {}; - int movingCardsUntilNumberOfHits = 1; - bool movingCardsUntilAutoPlay = false; FilterString movingCardsUntilFilter; int movingCardsUntilCounter = 0; + MoveTopCardsUntilOptions movingCardsUntilOptions; void moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); void moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); From 414567f8b671c3f7487aa69d9bec96ce5d79d05c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:43:42 +0100 Subject: [PATCH 062/116] Bump docker/login-action from 3 to 4 (#6736) --- .github/workflows/docker-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 2a4aab1ea..cdcf77e97 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -48,7 +48,7 @@ jobs: - name: Login to GitHub Container Registry if: github.ref_type == 'tag' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} From fa2934373c556e4c485a8748cb5de412ccdae5c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:44:31 +0100 Subject: [PATCH 063/116] Bump microsoft/setup-msbuild from 2 to 3 (#6735) --- .github/workflows/desktop-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 02c3f7aec..820044059 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -341,7 +341,7 @@ jobs: - name: Add msbuild to PATH if: matrix.os == 'Windows' id: add-msbuild - uses: microsoft/setup-msbuild@v2 + uses: microsoft/setup-msbuild@v3 with: msbuild-architecture: x64 From 51c684251fa96dbeca4725e4687ae69097e7223d Mon Sep 17 00:00:00 2001 From: tooomm Date: Tue, 24 Mar 2026 00:16:26 +0100 Subject: [PATCH 064/116] Add Docker to release template (#6732) --- .ci/release_template.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.ci/release_template.md b/.ci/release_template.md index 0e6c05165..b0924b92a 100644 --- a/.ci/release_template.md +++ b/.ci/release_template.md @@ -27,6 +27,8 @@ Available pre-compiled binaries for installation: We are also packaged in Arch Linux's official extra repository, courtesy of @FFY00. General Linux support is available via a flatpak package at Flathub! + +We provide a Docker image for "Servatrice" in GHCR. You can docker pull it or use our Docker Compose files! From aa85a39d6add9b19983e2f5a1426a6f6114100d7 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Tue, 24 Mar 2026 00:16:48 +0100 Subject: [PATCH 065/116] expand local game life total limits (#6730) --- .../src/interface/widgets/dialogs/dlg_local_game_options.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp index a9adfd907..d2d291556 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp @@ -28,8 +28,8 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent) startingLifeTotalLabel = new QLabel(tr("Starting life total:"), this); startingLifeTotalEdit = new QSpinBox(this); - startingLifeTotalEdit->setMinimum(1); - startingLifeTotalEdit->setMaximum(99999); + startingLifeTotalEdit->setMinimum(-999999999); + startingLifeTotalEdit->setMaximum(999999999); startingLifeTotalEdit->setValue(20); startingLifeTotalLabel->setBuddy(startingLifeTotalEdit); From 70b41c20958c3a1e87195540756635ea4cdb4891 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Tue, 24 Mar 2026 15:31:34 -0400 Subject: [PATCH 066/116] refactor: extract AbstractPlayerComponent interface for polymorphic player component management. (#6696) Non-QObject polymorphic interface with setShortcutsActive(), setShortcutsInactive(), and retranslateUi(). Uses regular multiple inheritance to avoid diamond inheritance with Qt's MOC. All zone menus, SayMenu, and AbstractCounter implement this interface. PlayerMenu manages them via a managedComponents list with two template helpers (addManagedMenu/registerManagedComponent), replacing individual if-guarded lifecycle calls with a single polymorphic loop. SayMenu now owns its shortcut and translation lifecycle instead of having PlayerMenu manage its title and shortcuts externally. Counters are iterated via Player::getCounters() rather than managedComponents to avoid duplicating the authoritative owner's map. --- cockatrice/src/game/board/abstract_counter.h | 9 +- .../player/menu/abstract_player_component.h | 32 +++++++ .../src/game/player/menu/custom_zone_menu.h | 12 ++- cockatrice/src/game/player/menu/grave_menu.h | 9 +- cockatrice/src/game/player/menu/hand_menu.h | 9 +- .../src/game/player/menu/library_menu.h | 9 +- .../src/game/player/menu/player_menu.cpp | 89 ++++--------------- cockatrice/src/game/player/menu/player_menu.h | 27 +++++- cockatrice/src/game/player/menu/rfg_menu.h | 11 ++- cockatrice/src/game/player/menu/say_menu.cpp | 34 ++++++- cockatrice/src/game/player/menu/say_menu.h | 11 ++- .../src/game/player/menu/sideboard_menu.h | 10 ++- .../src/game/player/menu/utility_menu.h | 10 ++- 13 files changed, 164 insertions(+), 108 deletions(-) create mode 100644 cockatrice/src/game/player/menu/abstract_player_component.h diff --git a/cockatrice/src/game/board/abstract_counter.h b/cockatrice/src/game/board/abstract_counter.h index ea13cb00f..074650d54 100644 --- a/cockatrice/src/game/board/abstract_counter.h +++ b/cockatrice/src/game/board/abstract_counter.h @@ -8,6 +8,7 @@ #define COUNTER_H #include "../../interface/widgets/menus/tearoff_menu.h" +#include "../player/menu/abstract_player_component.h" #include #include @@ -18,7 +19,7 @@ class QKeyEvent; class QMenu; class QString; -class AbstractCounter : public QObject, public QGraphicsItem +class AbstractCounter : public QObject, public QGraphicsItem, public AbstractPlayerComponent { Q_OBJECT Q_INTERFACES(QGraphicsItem) @@ -56,10 +57,10 @@ public: QGraphicsItem *parent = nullptr); ~AbstractCounter() override; - void retranslateUi(); + void retranslateUi() override; void setValue(int _value); - void setShortcutsActive(); - void setShortcutsInactive(); + void setShortcutsActive() override; + void setShortcutsInactive() override; void delCounter(); QMenu *getMenu() const diff --git a/cockatrice/src/game/player/menu/abstract_player_component.h b/cockatrice/src/game/player/menu/abstract_player_component.h new file mode 100644 index 000000000..989300d41 --- /dev/null +++ b/cockatrice/src/game/player/menu/abstract_player_component.h @@ -0,0 +1,32 @@ +/** + * @file abstract_player_component.h + * @ingroup GameMenusPlayers + * @brief Polymorphic interface for player-bound UI components managed by PlayerMenu. + */ + +#ifndef COCKATRICE_ABSTRACT_PLAYER_COMPONENT_H +#define COCKATRICE_ABSTRACT_PLAYER_COMPONENT_H + +/** + * @brief Interface for player-bound UI components that need shortcut and translation lifecycle management. + * + * Not a QObject — avoids diamond inheritance with Qt's MOC. Each concrete component + * inherits QObject through its Qt base class (QMenu, TearOffMenu, QGraphicsItem, etc.) + * and this interface through regular multiple inheritance. + */ +class AbstractPlayerComponent +{ +public: + virtual ~AbstractPlayerComponent() = default; + + /// Bind keyboard shortcuts. Called when this player gains focus. + virtual void setShortcutsActive() = 0; + + /// Unbind keyboard shortcuts. Called when this player loses focus. + virtual void setShortcutsInactive() = 0; + + /// Retranslate all user-visible strings. Called on language change. + virtual void retranslateUi() = 0; +}; + +#endif // COCKATRICE_ABSTRACT_PLAYER_COMPONENT_H diff --git a/cockatrice/src/game/player/menu/custom_zone_menu.h b/cockatrice/src/game/player/menu/custom_zone_menu.h index 0944029f4..c4e66754e 100644 --- a/cockatrice/src/game/player/menu/custom_zone_menu.h +++ b/cockatrice/src/game/player/menu/custom_zone_menu.h @@ -7,15 +7,23 @@ #ifndef COCKATRICE_CUSTOM_ZONE_MENU_H #define COCKATRICE_CUSTOM_ZONE_MENU_H +#include "abstract_player_component.h" + #include class Player; -class CustomZoneMenu : public QMenu +class CustomZoneMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: explicit CustomZoneMenu(Player *player); - void retranslateUi(); + void retranslateUi() override; + void setShortcutsActive() override + { + } + void setShortcutsInactive() override + { + } private: Player *player; diff --git a/cockatrice/src/game/player/menu/grave_menu.h b/cockatrice/src/game/player/menu/grave_menu.h index faaf497b6..429173afa 100644 --- a/cockatrice/src/game/player/menu/grave_menu.h +++ b/cockatrice/src/game/player/menu/grave_menu.h @@ -8,12 +8,13 @@ #define COCKATRICE_GRAVE_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "abstract_player_component.h" #include #include class Player; -class GraveyardMenu : public TearOffMenu +class GraveyardMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT signals: @@ -25,9 +26,9 @@ public: void createViewActions(); void populateRevealRandomMenuWithActivePlayers(); void onRevealRandomTriggered(); - void retranslateUi(); - void setShortcutsActive(); - void setShortcutsInactive(); + void retranslateUi() override; + void setShortcutsActive() override; + void setShortcutsInactive() override; QMenu *mRevealRandomGraveyardCard = nullptr; QMenu *moveGraveMenu = nullptr; diff --git a/cockatrice/src/game/player/menu/hand_menu.h b/cockatrice/src/game/player/menu/hand_menu.h index 51e071a62..76434cc98 100644 --- a/cockatrice/src/game/player/menu/hand_menu.h +++ b/cockatrice/src/game/player/menu/hand_menu.h @@ -8,6 +8,7 @@ #define COCKATRICE_HAND_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "abstract_player_component.h" #include #include @@ -15,7 +16,7 @@ class Player; class PlayerActions; -class HandMenu : public TearOffMenu +class HandMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT @@ -31,9 +32,9 @@ public: return mRevealRandomHandCard; } - void retranslateUi(); - void setShortcutsActive(); - void setShortcutsInactive(); + void retranslateUi() override; + void setShortcutsActive() override; + void setShortcutsInactive() override; private slots: void populateRevealHandMenuWithActivePlayers(); diff --git a/cockatrice/src/game/player/menu/library_menu.h b/cockatrice/src/game/player/menu/library_menu.h index c0883107c..444e8f516 100644 --- a/cockatrice/src/game/player/menu/library_menu.h +++ b/cockatrice/src/game/player/menu/library_menu.h @@ -8,6 +8,7 @@ #define COCKATRICE_LIBRARY_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "abstract_player_component.h" #include #include @@ -15,7 +16,7 @@ class Player; class PlayerActions; -class LibraryMenu : public TearOffMenu +class LibraryMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT public slots: @@ -28,15 +29,15 @@ public: void createShuffleActions(); void createMoveActions(); void createViewActions(); - void retranslateUi(); + void retranslateUi() override; void populateRevealLibraryMenuWithActivePlayers(); void populateLendLibraryMenuWithActivePlayers(); void populateRevealTopCardMenuWithActivePlayers(); void onRevealLibraryTriggered(); void onLendLibraryTriggered(); void onRevealTopCardTriggered(); - void setShortcutsActive(); - void setShortcutsInactive(); + void setShortcutsActive() override; + void setShortcutsInactive() override; [[nodiscard]] bool isAlwaysRevealTopCardChecked() const { diff --git a/cockatrice/src/game/player/menu/player_menu.cpp b/cockatrice/src/game/player/menu/player_menu.cpp index 3016a727f..7786ec3fc 100644 --- a/cockatrice/src/game/player/menu/player_menu.cpp +++ b/cockatrice/src/game/player/menu/player_menu.cpp @@ -15,33 +15,24 @@ PlayerMenu::PlayerMenu(Player *_player) : player(_player) playerMenu = new TearOffMenu(); if (player->getPlayerInfo()->getLocalOrJudge()) { - handMenu = new HandMenu(player, player->getPlayerActions(), playerMenu); - playerMenu->addMenu(handMenu); - - libraryMenu = new LibraryMenu(player, playerMenu); - playerMenu->addMenu(libraryMenu); + handMenu = addManagedMenu(player, player->getPlayerActions(), playerMenu); + libraryMenu = addManagedMenu(player, playerMenu); } else { handMenu = nullptr; libraryMenu = nullptr; } - graveMenu = new GraveyardMenu(player, playerMenu); - playerMenu->addMenu(graveMenu); - - rfgMenu = new RfgMenu(player, playerMenu); - playerMenu->addMenu(rfgMenu); + graveMenu = addManagedMenu(player, playerMenu); + rfgMenu = addManagedMenu(player, playerMenu); if (player->getPlayerInfo()->getLocalOrJudge()) { - sideboardMenu = new SideboardMenu(player, playerMenu); - playerMenu->addMenu(sideboardMenu); - - customZonesMenu = new CustomZoneMenu(player); - playerMenu->addMenu(customZonesMenu); + sideboardMenu = addManagedMenu(player, playerMenu); + customZonesMenu = addManagedMenu(player); playerMenu->addSeparator(); countersMenu = playerMenu->addMenu(QString()); - utilityMenu = new UtilityMenu(player, playerMenu); + utilityMenu = createManagedComponent(player, playerMenu); } else { sideboardMenu = nullptr; customZonesMenu = nullptr; @@ -50,8 +41,7 @@ PlayerMenu::PlayerMenu(Player *_player) : player(_player) } if (player->getPlayerInfo()->getLocal()) { - sayMenu = new SayMenu(player); - playerMenu->addMenu(sayMenu); + sayMenu = addManagedMenu(player); } else { sayMenu = nullptr; } @@ -99,40 +89,18 @@ void PlayerMenu::retranslateUi() { playerMenu->setTitle(tr("Player \"%1\"").arg(player->getPlayerInfo()->getName())); - if (handMenu) { - handMenu->retranslateUi(); - } - if (libraryMenu) { - libraryMenu->retranslateUi(); - } - - graveMenu->retranslateUi(); - rfgMenu->retranslateUi(); - - if (sideboardMenu) { - sideboardMenu->retranslateUi(); + for (auto *component : managedComponents) { + component->retranslateUi(); } if (countersMenu) { countersMenu->setTitle(tr("&Counters")); } - if (customZonesMenu) { - customZonesMenu->retranslateUi(); - } - QMapIterator counterIterator(player->getCounters()); while (counterIterator.hasNext()) { counterIterator.next().value()->retranslateUi(); } - - if (utilityMenu) { - utilityMenu->retranslateUi(); - } - - if (sayMenu) { - sayMenu->setTitle(tr("S&ay")); - } } void PlayerMenu::refreshShortcuts() @@ -153,52 +121,29 @@ void PlayerMenu::setShortcutsActive() { shortcutsActive = true; - if (handMenu) { - handMenu->setShortcutsActive(); - } - if (libraryMenu) { - libraryMenu->setShortcutsActive(); - } - graveMenu->setShortcutsActive(); - // No shortcuts for RfgMenu yet - - if (sideboardMenu) { - sideboardMenu->setShortcutsActive(); + for (auto *component : managedComponents) { + component->setShortcutsActive(); } + // Counters implement AbstractPlayerComponent but are iterated via Player::counters + // (the authoritative source) rather than managedComponents to avoid a redundant + // list that must stay in sync with the map. QMapIterator counterIterator(player->getCounters()); while (counterIterator.hasNext()) { counterIterator.next().value()->setShortcutsActive(); } - - if (utilityMenu) { - utilityMenu->setShortcutsActive(); - } } void PlayerMenu::setShortcutsInactive() { shortcutsActive = false; - if (handMenu) { - handMenu->setShortcutsInactive(); - } - if (libraryMenu) { - libraryMenu->setShortcutsInactive(); - } - graveMenu->setShortcutsInactive(); - // No shortcuts for RfgMenu yet - - if (sideboardMenu) { - sideboardMenu->setShortcutsInactive(); + for (auto *component : managedComponents) { + component->setShortcutsInactive(); } QMapIterator counterIterator(player->getCounters()); while (counterIterator.hasNext()) { counterIterator.next().value()->setShortcutsInactive(); } - - if (utilityMenu) { - utilityMenu->setShortcutsInactive(); - } } \ No newline at end of file diff --git a/cockatrice/src/game/player/menu/player_menu.h b/cockatrice/src/game/player/menu/player_menu.h index 882bfedc5..5fce27158 100644 --- a/cockatrice/src/game/player/menu/player_menu.h +++ b/cockatrice/src/game/player/menu/player_menu.h @@ -1,7 +1,7 @@ /** * @file player_menu.h * @ingroup GameMenusPlayers - * @brief TODO: Document this. + * @brief Orchestrates lifecycle management for all player-bound UI components. */ #ifndef COCKATRICE_PLAYER_MENU_H @@ -18,6 +18,7 @@ #include "sideboard_menu.h" #include "utility_menu.h" +#include #include #include @@ -37,6 +38,7 @@ private slots: public: PlayerMenu(Player *player); + /// Lifecycle methods: delegate to all managedComponents, plus counters separately via player->getCounters(). void retranslateUi(); QMenu *updateCardMenu(const CardItem *card); @@ -66,7 +68,9 @@ public: return shortcutsActive; } + /// Delegates to all managedComponents, plus counters separately. void setShortcutsActive(); + /// Delegates to all managedComponents, plus counters separately. void setShortcutsInactive(); private: @@ -82,9 +86,26 @@ private: SayMenu *sayMenu; CustomZoneMenu *customZonesMenu; - bool shortcutsActive; + /// Drives AbstractPlayerComponent lifecycle delegation. Counters are iterated separately via player->getCounters(). + QList managedComponents; + bool shortcutsActive = false; - void initSayMenu(); + /// Creates component, adds it as a submenu of playerMenu, and registers in managedComponents. + template MenuT *addManagedMenu(Args &&...args) + { + auto *menu = new MenuT(std::forward(args)...); + playerMenu->addMenu(menu); + managedComponents.append(menu); + return menu; + } + + /// Creates component and registers in managedComponents, but does NOT add it as a submenu. + template ComponentT *createManagedComponent(Args &&...args) + { + auto *component = new ComponentT(std::forward(args)...); + managedComponents.append(component); + return component; + } }; #endif // COCKATRICE_PLAYER_MENU_H diff --git a/cockatrice/src/game/player/menu/rfg_menu.h b/cockatrice/src/game/player/menu/rfg_menu.h index 0b4623d2a..8f79b2f4a 100644 --- a/cockatrice/src/game/player/menu/rfg_menu.h +++ b/cockatrice/src/game/player/menu/rfg_menu.h @@ -8,19 +8,26 @@ #define COCKATRICE_RFG_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "abstract_player_component.h" #include #include class Player; -class RfgMenu : public TearOffMenu +class RfgMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT public: explicit RfgMenu(Player *player, QWidget *parent = nullptr); void createMoveActions(); void createViewActions(); - void retranslateUi(); + void retranslateUi() override; + void setShortcutsActive() override + { + } + void setShortcutsInactive() override + { + } QMenu *moveRfgMenu = nullptr; diff --git a/cockatrice/src/game/player/menu/say_menu.cpp b/cockatrice/src/game/player/menu/say_menu.cpp index 3c4802aa5..116fba49a 100644 --- a/cockatrice/src/game/player/menu/say_menu.cpp +++ b/cockatrice/src/game/player/menu/say_menu.cpp @@ -8,6 +8,31 @@ SayMenu::SayMenu(Player *_player) : player(_player) { connect(&SettingsCache::instance().messages(), &MessageSettings::messageMacrosChanged, this, &SayMenu::initSayMenu); initSayMenu(); + retranslateUi(); +} + +void SayMenu::retranslateUi() +{ + setTitle(tr("S&ay")); +} + +void SayMenu::setShortcutsActive() +{ + shortcutsActive = true; + + const auto menuActions = actions(); + for (int i = 0; i < menuActions.size() && i < 10; ++i) { + menuActions[i]->setShortcut(QKeySequence("Ctrl+" + QString::number((i + 1) % 10))); + } +} + +void SayMenu::setShortcutsInactive() +{ + shortcutsActive = false; + + for (auto *action : actions()) { + action->setShortcut(QKeySequence()); + } } void SayMenu::initSayMenu() @@ -19,10 +44,11 @@ void SayMenu::initSayMenu() for (int i = 0; i < count; ++i) { auto *newAction = new QAction(SettingsCache::instance().messages().getMessageAt(i), this); - if (i < 10) { - newAction->setShortcut(QKeySequence("Ctrl+" + QString::number((i + 1) % 10))); - } connect(newAction, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actSayMessage); addAction(newAction); } -} \ No newline at end of file + + if (shortcutsActive) { + setShortcutsActive(); + } +} diff --git a/cockatrice/src/game/player/menu/say_menu.h b/cockatrice/src/game/player/menu/say_menu.h index 5dbde2277..fadf5f368 100644 --- a/cockatrice/src/game/player/menu/say_menu.h +++ b/cockatrice/src/game/player/menu/say_menu.h @@ -7,18 +7,27 @@ #ifndef COCKATRICE_SAY_MENU_H #define COCKATRICE_SAY_MENU_H +#include "abstract_player_component.h" + #include class Player; -class SayMenu : public QMenu +class SayMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: explicit SayMenu(Player *player); + + void retranslateUi() override; + void setShortcutsActive() override; + void setShortcutsInactive() override; + +private slots: void initSayMenu(); private: Player *player; + bool shortcutsActive = false; }; #endif // COCKATRICE_SAY_MENU_H diff --git a/cockatrice/src/game/player/menu/sideboard_menu.h b/cockatrice/src/game/player/menu/sideboard_menu.h index 22d5a2d69..4a77d1b52 100644 --- a/cockatrice/src/game/player/menu/sideboard_menu.h +++ b/cockatrice/src/game/player/menu/sideboard_menu.h @@ -7,18 +7,20 @@ #ifndef COCKATRICE_SIDEBOARD_MENU_H #define COCKATRICE_SIDEBOARD_MENU_H +#include "abstract_player_component.h" + #include class Player; -class SideboardMenu : public QMenu +class SideboardMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: explicit SideboardMenu(Player *player, QMenu *playerMenu); - void retranslateUi(); - void setShortcutsActive(); - void setShortcutsInactive(); + void retranslateUi() override; + void setShortcutsActive() override; + void setShortcutsInactive() override; private: Player *player; diff --git a/cockatrice/src/game/player/menu/utility_menu.h b/cockatrice/src/game/player/menu/utility_menu.h index ff57e7252..f6577d7d1 100644 --- a/cockatrice/src/game/player/menu/utility_menu.h +++ b/cockatrice/src/game/player/menu/utility_menu.h @@ -7,17 +7,19 @@ #ifndef COCKATRICE_UTILITY_MENU_H #define COCKATRICE_UTILITY_MENU_H +#include "abstract_player_component.h" + #include class Player; -class UtilityMenu : public QMenu +class UtilityMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public slots: void populatePredefinedTokensMenu(); - void retranslateUi(); - void setShortcutsActive(); - void setShortcutsInactive(); + void retranslateUi() override; + void setShortcutsActive() override; + void setShortcutsInactive() override; public: explicit UtilityMenu(Player *player, QMenu *playerMenu); From 94ea574c76ea214d7d74910b71795f225b28b75e Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Tue, 24 Mar 2026 16:45:52 -0400 Subject: [PATCH 067/116] Add moveToTable context menu action and extract tableRowToGridY helper (#6738) Adds a Table option to the Move menu, allowing cards to be moved directly to the battlefield from any zone. Extracts the repeated tableRow-to-grid-Y conversion logic into TableZone::tableRowToGridY(), consolidating five call sites and fixing a latent bug where cards with tableRow > 2 could land on the wrong row. --- .../src/client/settings/shortcuts_settings.h | 3 ++ .../src/game/player/card_menu_action_type.h | 5 ++- cockatrice/src/game/player/menu/move_menu.cpp | 9 +++- cockatrice/src/game/player/menu/move_menu.h | 1 + cockatrice/src/game/player/player_actions.cpp | 45 ++++++++++++++----- cockatrice/src/game/zones/table_zone.cpp | 8 ++++ cockatrice/src/game/zones/table_zone.h | 7 +++ 7 files changed, 64 insertions(+), 14 deletions(-) diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index 51615745b..d0849042b 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -563,6 +563,9 @@ private: {"Player/aMoveToTopLibrary", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Top of Library"), parseSequenceString(""), ShortcutGroup::Move_selected)}, + {"Player/aMoveToTable", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Battlefield"), + parseSequenceString(""), + ShortcutGroup::Move_selected)}, {"Player/aViewHand", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Hand"), parseSequenceString(""), ShortcutGroup::View)}, {"Player/aViewGraveyard", diff --git a/cockatrice/src/game/player/card_menu_action_type.h b/cockatrice/src/game/player/card_menu_action_type.h index aec6d6397..1b63674fa 100644 --- a/cockatrice/src/game/player/card_menu_action_type.h +++ b/cockatrice/src/game/player/card_menu_action_type.h @@ -9,17 +9,20 @@ enum CardMenuActionType { + // Per-card attribute actions (must be <= cmClone for cardMenuAction() dispatch) cmTap, cmUntap, cmDoesntUntap, cmFlip, cmPeek, cmClone, + // Move actions (must be > cmClone for cardMenuAction() dispatch) cmMoveToTopLibrary, cmMoveToBottomLibrary, cmMoveToHand, cmMoveToGraveyard, - cmMoveToExile + cmMoveToExile, + cmMoveToTable }; #endif // COCKATRICE_CARD_MENU_ACTION_TYPE_H diff --git a/cockatrice/src/game/player/menu/move_menu.cpp b/cockatrice/src/game/player/menu/move_menu.cpp index d27e16009..91e2d8d10 100644 --- a/cockatrice/src/game/player/menu/move_menu.cpp +++ b/cockatrice/src/game/player/menu/move_menu.cpp @@ -11,6 +11,8 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to")) aMoveToBottomLibrary = new QAction(this); aMoveToBottomLibrary->setData(cmMoveToBottomLibrary); aMoveToXfromTopOfLibrary = new QAction(this); + aMoveToTable = new QAction(this); + aMoveToTable->setData(cmMoveToTable); aMoveToGraveyard = new QAction(this); aMoveToHand = new QAction(this); aMoveToHand->setData(cmMoveToHand); @@ -22,6 +24,7 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to")) connect(aMoveToBottomLibrary, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); connect(aMoveToXfromTopOfLibrary, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actMoveCardXCardsFromTop); + connect(aMoveToTable, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); connect(aMoveToHand, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); connect(aMoveToGraveyard, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); connect(aMoveToExile, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); @@ -30,6 +33,8 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to")) addAction(aMoveToXfromTopOfLibrary); addAction(aMoveToBottomLibrary); addSeparator(); + addAction(aMoveToTable); + addSeparator(); addAction(aMoveToHand); addSeparator(); addAction(aMoveToGraveyard); @@ -47,6 +52,7 @@ void MoveMenu::setShortcutsActive() aMoveToTopLibrary->setShortcuts(shortcuts.getShortcut("Player/aMoveToTopLibrary")); aMoveToBottomLibrary->setShortcuts(shortcuts.getShortcut("Player/aMoveToBottomLibrary")); + aMoveToTable->setShortcuts(shortcuts.getShortcut("Player/aMoveToTable")); aMoveToHand->setShortcuts(shortcuts.getShortcut("Player/aMoveToHand")); aMoveToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveToGraveyard")); aMoveToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveToExile")); @@ -57,7 +63,8 @@ void MoveMenu::retranslateUi() aMoveToTopLibrary->setText(tr("&Top of library in random order")); aMoveToXfromTopOfLibrary->setText(tr("X cards from the top of library...")); aMoveToBottomLibrary->setText(tr("&Bottom of library in random order")); + aMoveToTable->setText(tr("T&able")); aMoveToHand->setText(tr("&Hand")); aMoveToGraveyard->setText(tr("&Graveyard")); aMoveToExile->setText(tr("&Exile")); -} \ No newline at end of file +} diff --git a/cockatrice/src/game/player/menu/move_menu.h b/cockatrice/src/game/player/menu/move_menu.h index 5bf657fa4..dc39cb6a5 100644 --- a/cockatrice/src/game/player/menu/move_menu.h +++ b/cockatrice/src/game/player/menu/move_menu.h @@ -23,6 +23,7 @@ public: QAction *aMoveToBottomLibrary = nullptr; QAction *aMoveToHand = nullptr; + QAction *aMoveToTable = nullptr; QAction *aMoveToGraveyard = nullptr; QAction *aMoveToExile = nullptr; }; diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 287231402..cee04ac6b 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -75,7 +75,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown) cmd.set_y(0); } else { tableRow = faceDown ? 2 : info.getUiAttributes().tableRow; - QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - tableRow)); + QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow)); cardToMove->set_face_down(faceDown); if (!faceDown) { cardToMove->set_pt(info.getPowTough().toStdString()); @@ -114,12 +114,7 @@ void PlayerActions::playCardToTable(const CardItem *card, bool faceDown) const CardInfo &info = exactCard.getInfo(); int tableRow = faceDown ? 2 : info.getUiAttributes().tableRow; - // default instant/sorcery cards to the noncreatures row - if (tableRow > 2) { - tableRow = 1; - } - - QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - tableRow)); + QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow)); cardToMove->set_face_down(faceDown); if (!faceDown) { cardToMove->set_pt(info.getPowTough().toStdString()); @@ -866,7 +861,7 @@ void PlayerActions::actCreateToken() ExactCard correctedCard = CardDatabaseManager::query()->guessCard({lastTokenInfo.name, lastTokenInfo.providerId}); if (correctedCard) { lastTokenInfo.name = correctedCard.getName(); - lastTokenTableRow = TableZone::clampValidTableRow(2 - correctedCard.getInfo().getUiAttributes().tableRow); + lastTokenTableRow = TableZone::tableRowToGridY(correctedCard.getInfo().getUiAttributes().tableRow); if (lastTokenInfo.pt.isEmpty()) { lastTokenInfo.pt = correctedCard.getInfo().getPowTough(); } @@ -917,7 +912,7 @@ void PlayerActions::setLastToken(CardInfoPtr cardInfo) .providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())}; - lastTokenTableRow = TableZone::clampValidTableRow(2 - cardInfo->getUiAttributes().tableRow); + lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow); utilityMenu->setAndEnableCreateAnotherTokenAction(tr("C&reate another %1 token").arg(lastTokenInfo.name)); } @@ -1085,9 +1080,7 @@ void PlayerActions::createCard(const CardItem *sourceCard, return; } - // get the target token's location - // TODO: Define this QPoint into its own function along with the one below - QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - cardInfo->getUiAttributes().tableRow)); + QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow)); // create the token for the related card Command_CreateToken cmd; @@ -1930,6 +1923,34 @@ void PlayerActions::cardMenuAction() commandList.append(cmd); break; } + case cmMoveToTable: { + // Each card needs its own command because table row, pt, and cipt vary per card + for (const auto &card : cardList) { + auto *cmd = new Command_MoveCard; + cmd->set_start_player_id(startPlayerId); + cmd->set_start_zone(startZone.toStdString()); + cmd->set_target_player_id(player->getPlayerInfo()->getId()); + cmd->set_target_zone(ZoneNames::TABLE); + cmd->set_x(-1); + + CardToMove *ctm = cmd->mutable_cards_to_move()->add_card(); + ctm->set_card_id(card->getId()); + ctm->set_face_down(false); + + int tableRow = 0; + ExactCard exactCard = card->getCard(); + if (exactCard) { + const CardInfo &info = exactCard.getInfo(); + tableRow = info.getUiAttributes().tableRow; + ctm->set_pt(info.getPowTough().toStdString()); + ctm->set_tapped(info.getUiAttributes().cipt); + } + + cmd->set_y(TableZone::tableRowToGridY(tableRow)); + commandList.append(cmd); + } + break; + } default: break; } diff --git a/cockatrice/src/game/zones/table_zone.cpp b/cockatrice/src/game/zones/table_zone.cpp index b6ac2150b..2a382fafe 100644 --- a/cockatrice/src/game/zones/table_zone.cpp +++ b/cockatrice/src/game/zones/table_zone.cpp @@ -382,3 +382,11 @@ int TableZone::clampValidTableRow(const int row) return TABLEROWS - 1; return row; } + +int TableZone::tableRowToGridY(int tableRow) +{ + if (tableRow > 2) { + tableRow = 1; + } + return clampValidTableRow(2 - tableRow); +} diff --git a/cockatrice/src/game/zones/table_zone.h b/cockatrice/src/game/zones/table_zone.h index 61eb48d7b..7a53a9eb4 100644 --- a/cockatrice/src/game/zones/table_zone.h +++ b/cockatrice/src/game/zones/table_zone.h @@ -151,6 +151,13 @@ public: static int clampValidTableRow(const int row); + /** + * Converts a card's logical table row (0=creatures, 1=noncreatures, 2=lands) + * to the corresponding grid Y coordinate. Cards with tableRow > 2 (e.g., + * instants/sorceries) default to the noncreatures row. + */ + static int tableRowToGridY(int tableRow); + /** Resizes the TableZone in case CardItems are within or outside of the TableZone constraints. From 5ef428b9d0fec65b1b264023ce2b2196945ea7c5 Mon Sep 17 00:00:00 2001 From: scotland0208 Date: Wed, 25 Mar 2026 16:15:08 -0500 Subject: [PATCH 068/116] Add visual indicator to toggle untap button (#6737) * Add visual indicator to toggle untap button * Rename button to match tooltip * Change name of string in shortcut settings --- cockatrice/src/client/settings/shortcuts_settings.h | 2 +- cockatrice/src/game/player/menu/card_menu.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index d0849042b..1de73c165 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -501,7 +501,7 @@ private: {"Player/aUntapAll", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Untap All"), parseSequenceString("Ctrl+U"), ShortcutGroup::Playing_Area)}, - {"Player/aDoesntUntap", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Untap"), + {"Player/aDoesntUntap", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Skip Untapping"), parseSequenceString("Alt+U"), ShortcutGroup::Playing_Area)}, {"Player/aFlip", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Turn Card Over"), diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp index cd77c2968..f2479e1da 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game/player/menu/card_menu.cpp @@ -35,6 +35,8 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive connect(aTap, &QAction::triggered, playerActions, &PlayerActions::cardMenuAction); aDoesntUntap = new QAction(this); aDoesntUntap->setData(cmDoesntUntap); + aDoesntUntap->setCheckable(true); + aDoesntUntap->setChecked(card != nullptr && card->getDoesntUntap()); connect(aDoesntUntap, &QAction::triggered, playerActions, &PlayerActions::cardMenuAction); aAttach = new QAction(this); connect(aAttach, &QAction::triggered, playerActions, &PlayerActions::actAttach); @@ -449,7 +451,7 @@ void CardMenu::retranslateUi() aRevealToAll->setText(tr("&All players")); //: Turn sideways or back again aTap->setText(tr("&Tap / Untap")); - aDoesntUntap->setText(tr("Toggle &normal untapping")); + aDoesntUntap->setText(tr("Skip &untapping")); //: Turn face up/face down aFlip->setText(tr("T&urn Over")); // Only the user facing names in client got renamed to "turn over" // All code and proto bits are still unchanged (flip) for compatibility reasons From dd053c76dfc357896b7a49a5ab636161b32a7aa7 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Wed, 25 Mar 2026 18:03:59 -0400 Subject: [PATCH 069/116] [Game] Improve context menus and fix face-down play from stack (#6739) Reorganize card context menus across table, stack, and graveyard/exile zones for better consistency: promote Draw Arrow and Clone actions, move related card entries to the bottom, add Play/Play Face Down to the stack menu, and flatten if/else blocks with early returns. Also fix playCard() ignoring the faceDown flag when routing instants/sorceries from the stack, which sent them to the graveyard instead of the table. --- cockatrice/src/game/player/menu/card_menu.cpp | 79 ++++++++++--------- cockatrice/src/game/player/player_actions.cpp | 2 +- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp index f2479e1da..66ca5e46b 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game/player/menu/card_menu.cpp @@ -110,6 +110,7 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive if (revealedCard) { addAction(aHide); + addSeparator(); addAction(aClone); addSeparator(); addAction(aSelectAll); @@ -148,16 +149,14 @@ void CardMenu::createTableMenu(bool canModifyCard) { // Card is on the battlefield if (!canModifyCard) { - addRelatedCardView(); - addRelatedCardActions(); - - addSeparator(); addAction(aDrawArrow); addSeparator(); addAction(aClone); addSeparator(); addAction(aSelectAll); addAction(aSelectRow); + addRelatedCardView(); + addRelatedCardActions(); return; } @@ -167,10 +166,9 @@ void CardMenu::createTableMenu(bool canModifyCard) if (card->getFaceDown()) { addAction(aPeek); } - - addRelatedCardView(); - addRelatedCardActions(); - + addSeparator(); + addAction(aClone); + addMenu(new MoveMenu(player)); addSeparator(); addAction(aAttach); if (card->getAttachedTo()) { @@ -181,9 +179,6 @@ void CardMenu::createTableMenu(bool canModifyCard) addMenu(new PtMenu(player)); addAction(aSetAnnotation); addSeparator(); - addAction(aClone); - addMenu(new MoveMenu(player)); - addSeparator(); addAction(aSelectAll); addAction(aSelectRow); @@ -199,27 +194,34 @@ void CardMenu::createTableMenu(bool canModifyCard) } addSeparator(); addMenu(mCardCounters); + addRelatedCardView(); + addRelatedCardActions(); } void CardMenu::createStackMenu(bool canModifyCard) { // Card is on the stack - if (canModifyCard) { - addAction(aAttach); - addAction(aDrawArrow); - addSeparator(); - addAction(aClone); - addMenu(new MoveMenu(player)); - addSeparator(); - addAction(aSelectAll); - } else { + if (!canModifyCard) { addAction(aDrawArrow); addSeparator(); addAction(aClone); addSeparator(); addAction(aSelectAll); + addRelatedCardView(); + addRelatedCardActions(); + return; } + addAction(aPlay); + addAction(aPlayFacedown); + addSeparator(); + addAction(aClone); + addMenu(new MoveMenu(player)); + addSeparator(); + addAction(aAttach); + addAction(aDrawArrow); + addSeparator(); + addAction(aSelectAll); addRelatedCardView(); addRelatedCardActions(); } @@ -227,29 +229,29 @@ void CardMenu::createStackMenu(bool canModifyCard) void CardMenu::createGraveyardOrExileMenu(bool canModifyCard) { // Card is in the graveyard or exile - if (canModifyCard) { - addAction(aPlay); - addAction(aPlayFacedown); - - addSeparator(); - addAction(aClone); - addMenu(new MoveMenu(player)); - addSeparator(); - addAction(aSelectAll); - addAction(aSelectColumn); - - addSeparator(); - addAction(aAttach); + if (!canModifyCard) { addAction(aDrawArrow); - } else { + addSeparator(); addAction(aClone); addSeparator(); addAction(aSelectAll); addAction(aSelectColumn); - addSeparator(); - addAction(aDrawArrow); + addRelatedCardView(); + addRelatedCardActions(); + return; } + addAction(aPlay); + addAction(aPlayFacedown); + addSeparator(); + addAction(aClone); + addMenu(new MoveMenu(player)); + addSeparator(); + addAction(aAttach); + addAction(aDrawArrow); + addSeparator(); + addAction(aSelectAll); + addAction(aSelectColumn); addRelatedCardView(); addRelatedCardActions(); } @@ -259,12 +261,11 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard) if (!canModifyCard) { addAction(aDrawArrow); addSeparator(); - addRelatedCardView(); - addRelatedCardActions(); - addSeparator(); addAction(aClone); addSeparator(); addAction(aSelectAll); + addRelatedCardView(); + addRelatedCardActions(); return; } diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index cee04ac6b..ca0967636 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -64,7 +64,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown) int tableRow = info.getUiAttributes().tableRow; bool playToStack = SettingsCache::instance().getPlayToStack(); QString currentZone = card->getZone()->getName(); - if (currentZone == ZoneNames::STACK && tableRow == 3) { + if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) { cmd.set_target_zone(ZoneNames::GRAVE); cmd.set_x(0); cmd.set_y(0); From 74cce5ccb2fbae2c4a6a424600b6ca4b155f2402 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:12:49 -0700 Subject: [PATCH 070/116] [SettingsManager] Properly handle multithreaded access (#6747) --- .../client/settings/card_counter_settings.cpp | 4 +++- .../settings/settings_manager.cpp | 24 ++++++++++++++++--- .../libcockatrice/settings/settings_manager.h | 5 +++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cockatrice/src/client/settings/card_counter_settings.cpp b/cockatrice/src/client/settings/card_counter_settings.cpp index 71ce4cfc6..399365c99 100644 --- a/cockatrice/src/client/settings/card_counter_settings.cpp +++ b/cockatrice/src/client/settings/card_counter_settings.cpp @@ -11,6 +11,8 @@ CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *p void CardCounterSettings::setColor(int counterId, const QColor &color) { + QSettings settings = getSettings(); + QString key = QString("cards/counters/%1/color").arg(counterId); if (settings.value(key).value() == color) @@ -36,7 +38,7 @@ QColor CardCounterSettings::color(int counterId) const defaultColor = QColor::fromHsv(h, s, v); } - return settings.value(QString("cards/counters/%1/color").arg(counterId), defaultColor).value(); + return getSettings().value(QString("cards/counters/%1/color").arg(counterId), defaultColor).value(); } QString CardCounterSettings::displayName(int counterId) const diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp index ede5a2027..3f3deb638 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp @@ -1,16 +1,22 @@ #include "settings_manager.h" -SettingsManager::SettingsManager(const QString &settingPath, +SettingsManager::SettingsManager(const QString &_settingPath, const QString &_defaultGroup, const QString &_defaultSubGroup, QObject *parent) - : QObject(parent), settings(settingPath, QSettings::IniFormat), defaultGroup(_defaultGroup), - defaultSubGroup(_defaultSubGroup) + : QObject(parent), settingPath(_settingPath), defaultGroup(_defaultGroup), defaultSubGroup(_defaultSubGroup) { } +QSettings SettingsManager::getSettings() const +{ + return QSettings(settingPath, QSettings::IniFormat); +} + void SettingsManager::setValue(const QVariant &value, const QString &name) { + auto settings = getSettings(); + if (!defaultGroup.isEmpty()) { settings.beginGroup(defaultGroup); } @@ -35,6 +41,8 @@ void SettingsManager::setValue(const QVariant &value, const QString &group, const QString &subGroup) { + auto settings = getSettings(); + if (!group.isEmpty()) { settings.beginGroup(group); } @@ -56,6 +64,8 @@ void SettingsManager::setValue(const QVariant &value, void SettingsManager::deleteValue(const QString &name) { + auto settings = getSettings(); + if (!defaultGroup.isEmpty()) { settings.beginGroup(defaultGroup); } @@ -77,6 +87,8 @@ void SettingsManager::deleteValue(const QString &name) void SettingsManager::deleteValue(const QString &name, const QString &group, const QString &subGroup) { + auto settings = getSettings(); + if (!group.isEmpty()) { settings.beginGroup(group); } @@ -98,6 +110,8 @@ void SettingsManager::deleteValue(const QString &name, const QString &group, con QVariant SettingsManager::getValue(const QString &name) { + auto settings = getSettings(); + if (!defaultGroup.isEmpty()) { settings.beginGroup(defaultGroup); } @@ -121,6 +135,8 @@ QVariant SettingsManager::getValue(const QString &name) QVariant SettingsManager::getValue(const QString &name, const QString &group, const QString &subGroup) { + auto settings = getSettings(); + if (!group.isEmpty()) { settings.beginGroup(group); } @@ -147,5 +163,7 @@ QVariant SettingsManager::getValue(const QString &name, const QString &group, co */ void SettingsManager::sync() { + auto settings = getSettings(); + settings.sync(); } \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.h b/libcockatrice_settings/libcockatrice/settings/settings_manager.h index 3592d8f8c..18abb8dbf 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_manager.h +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.h @@ -24,9 +24,12 @@ public: void sync(); protected: - QSettings settings; + QString settingPath; QString defaultGroup; QString defaultSubGroup; + + QSettings getSettings() const; + void setValue(const QVariant &value, const QString &name); void setValue(const QVariant &value, const QString &name, const QString &group, const QString &subGroup = QString()); From abf6e72ad1b341feda04676554a751cdec97169c Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Fri, 27 Mar 2026 18:08:51 +0100 Subject: [PATCH 071/116] add a nulcheck in the card item animation timer (#6740) --- cockatrice/src/game/board/card_item.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cockatrice/src/game/board/card_item.cpp b/cockatrice/src/game/board/card_item.cpp index 62de4a02e..cf3c7db20 100644 --- a/cockatrice/src/game/board/card_item.cpp +++ b/cockatrice/src/game/board/card_item.cpp @@ -460,6 +460,9 @@ void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) bool CardItem::animationEvent() { + if (owner == nullptr) { + return false; + } int rotation = ROTATION_DEGREES_PER_FRAME; bool animationIncomplete = true; if (!tapped) From 42bd8164a0b9170a3ecfe68b43ca399e1e08e4a7 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Fri, 27 Mar 2026 18:10:29 +0100 Subject: [PATCH 072/116] remove hardcoded white in vde banner widget (#6684) * remove hardcoded white in vde banner widget * set text color to white on higher opacities --- .../widgets/general/display/banner_widget.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/interface/widgets/general/display/banner_widget.cpp b/cockatrice/src/interface/widgets/general/display/banner_widget.cpp index f03869a4d..5de5457ea 100644 --- a/cockatrice/src/interface/widgets/general/display/banner_widget.cpp +++ b/cockatrice/src/interface/widgets/general/display/banner_widget.cpp @@ -7,8 +7,8 @@ #include #include -BannerWidget::BannerWidget(QWidget *parent, const QString &text, Qt::Orientation orientation, int transparency) - : QWidget(parent), gradientOrientation(orientation), transparency(qBound(0, transparency, 100)) +BannerWidget::BannerWidget(QWidget *parent, const QString &text, Qt::Orientation orientation, int transparency_) + : QWidget(parent), gradientOrientation(orientation), transparency(qBound(0, transparency_, 100)) { auto layout = new QHBoxLayout(this); @@ -18,7 +18,12 @@ BannerWidget::BannerWidget(QWidget *parent, const QString &text, Qt::Orientation // Create the banner label and set properties bannerLabel = new QLabel(text, this); bannerLabel->setAlignment(Qt::AlignCenter); - bannerLabel->setStyleSheet("font-size: 24px; font-weight: bold; color: white;"); + + QString textColor; + if (transparency > 50) { + textColor = " color: white;"; + } + bannerLabel->setStyleSheet("font-size: 24px; font-weight: bold;" + textColor); layout->addWidget(iconLabel); layout->addWidget(bannerLabel); From d8e3807ec50be2b384b2c07a552f9efd315b2bb1 Mon Sep 17 00:00:00 2001 From: tooomm Date: Fri, 27 Mar 2026 18:11:56 +0100 Subject: [PATCH 073/116] Dependabot: Enable git submodules tracking (#6727) * Enable gitsubmodules * update comment --- .github/dependabot.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1e278a418..fc25af67f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,19 +2,18 @@ version: 2 updates: - # # Enable version updates for git submodules - # Not yet possible to bump only on tags or releases, see: + # Enable version updates for git submodules + # If SemVer is used, updates will happen to new releases only (not HEAD) # https://github.com/dependabot/dependabot-core/issues/1639 # https://github.com/dependabot/dependabot-core/issues/2192 - # Alternative: Action that updates submodule and can be manually run on demand (workflow_dispatch) - # - package-ecosystem: "gitsubmodule" - # # Look for `.gitmodules` in the `root` directory - # directory: "/" - # # Check for updates once a month - # schedule: - # interval: "monthly" - # # Limit the amout of open PR's (default = 5, disabled = 0, security updates are not impacted) - # open-pull-requests-limit: 1 + - package-ecosystem: "gitsubmodule" + # Look for `.gitmodules` in the `root` directory + directory: "/" + # Check for updates once a month + schedule: + interval: "monthly" + # Limit the amout of open PR's (default = 5, disabled = 0, security updates are not impacted) + open-pull-requests-limit: 2 # # Enable version updates for Docker # Not yet possible to bump from one LTS version to the next and skip others, see: From 34a5b8b9ce95fce726301819638f52d1781b8088 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:13:25 -0700 Subject: [PATCH 074/116] [SettingsManager] Make setting getters const (#6748) * [SettingsManager] Make setting getters const * remove hashGameType from header --- .../interface_card_set_priority_controller.h | 6 +-- .../noop_card_set_priority_controller.h | 6 +-- .../settings/card_database_settings.cpp | 6 +-- .../settings/card_database_settings.h | 6 +-- .../settings/card_override_settings.cpp | 2 +- .../settings/card_override_settings.h | 2 +- .../libcockatrice/settings/debug_settings.cpp | 8 ++-- .../libcockatrice/settings/debug_settings.h | 8 ++-- .../settings/download_settings.cpp | 2 +- .../settings/download_settings.h | 2 +- .../settings/game_filters_settings.cpp | 38 +++++++++---------- .../settings/game_filters_settings.h | 36 +++++++++--------- .../settings/layouts_settings.cpp | 26 ++++++------- .../libcockatrice/settings/layouts_settings.h | 26 ++++++------- .../settings/message_settings.cpp | 4 +- .../libcockatrice/settings/message_settings.h | 4 +- .../settings/recents_settings.cpp | 4 +- .../libcockatrice/settings/recents_settings.h | 4 +- .../settings/servers_settings.cpp | 26 ++++++------- .../libcockatrice/settings/servers_settings.h | 26 ++++++------- .../settings/settings_manager.cpp | 4 +- .../libcockatrice/settings/settings_manager.h | 4 +- 22 files changed, 124 insertions(+), 126 deletions(-) diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_set_priority_controller.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_set_priority_controller.h index 46a5897f7..b8fbbc74a 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_set_priority_controller.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_set_priority_controller.h @@ -12,9 +12,9 @@ public: virtual void setEnabled(QString shortName, bool enabled) = 0; virtual void setIsKnown(QString shortName, bool isknown) = 0; - virtual unsigned int getSortKey(QString shortName) = 0; - virtual bool isEnabled(QString shortName) = 0; - virtual bool isKnown(QString shortName) = 0; + virtual unsigned int getSortKey(QString shortName) const = 0; + virtual bool isEnabled(QString shortName) const = 0; + virtual bool isKnown(QString shortName) const = 0; }; #endif // COCKATRICE_INTERFACE_CARD_SET_PRIORITY_CONTROLLER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_set_priority_controller.h b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_set_priority_controller.h index 949ab5a91..e5027648c 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_set_priority_controller.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_set_priority_controller.h @@ -16,15 +16,15 @@ public: { } - unsigned int getSortKey(QString /* shortName */) override + unsigned int getSortKey(QString /* shortName */) const override { return 0; } - bool isEnabled(QString /* shortName */) override + bool isEnabled(QString /* shortName */) const override { return true; } - bool isKnown(QString /* shortName */) override + bool isKnown(QString /* shortName */) const override { return true; } diff --git a/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp b/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp index 79738f1cd..26a91a4dd 100644 --- a/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp @@ -20,17 +20,17 @@ void CardDatabaseSettings::setIsKnown(QString shortName, bool isknown) setValue(isknown, "isknown", "sets", std::move(shortName)); } -unsigned int CardDatabaseSettings::getSortKey(QString shortName) +unsigned int CardDatabaseSettings::getSortKey(QString shortName) const { return getValue("sortkey", "sets", std::move(shortName)).toUInt(); } -bool CardDatabaseSettings::isEnabled(QString shortName) +bool CardDatabaseSettings::isEnabled(QString shortName) const { return getValue("enabled", "sets", std::move(shortName)).toBool(); } -bool CardDatabaseSettings::isKnown(QString shortName) +bool CardDatabaseSettings::isKnown(QString shortName) const { return getValue("isknown", "sets", std::move(shortName)).toBool(); } diff --git a/libcockatrice_settings/libcockatrice/settings/card_database_settings.h b/libcockatrice_settings/libcockatrice/settings/card_database_settings.h index 9a176a99b..bb946ea80 100644 --- a/libcockatrice_settings/libcockatrice/settings/card_database_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/card_database_settings.h @@ -22,9 +22,9 @@ public: void setEnabled(QString shortName, bool enabled) override; void setIsKnown(QString shortName, bool isknown) override; - unsigned int getSortKey(QString shortName) override; - bool isEnabled(QString shortName) override; - bool isKnown(QString shortName) override; + unsigned int getSortKey(QString shortName) const override; + bool isEnabled(QString shortName) const override; + bool isKnown(QString shortName) const override; private: explicit CardDatabaseSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/card_override_settings.cpp b/libcockatrice_settings/libcockatrice/settings/card_override_settings.cpp index 894358be6..a61a4693b 100644 --- a/libcockatrice_settings/libcockatrice/settings/card_override_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/card_override_settings.cpp @@ -15,7 +15,7 @@ void CardOverrideSettings::deleteCardPreferenceOverride(const QString &cardName) deleteValue(cardName); } -QString CardOverrideSettings::getCardPreferenceOverride(const QString &cardName) +QString CardOverrideSettings::getCardPreferenceOverride(const QString &cardName) const { return getValue(cardName).toString(); } \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/card_override_settings.h b/libcockatrice_settings/libcockatrice/settings/card_override_settings.h index d5ee0287b..3d9db4e65 100644 --- a/libcockatrice_settings/libcockatrice/settings/card_override_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/card_override_settings.h @@ -22,7 +22,7 @@ public: void deleteCardPreferenceOverride(const QString &cardName); - QString getCardPreferenceOverride(const QString &cardName); + QString getCardPreferenceOverride(const QString &cardName) const; private: explicit CardOverrideSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/debug_settings.cpp b/libcockatrice_settings/libcockatrice/settings/debug_settings.cpp index 084696dc1..5bf6eca30 100644 --- a/libcockatrice_settings/libcockatrice/settings/debug_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/debug_settings.cpp @@ -11,22 +11,22 @@ DebugSettings::DebugSettings(const QString &settingPath, QObject *parent) } } -bool DebugSettings::getShowCardId() +bool DebugSettings::getShowCardId() const { return getValue("showCardId").toBool(); } -bool DebugSettings::getLocalGameOnStartup() +bool DebugSettings::getLocalGameOnStartup() const { return getValue("onStartup", "localgame").toBool(); } -int DebugSettings::getLocalGamePlayerCount() +int DebugSettings::getLocalGamePlayerCount() const { return getValue("playerCount", "localgame").toInt(); } -QString DebugSettings::getDeckPathForPlayer(const QString &playerName) +QString DebugSettings::getDeckPathForPlayer(const QString &playerName) const { return getValue(playerName, "localgame", "deck").toString(); } \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/debug_settings.h b/libcockatrice_settings/libcockatrice/settings/debug_settings.h index 2087b16b3..30cdd5fa5 100644 --- a/libcockatrice_settings/libcockatrice/settings/debug_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/debug_settings.h @@ -17,12 +17,12 @@ class DebugSettings : public SettingsManager DebugSettings(const DebugSettings & /*other*/); public: - bool getShowCardId(); + bool getShowCardId() const; - bool getLocalGameOnStartup(); - int getLocalGamePlayerCount(); + bool getLocalGameOnStartup() const; + int getLocalGamePlayerCount() const; - QString getDeckPathForPlayer(const QString &playerName); + QString getDeckPathForPlayer(const QString &playerName) const; }; #endif // DEBUG_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp index ad4b81ec5..66525a598 100644 --- a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp @@ -18,7 +18,7 @@ void DownloadSettings::setDownloadUrls(const QStringList &downloadURLs) setValue(QVariant::fromValue(downloadURLs), "urls"); } -QStringList DownloadSettings::getAllURLs() +QStringList DownloadSettings::getAllURLs() const { return getValue("urls").toStringList(); } diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.h b/libcockatrice_settings/libcockatrice/settings/download_settings.h index ed3634ea1..b7442301e 100644 --- a/libcockatrice_settings/libcockatrice/settings/download_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.h @@ -19,7 +19,7 @@ class DownloadSettings : public SettingsManager public: explicit DownloadSettings(const QString &, QObject *); - QStringList getAllURLs(); + QStringList getAllURLs() const; void setDownloadUrls(const QStringList &downloadURLs); void resetToDefaultURLs(); }; diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp index e5db3010d..4f5bf52ee 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp @@ -8,11 +8,11 @@ GameFiltersSettings::GameFiltersSettings(const QString &settingPath, QObject *pa { } -/* +/** * The game type might contain special characters, so to use it in * QSettings we just hash it. */ -QString GameFiltersSettings::hashGameType(const QString &gameType) const +static QString hashGameType(const QString &gameType) { return QCryptographicHash::hash(gameType.toUtf8(), QCryptographicHash::Md5).toHex(); } @@ -22,7 +22,7 @@ void GameFiltersSettings::setHideBuddiesOnlyGames(bool hide) setValue(hide, "hide_buddies_only_games"); } -bool GameFiltersSettings::isHideBuddiesOnlyGames() +bool GameFiltersSettings::isHideBuddiesOnlyGames() const { QVariant previous = getValue("hide_buddies_only_games"); return previous == QVariant() ? false : previous.toBool(); @@ -33,7 +33,7 @@ void GameFiltersSettings::setHideFullGames(bool hide) setValue(hide, "hide_full_games"); } -bool GameFiltersSettings::isHideFullGames() +bool GameFiltersSettings::isHideFullGames() const { QVariant previous = getValue("hide_full_games"); return previous == QVariant() ? false : previous.toBool(); @@ -44,7 +44,7 @@ void GameFiltersSettings::setHideGamesThatStarted(bool hide) setValue(hide, "hide_games_that_started"); } -bool GameFiltersSettings::isHideGamesThatStarted() +bool GameFiltersSettings::isHideGamesThatStarted() const { QVariant previous = getValue("hide_games_that_started"); return previous == QVariant() ? false : previous.toBool(); @@ -55,7 +55,7 @@ void GameFiltersSettings::setHidePasswordProtectedGames(bool hide) setValue(hide, "hide_password_protected_games"); } -bool GameFiltersSettings::isHidePasswordProtectedGames() +bool GameFiltersSettings::isHidePasswordProtectedGames() const { QVariant previous = getValue("hide_password_protected_games"); return previous == QVariant() ? false : previous.toBool(); @@ -66,7 +66,7 @@ void GameFiltersSettings::setHideIgnoredUserGames(bool hide) setValue(hide, "hide_ignored_user_games"); } -bool GameFiltersSettings::isHideIgnoredUserGames() +bool GameFiltersSettings::isHideIgnoredUserGames() const { QVariant previous = getValue("hide_ignored_user_games"); return previous == QVariant() ? true : previous.toBool(); @@ -77,7 +77,7 @@ void GameFiltersSettings::setHideNotBuddyCreatedGames(bool hide) setValue(hide, "hide_not_buddy_created_games"); } -bool GameFiltersSettings::isHideNotBuddyCreatedGames() +bool GameFiltersSettings::isHideNotBuddyCreatedGames() const { QVariant previous = getValue("hide_not_buddy_created_games"); return previous == QVariant() ? false : previous.toBool(); @@ -88,7 +88,7 @@ void GameFiltersSettings::setHideOpenDecklistGames(bool hide) setValue(hide, "hide_open_decklist_games"); } -bool GameFiltersSettings::isHideOpenDecklistGames() +bool GameFiltersSettings::isHideOpenDecklistGames() const { QVariant previous = getValue("hide_open_decklist_games"); return previous == QVariant() ? false : previous.toBool(); @@ -99,7 +99,7 @@ void GameFiltersSettings::setGameNameFilter(QString gameName) setValue(gameName, "game_name_filter"); } -QString GameFiltersSettings::getGameNameFilter() +QString GameFiltersSettings::getGameNameFilter() const { return getValue("game_name_filter").toString(); } @@ -109,7 +109,7 @@ void GameFiltersSettings::setCreatorNameFilters(QStringList creatorName) setValue(creatorName, "creator_name_filter"); } -QStringList GameFiltersSettings::getCreatorNameFilters() +QStringList GameFiltersSettings::getCreatorNameFilters() const { return getValue("creator_name_filter").toStringList(); } @@ -119,7 +119,7 @@ void GameFiltersSettings::setMinPlayers(int min) setValue(min, "min_players"); } -int GameFiltersSettings::getMinPlayers() +int GameFiltersSettings::getMinPlayers() const { QVariant previous = getValue("min_players"); return previous == QVariant() ? 1 : previous.toInt(); @@ -130,7 +130,7 @@ void GameFiltersSettings::setMaxPlayers(int max) setValue(max, "max_players"); } -int GameFiltersSettings::getMaxPlayers() +int GameFiltersSettings::getMaxPlayers() const { QVariant previous = getValue("max_players"); return previous == QVariant() ? 99 : previous.toInt(); @@ -141,7 +141,7 @@ void GameFiltersSettings::setMaxGameAge(const QTime &maxGameAge) setValue(maxGameAge, "max_game_age_time"); } -QTime GameFiltersSettings::getMaxGameAge() +QTime GameFiltersSettings::getMaxGameAge() const { QVariant previous = getValue("max_game_age_time"); return previous.toTime(); @@ -157,7 +157,7 @@ void GameFiltersSettings::setGameHashedTypeEnabled(QString gametypeHASHED, bool setValue(enabled, gametypeHASHED); } -bool GameFiltersSettings::isGameTypeEnabled(QString gametype) +bool GameFiltersSettings::isGameTypeEnabled(QString gametype) const { QVariant previous = getValue("game_type/" + hashGameType(gametype)); return previous == QVariant() ? false : previous.toBool(); @@ -168,7 +168,7 @@ void GameFiltersSettings::setShowOnlyIfSpectatorsCanWatch(bool show) setValue(show, "show_only_if_spectators_can_watch"); } -bool GameFiltersSettings::isShowOnlyIfSpectatorsCanWatch() +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanWatch() const { QVariant previous = getValue("show_only_if_spectators_can_watch"); return previous == QVariant() ? false : previous.toBool(); @@ -179,7 +179,7 @@ void GameFiltersSettings::setShowSpectatorPasswordProtected(bool show) setValue(show, "show_spectator_password_protected"); } -bool GameFiltersSettings::isShowSpectatorPasswordProtected() +bool GameFiltersSettings::isShowSpectatorPasswordProtected() const { QVariant previous = getValue("show_spectator_password_protected"); return previous == QVariant() ? false : previous.toBool(); @@ -190,7 +190,7 @@ void GameFiltersSettings::setShowOnlyIfSpectatorsCanChat(bool show) setValue(show, "show_only_if_spectators_can_chat"); } -bool GameFiltersSettings::isShowOnlyIfSpectatorsCanChat() +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanChat() const { QVariant previous = getValue("show_only_if_spectators_can_chat"); return previous == QVariant() ? false : previous.toBool(); @@ -201,7 +201,7 @@ void GameFiltersSettings::setShowOnlyIfSpectatorsCanSeeHands(bool show) setValue(show, "show_only_if_spectators_can_see_hands"); } -bool GameFiltersSettings::isShowOnlyIfSpectatorsCanSeeHands() +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanSeeHands() const { QVariant previous = getValue("show_only_if_spectators_can_see_hands"); return previous == QVariant() ? false : previous.toBool(); diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h index 45e9b7441..c0e60551a 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h @@ -16,23 +16,23 @@ class GameFiltersSettings : public SettingsManager friend class SettingsCache; public: - bool isHideBuddiesOnlyGames(); - bool isHideFullGames(); - bool isHideGamesThatStarted(); - bool isHidePasswordProtectedGames(); - bool isHideIgnoredUserGames(); - bool isHideNotBuddyCreatedGames(); - bool isHideOpenDecklistGames(); - QString getGameNameFilter(); - QStringList getCreatorNameFilters(); - int getMinPlayers(); - int getMaxPlayers(); - QTime getMaxGameAge(); - bool isGameTypeEnabled(QString gametype); - bool isShowOnlyIfSpectatorsCanWatch(); - bool isShowSpectatorPasswordProtected(); - bool isShowOnlyIfSpectatorsCanChat(); - bool isShowOnlyIfSpectatorsCanSeeHands(); + bool isHideBuddiesOnlyGames() const; + bool isHideFullGames() const; + bool isHideGamesThatStarted() const; + bool isHidePasswordProtectedGames() const; + bool isHideIgnoredUserGames() const; + bool isHideNotBuddyCreatedGames() const; + bool isHideOpenDecklistGames() const; + QString getGameNameFilter() const; + QStringList getCreatorNameFilters() const; + int getMinPlayers() const; + int getMaxPlayers() const; + QTime getMaxGameAge() const; + bool isGameTypeEnabled(QString gametype) const; + bool isShowOnlyIfSpectatorsCanWatch() const; + bool isShowSpectatorPasswordProtected() const; + bool isShowOnlyIfSpectatorsCanChat() const; + bool isShowOnlyIfSpectatorsCanSeeHands() const; void setHideBuddiesOnlyGames(bool hide); void setHideIgnoredUserGames(bool hide); @@ -56,8 +56,6 @@ public: private: explicit GameFiltersSettings(const QString &settingPath, QObject *parent = nullptr); GameFiltersSettings(const GameFiltersSettings & /*other*/); - - [[nodiscard]] QString hashGameType(const QString &gameType) const; }; #endif // GAMEFILTERSSETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp b/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp index f7704cbc7..e914dc2d8 100644 --- a/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp @@ -23,12 +23,12 @@ void LayoutsSettings::setMainWindowGeometry(const QByteArray &value) setValue(value, GEOMETRY_PROP, GROUP_MAIN_WINDOW); } -QByteArray LayoutsSettings::getMainWindowGeometry() +QByteArray LayoutsSettings::getMainWindowGeometry() const { return getValue(GEOMETRY_PROP, GROUP_MAIN_WINDOW).toByteArray(); } -QByteArray LayoutsSettings::getDeckEditorLayoutState() +QByteArray LayoutsSettings::getDeckEditorLayoutState() const { return getValue(STATE_PROP, GROUP_DECK_EDITOR).toByteArray(); } @@ -38,7 +38,7 @@ void LayoutsSettings::setDeckEditorLayoutState(const QByteArray &value) setValue(value, STATE_PROP, GROUP_DECK_EDITOR); } -QByteArray LayoutsSettings::getDeckEditorGeometry() +QByteArray LayoutsSettings::getDeckEditorGeometry() const { return getValue(GEOMETRY_PROP, GROUP_DECK_EDITOR).toByteArray(); } @@ -48,7 +48,7 @@ void LayoutsSettings::setDeckEditorGeometry(const QByteArray &value) setValue(value, GEOMETRY_PROP, GROUP_DECK_EDITOR); } -QByteArray LayoutsSettings::getVisualDeckEditorLayoutState() +QByteArray LayoutsSettings::getVisualDeckEditorLayoutState() const { return getValue(STATE_PROP, GROUP_VISUAL_DECK_EDITOR).toByteArray(); } @@ -58,7 +58,7 @@ void LayoutsSettings::setVisualDeckEditorLayoutState(const QByteArray &value) setValue(value, STATE_PROP, GROUP_VISUAL_DECK_EDITOR); } -QByteArray LayoutsSettings::getVisualDeckEditorGeometry() +QByteArray LayoutsSettings::getVisualDeckEditorGeometry() const { return getValue(GEOMETRY_PROP, GROUP_VISUAL_DECK_EDITOR).toByteArray(); } @@ -68,7 +68,7 @@ void LayoutsSettings::setVisualDeckEditorGeometry(const QByteArray &value) setValue(value, GEOMETRY_PROP, GROUP_VISUAL_DECK_EDITOR); } -QByteArray LayoutsSettings::getDeckEditorDbHeaderState() +QByteArray LayoutsSettings::getDeckEditorDbHeaderState() const { return getValue(STATE_PROP, GROUP_DECK_EDITOR_DB, "header").toByteArray(); } @@ -78,7 +78,7 @@ void LayoutsSettings::setDeckEditorDbHeaderState(const QByteArray &value) setValue(value, STATE_PROP, GROUP_DECK_EDITOR_DB, "header"); } -QByteArray LayoutsSettings::getSetsDialogHeaderState() +QByteArray LayoutsSettings::getSetsDialogHeaderState() const { return getValue(STATE_PROP, GROUP_SETS_DIALOG, "header").toByteArray(); } @@ -93,7 +93,7 @@ void LayoutsSettings::setSetsDialogGeometry(const QByteArray &value) setValue(value, GEOMETRY_PROP, GROUP_SETS_DIALOG); } -QByteArray LayoutsSettings::getSetsDialogGeometry() +QByteArray LayoutsSettings::getSetsDialogGeometry() const { return getValue(GEOMETRY_PROP, GROUP_SETS_DIALOG).toByteArray(); } @@ -103,7 +103,7 @@ void LayoutsSettings::setTokenDialogGeometry(const QByteArray &value) setValue(value, GEOMETRY_PROP, GROUP_TOKEN_DIALOG); } -QByteArray LayoutsSettings::getTokenDialogGeometry() +QByteArray LayoutsSettings::getTokenDialogGeometry() const { return getValue(GEOMETRY_PROP, GROUP_TOKEN_DIALOG).toByteArray(); } @@ -118,12 +118,12 @@ void LayoutsSettings::setGamePlayAreaState(const QByteArray &value) setValue(value, STATE_PROP, GROUP_GAME_PLAY_AREA); } -QByteArray LayoutsSettings::getGamePlayAreaLayoutState() +QByteArray LayoutsSettings::getGamePlayAreaLayoutState() const { return getValue(STATE_PROP, GROUP_GAME_PLAY_AREA).toByteArray(); } -QByteArray LayoutsSettings::getGamePlayAreaGeometry() +QByteArray LayoutsSettings::getGamePlayAreaGeometry() const { return getValue(GEOMETRY_PROP, GROUP_GAME_PLAY_AREA).toByteArray(); } @@ -138,12 +138,12 @@ void LayoutsSettings::setReplayPlayAreaState(const QByteArray &value) setValue(value, STATE_PROP, GROUP_REPLAY_PLAY_AREA); } -QByteArray LayoutsSettings::getReplayPlayAreaLayoutState() +QByteArray LayoutsSettings::getReplayPlayAreaLayoutState() const { return getValue(STATE_PROP, GROUP_REPLAY_PLAY_AREA).toByteArray(); } -QByteArray LayoutsSettings::getReplayPlayAreaGeometry() +QByteArray LayoutsSettings::getReplayPlayAreaGeometry() const { return getValue(GEOMETRY_PROP, GROUP_REPLAY_PLAY_AREA).toByteArray(); } diff --git a/libcockatrice_settings/libcockatrice/settings/layouts_settings.h b/libcockatrice_settings/libcockatrice/settings/layouts_settings.h index cab9a456e..5353ce15a 100644 --- a/libcockatrice_settings/libcockatrice/settings/layouts_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/layouts_settings.h @@ -36,24 +36,24 @@ public: void setReplayPlayAreaGeometry(const QByteArray &value); void setReplayPlayAreaState(const QByteArray &value); - QByteArray getMainWindowGeometry(); + QByteArray getMainWindowGeometry() const; - QByteArray getDeckEditorLayoutState(); - QByteArray getDeckEditorGeometry(); + QByteArray getDeckEditorLayoutState() const; + QByteArray getDeckEditorGeometry() const; - QByteArray getVisualDeckEditorLayoutState(); - QByteArray getVisualDeckEditorGeometry(); + QByteArray getVisualDeckEditorLayoutState() const; + QByteArray getVisualDeckEditorGeometry() const; - QByteArray getDeckEditorDbHeaderState(); - QByteArray getSetsDialogHeaderState(); - QByteArray getSetsDialogGeometry(); - QByteArray getTokenDialogGeometry(); + QByteArray getDeckEditorDbHeaderState() const; + QByteArray getSetsDialogHeaderState() const; + QByteArray getSetsDialogGeometry() const; + QByteArray getTokenDialogGeometry() const; - QByteArray getGamePlayAreaLayoutState(); - QByteArray getGamePlayAreaGeometry(); + QByteArray getGamePlayAreaLayoutState() const; + QByteArray getGamePlayAreaGeometry() const; - QByteArray getReplayPlayAreaLayoutState(); - QByteArray getReplayPlayAreaGeometry(); + QByteArray getReplayPlayAreaLayoutState() const; + QByteArray getReplayPlayAreaGeometry() const; signals: public slots: diff --git a/libcockatrice_settings/libcockatrice/settings/message_settings.cpp b/libcockatrice_settings/libcockatrice/settings/message_settings.cpp index 761c94484..50da39df6 100644 --- a/libcockatrice_settings/libcockatrice/settings/message_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/message_settings.cpp @@ -5,12 +5,12 @@ MessageSettings::MessageSettings(const QString &settingPath, QObject *parent) { } -QString MessageSettings::getMessageAt(int index) +QString MessageSettings::getMessageAt(int index) const { return getValue(QString("msg%1").arg(index)).toString(); } -int MessageSettings::getCount() +int MessageSettings::getCount() const { return getValue("count").toInt(); } diff --git a/libcockatrice_settings/libcockatrice/settings/message_settings.h b/libcockatrice_settings/libcockatrice/settings/message_settings.h index 265c455e1..ec70027af 100644 --- a/libcockatrice_settings/libcockatrice/settings/message_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/message_settings.h @@ -15,8 +15,8 @@ class MessageSettings : public SettingsManager friend class SettingsCache; public: - int getCount(); - QString getMessageAt(int index); + int getCount() const; + QString getMessageAt(int index) const; void setCount(int count); void setMessageAt(int index, QString message); diff --git a/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp b/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp index e64dd7494..76bc4069e 100644 --- a/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp @@ -7,7 +7,7 @@ RecentsSettings::RecentsSettings(const QString &settingPath, QObject *parent) { } -QStringList RecentsSettings::getRecentlyOpenedDeckPaths() +QStringList RecentsSettings::getRecentlyOpenedDeckPaths() const { return getValue("deckpaths").toStringList(); } @@ -31,7 +31,7 @@ void RecentsSettings::updateRecentlyOpenedDeckPaths(const QString &deckPath) emit recentlyOpenedDeckPathsChanged(); } -QString RecentsSettings::getLatestDeckDirPath() +QString RecentsSettings::getLatestDeckDirPath() const { return getValue("latestDeckDir", "dirs").toString(); } diff --git a/libcockatrice_settings/libcockatrice/settings/recents_settings.h b/libcockatrice_settings/libcockatrice/settings/recents_settings.h index 23c8f1a9f..3aebff334 100644 --- a/libcockatrice_settings/libcockatrice/settings/recents_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/recents_settings.h @@ -18,11 +18,11 @@ class RecentsSettings : public SettingsManager RecentsSettings(const RecentsSettings & /*other*/); public: - QStringList getRecentlyOpenedDeckPaths(); + QStringList getRecentlyOpenedDeckPaths() const; void clearRecentlyOpenedDeckPaths(); void updateRecentlyOpenedDeckPaths(const QString &deckPath); - QString getLatestDeckDirPath(); + QString getLatestDeckDirPath() const; void setLatestDeckDirPath(const QString &dirPath); signals: diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index 5f69f47c9..0140182be 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -13,7 +13,7 @@ void ServersSettings::setPreviousHostLogin(int previous) setValue(previous, "previoushostlogin"); } -int ServersSettings::getPreviousHostLogin() +int ServersSettings::getPreviousHostLogin() const { QVariant previous = getValue("previoushostlogin"); return previous == QVariant() ? 1 : previous.toInt(); @@ -24,7 +24,7 @@ void ServersSettings::setPreviousHostList(QStringList list) setValue(list, "previoushosts"); } -QStringList ServersSettings::getPreviousHostList() +QStringList ServersSettings::getPreviousHostList() const { return getValue("previoushosts").toStringList(); } @@ -48,13 +48,13 @@ QString ServersSettings::getSite(QString defaultSite) return site == QVariant() ? std::move(defaultSite) : site.toString(); } -QString ServersSettings::getPrevioushostName() +QString ServersSettings::getPrevioushostName() const { QVariant value = getValue("previoushostName"); return value == QVariant() ? "Rooster Ranges" : value.toString(); } -int ServersSettings::getPrevioushostindex(const QString &saveName) +int ServersSettings::getPrevioushostindex(const QString &saveName) const { int size = getValue("totalServers", "server", "server_details").toInt(); @@ -65,14 +65,14 @@ int ServersSettings::getPrevioushostindex(const QString &saveName) return -1; } -QString ServersSettings::getHostname(QString defaultHost) +QString ServersSettings::getHostname(QString defaultHost) const { int index = getPrevioushostindex(getPrevioushostName()); QVariant hostname = getValue(QString("server%1").arg(index), "server", "server_details"); return hostname == QVariant() ? std::move(defaultHost) : hostname.toString(); } -QString ServersSettings::getPort(QString defaultPort) +QString ServersSettings::getPort(QString defaultPort) const { int index = getPrevioushostindex(getPrevioushostName()); QVariant port = getValue(QString("port%1").arg(index), "server", "server_details"); @@ -80,7 +80,7 @@ QString ServersSettings::getPort(QString defaultPort) return port == QVariant() ? std::move(defaultPort) : port.toString(); } -QString ServersSettings::getPlayerName(QString defaultName) +QString ServersSettings::getPlayerName(QString defaultName) const { int index = getPrevioushostindex(getPrevioushostName()); QVariant name = getValue(QString("username%1").arg(index), "server", "server_details"); @@ -98,7 +98,7 @@ QString ServersSettings::getPassword() return QString(); } -bool ServersSettings::getSavePassword() +bool ServersSettings::getSavePassword() const { int index = getPrevioushostindex(getPrevioushostName()); bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool(); @@ -110,7 +110,7 @@ void ServersSettings::setAutoConnect(int autoconnect) setValue(autoconnect, "auto_connect"); } -int ServersSettings::getAutoConnect() +int ServersSettings::getAutoConnect() const { QVariant autoconnect = getValue("auto_connect"); return autoconnect == QVariant() ? 0 : autoconnect.toInt(); @@ -121,7 +121,7 @@ void ServersSettings::setFPHostName(QString hostname) setValue(hostname, "fphostname"); } -QString ServersSettings::getFPHostname(QString defaultHost) +QString ServersSettings::getFPHostname(QString defaultHost) const { QVariant hostname = getValue("fphostname"); return hostname == QVariant() ? std::move(defaultHost) : hostname.toString(); @@ -132,7 +132,7 @@ void ServersSettings::setFPPort(QString port) setValue(port, "fpport"); } -QString ServersSettings::getFPPort(QString defaultPort) +QString ServersSettings::getFPPort(QString defaultPort) const { QVariant port = getValue("fpport"); return port == QVariant() ? std::move(defaultPort) : port.toString(); @@ -143,7 +143,7 @@ void ServersSettings::setFPPlayerName(QString playerName) setValue(playerName, "fpplayername"); } -QString ServersSettings::getFPPlayerName(QString defaultName) +QString ServersSettings::getFPPlayerName(QString defaultName) const { QVariant name = getValue("fpplayername"); return name == QVariant() ? std::move(defaultName) : name.toString(); @@ -154,7 +154,7 @@ void ServersSettings::setClearDebugLogStatus(bool abIsChecked) setValue(abIsChecked, "save_debug_log"); } -bool ServersSettings::getClearDebugLogStatus(bool abDefaultValue) +bool ServersSettings::getClearDebugLogStatus(bool abDefaultValue) const { QVariant cbFlushLog = getValue("save_debug_log"); return cbFlushLog == QVariant() ? abDefaultValue : cbFlushLog.toBool(); diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.h b/libcockatrice_settings/libcockatrice/settings/servers_settings.h index 4d92c4647..22603a356 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.h @@ -22,21 +22,21 @@ class ServersSettings : public SettingsManager friend class SettingsCache; public: - int getPreviousHostLogin(); - int getPrevioushostindex(const QString &); - QStringList getPreviousHostList(); - QString getPrevioushostName(); - QString getHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST); - QString getPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT); - QString getPlayerName(QString defaultName = ""); - QString getFPHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST); - QString getFPPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT); - QString getFPPlayerName(QString defaultName = ""); + int getPreviousHostLogin() const; + int getPrevioushostindex(const QString &) const; + QStringList getPreviousHostList() const; + QString getPrevioushostName() const; + QString getHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST) const; + QString getPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT) const; + QString getPlayerName(QString defaultName = "") const; + QString getFPHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST) const; + QString getFPPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT) const; + QString getFPPlayerName(QString defaultName = "") const; QString getPassword(); QString getSaveName(QString defaultname = ""); QString getSite(QString defaultName = ""); - bool getSavePassword(); - int getAutoConnect(); + bool getSavePassword() const; + int getAutoConnect() const; void setPreviousHostLogin(int previous); void setPrevioushostName(const QString &); @@ -67,7 +67,7 @@ public: QString port = QString(), QString site = QString()); void setClearDebugLogStatus(bool abIsChecked); - bool getClearDebugLogStatus(bool abDefaultValue); + bool getClearDebugLogStatus(bool abDefaultValue) const; private: explicit ServersSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp index 3f3deb638..2d4f1c441 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp @@ -108,7 +108,7 @@ void SettingsManager::deleteValue(const QString &name, const QString &group, con } } -QVariant SettingsManager::getValue(const QString &name) +QVariant SettingsManager::getValue(const QString &name) const { auto settings = getSettings(); @@ -133,7 +133,7 @@ QVariant SettingsManager::getValue(const QString &name) return value; } -QVariant SettingsManager::getValue(const QString &name, const QString &group, const QString &subGroup) +QVariant SettingsManager::getValue(const QString &name, const QString &group, const QString &subGroup) const { auto settings = getSettings(); diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.h b/libcockatrice_settings/libcockatrice/settings/settings_manager.h index 18abb8dbf..ad828f089 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_manager.h +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.h @@ -19,8 +19,8 @@ public: const QString &defaultGroup = QString(), const QString &defaultSubGroup = QString(), QObject *parent = nullptr); - QVariant getValue(const QString &name); - QVariant getValue(const QString &name, const QString &group, const QString &subGroup = QString()); + QVariant getValue(const QString &name) const; + QVariant getValue(const QString &name, const QString &group, const QString &subGroup = QString()) const; void sync(); protected: From 7caa88bc58b649b2536d83fed35fdcf2743dbdb6 Mon Sep 17 00:00:00 2001 From: Bruno Alexandre Rosa <1791393+brunoalr@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:37:30 -0300 Subject: [PATCH 075/116] fix: solve deprecated literal operator error by updating peglib (#6745) * fix: solve deprecated literal operator error * update peglib https://github.com/yhirose/cpp-peglib/commit/dcabc63cf4b966eb6f33abd571ae4cda6bf707f0 --- .../libcockatrice/utility/peglib.h | 8083 +++++++++-------- 1 file changed, 4452 insertions(+), 3631 deletions(-) diff --git a/libcockatrice_utility/libcockatrice/utility/peglib.h b/libcockatrice_utility/libcockatrice/utility/peglib.h index 6a5b87b2d..3ae6040c4 100644 --- a/libcockatrice_utility/libcockatrice/utility/peglib.h +++ b/libcockatrice_utility/libcockatrice/utility/peglib.h @@ -1,4 +1,4 @@ -// +// // peglib.h // // Copyright (c) 2022 Yuji Hirose. All rights reserved. @@ -17,6 +17,7 @@ #include #include +#include #include #include #if __has_include() @@ -30,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -51,28 +53,28 @@ namespace peg { // "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189". template struct scope_exit { - explicit scope_exit(EF &&f) - : exit_function(std::move(f)), execute_on_destruction{true} {} + explicit scope_exit(EF &&f) + : exit_function(std::move(f)), execute_on_destruction{true} {} - scope_exit(scope_exit &&rhs) - : exit_function(std::move(rhs.exit_function)), - execute_on_destruction{rhs.execute_on_destruction} { - rhs.release(); - } + scope_exit(scope_exit &&rhs) + : exit_function(std::move(rhs.exit_function)), + execute_on_destruction{rhs.execute_on_destruction} { + rhs.release(); + } - ~scope_exit() { - if (execute_on_destruction) { this->exit_function(); } - } + ~scope_exit() { + if (execute_on_destruction) { this->exit_function(); } + } - void release() { this->execute_on_destruction = false; } + void release() { this->execute_on_destruction = false; } private: - scope_exit(const scope_exit &) = delete; - void operator=(const scope_exit &) = delete; - scope_exit &operator=(scope_exit &&) = delete; + scope_exit(const scope_exit &) = delete; + void operator=(const scope_exit &) = delete; + scope_exit &operator=(scope_exit &&) = delete; - EF exit_function; - bool execute_on_destruction; + EF exit_function; + bool execute_on_destruction; }; /*----------------------------------------------------------------------------- @@ -80,130 +82,136 @@ private: *---------------------------------------------------------------------------*/ inline size_t codepoint_length(const char *s8, size_t l) { - if (l) { - auto b = static_cast(s8[0]); - if ((b & 0x80) == 0) { - return 1; - } else if ((b & 0xE0) == 0xC0 && l >= 2) { - return 2; - } else if ((b & 0xF0) == 0xE0 && l >= 3) { - return 3; - } else if ((b & 0xF8) == 0xF0 && l >= 4) { - return 4; - } + if (l) { + auto b = static_cast(s8[0]); + if ((b & 0x80) == 0) { + return 1; + } else if ((b & 0xE0) == 0xC0 && l >= 2) { + return 2; + } else if ((b & 0xF0) == 0xE0 && l >= 3) { + return 3; + } else if ((b & 0xF8) == 0xF0 && l >= 4) { + return 4; } - return 0; + } + return 0; } inline size_t codepoint_count(const char *s8, size_t l) { - size_t count = 0; - for (size_t i = 0; i < l; i += codepoint_length(s8 + i, l - i)) { - count++; + size_t count = 0; + for (size_t i = 0; i < l;) { + auto len = codepoint_length(s8 + i, l - i); + if (len == 0) { + // Invalid UTF-8 byte, treat as single byte to avoid infinite loop + len = 1; } - return count; + i += len; + count++; + } + return count; } inline size_t encode_codepoint(char32_t cp, char *buff) { - if (cp < 0x0080) { - buff[0] = static_cast(cp & 0x7F); - return 1; - } else if (cp < 0x0800) { - buff[0] = static_cast(0xC0 | ((cp >> 6) & 0x1F)); - buff[1] = static_cast(0x80 | (cp & 0x3F)); - return 2; - } else if (cp < 0xD800) { - buff[0] = static_cast(0xE0 | ((cp >> 12) & 0xF)); - buff[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); - buff[2] = static_cast(0x80 | (cp & 0x3F)); - return 3; - } else if (cp < 0xE000) { - // D800 - DFFF is invalid... - return 0; - } else if (cp < 0x10000) { - buff[0] = static_cast(0xE0 | ((cp >> 12) & 0xF)); - buff[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); - buff[2] = static_cast(0x80 | (cp & 0x3F)); - return 3; - } else if (cp < 0x110000) { - buff[0] = static_cast(0xF0 | ((cp >> 18) & 0x7)); - buff[1] = static_cast(0x80 | ((cp >> 12) & 0x3F)); - buff[2] = static_cast(0x80 | ((cp >> 6) & 0x3F)); - buff[3] = static_cast(0x80 | (cp & 0x3F)); - return 4; - } + if (cp < 0x0080) { + buff[0] = static_cast(cp & 0x7F); + return 1; + } else if (cp < 0x0800) { + buff[0] = static_cast(0xC0 | ((cp >> 6) & 0x1F)); + buff[1] = static_cast(0x80 | (cp & 0x3F)); + return 2; + } else if (cp < 0xD800) { + buff[0] = static_cast(0xE0 | ((cp >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (cp & 0x3F)); + return 3; + } else if (cp < 0xE000) { + // D800 - DFFF is invalid... return 0; + } else if (cp < 0x10000) { + buff[0] = static_cast(0xE0 | ((cp >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (cp & 0x3F)); + return 3; + } else if (cp < 0x110000) { + buff[0] = static_cast(0xF0 | ((cp >> 18) & 0x7)); + buff[1] = static_cast(0x80 | ((cp >> 12) & 0x3F)); + buff[2] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[3] = static_cast(0x80 | (cp & 0x3F)); + return 4; + } + return 0; } inline std::string encode_codepoint(char32_t cp) { - char buff[4]; - auto l = encode_codepoint(cp, buff); - return std::string(buff, l); + char buff[4]; + auto l = encode_codepoint(cp, buff); + return std::string(buff, l); } inline bool decode_codepoint(const char *s8, size_t l, size_t &bytes, char32_t &cp) { - if (l) { - auto b = static_cast(s8[0]); - if ((b & 0x80) == 0) { - bytes = 1; - cp = b; - return true; - } else if ((b & 0xE0) == 0xC0) { - if (l >= 2) { - bytes = 2; - cp = ((static_cast(s8[0] & 0x1F)) << 6) | - (static_cast(s8[1] & 0x3F)); - return true; - } - } else if ((b & 0xF0) == 0xE0) { - if (l >= 3) { - bytes = 3; - cp = ((static_cast(s8[0] & 0x0F)) << 12) | - ((static_cast(s8[1] & 0x3F)) << 6) | - (static_cast(s8[2] & 0x3F)); - return true; - } - } else if ((b & 0xF8) == 0xF0) { - if (l >= 4) { - bytes = 4; - cp = ((static_cast(s8[0] & 0x07)) << 18) | - ((static_cast(s8[1] & 0x3F)) << 12) | - ((static_cast(s8[2] & 0x3F)) << 6) | - (static_cast(s8[3] & 0x3F)); - return true; - } - } + if (l) { + auto b = static_cast(s8[0]); + if ((b & 0x80) == 0) { + bytes = 1; + cp = b; + return true; + } else if ((b & 0xE0) == 0xC0) { + if (l >= 2) { + bytes = 2; + cp = ((static_cast(s8[0] & 0x1F)) << 6) | + (static_cast(s8[1] & 0x3F)); + return true; + } + } else if ((b & 0xF0) == 0xE0) { + if (l >= 3) { + bytes = 3; + cp = ((static_cast(s8[0] & 0x0F)) << 12) | + ((static_cast(s8[1] & 0x3F)) << 6) | + (static_cast(s8[2] & 0x3F)); + return true; + } + } else if ((b & 0xF8) == 0xF0) { + if (l >= 4) { + bytes = 4; + cp = ((static_cast(s8[0] & 0x07)) << 18) | + ((static_cast(s8[1] & 0x3F)) << 12) | + ((static_cast(s8[2] & 0x3F)) << 6) | + (static_cast(s8[3] & 0x3F)); + return true; + } } - return false; + } + return false; } inline size_t decode_codepoint(const char *s8, size_t l, char32_t &cp) { - size_t bytes; - if (decode_codepoint(s8, l, bytes, cp)) { return bytes; } - return 0; + size_t bytes; + if (decode_codepoint(s8, l, bytes, cp)) { return bytes; } + return 0; } inline char32_t decode_codepoint(const char *s8, size_t l) { - char32_t cp = 0; - decode_codepoint(s8, l, cp); - return cp; + char32_t cp = 0; + decode_codepoint(s8, l, cp); + return cp; } inline std::u32string decode(const char *s8, size_t l) { - std::u32string out; - size_t i = 0; - while (i < l) { - auto beg = i++; - while (i < l && (s8[i] & 0xc0) == 0x80) { - i++; - } - out += decode_codepoint(&s8[beg], (i - beg)); + std::u32string out; + size_t i = 0; + while (i < l) { + auto beg = i++; + while (i < l && (s8[i] & 0xc0) == 0x80) { + i++; } - return out; + out += decode_codepoint(&s8[beg], (i - beg)); + } + return out; } template const char *u8(const T *s) { - return reinterpret_cast(s); + return reinterpret_cast(s); } /*----------------------------------------------------------------------------- @@ -211,23 +219,23 @@ template const char *u8(const T *s) { *---------------------------------------------------------------------------*/ inline std::string escape_characters(const char *s, size_t n) { - std::string str; - for (size_t i = 0; i < n; i++) { - auto c = s[i]; - switch (c) { - case '\f': str += "\\f"; break; - case '\n': str += "\\n"; break; - case '\r': str += "\\r"; break; - case '\t': str += "\\t"; break; - case '\v': str += "\\v"; break; - default: str += c; break; - } + std::string str; + for (size_t i = 0; i < n; i++) { + auto c = s[i]; + switch (c) { + case '\f': str += "\\f"; break; + case '\n': str += "\\n"; break; + case '\r': str += "\\r"; break; + case '\t': str += "\\t"; break; + case '\v': str += "\\v"; break; + default: str += c; break; } - return str; + } + return str; } inline std::string escape_characters(std::string_view sv) { - return escape_characters(sv.data(), sv.size()); + return escape_characters(sv.data(), sv.size()); } /*----------------------------------------------------------------------------- @@ -235,120 +243,121 @@ inline std::string escape_characters(std::string_view sv) { *---------------------------------------------------------------------------*/ inline bool is_hex(char c, int &v) { - if ('0' <= c && c <= '9') { - v = c - '0'; - return true; - } else if ('a' <= c && c <= 'f') { - v = c - 'a' + 10; - return true; - } else if ('A' <= c && c <= 'F') { - v = c - 'A' + 10; - return true; - } - return false; + if ('0' <= c && c <= '9') { + v = c - '0'; + return true; + } else if ('a' <= c && c <= 'f') { + v = c - 'a' + 10; + return true; + } else if ('A' <= c && c <= 'F') { + v = c - 'A' + 10; + return true; + } + return false; } inline bool is_digit(char c, int &v) { - if ('0' <= c && c <= '9') { - v = c - '0'; - return true; - } - return false; + if ('0' <= c && c <= '9') { + v = c - '0'; + return true; + } + return false; } inline std::pair parse_hex_number(const char *s, size_t n, size_t i) { - int ret = 0; - int val; - while (i < n && is_hex(s[i], val)) { - ret = static_cast(ret * 16 + val); - i++; - } - return std::pair(ret, i); + int ret = 0; + int val; + while (i < n && is_hex(s[i], val)) { + ret = static_cast(ret * 16 + val); + i++; + } + return std::pair(ret, i); } inline std::pair parse_octal_number(const char *s, size_t n, size_t i) { - int ret = 0; - int val; - while (i < n && is_digit(s[i], val)) { - ret = static_cast(ret * 8 + val); - i++; - } - return std::pair(ret, i); + int ret = 0; + int val; + while (i < n && is_digit(s[i], val)) { + ret = static_cast(ret * 8 + val); + i++; + } + return std::pair(ret, i); } inline std::string resolve_escape_sequence(const char *s, size_t n) { - std::string r; - r.reserve(n); + std::string r; + r.reserve(n); - size_t i = 0; - while (i < n) { - auto ch = s[i]; - if (ch == '\\') { - i++; - if (i == n) { throw std::runtime_error("Invalid escape sequence..."); } - switch (s[i]) { - case 'f': - r += '\f'; - i++; - break; - case 'n': - r += '\n'; - i++; - break; - case 'r': - r += '\r'; - i++; - break; - case 't': - r += '\t'; - i++; - break; - case 'v': - r += '\v'; - i++; - break; - case '\'': - r += '\''; - i++; - break; - case '"': - r += '"'; - i++; - break; - case '[': - r += '['; - i++; - break; - case ']': - r += ']'; - i++; - break; - case '\\': - r += '\\'; - i++; - break; - case 'x': - case 'u': { - char32_t cp; - std::tie(cp, i) = parse_hex_number(s, n, i + 1); - r += encode_codepoint(cp); - break; - } - default: { - char32_t cp; - std::tie(cp, i) = parse_octal_number(s, n, i); - r += encode_codepoint(cp); - break; - } - } - } else { - r += ch; - i++; - } + size_t i = 0; + while (i < n) { + auto ch = s[i]; + if (ch == '\\') { + i++; + assert(i < n); + + switch (s[i]) { + case 'f': + r += '\f'; + i++; + break; + case 'n': + r += '\n'; + i++; + break; + case 'r': + r += '\r'; + i++; + break; + case 't': + r += '\t'; + i++; + break; + case 'v': + r += '\v'; + i++; + break; + case '\'': + r += '\''; + i++; + break; + case '"': + r += '"'; + i++; + break; + case '[': + r += '['; + i++; + break; + case ']': + r += ']'; + i++; + break; + case '\\': + r += '\\'; + i++; + break; + case 'x': + case 'u': { + char32_t cp; + std::tie(cp, i) = parse_hex_number(s, n, i + 1); + r += encode_codepoint(cp); + break; + } + default: { + char32_t cp; + std::tie(cp, i) = parse_octal_number(s, n, i); + r += encode_codepoint(cp); + break; + } + } + } else { + r += ch; + i++; } - return r; + } + return r; } /*----------------------------------------------------------------------------- @@ -356,19 +365,26 @@ inline std::string resolve_escape_sequence(const char *s, size_t n) { *---------------------------------------------------------------------------*/ template T token_to_number_(std::string_view sv) { - T n = 0; + T n = 0; #if __has_include() - if constexpr (!std::is_floating_point::value) { - std::from_chars(sv.data(), sv.data() + sv.size(), n); + if constexpr (!std::is_floating_point::value) { + std::from_chars(sv.data(), sv.data() + sv.size(), n); #else - if constexpr (false) { + if constexpr (false) { #endif - } else { - auto s = std::string(sv); - std::istringstream ss(s); - ss >> n; - } - return n; + } else { + auto s = std::string(sv); + std::istringstream ss(s); + ss >> n; + } + return n; +} + +inline std::string to_lower(std::string s) { + for (auto &c : s) { + c = static_cast(std::tolower(static_cast(c))); + } + return s; } /*----------------------------------------------------------------------------- @@ -377,75 +393,75 @@ template T token_to_number_(std::string_view sv) { class Trie { public: - Trie(const std::vector &items, bool ignore_case) - : ignore_case_(ignore_case) { - size_t id = 0; - for (const auto &item : items) { - const auto &s = ignore_case ? to_lower(item) : item; - for (size_t len = 1; len <= item.size(); len++) { - auto last = len == item.size(); - std::string_view sv(s.data(), len); - auto it = dic_.find(sv); - if (it == dic_.end()) { - dic_.emplace(sv, Info{last, last, id}); - } else if (last) { - it->second.match = true; - } else { - it->second.done = false; - } - } - id++; + Trie(const std::vector &items, bool ignore_case) + : ignore_case_(ignore_case), items_count_(items.size()) { + size_t id = 0; + for (const auto &item : items) { + const auto &s = ignore_case ? to_lower(item) : item; + if (item.size() > max_len_) { max_len_ = item.size(); } + for (size_t len = 1; len <= item.size(); len++) { + auto last = len == item.size(); + std::string_view sv(s.data(), len); + auto it = dic_.find(sv); + if (it == dic_.end()) { + dic_.emplace(sv, Info{last, last, id}); + } else if (last) { + it->second.match = true; + } else { + it->second.done = false; } + } + id++; + } + } + + size_t match(const char *text, size_t text_len, size_t &id) const { + auto limit = std::min(text_len, max_len_); + std::string lower_text; + if (ignore_case_) { + lower_text = to_lower(std::string(text, limit)); + text = lower_text.data(); } - size_t match(const char *text, size_t text_len, size_t &id) const { - std::string lower_text; - if (ignore_case_) { - lower_text = to_lower(text); - text = lower_text.data(); + size_t match_len = 0; + auto done = false; + size_t len = 1; + while (!done && len <= limit) { + std::string_view sv(text, len); + auto it = dic_.find(sv); + if (it == dic_.end()) { + done = true; + } else { + if (it->second.match) { + match_len = len; + id = it->second.id; } - - size_t match_len = 0; - auto done = false; - size_t len = 1; - while (!done && len <= text_len) { - std::string_view sv(text, len); - auto it = dic_.find(sv); - if (it == dic_.end()) { - done = true; - } else { - if (it->second.match) { - match_len = len; - id = it->second.id; - } - if (it->second.done) { done = true; } - } - len += 1; - } - return match_len; + if (it->second.done) { done = true; } + } + len += 1; } + return match_len; + } - size_t size() const { return dic_.size(); } + size_t size() const { return dic_.size(); } + size_t items_count() const { return items_count_; } + + friend struct ComputeFirstSet; private: - std::string to_lower(std::string s) const { - for (char &c : s) { - c = std::tolower(c); - } - return s; - } + struct Info { + bool done; + bool match; + size_t id; + }; - struct Info { - bool done; - bool match; - size_t id; - }; + // TODO: Use unordered_map when heterogeneous lookup is supported in C++20 + // std::unordered_map dic_; + std::map> dic_; - //! \todo Use unordered_map when heterogeneous lookup is supported in C++20 - //! \todo std::unordered_map dic_; - std::map> dic_; - - bool ignore_case_; + bool ignore_case_; + size_t items_count_; + size_t max_len_ = 0; }; /*----------------------------------------------------------------------------- @@ -456,21 +472,21 @@ private: * Line information utility function */ inline std::pair line_info(const char *start, const char *cur) { - auto p = start; - auto col_ptr = p; - auto no = 1; + auto p = start; + auto col_ptr = p; + auto no = 1; - while (p < cur) { - if (*p == '\n') { - no++; - col_ptr = p + 1; - } - p++; + while (p < cur) { + if (*p == '\n') { + no++; + col_ptr = p + 1; } + p++; + } - auto col = codepoint_count(col_ptr, p - col_ptr) + 1; + auto col = codepoint_count(col_ptr, p - col_ptr) + 1; - return std::pair(no, col); + return std::pair(no, col); } /* @@ -478,19 +494,19 @@ inline std::pair line_info(const char *start, const char *cur) { */ inline constexpr unsigned int str2tag_core(const char *s, size_t l, unsigned int h) { - return (l == 0) ? h - : str2tag_core(s + 1, l - 1, - (h * 33) ^ static_cast(*s)); + return (l == 0) ? h + : str2tag_core(s + 1, l - 1, + (h * 33) ^ static_cast(*s)); } inline constexpr unsigned int str2tag(std::string_view sv) { - return str2tag_core(sv.data(), sv.size(), 0); + return str2tag_core(sv.data(), sv.size(), 0); } namespace udl { -inline constexpr unsigned int operator"" _(const char *s, size_t l) { - return str2tag_core(s, l, 0); +inline constexpr unsigned int operator""_(const char *s, size_t l) { + return str2tag_core(s, l, 0); } } // namespace udl @@ -501,126 +517,113 @@ inline constexpr unsigned int operator"" _(const char *s, size_t l) { class Context; struct SemanticValues : protected std::vector { - SemanticValues() = default; - SemanticValues(Context *c) : c_(c) {} + SemanticValues() = default; + SemanticValues(Context *c) : c_(c) {} - // Input text - const char *path = nullptr; - const char *ss = nullptr; + // Input text + const char *path = nullptr; + const char *ss = nullptr; - // Matched string - std::string_view sv() const { return sv_; } + // Matched string + std::string_view sv() const { return sv_; } - // Definition name - const std::string &name() const { return name_; } + // Definition name + const std::string &name() const { return name_; } - std::vector tags; + std::vector tags; - // Line number and column at which the matched string is - std::pair line_info() const; + // Line number and column at which the matched string is + std::pair line_info() const; - // Choice count - size_t choice_count() const { return choice_count_; } + // Choice count + size_t choice_count() const { return choice_count_; } - // Choice number (0 based index) - size_t choice() const { return choice_; } + // Choice number (0 based index) + size_t choice() const { return choice_; } - // Tokens - std::vector tokens; + // Tokens + std::vector tokens; - std::string_view token(size_t id = 0) const { - if (tokens.empty()) { return sv_; } - assert(id < tokens.size()); - return tokens[id]; + std::string_view token(size_t id = 0) const { + if (tokens.empty()) { return sv_; } + assert(id < tokens.size()); + return tokens[id]; + } + + // Token conversion + std::string token_to_string(size_t id = 0) const { + return std::string(token(id)); + } + + template T token_to_number() const { + return token_to_number_(token()); + } + + // Transform the semantic value vector to another vector + template + std::vector transform(size_t beg = 0, + size_t end = static_cast(-1)) const { + std::vector r; + end = (std::min)(end, size()); + for (size_t i = beg; i < end; i++) { + r.emplace_back(std::any_cast((*this)[i])); } + return r; + } - // Token conversion - std::string token_to_string(size_t id = 0) const { - return std::string(token(id)); - } - - template T token_to_number() const { - return token_to_number_(token()); - } - - // Transform the semantic value vector to another vector - template - std::vector transform(size_t beg = 0, - size_t end = static_cast(-1)) const { - std::vector r; - end = (std::min)(end, size()); - for (size_t i = beg; i < end; i++) { - r.emplace_back(std::any_cast((*this)[i])); - } - return r; - } - - void append(SemanticValues &chvs) { - sv_ = chvs.sv_; - for (auto &v : chvs) { - emplace_back(std::move(v)); - } - for (auto &tag : chvs.tags) { - tags.emplace_back(std::move(tag)); - } - for (auto &tok : chvs.tokens) { - tokens.emplace_back(std::move(tok)); - } - } - - using std::vector::iterator; - using std::vector::const_iterator; - using std::vector::size; - using std::vector::empty; - using std::vector::assign; - using std::vector::begin; - using std::vector::end; - using std::vector::rbegin; - using std::vector::rend; - using std::vector::operator[]; - using std::vector::at; - using std::vector::resize; - using std::vector::front; - using std::vector::back; - using std::vector::push_back; - using std::vector::pop_back; - using std::vector::insert; - using std::vector::erase; - using std::vector::clear; - using std::vector::swap; - using std::vector::emplace; - using std::vector::emplace_back; + using std::vector::iterator; + using std::vector::const_iterator; + using std::vector::size; + using std::vector::empty; + using std::vector::assign; + using std::vector::begin; + using std::vector::end; + using std::vector::rbegin; + using std::vector::rend; + using std::vector::operator[]; + using std::vector::at; + using std::vector::resize; + using std::vector::front; + using std::vector::back; + using std::vector::push_back; + using std::vector::pop_back; + using std::vector::insert; + using std::vector::erase; + using std::vector::clear; + using std::vector::swap; + using std::vector::emplace; + using std::vector::emplace_back; private: - friend class Context; - friend class Dictionary; - friend class Sequence; - friend class PrioritizedChoice; - friend class Repetition; - friend class Holder; - friend class PrecedenceClimbing; + friend class Context; + friend class Dictionary; + friend class Sequence; + friend class PrioritizedChoice; + friend class Repetition; + friend class Holder; + friend class PrecedenceClimbing; - Context *c_ = nullptr; - std::string_view sv_; - size_t choice_count_ = 0; - size_t choice_ = 0; - std::string name_; + Context *c_ = nullptr; + std::string_view sv_; + size_t choice_count_ = 0; + size_t choice_ = 0; + std::string name_; }; /* * Semantic action */ template std::any call(F fn, Args &&...args) { - using R = decltype(fn(std::forward(args)...)); - if constexpr (std::is_void::value) { - fn(std::forward(args)...); - return std::any(); - } else if constexpr (std::is_same::type, - std::any>::value) { - return fn(std::forward(args)...); - } else { - return std::any(fn(std::forward(args)...)); - } + using R = decltype(fn(std::forward(args)...)); + if constexpr (std::is_void::value) { + fn(std::forward(args)...); + return std::any(); + } else if constexpr (std::is_same::type, + std::any>::value) { + return fn(std::forward(args)...); + } else { + return std::any(fn(std::forward(args)...)); + } } template @@ -637,30 +640,74 @@ struct argument_count class Action { public: - Action() = default; - Action(Action &&rhs) = default; - template Action(F fn) : fn_(make_adaptor(fn)) {} - template void operator=(F fn) { fn_ = make_adaptor(fn); } - Action &operator=(const Action &rhs) = default; + Action() = default; + Action(Action &&rhs) = default; + template Action(F fn) : fn_(make_adaptor(fn)) {} + template void operator=(F fn) { fn_ = make_adaptor(fn); } + Action &operator=(const Action &rhs) = default; - operator bool() const { return bool(fn_); } + operator bool() const { return bool(fn_); } - std::any operator()(SemanticValues &vs, std::any &dt) const { - return fn_(vs, dt); - } + std::any operator()(SemanticValues &vs, std::any &dt, + const std::any &predicate_data) const { + return fn_(vs, dt, predicate_data); + } private: - using Fty = std::function; + using Fty = std::function; - template Fty make_adaptor(F fn) { - if constexpr (argument_count::value == 1) { - return [fn](auto &vs, auto & /*dt*/) { return call(fn, vs); }; - } else { - return [fn](auto &vs, auto &dt) { return call(fn, vs, dt); }; - } + template Fty make_adaptor(F fn) { + if constexpr (argument_count::value == 1) { + return [fn](auto &vs, auto & /*dt*/, const auto & /*predicate_data*/) { + return call(fn, vs); + }; + } else if constexpr (argument_count::value == 2) { + return [fn](auto &vs, auto &dt, const auto & /*predicate_data*/) { + return call(fn, vs, dt); + }; + } else { + return [fn](auto &vs, auto &dt, const auto &predicate_data) { + return call(fn, vs, dt, predicate_data); + }; } + } - Fty fn_; + Fty fn_; +}; + +class Predicate { +public: + Predicate() = default; + Predicate(Predicate &&rhs) = default; + template Predicate(F fn) : fn_(make_adaptor(fn)) {} + template void operator=(F fn) { fn_ = make_adaptor(fn); } + Predicate &operator=(const Predicate &rhs) = default; + + operator bool() const { return bool(fn_); } + + bool operator()(const SemanticValues &vs, const std::any &dt, + std::string &msg, std::any &predicate_data) const { + return fn_(vs, dt, msg, predicate_data); + } + +private: + using Fty = std::function; + + template Fty make_adaptor(F fn) { + if constexpr (argument_count::value == 3) { + return [fn](const auto &vs, const auto &dt, auto &msg, + auto & /*predicate_data*/) { return fn(vs, dt, msg); }; + } else { + return [fn](const auto &vs, const auto &dt, auto &msg, + auto &predicate_data) { + return fn(vs, dt, msg, predicate_data); + }; + } + } + + Fty fn_; }; /* @@ -682,67 +729,67 @@ using Log = std::function> expected_tokens; - const char *message_pos = nullptr; - std::string message; - std::string label; - const char *last_output_pos = nullptr; - bool keep_previous_token = false; + const char *error_pos = nullptr; + std::vector> expected_tokens; + const char *message_pos = nullptr; + std::string message; + std::string label; + const char *last_output_pos = nullptr; + bool keep_previous_token = false; - void clear() { - error_pos = nullptr; - expected_tokens.clear(); - message_pos = nullptr; - message.clear(); + void clear() { + error_pos = nullptr; + expected_tokens.clear(); + message_pos = nullptr; + message.clear(); + } + + void add(const char *error_literal, const Definition *error_rule) { + for (const auto &[t, r] : expected_tokens) { + if (t == error_literal && r == error_rule) { return; } } + expected_tokens.emplace_back(error_literal, error_rule); + } - void add(const char *error_literal, const Definition *error_rule) { - for (const auto &[t, r] : expected_tokens) { - if (t == error_literal && r == error_rule) { return; } - } - expected_tokens.emplace_back(error_literal, error_rule); - } - - void output_log(const Log &log, const char *s, size_t n); + void output_log(const Log &log, const char *s, size_t n); private: - int cast_char(char c) const { return static_cast(c); } + int cast_char(char c) const { return static_cast(c); } - std::string heuristic_error_token(const char *s, size_t n, - const char *pos) const { - auto len = n - std::distance(s, pos); - if (len) { - size_t i = 0; - auto c = cast_char(pos[i++]); - if (!std::ispunct(c) && !std::isspace(c)) { - while (i < len && !std::ispunct(cast_char(pos[i])) && - !std::isspace(cast_char(pos[i]))) { - i++; - } - } - - size_t count = CPPPEGLIB_HEURISTIC_ERROR_TOKEN_MAX_CHAR_COUNT; - size_t j = 0; - while (count > 0 && j < i) { - j += codepoint_length(&pos[j], i - j); - count--; - } - - return escape_characters(pos, j); + std::string heuristic_error_token(const char *s, size_t n, + const char *pos) const { + auto len = n - std::distance(s, pos); + if (len) { + size_t i = 0; + auto c = cast_char(pos[i++]); + if (!std::ispunct(c) && !std::isspace(c)) { + while (i < len && !std::ispunct(cast_char(pos[i])) && + !std::isspace(cast_char(pos[i]))) { + i++; } - return std::string(); - } + } - std::string replace_all(std::string str, const std::string &from, - const std::string &to) const { - size_t pos = 0; - while ((pos = str.find(from, pos)) != std::string::npos) { - str.replace(pos, from.length(), to); - pos += to.length(); - } - return str; + size_t count = CPPPEGLIB_HEURISTIC_ERROR_TOKEN_MAX_CHAR_COUNT; + size_t j = 0; + while (count > 0 && j < i) { + j += codepoint_length(&pos[j], i - j); + count--; + } + + return escape_characters(pos, j); } + return std::string(); + } + + std::string replace_all(std::string str, const std::string &from, + const std::string &to) const { + size_t pos = 0; + while ((pos = str.find(from, pos)) != std::string::npos) { + str.replace(pos, from.length(), to); + pos += to.length(); + } + return str; + } }; /* @@ -762,211 +809,269 @@ using TracerStartOrEnd = std::function; class Context { public: - const char *path; - const char *s; - const size_t l; + const char *path; + const char *s; + const size_t l; - ErrorInfo error_info; - bool recovered = false; + ErrorInfo error_info; + bool recovered = false; - std::vector> value_stack; - size_t value_stack_size = 0; + std::vector> value_stack; + size_t value_stack_size = 0; - std::vector rule_stack; - std::vector>> args_stack; + std::vector rule_stack; + std::vector>> args_stack; - size_t in_token_boundary_count = 0; + size_t in_token_boundary_count = 0; - std::shared_ptr whitespaceOpe; - bool in_whitespace = false; + std::shared_ptr whitespaceOpe; + bool in_whitespace = false; - std::shared_ptr wordOpe; + std::shared_ptr wordOpe; - std::vector> capture_scope_stack; - size_t capture_scope_stack_size = 0; + std::vector> capture_entries; - std::vector cut_stack; + std::vector cut_stack; - const size_t def_count; - const bool enablePackratParsing; - std::vector cache_registered; - std::vector cache_success; + const size_t def_count; + const bool enablePackratParsing; + std::vector cache_registered; + std::vector cache_success; - std::map, std::tuple> - cache_values; + std::map, std::tuple> + cache_values; - TracerEnter tracer_enter; - TracerLeave tracer_leave; - std::any trace_data; - const bool verbose_trace; + // Left recursion support + struct LRMemo { + size_t len = static_cast(-1); + std::any val; + }; + std::map, LRMemo> lr_memo; - Log log; + // Rules whose lr_memo was hit during the current parse scope. + // Used to track LR cycle membership. + std::set lr_refs_hit; - Context(const char *path, const char *s, size_t l, size_t def_count, - std::shared_ptr whitespaceOpe, std::shared_ptr wordOpe, - bool enablePackratParsing, TracerEnter tracer_enter, - TracerLeave tracer_leave, std::any trace_data, bool verbose_trace, - Log log) - : path(path), s(s), l(l), whitespaceOpe(whitespaceOpe), wordOpe(wordOpe), - def_count(def_count), enablePackratParsing(enablePackratParsing), - cache_registered(enablePackratParsing ? def_count * (l + 1) : 0), - cache_success(enablePackratParsing ? def_count * (l + 1) : 0), - tracer_enter(tracer_enter), tracer_leave(tracer_leave), - trace_data(trace_data), verbose_trace(verbose_trace), log(log) { + // Rules currently in their seeding/growing phase at a given position. + // Protected from having their lr_memo erased by inner growers. + std::set> lr_active_seeds; - push_args({}); - push_capture_scope(); + void clear_packrat_cache(const char *pos, size_t def_id) { + if (!enablePackratParsing) { return; } + auto col = static_cast(pos - s); + auto idx = def_count * col + def_id; + if (idx < cache_registered.size()) { + cache_registered[idx] = false; + cache_success[idx] = false; + } + cache_values.erase(std::make_pair(col, def_id)); + } + + void write_packrat_cache(const char *pos, size_t def_id, size_t len, + const std::any &val) { + if (!enablePackratParsing) { return; } + auto col = pos - s; + auto idx = def_count * static_cast(col) + def_id; + if (idx >= cache_registered.size()) { return; } + cache_registered[idx] = true; + cache_success[idx] = true; + auto key = std::pair(col, def_id); + cache_values[key] = std::pair(len, val); + } + + TracerEnter tracer_enter; + TracerLeave tracer_leave; + std::any trace_data; + const bool verbose_trace; + + Log log; + + Context(const char *path, const char *s, size_t l, size_t def_count, + std::shared_ptr whitespaceOpe, std::shared_ptr wordOpe, + bool enablePackratParsing, TracerEnter tracer_enter, + TracerLeave tracer_leave, std::any trace_data, bool verbose_trace, + Log log) + : path(path), s(s), l(l), whitespaceOpe(whitespaceOpe), wordOpe(wordOpe), + def_count(def_count), enablePackratParsing(enablePackratParsing), + cache_registered(enablePackratParsing ? def_count * (l + 1) : 0), + cache_success(enablePackratParsing ? def_count * (l + 1) : 0), + tracer_enter(tracer_enter), tracer_leave(tracer_leave), + trace_data(trace_data), verbose_trace(verbose_trace), log(log) { + + push_args({}); + } + + ~Context() { + assert(!value_stack_size); + assert(cut_stack.empty()); + } + + Context(const Context &) = delete; + Context(Context &&) = delete; + Context operator=(const Context &) = delete; + + // Per-rule packrat stats (populated when packrat_stats is non-null) + struct PackratStats { + size_t hits = 0; + size_t misses = 0; + }; + std::vector *packrat_stats = nullptr; + + // Per-rule packrat filter: if set, only rules with filter[def_id]=true + // use full memoization (cache_values map). Others use bitvector-only + // re-entry guard. + const std::vector *packrat_rule_filter = nullptr; + + template + void packrat(const char *a_s, size_t def_id, size_t &len, std::any &val, + T fn) { + if (!enablePackratParsing) { + fn(val); + return; } - ~Context() { - pop_capture_scope(); + auto col = a_s - s; + auto idx = def_count * static_cast(col) + def_id; - assert(!value_stack_size); - assert(!capture_scope_stack_size); - assert(cut_stack.empty()); + if (cache_registered[idx]) { + if (packrat_stats && def_id < packrat_stats->size()) { + (*packrat_stats)[def_id].hits++; + } + if (cache_success[idx]) { + auto key = std::pair(col, def_id); + std::tie(len, val) = cache_values[key]; + return; + } else { + len = static_cast(-1); + return; + } + } else { + // Pre-register as failure (re-entry guard for all rules) + cache_registered[idx] = true; + cache_success[idx] = false; + + if (packrat_stats && def_id < packrat_stats->size()) { + (*packrat_stats)[def_id].misses++; + } + + fn(val); + + bool full_memo = + !packrat_rule_filter || (def_id < packrat_rule_filter->size() && + (*packrat_rule_filter)[def_id]); + if (full_memo) { + if (success(len)) { write_packrat_cache(a_s, def_id, len, val); } + } else { + // Guard-only: undo registration so future calls re-parse + cache_registered[idx] = false; + } + return; + } + } + + // Semantic values + SemanticValues &push_semantic_values_scope() { + assert(value_stack_size <= value_stack.size()); + if (value_stack_size == value_stack.size()) { + value_stack.emplace_back(std::make_shared(this)); + } else { + auto &vs = *value_stack[value_stack_size]; + if (!vs.empty()) { + vs.clear(); + if (!vs.tags.empty()) { vs.tags.clear(); } + } + vs.sv_ = std::string_view(); + vs.choice_count_ = 0; + vs.choice_ = 0; + if (!vs.tokens.empty()) { vs.tokens.clear(); } } - Context(const Context &) = delete; - Context(Context &&) = delete; - Context operator=(const Context &) = delete; + auto &vs = *value_stack[value_stack_size++]; + vs.path = path; + vs.ss = s; + return vs; + } - template - void packrat(const char *a_s, size_t def_id, size_t &len, std::any &val, - T fn) { - if (!enablePackratParsing) { - fn(val); - return; - } + void pop_semantic_values_scope() { value_stack_size--; } - auto col = a_s - s; - auto idx = def_count * static_cast(col) + def_id; + // Arguments + void push_args(std::vector> &&args) { + args_stack.emplace_back(std::move(args)); + } - if (cache_registered[idx]) { - if (cache_success[idx]) { - auto key = std::pair(col, def_id); - std::tie(len, val) = cache_values[key]; - return; - } else { - len = static_cast(-1); - return; - } - } else { - fn(val); - cache_registered[idx] = true; - cache_success[idx] = success(len); - if (success(len)) { - auto key = std::pair(col, def_id); - cache_values[key] = std::pair(len, val); - } - return; - } - } + void pop_args() { args_stack.pop_back(); } - SemanticValues &push() { - push_capture_scope(); - return push_semantic_values_scope(); - } + const std::vector> &top_args() const { + return args_stack[args_stack.size() - 1]; + } - void pop() { - pop_capture_scope(); - pop_semantic_values_scope(); - } + // Snapshot/Rollback + struct Snapshot { + size_t sv_size; + size_t sv_tags_size; + size_t sv_tokens_size; + std::string_view sv_sv; + size_t choice_count; + size_t choice; + size_t capture_size; + }; - // Semantic values - SemanticValues &push_semantic_values_scope() { - assert(value_stack_size <= value_stack.size()); - if (value_stack_size == value_stack.size()) { - value_stack.emplace_back(std::make_shared(this)); - } else { - auto &vs = *value_stack[value_stack_size]; - if (!vs.empty()) { - vs.clear(); - if (!vs.tags.empty()) { vs.tags.clear(); } - } - vs.sv_ = std::string_view(); - vs.choice_count_ = 0; - vs.choice_ = 0; - if (!vs.tokens.empty()) { vs.tokens.clear(); } - } + Snapshot snapshot(const SemanticValues &vs) const { + return {vs.size(), vs.tags.size(), vs.tokens.size(), vs.sv_, + vs.choice_count_, vs.choice_, capture_entries.size()}; + } - auto &vs = *value_stack[value_stack_size++]; - vs.path = path; - vs.ss = s; - return vs; - } + void rollback(SemanticValues &vs, const Snapshot &snap) { + vs.resize(snap.sv_size); + vs.tags.resize(snap.sv_tags_size); + vs.tokens.resize(snap.sv_tokens_size); + vs.sv_ = snap.sv_sv; + vs.choice_count_ = snap.choice_count; + vs.choice_ = snap.choice; + capture_entries.resize(snap.capture_size); + } - void pop_semantic_values_scope() { value_stack_size--; } + // Skip trailing whitespace with trace suppression. + // Returns whitespace length, or -1 on failure. + // No-op (returns 0) if inside a token boundary or no whitespaceOpe. + size_t skip_whitespace(const char *a_s, size_t n, SemanticValues &vs, + std::any &dt); - // Arguments - void push_args(std::vector> &&args) { - args_stack.emplace_back(args); - } + // Error + void set_error_pos(const char *a_s, const char *literal = nullptr); - void pop_args() { args_stack.pop_back(); } + // Trace + void trace_enter(const Ope &ope, const char *a_s, size_t n, + const SemanticValues &vs, std::any &dt); + void trace_leave(const Ope &ope, const char *a_s, size_t n, + const SemanticValues &vs, std::any &dt, size_t len); + bool is_traceable(const Ope &ope) const; - const std::vector> &top_args() const { - return args_stack[args_stack.size() - 1]; - } + // Line info + std::pair line_info(const char *cur) const { + std::call_once(source_line_index_init_, [this]() { + for (size_t pos = 0; pos < l; pos++) { + if (s[pos] == '\n') { source_line_index.push_back(pos); } + } + source_line_index.push_back(l); + }); - // Capture scope - void push_capture_scope() { - assert(capture_scope_stack_size <= capture_scope_stack.size()); - if (capture_scope_stack_size == capture_scope_stack.size()) { - capture_scope_stack.emplace_back( - std::map()); - } else { - auto &cs = capture_scope_stack[capture_scope_stack_size]; - if (!cs.empty()) { cs.clear(); } - } - capture_scope_stack_size++; - } + auto pos = static_cast(std::distance(s, cur)); - void pop_capture_scope() { capture_scope_stack_size--; } + auto it = std::lower_bound( + source_line_index.begin(), source_line_index.end(), pos, + [](size_t element, size_t value) { return element < value; }); - void shift_capture_values() { - assert(capture_scope_stack_size >= 2); - auto curr = &capture_scope_stack[capture_scope_stack_size - 1]; - auto prev = curr - 1; - for (const auto &[k, v] : *curr) { - (*prev)[k] = v; - } - } + auto id = static_cast(std::distance(source_line_index.begin(), it)); + auto off = pos - (id == 0 ? 0 : source_line_index[id - 1] + 1); + return std::pair(id + 1, off + 1); + } - // Error - void set_error_pos(const char *a_s, const char *literal = nullptr); - - // Trace - void trace_enter(const Ope &ope, const char *a_s, size_t n, - const SemanticValues &vs, std::any &dt); - void trace_leave(const Ope &ope, const char *a_s, size_t n, - const SemanticValues &vs, std::any &dt, size_t len); - bool is_traceable(const Ope &ope) const; - - // Line info - std::pair line_info(const char *cur) const { - std::call_once(source_line_index_init_, [this]() { - for (size_t pos = 0; pos < l; pos++) { - if (s[pos] == '\n') { source_line_index.push_back(pos); } - } - source_line_index.push_back(l); - }); - - auto pos = static_cast(std::distance(s, cur)); - - auto it = std::lower_bound( - source_line_index.begin(), source_line_index.end(), pos, - [](size_t element, size_t value) { return element < value; }); - - auto id = static_cast(std::distance(source_line_index.begin(), it)); - auto off = pos - (id == 0 ? 0 : source_line_index[id - 1] + 1); - return std::pair(id + 1, off + 1); - } - - size_t next_trace_id = 0; - std::vector trace_ids; - bool ignore_trace_state = false; - mutable std::once_flag source_line_index_init_; - mutable std::vector source_line_index; + size_t next_trace_id = 0; + std::vector trace_ids; + bool ignore_trace_state = false; + mutable std::once_flag source_line_index_init_; + mutable std::vector source_line_index; }; /* @@ -974,416 +1079,590 @@ public: */ class Ope { public: - struct Visitor; + struct Visitor; - virtual ~Ope() = default; - size_t parse(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const; - virtual size_t parse_core(const char *s, size_t n, SemanticValues &vs, - Context &c, std::any &dt) const = 0; - virtual void accept(Visitor &v) = 0; + virtual ~Ope() = default; + size_t parse(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const; + virtual size_t parse_core(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt) const = 0; + virtual void accept(Visitor &v) = 0; + + bool is_token_boundary = false; + bool is_choice_like = false; +}; + +// Keyword-guarded identifier data, heap-allocated only for matching Sequences. +// Avoids bloating all Sequence objects with bitsets and keyword sets. +struct KeywordGuardData { + std::bitset<256> identifier_first; // first char of identifier + std::bitset<256> identifier_rest; // subsequent chars of identifier + std::vector exact_keywords; // single-word keywords (lowercase) + std::vector prefix_keywords; // first word of compound keywords + size_t min_keyword_len = 0; + size_t max_keyword_len = 0; + + static bool matches_any(const std::vector &keywords, + std::string_view input) { + return std::any_of(keywords.begin(), keywords.end(), + [&](const auto &kw) { return kw == input; }); + } }; class Sequence : public Ope { public: - template - Sequence(const Args &...args) - : opes_{static_cast>(args)...} {} - Sequence(const std::vector> &opes) : opes_(opes) {} - Sequence(std::vector> &&opes) : opes_(opes) {} + template + Sequence(const Args &...args) + : opes_{static_cast>(args)...} {} + Sequence(const std::vector> &opes) : opes_(opes) {} + Sequence(std::vector> &&opes) : opes_(std::move(opes)) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override { - auto &chvs = c.push_semantic_values_scope(); - auto se = scope_exit([&]() { c.pop_semantic_values_scope(); }); - size_t i = 0; - for (const auto &ope : opes_) { - auto len = ope->parse(s + i, n - i, chvs, c, dt); - if (fail(len)) { return len; } - i += len; - } - vs.append(chvs); - return i; + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + // Keyword-guarded identifier fast path: + // Fuses !ReservedKeyword into scan-then-lookup + if (kw_guard_) { + if (auto result = parse_keyword_guarded(s, n, vs, c, dt)) { + return *result; + } + // nullopt means prefix keyword match — fall through to normal path } + size_t i = 0; + for (const auto &ope : opes_) { + auto len = ope->parse(s + i, n - i, vs, c, dt); + if (fail(len)) { return len; } + i += len; + } + return i; + } - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::vector> opes_; + std::vector> opes_; + +private: + friend struct SetupFirstSets; + std::unique_ptr kw_guard_; + + // Returns parse result, or nullopt to fall through to normal path + std::optional parse_keyword_guarded(const char *s, size_t n, + SemanticValues &vs, Context &c, + std::any &dt) const { + const auto &kw = *kw_guard_; + if (n < 1 || !kw.identifier_first.test(static_cast(*s))) { + c.set_error_pos(s); + return static_cast(-1); + } + // Scan identifier using bitset + size_t id_len = 1; + while (id_len < n && + kw.identifier_rest.test(static_cast(s[id_len]))) { + id_len++; + } + // Skip keyword matching if identifier length is out of range + if (id_len >= kw.min_keyword_len && id_len <= kw.max_keyword_len) { + char lower_buf[64]; + std::unique_ptr lower_heap; + char *lower = lower_buf; + if (id_len > sizeof(lower_buf)) { + lower_heap.reset(new char[id_len]); + lower = lower_heap.get(); + } + std::transform(s, s + id_len, lower, [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + std::string_view lower_sv(lower, id_len); + + if (KeywordGuardData::matches_any(kw.exact_keywords, lower_sv)) { + c.set_error_pos(s); + return static_cast(-1); + } + if (KeywordGuardData::matches_any(kw.prefix_keywords, lower_sv)) { + return std::nullopt; + } + } + // Success: emit token and consume trailing whitespace + vs.tokens.emplace_back(std::string_view(s, id_len)); + auto wl = c.skip_whitespace(s + id_len, n - id_len, vs, dt); + if (fail(wl)) { return wl; } + return id_len + wl; + } +}; + +struct FirstSet { + // First-Set: set of possible first bytes for an expression. + // Used by PrioritizedChoice to skip alternatives that cannot match. + std::bitset<256> chars; // byte values that can appear as the first byte + bool can_be_empty = false; // true if the expression can match empty string + bool any_char = false; // true if any character can appear (cannot filter) + const char *first_literal = nullptr; // first literal for error reporting + const Definition *first_rule = + nullptr; // first token rule for error reporting + + void merge(const FirstSet &other) { + chars |= other.chars; + if (other.can_be_empty) { can_be_empty = true; } + if (other.any_char) { any_char = true; } + // Note: first_literal/first_rule are NOT merged — per-alternative + } }; class PrioritizedChoice : public Ope { public: - template - PrioritizedChoice(bool for_label, const Args &...args) - : opes_{static_cast>(args)...}, - for_label_(for_label) {} - PrioritizedChoice(const std::vector> &opes) - : opes_(opes) {} - PrioritizedChoice(std::vector> &&opes) : opes_(opes) {} + template + PrioritizedChoice(bool for_label, const Args &...args) + : opes_{static_cast>(args)...}, + for_label_(for_label) { + is_choice_like = true; + } + PrioritizedChoice(const std::vector> &opes) + : opes_(opes) { + is_choice_like = true; + } + PrioritizedChoice(std::vector> &&opes) + : opes_(std::move(opes)) { + is_choice_like = true; + } - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override { - size_t len = static_cast(-1); + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + size_t len = static_cast(-1); - if (!for_label_) { c.cut_stack.push_back(false); } - auto se1 = scope_exit([&]() { - if (!for_label_) { c.cut_stack.pop_back(); } - }); + if (!for_label_) { c.cut_stack.push_back(false); } + auto se = scope_exit([&]() { + if (!for_label_) { c.cut_stack.pop_back(); } + }); - size_t id = 0; - for (const auto &ope : opes_) { - if (!c.cut_stack.empty()) { c.cut_stack.back() = false; } - - auto &chvs = c.push(); - c.error_info.keep_previous_token = id > 0; - auto se2 = scope_exit([&]() { - c.pop(); - c.error_info.keep_previous_token = false; - }); - - len = ope->parse(s, n, chvs, c, dt); - - if (success(len)) { - vs.append(chvs); - vs.choice_count_ = opes_.size(); - vs.choice_ = id; - c.shift_capture_values(); - break; - } else if (!c.cut_stack.empty() && c.cut_stack.back()) { - break; + size_t id = 0; + for (const auto &ope : opes_) { + // First-Set filtering: skip if next byte cannot start this alternative + if (n > 0 && id < first_sets_.size()) { + const auto &fs = first_sets_[id]; + if (!fs.any_char && !fs.can_be_empty && + !fs.chars.test(static_cast(*s))) { + if (c.log && (fs.first_literal || fs.first_rule)) { + if (c.error_info.error_pos <= s) { + if (c.error_info.error_pos < s || !(id > 0)) { + c.error_info.error_pos = s; + c.error_info.expected_tokens.clear(); + } + if (fs.first_literal) { + c.error_info.add(fs.first_literal, nullptr); + } else { + c.error_info.add(nullptr, fs.first_rule); + } } - - id++; + } + id++; + continue; } + } - return len; + if (!c.cut_stack.empty()) { c.cut_stack.back() = false; } + + auto snap = c.snapshot(vs); + c.error_info.keep_previous_token = id > 0; + + len = ope->parse(s, n, vs, c, dt); + + if (success(len)) { + vs.choice_count_ = opes_.size(); + vs.choice_ = id; + break; + } + + c.rollback(vs, snap); + + if (!c.cut_stack.empty() && c.cut_stack.back()) { break; } + + id++; } - void accept(Visitor &v) override; + c.error_info.keep_previous_token = false; + return len; + } - size_t size() const { return opes_.size(); } + void accept(Visitor &v) override; - std::vector> opes_; - bool for_label_ = false; + size_t size() const { return opes_.size(); } + + std::vector> opes_; + bool for_label_ = false; + std::vector first_sets_; }; class Repetition : public Ope { public: - Repetition(const std::shared_ptr &ope, size_t min, size_t max) - : ope_(ope), min_(min), max_(max) {} + Repetition(const std::shared_ptr &ope, size_t min, size_t max) + : ope_(ope), min_(min), max_(max) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override { - size_t count = 0; - size_t i = 0; - while (count < min_) { - auto &chvs = c.push(); - auto se = scope_exit([&]() { c.pop(); }); - - auto len = ope_->parse(s + i, n - i, chvs, c, dt); - - if (success(len)) { - vs.append(chvs); - c.shift_capture_values(); - } else { - return len; - } - i += len; - count++; + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + // ISpan fast path: tight loop for ASCII CharacterClass repetition. + // Safe because each ASCII match is exactly 1 byte, so byte count == match + // count. + if (span_bitset_) { + const auto &bitset = *span_bitset_; + size_t i = 0; + if (max_ == std::numeric_limits::max()) { + // Unbounded repetition (*, +): no per-iteration max check + while (i < n && bitset.test(static_cast(s[i]))) { + i++; } - - while (count < max_) { - auto &chvs = c.push(); - auto se = scope_exit([&]() { c.pop(); }); - - auto len = ope_->parse(s + i, n - i, chvs, c, dt); - - if (success(len)) { - vs.append(chvs); - c.shift_capture_values(); - } else { - break; - } - i += len; - count++; + } else { + auto limit = std::min(n, max_); + while (i < limit && bitset.test(static_cast(s[i]))) { + i++; } - return i; + } + if (i < min_) { + c.set_error_pos(s + i); + return static_cast(-1); + } + return i; } - void accept(Visitor &v) override; - - bool is_zom() const { - return min_ == 0 && max_ == std::numeric_limits::max(); + size_t count = 0; + size_t i = 0; + while (count < min_) { + auto len = ope_->parse(s + i, n - i, vs, c, dt); + if (fail(len)) { return len; } + i += len; + count++; } - static std::shared_ptr zom(const std::shared_ptr &ope) { - return std::make_shared(ope, 0, - std::numeric_limits::max()); + while (count < max_) { + auto snap = c.snapshot(vs); + auto len = ope_->parse(s + i, n - i, vs, c, dt); + if (fail(len)) { + c.rollback(vs, snap); + break; + } + i += len; + count++; } + return i; + } - static std::shared_ptr oom(const std::shared_ptr &ope) { - return std::make_shared(ope, 1, - std::numeric_limits::max()); - } + void accept(Visitor &v) override; - static std::shared_ptr opt(const std::shared_ptr &ope) { - return std::make_shared(ope, 0, 1); - } + bool is_zom() const { + return min_ == 0 && max_ == std::numeric_limits::max(); + } - std::shared_ptr ope_; - size_t min_; - size_t max_; + static std::shared_ptr zom(const std::shared_ptr &ope) { + return std::make_shared(ope, 0, + std::numeric_limits::max()); + } + + static std::shared_ptr oom(const std::shared_ptr &ope) { + return std::make_shared(ope, 1, + std::numeric_limits::max()); + } + + static std::shared_ptr opt(const std::shared_ptr &ope) { + return std::make_shared(ope, 0, 1); + } + + std::shared_ptr ope_; + size_t min_; + size_t max_; + const std::bitset<256> *span_bitset_ = + nullptr; // non-owning, set by SetupFirstSets }; class AndPredicate : public Ope { public: - AndPredicate(const std::shared_ptr &ope) : ope_(ope) {} + AndPredicate(const std::shared_ptr &ope) : ope_(ope) {} - size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, - Context &c, std::any &dt) const override { - auto &chvs = c.push(); - auto se = scope_exit([&]() { c.pop(); }); - - auto len = ope_->parse(s, n, chvs, c, dt); - - if (success(len)) { - return 0; - } else { - return len; - } + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto snap = c.snapshot(vs); + auto len = ope_->parse(s, n, vs, c, dt); + c.rollback(vs, snap); // Always rollback — predicates consume nothing + if (success(len)) { + return 0; + } else { + return len; } + } - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::shared_ptr ope_; + std::shared_ptr ope_; }; class NotPredicate : public Ope { public: - NotPredicate(const std::shared_ptr &ope) : ope_(ope) {} + NotPredicate(const std::shared_ptr &ope) : ope_(ope) {} - size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, - Context &c, std::any &dt) const override { - auto &chvs = c.push(); - auto se = scope_exit([&]() { c.pop(); }); - auto len = ope_->parse(s, n, chvs, c, dt); - if (success(len)) { - c.set_error_pos(s); - return static_cast(-1); - } else { - return 0; - } + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto snap = c.snapshot(vs); + auto len = ope_->parse(s, n, vs, c, dt); + c.rollback(vs, snap); // Always rollback — predicates consume nothing + if (success(len)) { + c.set_error_pos(s); + return static_cast(-1); + } else { + return 0; } + } - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::shared_ptr ope_; + std::shared_ptr ope_; }; class Dictionary : public Ope, public std::enable_shared_from_this { public: - Dictionary(const std::vector &v, bool ignore_case) - : trie_(v, ignore_case) {} + Dictionary(const std::vector &v, bool ignore_case) + : trie_(v, ignore_case) { + is_choice_like = true; + } - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override; + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; - void accept(Visitor &v) override; + void accept(Visitor &v) override; - Trie trie_; + Trie trie_; }; class LiteralString : public Ope, public std::enable_shared_from_this { public: - LiteralString(std::string &&s, bool ignore_case) - : lit_(s), ignore_case_(ignore_case), is_word_(false) {} + LiteralString(std::string &&s, bool ignore_case) + : lit_(std::move(s)), ignore_case_(ignore_case), + lower_lit_(ignore_case ? to_lower(lit_) : std::string()), + is_word_(false) {} - LiteralString(const std::string &s, bool ignore_case) - : lit_(s), ignore_case_(ignore_case), is_word_(false) {} + LiteralString(const std::string &s, bool ignore_case) + : lit_(s), ignore_case_(ignore_case), + lower_lit_(ignore_case ? to_lower(lit_) : std::string()), + is_word_(false) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override; + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::string lit_; - bool ignore_case_; - mutable std::once_flag init_is_word_; - mutable bool is_word_; + std::string lit_; + bool ignore_case_; + std::string lower_lit_; // pre-computed for ignore_case + mutable std::once_flag init_is_word_; + mutable bool is_word_; }; class CharacterClass : public Ope, public std::enable_shared_from_this { public: - CharacterClass(const std::string &s, bool negated, bool ignore_case) - : negated_(negated), ignore_case_(ignore_case) { - auto chars = decode(s.data(), s.length()); - auto i = 0u; - while (i < chars.size()) { - if (i + 2 < chars.size() && chars[i + 1] == '-') { - auto cp1 = chars[i]; - auto cp2 = chars[i + 2]; - ranges_.emplace_back(std::pair(cp1, cp2)); - i += 3; - } else { - auto cp = chars[i]; - ranges_.emplace_back(std::pair(cp, cp)); - i += 1; - } - } - assert(!ranges_.empty()); + CharacterClass(const std::string &s, bool negated, bool ignore_case) + : negated_(negated), ignore_case_(ignore_case) { + auto chars = decode(s.data(), s.length()); + auto i = 0u; + while (i < chars.size()) { + if (i + 2 < chars.size() && chars[i + 1] == '-') { + auto cp1 = chars[i]; + auto cp2 = chars[i + 2]; + ranges_.emplace_back(std::pair(cp1, cp2)); + i += 3; + } else { + auto cp = chars[i]; + ranges_.emplace_back(std::pair(cp, cp)); + i += 1; + } + } + assert(!ranges_.empty()); + setup_ascii_bitset(); + } + + CharacterClass(const std::vector> &ranges, + bool negated, bool ignore_case) + : ranges_(ranges), negated_(negated), ignore_case_(ignore_case) { + assert(!ranges_.empty()); + setup_ascii_bitset(); + } + + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + if (n < 1) { + c.set_error_pos(s); + return static_cast(-1); } - CharacterClass(const std::vector> &ranges, - bool negated, bool ignore_case) - : ranges_(ranges), negated_(negated), ignore_case_(ignore_case) { - assert(!ranges_.empty()); - } - - size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, - Context &c, std::any & /*dt*/) const override { - if (n < 1) { - c.set_error_pos(s); - return static_cast(-1); - } - - char32_t cp = 0; - auto len = decode_codepoint(s, n, cp); - - for (const auto &range : ranges_) { - if (in_range(range, cp)) { - if (negated_) { - c.set_error_pos(s); - return static_cast(-1); - } else { - return len; - } - } - } + char32_t cp = 0; + auto len = decode_codepoint(s, n, cp); + for (const auto &range : ranges_) { + if (in_range(range, cp)) { if (negated_) { - return len; + c.set_error_pos(s); + return static_cast(-1); } else { - c.set_error_pos(s); - return static_cast(-1); + return len; } + } } - void accept(Visitor &v) override; + if (negated_) { + return len; + } else { + c.set_error_pos(s); + return static_cast(-1); + } + } + + void accept(Visitor &v) override; + + friend struct ComputeFirstSet; + + bool is_ascii_only() const { return is_ascii_only_; } + const std::bitset<256> &ascii_bitset() const { return ascii_bitset_; } private: - bool in_range(const std::pair &range, char32_t cp) const { - if (ignore_case_) { - auto cpl = std::tolower(cp); - return std::tolower(range.first) <= cpl && - cpl <= std::tolower(range.second); - } else { - return range.first <= cp && cp <= range.second; - } + bool in_range(const std::pair &range, char32_t cp) const { + if (ignore_case_) { + auto cpl = std::tolower(cp); + return std::tolower(range.first) <= cpl && + cpl <= std::tolower(range.second); + } else { + return range.first <= cp && cp <= range.second; } + } - std::vector> ranges_; - bool negated_; - bool ignore_case_; + void setup_ascii_bitset() { + if (negated_) { return; } // negated classes can match non-ASCII + for (const auto &[lo, hi] : ranges_) { + if (lo > 0x7F || hi > 0x7F) { return; } + } + is_ascii_only_ = true; + for (const auto &[lo, hi] : ranges_) { + for (auto cp = lo; cp <= hi; cp++) { + auto ch = static_cast(cp); + ascii_bitset_.set(ch); + if (ignore_case_) { + ascii_bitset_.set(static_cast(std::toupper(ch))); + ascii_bitset_.set(static_cast(std::tolower(ch))); + } + } + } + } + + std::vector> ranges_; + bool negated_; + bool ignore_case_; + std::bitset<256> ascii_bitset_; + bool is_ascii_only_ = false; }; class Character : public Ope, public std::enable_shared_from_this { public: - Character(char ch) : ch_(ch) {} + Character(char32_t ch) : ch_(ch) {} - size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, - Context &c, std::any & /*dt*/) const override { - if (n < 1 || s[0] != ch_) { - c.set_error_pos(s); - return static_cast(-1); - } - return 1; + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + if (n < 1) { + c.set_error_pos(s); + return static_cast(-1); } - void accept(Visitor &v) override; + char32_t cp = 0; + auto len = decode_codepoint(s, n, cp); - char ch_; + if (cp != ch_) { + c.set_error_pos(s); + return static_cast(-1); + } + return len; + } + + void accept(Visitor &v) override; + + char32_t ch_; }; class AnyCharacter : public Ope, public std::enable_shared_from_this { public: - size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, - Context &c, std::any & /*dt*/) const override { - auto len = codepoint_length(s, n); - if (len < 1) { - c.set_error_pos(s); - return static_cast(-1); - } - return len; + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + auto len = codepoint_length(s, n); + if (len < 1) { + c.set_error_pos(s); + return static_cast(-1); } + return len; + } - void accept(Visitor &v) override; + void accept(Visitor &v) override; }; class CaptureScope : public Ope { public: - CaptureScope(const std::shared_ptr &ope) : ope_(ope) {} + CaptureScope(const std::shared_ptr &ope) : ope_(ope) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override { - c.push_capture_scope(); - auto se = scope_exit([&]() { c.pop_capture_scope(); }); - return ope_->parse(s, n, vs, c, dt); - } + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto cap_snap = c.capture_entries.size(); + auto len = ope_->parse(s, n, vs, c, dt); + c.capture_entries.resize(cap_snap); // Always rollback (isolation) + return len; + } - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::shared_ptr ope_; + std::shared_ptr ope_; }; class Capture : public Ope { public: - using MatchAction = std::function; + using MatchAction = std::function; - Capture(const std::shared_ptr &ope, MatchAction ma) - : ope_(ope), match_action_(ma) {} + Capture(const std::shared_ptr &ope, MatchAction ma) + : ope_(ope), match_action_(ma) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override { - auto len = ope_->parse(s, n, vs, c, dt); - if (success(len) && match_action_) { match_action_(s, len, c); } - return len; - } + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto len = ope_->parse(s, n, vs, c, dt); + if (success(len) && match_action_) { match_action_(s, len, c); } + return len; + } - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::shared_ptr ope_; - MatchAction match_action_; + std::shared_ptr ope_; + MatchAction match_action_; }; class TokenBoundary : public Ope { public: - TokenBoundary(const std::shared_ptr &ope) : ope_(ope) {} + TokenBoundary(const std::shared_ptr &ope) : ope_(ope) { + is_token_boundary = true; + } - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override; + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::shared_ptr ope_; + std::shared_ptr ope_; }; class Ignore : public Ope { public: - Ignore(const std::shared_ptr &ope) : ope_(ope) {} + Ignore(const std::shared_ptr &ope) : ope_(ope) {} - size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, - Context &c, std::any &dt) const override { - auto &chvs = c.push_semantic_values_scope(); - auto se = scope_exit([&]() { c.pop_semantic_values_scope(); }); - return ope_->parse(s, n, chvs, c, dt); - } + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any &dt) const override { + auto &chvs = c.push_semantic_values_scope(); + auto se = scope_exit([&]() { c.pop_semantic_values_scope(); }); + return ope_->parse(s, n, chvs, c, dt); + } - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::shared_ptr ope_; + std::shared_ptr ope_; }; using Parser = std::function - fn_; + User(Parser fn) : fn_(fn) {} + size_t parse_core(const char *s, size_t n, SemanticValues &vs, + Context & /*c*/, std::any &dt) const override { + assert(fn_); + return fn_(s, n, vs, dt); + } + void accept(Visitor &v) override; + std::function + fn_; }; class WeakHolder : public Ope { public: - WeakHolder(const std::shared_ptr &ope) : weak_(ope) {} + WeakHolder(const std::shared_ptr &ope) : weak_(ope) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override { - auto ope = weak_.lock(); - assert(ope); - return ope->parse(s, n, vs, c, dt); - } + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto ope = weak_.lock(); + assert(ope); + return ope->parse(s, n, vs, c, dt); + } - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::weak_ptr weak_; + std::weak_ptr weak_; }; class Holder : public Ope { public: - Holder(Definition *outer) : outer_(outer) {} + Holder(Definition *outer) : outer_(outer) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override; + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::any reduce(SemanticValues &vs, std::any &dt) const; + std::any reduce(SemanticValues &vs, std::any &dt, + const std::any &predicate_data) const; - const std::string &name() const; - const std::string &trace_name() const; + const std::string &name() const; + const std::string &trace_name() const; - std::shared_ptr ope_; - Definition *outer_; - mutable std::once_flag trace_name_init_; - mutable std::string trace_name_; + std::shared_ptr ope_; + Definition *outer_; + mutable std::once_flag trace_name_init_; + mutable std::string trace_name_; - friend class Definition; + friend class Definition; }; using Grammar = std::unordered_map; class Reference : public Ope, public std::enable_shared_from_this { public: - Reference(const Grammar &grammar, const std::string &name, const char *s, - bool is_macro, const std::vector> &args) - : grammar_(grammar), name_(name), s_(s), is_macro_(is_macro), args_(args), - rule_(nullptr), iarg_(0) {} + Reference(const Grammar &grammar, const std::string &name, const char *s, + bool is_macro, const std::vector> &args) + : grammar_(grammar), name_(name), s_(s), is_macro_(is_macro), args_(args), + rule_(nullptr), iarg_(0) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override; + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::shared_ptr get_core_operator() const; + std::shared_ptr get_core_operator() const; - const Grammar &grammar_; - const std::string name_; - const char *s_; + const Grammar &grammar_; + const std::string name_; + const char *s_; - const bool is_macro_; - const std::vector> args_; + const bool is_macro_; + const std::vector> args_; - Definition *rule_; - size_t iarg_; + Definition *rule_; + size_t iarg_; }; class Whitespace : public Ope { public: - Whitespace(const std::shared_ptr &ope) : ope_(ope) {} + Whitespace(const std::shared_ptr &ope) : ope_(ope) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override { - if (c.in_whitespace) { return 0; } - c.in_whitespace = true; - auto se = scope_exit([&]() { c.in_whitespace = false; }); - return ope_->parse(s, n, vs, c, dt); - } + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + if (c.in_whitespace) { return 0; } + c.in_whitespace = true; + auto se = scope_exit([&]() { c.in_whitespace = false; }); + return ope_->parse(s, n, vs, c, dt); + } - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::shared_ptr ope_; + std::shared_ptr ope_; }; class BackReference : public Ope { public: - BackReference(std::string &&name) : name_(name) {} + BackReference(std::string &&name) : name_(std::move(name)) {} - BackReference(const std::string &name) : name_(name) {} + BackReference(const std::string &name) : name_(name) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override; + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::string name_; + std::string name_; }; class PrecedenceClimbing : public Ope { public: - using BinOpeInfo = std::map>; + using BinOpeInfo = std::map>; - PrecedenceClimbing(const std::shared_ptr &atom, - const std::shared_ptr &binop, const BinOpeInfo &info, - const Definition &rule) - : atom_(atom), binop_(binop), info_(info), rule_(rule) {} + PrecedenceClimbing(const std::shared_ptr &atom, + const std::shared_ptr &binop, const BinOpeInfo &info, + const Definition &rule) + : atom_(atom), binop_(binop), info_(info), rule_(rule) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override { - return parse_expression(s, n, vs, c, dt, 0); - } + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + return parse_expression(s, n, vs, c, dt, 0); + } - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::shared_ptr atom_; - std::shared_ptr binop_; - BinOpeInfo info_; - const Definition &rule_; + std::shared_ptr atom_; + std::shared_ptr binop_; + BinOpeInfo info_; + const Definition &rule_; private: - size_t parse_expression(const char *s, size_t n, SemanticValues &vs, - Context &c, std::any &dt, size_t min_prec) const; + size_t parse_expression(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt, size_t min_prec) const; - Definition &get_reference_for_binop(Context &c) const; + Definition &get_reference_for_binop(Context &c) const; }; class Recovery : public Ope { public: - Recovery(const std::shared_ptr &ope) : ope_(ope) {} + Recovery(const std::shared_ptr &ope) : ope_(ope) {} - size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, - std::any &dt) const override; + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; - void accept(Visitor &v) override; + void accept(Visitor &v) override; - std::shared_ptr ope_; + std::shared_ptr ope_; }; class Cut : public Ope, public std::enable_shared_from_this { public: - size_t parse_core(const char * /*s*/, size_t /*n*/, SemanticValues & /*vs*/, - Context &c, std::any & /*dt*/) const override { - if (!c.cut_stack.empty()) { c.cut_stack.back() = true; } - return 0; - } + size_t parse_core(const char * /*s*/, size_t /*n*/, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + if (!c.cut_stack.empty()) { c.cut_stack.back() = true; } + return 0; + } - void accept(Visitor &v) override; + void accept(Visitor &v) override; }; /* * Factories */ template std::shared_ptr seq(Args &&...args) { - return std::make_shared(static_cast>(args)...); + return std::make_shared(static_cast>(args)...); } template std::shared_ptr cho(Args &&...args) { - return std::make_shared( - false, static_cast>(args)...); + return std::make_shared( + false, static_cast>(args)...); } template std::shared_ptr cho4label_(Args &&...args) { - return std::make_shared( - true, static_cast>(args)...); + return std::make_shared( + true, static_cast>(args)...); } inline std::shared_ptr zom(const std::shared_ptr &ope) { - return Repetition::zom(ope); + return Repetition::zom(ope); } inline std::shared_ptr oom(const std::shared_ptr &ope) { - return Repetition::oom(ope); + return Repetition::oom(ope); } inline std::shared_ptr opt(const std::shared_ptr &ope) { - return Repetition::opt(ope); + return Repetition::opt(ope); } inline std::shared_ptr rep(const std::shared_ptr &ope, size_t min, size_t max) { - return std::make_shared(ope, min, max); + return std::make_shared(ope, min, max); } inline std::shared_ptr apd(const std::shared_ptr &ope) { - return std::make_shared(ope); + return std::make_shared(ope); } inline std::shared_ptr npd(const std::shared_ptr &ope) { - return std::make_shared(ope); + return std::make_shared(ope); } inline std::shared_ptr dic(const std::vector &v, bool ignore_case) { - return std::make_shared(v, ignore_case); + return std::make_shared(v, ignore_case); } inline std::shared_ptr lit(std::string &&s) { - return std::make_shared(s, false); + return std::make_shared(s, false); } inline std::shared_ptr liti(std::string &&s) { - return std::make_shared(s, true); + return std::make_shared(s, true); } inline std::shared_ptr cls(const std::string &s) { - return std::make_shared(s, false, false); + return std::make_shared(s, false, false); } inline std::shared_ptr cls(const std::vector> &ranges, bool ignore_case = false) { - return std::make_shared(ranges, false, ignore_case); + return std::make_shared(ranges, false, ignore_case); } inline std::shared_ptr ncls(const std::string &s) { - return std::make_shared(s, true, false); + return std::make_shared(s, true, false); } inline std::shared_ptr ncls(const std::vector> &ranges, bool ignore_case = false) { - return std::make_shared(ranges, true, ignore_case); + return std::make_shared(ranges, true, ignore_case); } -inline std::shared_ptr chr(char dt) { - return std::make_shared(dt); +inline std::shared_ptr chr(char32_t dt) { + return std::make_shared(dt); } inline std::shared_ptr dot() { return std::make_shared(); } inline std::shared_ptr csc(const std::shared_ptr &ope) { - return std::make_shared(ope); + return std::make_shared(ope); } inline std::shared_ptr cap(const std::shared_ptr &ope, Capture::MatchAction ma) { - return std::make_shared(ope, ma); + return std::make_shared(ope, ma); } inline std::shared_ptr tok(const std::shared_ptr &ope) { - return std::make_shared(ope); + return std::make_shared(ope); } inline std::shared_ptr ign(const std::shared_ptr &ope) { - return std::make_shared(ope); + return std::make_shared(ope); } inline std::shared_ptr usr(std::function fn) { - return std::make_shared(fn); + return std::make_shared(fn); } inline std::shared_ptr ref(const Grammar &grammar, const std::string &name, const char *s, bool is_macro, const std::vector> &args) { - return std::make_shared(grammar, name, s, is_macro, args); + return std::make_shared(grammar, name, s, is_macro, args); } inline std::shared_ptr wsp(const std::shared_ptr &ope) { - return std::make_shared(std::make_shared(ope)); + return std::make_shared(std::make_shared(ope)); } inline std::shared_ptr bkr(std::string &&name) { - return std::make_shared(name); + return std::make_shared(name); } inline std::shared_ptr pre(const std::shared_ptr &atom, const std::shared_ptr &binop, const PrecedenceClimbing::BinOpeInfo &info, const Definition &rule) { - return std::make_shared(atom, binop, info, rule); + return std::make_shared(atom, binop, info, rule); } inline std::shared_ptr rec(const std::shared_ptr &ope) { - return std::make_shared(ope); + return std::make_shared(ope); } inline std::shared_ptr cut() { return std::make_shared(); } @@ -1686,511 +1966,600 @@ inline std::shared_ptr cut() { return std::make_shared(); } * Visitor */ struct Ope::Visitor { - virtual ~Visitor() {} - virtual void visit(Sequence &) {} - virtual void visit(PrioritizedChoice &) {} - virtual void visit(Repetition &) {} - virtual void visit(AndPredicate &) {} - virtual void visit(NotPredicate &) {} - virtual void visit(Dictionary &) {} - virtual void visit(LiteralString &) {} - virtual void visit(CharacterClass &) {} - virtual void visit(Character &) {} - virtual void visit(AnyCharacter &) {} - virtual void visit(CaptureScope &) {} - virtual void visit(Capture &) {} - virtual void visit(TokenBoundary &) {} - virtual void visit(Ignore &) {} - virtual void visit(User &) {} - virtual void visit(WeakHolder &) {} - virtual void visit(Holder &) {} - virtual void visit(Reference &) {} - virtual void visit(Whitespace &) {} - virtual void visit(BackReference &) {} - virtual void visit(PrecedenceClimbing &) {} - virtual void visit(Recovery &) {} - virtual void visit(Cut &) {} + virtual ~Visitor() {} + virtual void visit(Sequence &) {} + virtual void visit(PrioritizedChoice &) {} + virtual void visit(Repetition &) {} + virtual void visit(AndPredicate &) {} + virtual void visit(NotPredicate &) {} + virtual void visit(Dictionary &) {} + virtual void visit(LiteralString &) {} + virtual void visit(CharacterClass &) {} + virtual void visit(Character &) {} + virtual void visit(AnyCharacter &) {} + virtual void visit(CaptureScope &) {} + virtual void visit(Capture &) {} + virtual void visit(TokenBoundary &) {} + virtual void visit(Ignore &) {} + virtual void visit(User &) {} + virtual void visit(WeakHolder &) {} + virtual void visit(Holder &) {} + virtual void visit(Reference &) {} + virtual void visit(Whitespace &) {} + virtual void visit(BackReference &) {} + virtual void visit(PrecedenceClimbing &) {} + virtual void visit(Recovery &) {} + virtual void visit(Cut &) {} +}; + +struct TraversalVisitor : public Ope::Visitor { + using Ope::Visitor::visit; + void visit(Sequence &ope) override { + for (auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(PrioritizedChoice &ope) override { + for (auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(Repetition &ope) override { ope.ope_->accept(*this); } + void visit(AndPredicate &ope) override { ope.ope_->accept(*this); } + void visit(NotPredicate &ope) override { ope.ope_->accept(*this); } + void visit(CaptureScope &ope) override { ope.ope_->accept(*this); } + void visit(Capture &ope) override { ope.ope_->accept(*this); } + void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } + void visit(Ignore &ope) override { ope.ope_->accept(*this); } + void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } + void visit(Holder &ope) override { ope.ope_->accept(*this); } + void visit(Whitespace &ope) override { ope.ope_->accept(*this); } + void visit(Recovery &ope) override { ope.ope_->accept(*this); } + void visit(PrecedenceClimbing &ope) override { ope.atom_->accept(*this); } }; struct TraceOpeName : public Ope::Visitor { - using Ope::Visitor::visit; + using Ope::Visitor::visit; - void visit(Sequence &) override { name_ = "Sequence"; } - void visit(PrioritizedChoice &) override { name_ = "PrioritizedChoice"; } - void visit(Repetition &) override { name_ = "Repetition"; } - void visit(AndPredicate &) override { name_ = "AndPredicate"; } - void visit(NotPredicate &) override { name_ = "NotPredicate"; } - void visit(Dictionary &) override { name_ = "Dictionary"; } - void visit(LiteralString &) override { name_ = "LiteralString"; } - void visit(CharacterClass &) override { name_ = "CharacterClass"; } - void visit(Character &) override { name_ = "Character"; } - void visit(AnyCharacter &) override { name_ = "AnyCharacter"; } - void visit(CaptureScope &) override { name_ = "CaptureScope"; } - void visit(Capture &) override { name_ = "Capture"; } - void visit(TokenBoundary &) override { name_ = "TokenBoundary"; } - void visit(Ignore &) override { name_ = "Ignore"; } - void visit(User &) override { name_ = "User"; } - void visit(WeakHolder &) override { name_ = "WeakHolder"; } - void visit(Holder &ope) override { name_ = ope.trace_name().data(); } - void visit(Reference &) override { name_ = "Reference"; } - void visit(Whitespace &) override { name_ = "Whitespace"; } - void visit(BackReference &) override { name_ = "BackReference"; } - void visit(PrecedenceClimbing &) override { name_ = "PrecedenceClimbing"; } - void visit(Recovery &) override { name_ = "Recovery"; } - void visit(Cut &) override { name_ = "Cut"; } + void visit(Sequence &) override { name_ = "Sequence"; } + void visit(PrioritizedChoice &) override { name_ = "PrioritizedChoice"; } + void visit(Repetition &) override { name_ = "Repetition"; } + void visit(AndPredicate &) override { name_ = "AndPredicate"; } + void visit(NotPredicate &) override { name_ = "NotPredicate"; } + void visit(Dictionary &) override { name_ = "Dictionary"; } + void visit(LiteralString &) override { name_ = "LiteralString"; } + void visit(CharacterClass &) override { name_ = "CharacterClass"; } + void visit(Character &) override { name_ = "Character"; } + void visit(AnyCharacter &) override { name_ = "AnyCharacter"; } + void visit(CaptureScope &) override { name_ = "CaptureScope"; } + void visit(Capture &) override { name_ = "Capture"; } + void visit(TokenBoundary &) override { name_ = "TokenBoundary"; } + void visit(Ignore &) override { name_ = "Ignore"; } + void visit(User &) override { name_ = "User"; } + void visit(WeakHolder &) override { name_ = "WeakHolder"; } + void visit(Holder &ope) override { name_ = ope.trace_name().data(); } + void visit(Reference &) override { name_ = "Reference"; } + void visit(Whitespace &) override { name_ = "Whitespace"; } + void visit(BackReference &) override { name_ = "BackReference"; } + void visit(PrecedenceClimbing &) override { name_ = "PrecedenceClimbing"; } + void visit(Recovery &) override { name_ = "Recovery"; } + void visit(Cut &) override { name_ = "Cut"; } - static std::string get(Ope &ope) { - TraceOpeName vis; - ope.accept(vis); - return vis.name_; - } + static std::string get(Ope &ope) { + TraceOpeName vis; + ope.accept(vis); + return vis.name_; + } private: - const char *name_ = nullptr; + const char *name_ = nullptr; }; -struct AssignIDToDefinition : public Ope::Visitor { - using Ope::Visitor::visit; +struct AssignIDToDefinition : public TraversalVisitor { + using TraversalVisitor::visit; - void visit(Sequence &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - } - } - void visit(PrioritizedChoice &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - } - } - void visit(Repetition &ope) override { ope.ope_->accept(*this); } - void visit(AndPredicate &ope) override { ope.ope_->accept(*this); } - void visit(NotPredicate &ope) override { ope.ope_->accept(*this); } - void visit(CaptureScope &ope) override { ope.ope_->accept(*this); } - void visit(Capture &ope) override { ope.ope_->accept(*this); } - void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } - void visit(Ignore &ope) override { ope.ope_->accept(*this); } - void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } - void visit(Holder &ope) override; - void visit(Reference &ope) override; - void visit(Whitespace &ope) override { ope.ope_->accept(*this); } - void visit(PrecedenceClimbing &ope) override; - void visit(Recovery &ope) override { ope.ope_->accept(*this); } + void visit(Holder &ope) override; + void visit(Reference &ope) override; + void visit(PrecedenceClimbing &ope) override; - std::unordered_map ids; + std::unordered_map ids; }; struct IsLiteralToken : public Ope::Visitor { - using Ope::Visitor::visit; + using Ope::Visitor::visit; - void visit(PrioritizedChoice &ope) override { - for (auto op : ope.opes_) { - if (!IsLiteralToken::check(*op)) { return; } - } - result_ = true; + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + if (!IsLiteralToken::check(*op)) { return; } } + result_ = true; + } - void visit(Dictionary &) override { result_ = true; } - void visit(LiteralString &) override { result_ = true; } + void visit(Dictionary &) override { result_ = true; } + void visit(LiteralString &) override { result_ = true; } - static bool check(Ope &ope) { - IsLiteralToken vis; - ope.accept(vis); - return vis.result_; - } + static bool check(Ope &ope) { + IsLiteralToken vis; + ope.accept(vis); + return vis.result_; + } private: - bool result_ = false; + bool result_ = false; }; -struct TokenChecker : public Ope::Visitor { - using Ope::Visitor::visit; +struct TokenChecker : public TraversalVisitor { + using TraversalVisitor::visit; - void visit(Sequence &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - } - } - void visit(PrioritizedChoice &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - } - } - void visit(Repetition &ope) override { ope.ope_->accept(*this); } - void visit(CaptureScope &ope) override { ope.ope_->accept(*this); } - void visit(Capture &ope) override { ope.ope_->accept(*this); } - void visit(TokenBoundary &) override { has_token_boundary_ = true; } - void visit(Ignore &ope) override { ope.ope_->accept(*this); } - void visit(WeakHolder &) override { has_rule_ = true; } - void visit(Holder &ope) override { ope.ope_->accept(*this); } - void visit(Reference &ope) override; - void visit(Whitespace &ope) override { ope.ope_->accept(*this); } - void visit(PrecedenceClimbing &ope) override { ope.atom_->accept(*this); } - void visit(Recovery &ope) override { ope.ope_->accept(*this); } + void visit(TokenBoundary &) override { has_token_boundary_ = true; } + void visit(AndPredicate &) override {} + void visit(NotPredicate &) override {} + void visit(WeakHolder &) override { has_rule_ = true; } + void visit(Reference &ope) override; - static bool is_token(Ope &ope) { - if (IsLiteralToken::check(ope)) { return true; } + static bool is_token(Ope &ope) { + if (IsLiteralToken::check(ope)) { return true; } - TokenChecker vis; - ope.accept(vis); - return vis.has_token_boundary_ || !vis.has_rule_; - } + TokenChecker vis; + ope.accept(vis); + return vis.has_token_boundary_ || !vis.has_rule_; + } private: - bool has_token_boundary_ = false; - bool has_rule_ = false; + bool has_token_boundary_ = false; + bool has_rule_ = false; }; struct FindLiteralToken : public Ope::Visitor { - using Ope::Visitor::visit; + using Ope::Visitor::visit; - void visit(LiteralString &ope) override { token_ = ope.lit_.data(); } - void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } - void visit(Ignore &ope) override { ope.ope_->accept(*this); } - void visit(Reference &ope) override; - void visit(Recovery &ope) override { ope.ope_->accept(*this); } + void visit(LiteralString &ope) override { token_ = ope.lit_.data(); } + void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } + void visit(Ignore &ope) override { ope.ope_->accept(*this); } + void visit(Reference &ope) override; + void visit(Recovery &ope) override { ope.ope_->accept(*this); } - static const char *token(Ope &ope) { - FindLiteralToken vis; - ope.accept(vis); - return vis.token_; - } + static const char *token(Ope &ope) { + FindLiteralToken vis; + ope.accept(vis); + return vis.token_; + } private: - const char *token_ = nullptr; + const char *token_ = nullptr; }; -struct DetectLeftRecursion : public Ope::Visitor { - using Ope::Visitor::visit; +struct DetectLeftRecursion : public TraversalVisitor { + using TraversalVisitor::visit; - DetectLeftRecursion(const std::string &name) : name_(name) {} + DetectLeftRecursion(const std::string &name) : name_(name) {} - void visit(Sequence &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - if (done_) { - break; - } else if (error_s) { - done_ = true; - break; - } - } + void visit(Sequence &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (done_) { + break; + } else if (error_s) { + done_ = true; + break; + } } - void visit(PrioritizedChoice &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - if (error_s) { - done_ = true; - break; - } - } + } + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (error_s) { + done_ = true; + break; + } } - void visit(Repetition &ope) override { - ope.ope_->accept(*this); - done_ = ope.min_ > 0; - } - void visit(AndPredicate &ope) override { - ope.ope_->accept(*this); - done_ = false; - } - void visit(NotPredicate &ope) override { - ope.ope_->accept(*this); - done_ = false; - } - void visit(Dictionary &) override { done_ = true; } - void visit(LiteralString &ope) override { done_ = !ope.lit_.empty(); } - void visit(CharacterClass &) override { done_ = true; } - void visit(Character &) override { done_ = true; } - void visit(AnyCharacter &) override { done_ = true; } - void visit(CaptureScope &ope) override { ope.ope_->accept(*this); } - void visit(Capture &ope) override { ope.ope_->accept(*this); } - void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } - void visit(Ignore &ope) override { ope.ope_->accept(*this); } - void visit(User &) override { done_ = true; } - void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } - void visit(Holder &ope) override { ope.ope_->accept(*this); } - void visit(Reference &ope) override; - void visit(Whitespace &ope) override { ope.ope_->accept(*this); } - void visit(BackReference &) override { done_ = true; } - void visit(PrecedenceClimbing &ope) override { ope.atom_->accept(*this); } - void visit(Recovery &ope) override { ope.ope_->accept(*this); } - void visit(Cut &) override { done_ = true; } + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + done_ = ope.min_ > 0; + } + void visit(AndPredicate &ope) override { + ope.ope_->accept(*this); + done_ = false; + } + void visit(NotPredicate &ope) override { + ope.ope_->accept(*this); + done_ = false; + } + void visit(Dictionary &) override { done_ = true; } + void visit(LiteralString &ope) override { done_ = !ope.lit_.empty(); } + void visit(CharacterClass &) override { done_ = true; } + void visit(Character &) override { done_ = true; } + void visit(AnyCharacter &) override { done_ = true; } + void visit(User &) override { done_ = true; } + void visit(Reference &ope) override; + void visit(BackReference &) override { done_ = true; } + void visit(Cut &) override { done_ = true; } - const char *error_s = nullptr; + const char *error_s = nullptr; + + std::shared_ptr resolve_macro_arg(size_t iarg) const; private: - std::string name_; - std::unordered_set refs_; - bool done_ = false; + std::string name_; + std::unordered_set refs_; + bool done_ = false; + std::vector> *> macro_args_stack_; }; -struct HasEmptyElement : public Ope::Visitor { - using Ope::Visitor::visit; +struct ComputeCanBeEmpty : public TraversalVisitor { + using TraversalVisitor::visit; - HasEmptyElement(std::vector> &refs, - std::unordered_map &has_error_cache) - : refs_(refs), has_error_cache_(has_error_cache) {} + bool result = false; - void visit(Sequence &ope) override; - void visit(PrioritizedChoice &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - if (is_empty) { return; } - } - } - void visit(Repetition &ope) override { - if (ope.min_ == 0) { - set_error(); - } else { - ope.ope_->accept(*this); - } - } - void visit(AndPredicate &) override { set_error(); } - void visit(NotPredicate &) override { set_error(); } - void visit(LiteralString &ope) override { - if (ope.lit_.empty()) { set_error(); } - } - void visit(CaptureScope &ope) override { ope.ope_->accept(*this); } - void visit(Capture &ope) override { ope.ope_->accept(*this); } - void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } - void visit(Ignore &ope) override { ope.ope_->accept(*this); } - void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } - void visit(Holder &ope) override { ope.ope_->accept(*this); } - void visit(Reference &ope) override; - void visit(Whitespace &ope) override { ope.ope_->accept(*this); } - void visit(PrecedenceClimbing &ope) override { ope.atom_->accept(*this); } - void visit(Recovery &ope) override { ope.ope_->accept(*this); } - - bool is_empty = false; - const char *error_s = nullptr; - std::string error_name; - -private: - void set_error() { - is_empty = true; - tie(error_s, error_name) = refs_.back(); - } - std::vector> &refs_; - std::unordered_map &has_error_cache_; + void visit(Sequence &ope) override { + result = std::all_of(ope.opes_.begin(), ope.opes_.end(), [](auto &op) { + ComputeCanBeEmpty vis; + op->accept(vis); + return vis.result; + }); + } + void visit(PrioritizedChoice &ope) override { + result = std::any_of(ope.opes_.begin(), ope.opes_.end(), [](auto &op) { + ComputeCanBeEmpty vis; + op->accept(vis); + return vis.result; + }); + } + void visit(Repetition &ope) override { result = ope.min_ == 0; } + void visit(AndPredicate &) override { result = true; } + void visit(NotPredicate &) override { result = true; } + void visit(Dictionary &) override { result = false; } + void visit(LiteralString &ope) override { result = ope.lit_.empty(); } + void visit(CharacterClass &) override { result = false; } + void visit(Character &) override { result = false; } + void visit(AnyCharacter &) override { result = false; } + void visit(User &) override { result = false; } + void visit(Reference &ope) override; + void visit(BackReference &) override { result = false; } + void visit(Cut &) override { result = false; } }; -struct DetectInfiniteLoop : public Ope::Visitor { - using Ope::Visitor::visit; +struct HasEmptyElement : public TraversalVisitor { + using TraversalVisitor::visit; - DetectInfiniteLoop(const char *s, const std::string &name, - std::vector> &refs, - std::unordered_map &has_error_cache) - : refs_(refs), has_error_cache_(has_error_cache) { - refs_.emplace_back(s, name); - } + HasEmptyElement(std::vector> &refs, + std::unordered_map &has_error_cache) + : refs_(refs), has_error_cache_(has_error_cache) {} - DetectInfiniteLoop(std::vector> &refs, - std::unordered_map &has_error_cache) - : refs_(refs), has_error_cache_(has_error_cache) {} + void visit(Sequence &ope) override; + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (is_empty) { return; } + } + } + void visit(Repetition &ope) override { + if (ope.min_ == 0) { + set_error(); + } else { + ope.ope_->accept(*this); + } + } + void visit(AndPredicate &) override { set_error(); } + void visit(NotPredicate &) override { set_error(); } + void visit(LiteralString &ope) override { + if (ope.lit_.empty()) { set_error(); } + } + void visit(Reference &ope) override; - void visit(Sequence &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - if (has_error) { return; } - } - } - void visit(PrioritizedChoice &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - if (has_error) { return; } - } - } - void visit(Repetition &ope) override { - if (ope.max_ == std::numeric_limits::max()) { - HasEmptyElement vis(refs_, has_error_cache_); - ope.ope_->accept(vis); - if (vis.is_empty) { - has_error = true; - error_s = vis.error_s; - error_name = vis.error_name; - } - } else { - ope.ope_->accept(*this); - } - } - void visit(AndPredicate &ope) override { ope.ope_->accept(*this); } - void visit(NotPredicate &ope) override { ope.ope_->accept(*this); } - void visit(CaptureScope &ope) override { ope.ope_->accept(*this); } - void visit(Capture &ope) override { ope.ope_->accept(*this); } - void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } - void visit(Ignore &ope) override { ope.ope_->accept(*this); } - void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } - void visit(Holder &ope) override { ope.ope_->accept(*this); } - void visit(Reference &ope) override; - void visit(Whitespace &ope) override { ope.ope_->accept(*this); } - void visit(PrecedenceClimbing &ope) override { ope.atom_->accept(*this); } - void visit(Recovery &ope) override { ope.ope_->accept(*this); } - - bool has_error = false; - const char *error_s = nullptr; - std::string error_name; + bool is_empty = false; + const char *error_s = nullptr; + std::string error_name; private: - std::vector> &refs_; - std::unordered_map &has_error_cache_; + void set_error() { + is_empty = true; + tie(error_s, error_name) = refs_.back(); + } + std::vector> &refs_; + std::unordered_map &has_error_cache_; }; -struct ReferenceChecker : public Ope::Visitor { - using Ope::Visitor::visit; +struct DetectInfiniteLoop : public TraversalVisitor { + using TraversalVisitor::visit; - ReferenceChecker(const Grammar &grammar, - const std::vector ¶ms) - : grammar_(grammar), params_(params) {} + DetectInfiniteLoop(const char *s, const std::string &name, + std::vector> &refs, + std::unordered_map &has_error_cache) + : refs_(refs), has_error_cache_(has_error_cache) { + refs_.emplace_back(s, name); + } - void visit(Sequence &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - } + DetectInfiniteLoop(std::vector> &refs, + std::unordered_map &has_error_cache) + : refs_(refs), has_error_cache_(has_error_cache) {} + + void visit(Sequence &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (has_error) { return; } } - void visit(PrioritizedChoice &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - } + } + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (has_error) { return; } } - void visit(Repetition &ope) override { ope.ope_->accept(*this); } - void visit(AndPredicate &ope) override { ope.ope_->accept(*this); } - void visit(NotPredicate &ope) override { ope.ope_->accept(*this); } - void visit(CaptureScope &ope) override { ope.ope_->accept(*this); } - void visit(Capture &ope) override { ope.ope_->accept(*this); } - void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } - void visit(Ignore &ope) override { ope.ope_->accept(*this); } - void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } - void visit(Holder &ope) override { ope.ope_->accept(*this); } - void visit(Reference &ope) override; - void visit(Whitespace &ope) override { ope.ope_->accept(*this); } - void visit(PrecedenceClimbing &ope) override { ope.atom_->accept(*this); } - void visit(Recovery &ope) override { ope.ope_->accept(*this); } + } + void visit(Repetition &ope) override { + if (ope.max_ == std::numeric_limits::max()) { + HasEmptyElement vis(refs_, has_error_cache_); + ope.ope_->accept(vis); + if (vis.is_empty) { + has_error = true; + error_s = vis.error_s; + error_name = vis.error_name; + } + } else { + ope.ope_->accept(*this); + } + } + void visit(Reference &ope) override; - std::unordered_map error_s; - std::unordered_map error_message; - std::unordered_set referenced; + bool has_error = false; + const char *error_s = nullptr; + std::string error_name; private: - const Grammar &grammar_; - const std::vector ¶ms_; + std::vector> &refs_; + std::unordered_map &has_error_cache_; }; -struct LinkReferences : public Ope::Visitor { - using Ope::Visitor::visit; +struct ReferenceChecker : public TraversalVisitor { + using TraversalVisitor::visit; - LinkReferences(Grammar &grammar, const std::vector ¶ms) - : grammar_(grammar), params_(params) {} + ReferenceChecker(const Grammar &grammar, + const std::vector ¶ms) + : grammar_(grammar), params_(params) {} - void visit(Sequence &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - } - } - void visit(PrioritizedChoice &ope) override { - for (auto op : ope.opes_) { - op->accept(*this); - } - } - void visit(Repetition &ope) override { ope.ope_->accept(*this); } - void visit(AndPredicate &ope) override { ope.ope_->accept(*this); } - void visit(NotPredicate &ope) override { ope.ope_->accept(*this); } - void visit(CaptureScope &ope) override { ope.ope_->accept(*this); } - void visit(Capture &ope) override { ope.ope_->accept(*this); } - void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } - void visit(Ignore &ope) override { ope.ope_->accept(*this); } - void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } - void visit(Holder &ope) override { ope.ope_->accept(*this); } - void visit(Reference &ope) override; - void visit(Whitespace &ope) override { ope.ope_->accept(*this); } - void visit(PrecedenceClimbing &ope) override { ope.atom_->accept(*this); } - void visit(Recovery &ope) override { ope.ope_->accept(*this); } + void visit(Reference &ope) override; + + std::unordered_map error_s; + std::unordered_map error_message; + std::unordered_set referenced; private: - Grammar &grammar_; - const std::vector ¶ms_; + const Grammar &grammar_; + const std::vector ¶ms_; +}; + +struct LinkReferences : public TraversalVisitor { + using TraversalVisitor::visit; + + LinkReferences(Grammar &grammar, const std::vector ¶ms) + : grammar_(grammar), params_(params) {} + + void visit(Reference &ope) override; + +private: + Grammar &grammar_; + const std::vector ¶ms_; }; struct FindReference : public Ope::Visitor { - using Ope::Visitor::visit; + using Ope::Visitor::visit; - FindReference(const std::vector> &args, - const std::vector ¶ms) - : args_(args), params_(params) {} + FindReference(const std::vector> &args, + const std::vector ¶ms) + : args_(args), params_(params) {} - void visit(Sequence &ope) override { - std::vector> opes; - for (auto o : ope.opes_) { - o->accept(*this); - opes.push_back(found_ope); - } - found_ope = std::make_shared(opes); + void visit(Sequence &ope) override { + std::vector> opes; + for (const auto &o : ope.opes_) { + o->accept(*this); + opes.emplace_back(std::move(found_ope)); } - void visit(PrioritizedChoice &ope) override { - std::vector> opes; - for (auto o : ope.opes_) { - o->accept(*this); - opes.push_back(found_ope); - } - found_ope = std::make_shared(opes); + found_ope = std::make_shared(opes); + } + void visit(PrioritizedChoice &ope) override { + std::vector> opes; + for (const auto &o : ope.opes_) { + o->accept(*this); + opes.emplace_back(std::move(found_ope)); } - void visit(Repetition &ope) override { - ope.ope_->accept(*this); - found_ope = rep(found_ope, ope.min_, ope.max_); - } - void visit(AndPredicate &ope) override { - ope.ope_->accept(*this); - found_ope = apd(found_ope); - } - void visit(NotPredicate &ope) override { - ope.ope_->accept(*this); - found_ope = npd(found_ope); - } - void visit(Dictionary &ope) override { found_ope = ope.shared_from_this(); } - void visit(LiteralString &ope) override { - found_ope = ope.shared_from_this(); - } - void visit(CharacterClass &ope) override { - found_ope = ope.shared_from_this(); - } - void visit(Character &ope) override { found_ope = ope.shared_from_this(); } - void visit(AnyCharacter &ope) override { found_ope = ope.shared_from_this(); } - void visit(CaptureScope &ope) override { - ope.ope_->accept(*this); - found_ope = csc(found_ope); - } - void visit(Capture &ope) override { - ope.ope_->accept(*this); - found_ope = cap(found_ope, ope.match_action_); - } - void visit(TokenBoundary &ope) override { - ope.ope_->accept(*this); - found_ope = tok(found_ope); - } - void visit(Ignore &ope) override { - ope.ope_->accept(*this); - found_ope = ign(found_ope); - } - void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } - void visit(Holder &ope) override { ope.ope_->accept(*this); } - void visit(Reference &ope) override; - void visit(Whitespace &ope) override { - ope.ope_->accept(*this); - found_ope = wsp(found_ope); - } - void visit(PrecedenceClimbing &ope) override { - ope.atom_->accept(*this); - found_ope = csc(found_ope); - } - void visit(Recovery &ope) override { - ope.ope_->accept(*this); - found_ope = rec(found_ope); - } - void visit(Cut &ope) override { found_ope = ope.shared_from_this(); } + found_ope = std::make_shared(opes); + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + found_ope = rep(found_ope, ope.min_, ope.max_); + } + void visit(AndPredicate &ope) override { + ope.ope_->accept(*this); + found_ope = apd(found_ope); + } + void visit(NotPredicate &ope) override { + ope.ope_->accept(*this); + found_ope = npd(found_ope); + } + void visit(Dictionary &ope) override { found_ope = ope.shared_from_this(); } + void visit(LiteralString &ope) override { + found_ope = ope.shared_from_this(); + } + void visit(CharacterClass &ope) override { + found_ope = ope.shared_from_this(); + } + void visit(Character &ope) override { found_ope = ope.shared_from_this(); } + void visit(AnyCharacter &ope) override { found_ope = ope.shared_from_this(); } + void visit(CaptureScope &ope) override { + ope.ope_->accept(*this); + found_ope = csc(found_ope); + } + void visit(Capture &ope) override { + ope.ope_->accept(*this); + found_ope = cap(found_ope, ope.match_action_); + } + void visit(TokenBoundary &ope) override { + ope.ope_->accept(*this); + found_ope = tok(found_ope); + } + void visit(Ignore &ope) override { + ope.ope_->accept(*this); + found_ope = ign(found_ope); + } + void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } + void visit(Holder &ope) override { ope.ope_->accept(*this); } + void visit(Reference &ope) override; + void visit(Whitespace &ope) override { + ope.ope_->accept(*this); + found_ope = wsp(found_ope); + } + void visit(PrecedenceClimbing &ope) override { + ope.atom_->accept(*this); + found_ope = csc(found_ope); + } + void visit(Recovery &ope) override { + ope.ope_->accept(*this); + found_ope = rec(found_ope); + } + void visit(Cut &ope) override { found_ope = ope.shared_from_this(); } - std::shared_ptr found_ope; + std::shared_ptr found_ope; private: - const std::vector> &args_; - const std::vector ¶ms_; + const std::vector> &args_; + const std::vector ¶ms_; +}; + +/* + * First-Set computation + */ +struct ComputeFirstSet : public TraversalVisitor { + using TraversalVisitor::visit; + + void visit(Sequence &ope) override { + for (const auto &op : ope.opes_) { + auto save = result_; + result_ = FirstSet{}; + op->accept(*this); + auto element_fs = result_; + result_ = save; + result_.chars |= element_fs.chars; + if (element_fs.any_char) { result_.any_char = true; } + if (!result_.first_literal) { + result_.first_literal = element_fs.first_literal; + } + if (!result_.first_rule) { result_.first_rule = element_fs.first_rule; } + if (!element_fs.can_be_empty) { return; } + // This element can be empty, continue to next + } + result_.can_be_empty = true; + } + void visit(PrioritizedChoice &ope) override { + auto save = result_; + for (const auto &op : ope.opes_) { + result_ = FirstSet{}; + op->accept(*this); + save.merge(result_); + } + result_ = save; + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + if (ope.min_ == 0) { result_.can_be_empty = true; } + } + void visit(AndPredicate &) override { result_.can_be_empty = true; } + void visit(NotPredicate &) override { result_.can_be_empty = true; } + void visit(Dictionary &ope) override { + for (const auto &[key, info] : ope.trie_.dic_) { + if (!key.empty()) { + auto ch = static_cast(key[0]); + result_.chars.set(ch); + if (ope.trie_.ignore_case_) { + result_.chars.set(static_cast(std::toupper(ch))); + result_.chars.set(static_cast(std::tolower(ch))); + } + } + } + } + void visit(LiteralString &ope) override { + if (ope.lit_.empty()) { + result_.can_be_empty = true; + } else { + auto ch = static_cast(ope.lit_[0]); + result_.chars.set(ch); + if (ope.ignore_case_) { + result_.chars.set(static_cast(std::toupper(ch))); + result_.chars.set(static_cast(std::tolower(ch))); + } + if (!result_.first_literal) { result_.first_literal = ope.lit_.c_str(); } + } + } + void visit(CharacterClass &ope) override { + for (const auto &range : ope.ranges_) { + auto cp1 = range.first; + auto cp2 = range.second; + if (cp1 > 0x7F || cp2 > 0x7F) { + // Non-ASCII range: conservative fallback + result_.any_char = true; + return; + } + for (auto cp = cp1; cp <= cp2; cp++) { + auto ch = static_cast(cp); + result_.chars.set(ch); + if (ope.ignore_case_) { + result_.chars.set(static_cast(std::toupper(ch))); + result_.chars.set(static_cast(std::tolower(ch))); + } + } + } + if (ope.negated_) { + result_.chars.flip(); + result_.any_char = true; // negated class can match non-ASCII + } + } + void visit(Character &ope) override { + if (ope.ch_ > 0x7F) { + result_.any_char = true; + } else { + result_.chars.set(static_cast(ope.ch_)); + } + } + void visit(AnyCharacter &) override { result_.any_char = true; } + void visit(User &) override { result_.any_char = true; } + void visit(Reference &ope) override; + void visit(BackReference &) override { result_.any_char = true; } + void visit(Cut &) override { result_.can_be_empty = true; } + + FirstSet result_; + +private: + std::unordered_set refs_; +}; + +struct SetupFirstSets : public TraversalVisitor { + using TraversalVisitor::visit; + + void visit(Sequence &ope) override; + void setup_keyword_guarded_identifier(Sequence &ope); + + void visit(PrioritizedChoice &ope) override { + ope.first_sets_.clear(); + ope.first_sets_.reserve(ope.opes_.size()); + for (const auto &op : ope.opes_) { + ComputeFirstSet cfs; + op->accept(cfs); + ope.first_sets_.push_back(cfs.result_); + } + for (const auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + // ISpan optimization: detect Repetition + ASCII CharacterClass + auto cc = dynamic_cast(ope.ope_.get()); + if (cc && cc->is_ascii_only()) { ope.span_bitset_ = &cc->ascii_bitset(); } + } + void visit(Reference &ope) override; + +private: + std::unordered_set refs_; }; /* @@ -2205,279 +2574,299 @@ static const char *RECOVER_DEFINITION_NAME = "%recover"; */ class Definition { public: - struct Result { - bool ret; - bool recovered; - size_t len; - ErrorInfo error_info; - }; + struct Result { + bool ret; + bool recovered; + size_t len; + ErrorInfo error_info; + }; - Definition() : holder_(std::make_shared(this)) {} + Definition() : holder_(std::make_shared(this)) {} - Definition(const Definition &rhs) : name(rhs.name), holder_(rhs.holder_) { - holder_->outer_ = this; + Definition(const Definition &rhs) : name(rhs.name), holder_(rhs.holder_) { + holder_->outer_ = this; + } + + Definition(const std::shared_ptr &ope) + : holder_(std::make_shared(this)) { + *this <= ope; + } + + operator std::shared_ptr() { + return std::make_shared(holder_); + } + + Definition &operator<=(const std::shared_ptr &ope) { + holder_->ope_ = ope; + return *this; + } + + Result parse(const char *s, size_t n, const char *path = nullptr, + Log log = nullptr) const { + SemanticValues vs; + std::any dt; + return parse_core(s, n, vs, dt, path, log); + } + + Result parse(const char *s, const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse(s, n, path, log); + } + + Result parse(const char *s, size_t n, std::any &dt, + const char *path = nullptr, Log log = nullptr) const { + SemanticValues vs; + return parse_core(s, n, vs, dt, path, log); + } + + Result parse(const char *s, std::any &dt, const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse(s, n, dt, path, log); + } + + template + Result parse_and_get_value(const char *s, size_t n, T &val, + const char *path = nullptr, + Log log = nullptr) const { + SemanticValues vs; + std::any dt; + auto r = parse_core(s, n, vs, dt, path, log); + if (r.ret && !vs.empty() && vs.front().has_value()) { + val = std::any_cast(vs[0]); } + return r; + } - Definition(const std::shared_ptr &ope) - : holder_(std::make_shared(this)) { - *this <= ope; - } + template + Result parse_and_get_value(const char *s, T &val, const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse_and_get_value(s, n, val, path, log); + } - operator std::shared_ptr() { - return std::make_shared(holder_); + template + Result parse_and_get_value(const char *s, size_t n, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + SemanticValues vs; + auto r = parse_core(s, n, vs, dt, path, log); + if (r.ret && !vs.empty() && vs.front().has_value()) { + val = std::any_cast(vs[0]); } + return r; + } - Definition &operator<=(const std::shared_ptr &ope) { - holder_->ope_ = ope; - return *this; - } - - Result parse(const char *s, size_t n, const char *path = nullptr, - Log log = nullptr) const { - SemanticValues vs; - std::any dt; - return parse_core(s, n, vs, dt, path, log); - } - - Result parse(const char *s, const char *path = nullptr, - Log log = nullptr) const { - auto n = strlen(s); - return parse(s, n, path, log); - } - - Result parse(const char *s, size_t n, std::any &dt, - const char *path = nullptr, Log log = nullptr) const { - SemanticValues vs; - return parse_core(s, n, vs, dt, path, log); - } - - Result parse(const char *s, std::any &dt, const char *path = nullptr, - Log log = nullptr) const { - auto n = strlen(s); - return parse(s, n, dt, path, log); - } - - template - Result parse_and_get_value(const char *s, size_t n, T &val, - const char *path = nullptr, - Log log = nullptr) const { - SemanticValues vs; - std::any dt; - auto r = parse_core(s, n, vs, dt, path, log); - if (r.ret && !vs.empty() && vs.front().has_value()) { - val = std::any_cast(vs[0]); - } - return r; - } - - template - Result parse_and_get_value(const char *s, T &val, const char *path = nullptr, - Log log = nullptr) const { - auto n = strlen(s); - return parse_and_get_value(s, n, val, path, log); - } - - template - Result parse_and_get_value(const char *s, size_t n, std::any &dt, T &val, - const char *path = nullptr, - Log log = nullptr) const { - SemanticValues vs; - auto r = parse_core(s, n, vs, dt, path, log); - if (r.ret && !vs.empty() && vs.front().has_value()) { - val = std::any_cast(vs[0]); - } - return r; - } - - template - Result parse_and_get_value(const char *s, std::any &dt, T &val, - const char *path = nullptr, - Log log = nullptr) const { - auto n = strlen(s); - return parse_and_get_value(s, n, dt, val, path, log); - } + template + Result parse_and_get_value(const char *s, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse_and_get_value(s, n, dt, val, path, log); + } #if defined(__cpp_lib_char8_t) - Result parse(const char8_t *s, size_t n, const char *path = nullptr, - Log log = nullptr) const { - return parse(reinterpret_cast(s), n, path, log); - } + Result parse(const char8_t *s, size_t n, const char *path = nullptr, + Log log = nullptr) const { + return parse(reinterpret_cast(s), n, path, log); + } - Result parse(const char8_t *s, const char *path = nullptr, - Log log = nullptr) const { - return parse(reinterpret_cast(s), path, log); - } + Result parse(const char8_t *s, const char *path = nullptr, + Log log = nullptr) const { + return parse(reinterpret_cast(s), path, log); + } - Result parse(const char8_t *s, size_t n, std::any &dt, - const char *path = nullptr, Log log = nullptr) const { - return parse(reinterpret_cast(s), n, dt, path, log); - } + Result parse(const char8_t *s, size_t n, std::any &dt, + const char *path = nullptr, Log log = nullptr) const { + return parse(reinterpret_cast(s), n, dt, path, log); + } - Result parse(const char8_t *s, std::any &dt, const char *path = nullptr, - Log log = nullptr) const { - return parse(reinterpret_cast(s), dt, path, log); - } + Result parse(const char8_t *s, std::any &dt, const char *path = nullptr, + Log log = nullptr) const { + return parse(reinterpret_cast(s), dt, path, log); + } - template - Result parse_and_get_value(const char8_t *s, size_t n, T &val, - const char *path = nullptr, - Log log = nullptr) const { - return parse_and_get_value(reinterpret_cast(s), n, val, *path, - log); - } + template + Result parse_and_get_value(const char8_t *s, size_t n, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), n, val, path, + log); + } - template - Result parse_and_get_value(const char8_t *s, T &val, - const char *path = nullptr, - Log log = nullptr) const { - return parse_and_get_value(reinterpret_cast(s), val, *path, - log); - } + template + Result parse_and_get_value(const char8_t *s, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), val, path, + log); + } - template - Result parse_and_get_value(const char8_t *s, size_t n, std::any &dt, T &val, - const char *path = nullptr, - Log log = nullptr) const { - return parse_and_get_value(reinterpret_cast(s), n, dt, val, - *path, log); - } + template + Result parse_and_get_value(const char8_t *s, size_t n, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), n, dt, val, + path, log); + } - template - Result parse_and_get_value(const char8_t *s, std::any &dt, T &val, - const char *path = nullptr, - Log log = nullptr) const { - return parse_and_get_value(reinterpret_cast(s), dt, val, - *path, log); - } + template + Result parse_and_get_value(const char8_t *s, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), dt, val, path, + log); + } #endif - void operator=(Action a) { action = a; } + void operator=(Action a) { action = a; } - template Definition &operator,(T fn) { - operator=(fn); - return *this; - } + template Definition &operator,(T fn) { + operator=(fn); + return *this; + } - Definition &operator~() { - ignoreSemanticValue = true; - return *this; - } + Definition &operator~() { + ignoreSemanticValue = true; + return *this; + } - void accept(Ope::Visitor &v) { holder_->accept(v); } + void accept(Ope::Visitor &v) { holder_->accept(v); } - std::shared_ptr get_core_operator() const { return holder_->ope_; } + std::shared_ptr get_core_operator() const { return holder_->ope_; } - bool is_token() const { - std::call_once(is_token_init_, [this]() { - is_token_ = TokenChecker::is_token(*get_core_operator()); - }); - return is_token_; - } + bool is_token() const { + std::call_once(is_token_init_, [this]() { + is_token_ = TokenChecker::is_token(*get_core_operator()); + }); + return is_token_; + } - std::string name; - const char *s_ = nullptr; - std::pair line_ = {1, 1}; + std::string name; + const char *s_ = nullptr; + std::pair line_ = {1, 1}; - std::function - predicate; + Predicate predicate; - size_t id = 0; - Action action; - std::function - enter; - std::function - leave; - bool ignoreSemanticValue = false; - std::shared_ptr whitespaceOpe; - std::shared_ptr wordOpe; - bool enablePackratParsing = false; - bool is_macro = false; - std::vector params; - bool disable_action = false; + size_t id = 0; + Action action; + std::function + enter; + std::function + leave; + bool ignoreSemanticValue = false; + std::shared_ptr whitespaceOpe; + std::shared_ptr wordOpe; + bool enablePackratParsing = false; + bool is_macro = false; + std::vector params; + bool disable_action = false; + bool is_left_recursive = false; + bool can_be_empty = false; - TracerEnter tracer_enter; - TracerLeave tracer_leave; - bool verbose_trace = false; - TracerStartOrEnd tracer_start; - TracerStartOrEnd tracer_end; + TracerEnter tracer_enter; + TracerLeave tracer_leave; + bool verbose_trace = false; + TracerStartOrEnd tracer_start; + TracerStartOrEnd tracer_end; - std::string error_message; - bool no_ast_opt = false; + std::string error_message; + bool no_ast_opt = false; - bool eoi_check = true; + bool eoi_check = true; + + // Per-rule packrat stats (optional, for profiling) + mutable bool collect_packrat_stats = false; + mutable std::vector packrat_stats_; private: - friend class Reference; - friend class ParserGenerator; + friend class Reference; + friend class ParserGenerator; - Definition &operator=(const Definition &rhs); - Definition &operator=(Definition &&rhs); + Definition &operator=(const Definition &rhs); + Definition &operator=(Definition &&rhs); - void initialize_definition_ids() const { - std::call_once(definition_ids_init_, [&]() { - AssignIDToDefinition vis; - holder_->accept(vis); - if (whitespaceOpe) { whitespaceOpe->accept(vis); } - if (wordOpe) { wordOpe->accept(vis); } - definition_ids_.swap(vis.ids); - }); + void initialize_definition_ids() const { + std::call_once(definition_ids_init_, [&]() { + AssignIDToDefinition vis; + holder_->accept(vis); + if (whitespaceOpe) { whitespaceOpe->accept(vis); } + if (wordOpe) { wordOpe->accept(vis); } + definition_ids_.swap(vis.ids); + }); + } + + void initialize_packrat_filter() const; + + Result parse_core(const char *s, size_t n, SemanticValues &vs, std::any &dt, + const char *path, Log log) const { + initialize_definition_ids(); + + std::shared_ptr ope = holder_; + + std::any trace_data; + if (tracer_start) { tracer_start(trace_data); } + auto se = scope_exit([&]() { + if (tracer_end) { tracer_end(trace_data); } + }); + + Context c(path, s, n, definition_ids_.size(), whitespaceOpe, wordOpe, + enablePackratParsing, tracer_enter, tracer_leave, trace_data, + verbose_trace, log); + + if (collect_packrat_stats) { + packrat_stats_.resize(definition_ids_.size()); + c.packrat_stats = &packrat_stats_; } - Result parse_core(const char *s, size_t n, SemanticValues &vs, std::any &dt, - const char *path, Log log) const { - initialize_definition_ids(); - - std::shared_ptr ope = holder_; - - std::any trace_data; - if (tracer_start) { tracer_start(trace_data); } - auto se1 = scope_exit([&]() { - if (tracer_end) { tracer_end(trace_data); } - }); - - Context c(path, s, n, definition_ids_.size(), whitespaceOpe, wordOpe, - enablePackratParsing, tracer_enter, tracer_leave, trace_data, - verbose_trace, log); - - size_t i = 0; - - if (whitespaceOpe) { - auto save_ignore_trace_state = c.ignore_trace_state; - c.ignore_trace_state = !c.verbose_trace; - auto se2 = - scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); - - auto len = whitespaceOpe->parse(s, n, vs, c, dt); - if (fail(len)) { return Result{false, c.recovered, i, c.error_info}; } - - i = len; - } - - auto len = ope->parse(s + i, n - i, vs, c, dt); - auto ret = success(len); - if (ret) { - i += len; - if (eoi_check) { - if (i < n) { - if (c.error_info.error_pos - c.s < s + i - c.s) { - c.error_info.message_pos = s + i; - c.error_info.message = "expected end of input"; - } - ret = false; - } - } - } - return Result{ret, c.recovered, i, c.error_info}; + if (enablePackratParsing) { + initialize_packrat_filter(); + if (!packrat_filter_.empty()) { + c.packrat_rule_filter = &packrat_filter_; + } } - std::shared_ptr holder_; - mutable std::once_flag is_token_init_; - mutable bool is_token_ = false; - mutable std::once_flag assign_id_to_definition_init_; - mutable std::once_flag definition_ids_init_; - mutable std::unordered_map definition_ids_; + size_t i = 0; + + if (whitespaceOpe) { + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + auto len = whitespaceOpe->parse(s, n, vs, c, dt); + if (fail(len)) { return Result{false, c.recovered, i, c.error_info}; } + + i = len; + } + + auto len = ope->parse(s + i, n - i, vs, c, dt); + auto ret = success(len); + if (ret) { + i += len; + if (eoi_check) { + if (i < n) { + if (c.error_info.error_pos - c.s < s + i - c.s) { + c.error_info.message_pos = s + i; + c.error_info.message = "expected end of input"; + } + ret = false; + } + } + } + return Result{ret, c.recovered, i, c.error_info}; + } + + std::shared_ptr holder_; + mutable std::once_flag is_token_init_; + mutable bool is_token_ = false; + mutable std::once_flag assign_id_to_definition_init_; + mutable std::once_flag definition_ids_init_; + mutable std::unordered_map definition_ids_; + mutable std::once_flag packrat_filter_init_; + mutable std::vector packrat_filter_; }; /* @@ -2487,592 +2876,704 @@ private: inline size_t parse_literal(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt, const std::string &lit, std::once_flag &init_is_word, bool &is_word, - bool ignore_case) { - size_t i = 0; - for (; i < lit.size(); i++) { - if (i >= n || (ignore_case ? (std::tolower(s[i]) != std::tolower(lit[i])) - : (s[i] != lit[i]))) { - c.set_error_pos(s, lit.data()); - return static_cast(-1); - } + bool ignore_case, const std::string &lower_lit) { + size_t i = 0; + for (; i < lit.size(); i++) { + if (i >= n || + (ignore_case ? (static_cast(std::tolower( + static_cast(s[i]))) != lower_lit[i]) + : (s[i] != lit[i]))) { + c.set_error_pos(s, lit.data()); + return static_cast(-1); } + } - // Word check - if (c.wordOpe) { - auto save_ignore_trace_state = c.ignore_trace_state; - c.ignore_trace_state = !c.verbose_trace; - auto se = - scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + // Word check + if (c.wordOpe) { + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); - std::call_once(init_is_word, [&]() { - SemanticValues dummy_vs; - Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, - nullptr, nullptr, false, nullptr); - std::any dummy_dt; + std::call_once(init_is_word, [&]() { + SemanticValues dummy_vs; + Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, + nullptr, nullptr, false, nullptr); + std::any dummy_dt; - auto len = - c.wordOpe->parse(lit.data(), lit.size(), dummy_vs, dummy_c, dummy_dt); - is_word = success(len); - }); + auto len = + c.wordOpe->parse(lit.data(), lit.size(), dummy_vs, dummy_c, dummy_dt); + is_word = success(len); + }); - if (is_word) { - SemanticValues dummy_vs; - Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, - nullptr, nullptr, false, nullptr); - std::any dummy_dt; + if (is_word) { + SemanticValues dummy_vs; + Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, + nullptr, nullptr, false, nullptr); + std::any dummy_dt; - NotPredicate ope(c.wordOpe); - auto len = ope.parse(s + i, n - i, dummy_vs, dummy_c, dummy_dt); - if (fail(len)) { - c.set_error_pos(s, lit.data()); - return len; - } - i += len; - } + NotPredicate ope(c.wordOpe); + auto len = ope.parse(s + i, n - i, dummy_vs, dummy_c, dummy_dt); + if (fail(len)) { + c.set_error_pos(s, lit.data()); + return len; + } + i += len; } + } - // Skip whitespace - if (!c.in_token_boundary_count && c.whitespaceOpe) { - auto save_ignore_trace_state = c.ignore_trace_state; - c.ignore_trace_state = !c.verbose_trace; - auto se = - scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + // Skip whitespace + auto wl = c.skip_whitespace(s + i, n - i, vs, dt); + if (fail(wl)) { return wl; } + i += wl; - auto len = c.whitespaceOpe->parse(s + i, n - i, vs, c, dt); - if (fail(len)) { return len; } - i += len; - } - - return i; + return i; } inline std::pair SemanticValues::line_info() const { - assert(c_); - return c_->line_info(sv_.data()); + assert(c_); + return c_->line_info(sv_.data()); } inline void ErrorInfo::output_log(const Log &log, const char *s, size_t n) { - if (message_pos) { - if (message_pos > last_output_pos) { - last_output_pos = message_pos; - auto line = line_info(s, message_pos); - std::string msg; - if (auto unexpected_token = heuristic_error_token(s, n, message_pos); - !unexpected_token.empty()) { - msg = replace_all(message, "%t", unexpected_token); + if (message_pos) { + if (message_pos > last_output_pos) { + last_output_pos = message_pos; + auto line = line_info(s, message_pos); + std::string msg; + if (auto unexpected_token = heuristic_error_token(s, n, message_pos); + !unexpected_token.empty()) { + msg = replace_all(message, "%t", unexpected_token); - auto unexpected_char = unexpected_token.substr( - 0, - codepoint_length(unexpected_token.data(), unexpected_token.size())); + auto unexpected_char = unexpected_token.substr( + 0, + codepoint_length(unexpected_token.data(), unexpected_token.size())); - msg = replace_all(msg, "%c", unexpected_char); - } else { - msg = message; - } - log(line.first, line.second, msg, label); - } - } else if (error_pos) { - if (error_pos > last_output_pos) { - last_output_pos = error_pos; - auto line = line_info(s, error_pos); - - std::string msg; - if (expected_tokens.empty()) { - msg = "syntax error."; - } else { - msg = "syntax error"; - - // unexpected token - if (auto unexpected_token = heuristic_error_token(s, n, error_pos); - !unexpected_token.empty()) { - msg += ", unexpected '"; - msg += unexpected_token; - msg += "'"; - } - - auto first_item = true; - size_t i = 0; - while (i < expected_tokens.size()) { - auto [error_literal, error_rule] = expected_tokens[i]; - - // Skip rules start with '_' - if (!(error_rule && error_rule->name[0] == '_')) { - msg += (first_item ? ", expecting " : ", "); - if (error_literal) { - msg += "'"; - msg += error_literal; - msg += "'"; - } else { - msg += "<" + error_rule->name + ">"; - if (label.empty()) { label = error_rule->name; } - } - first_item = false; - } - - i++; - } - msg += "."; - } - log(line.first, line.second, msg, label); - } + msg = replace_all(msg, "%c", unexpected_char); + } else { + msg = message; + } + log(line.first, line.second, msg, label); } + } else if (error_pos) { + if (error_pos > last_output_pos) { + last_output_pos = error_pos; + auto line = line_info(s, error_pos); + + std::string msg; + if (expected_tokens.empty()) { + msg = "syntax error."; + } else { + msg = "syntax error"; + + // unexpected token + if (auto unexpected_token = heuristic_error_token(s, n, error_pos); + !unexpected_token.empty()) { + msg += ", unexpected '"; + msg += unexpected_token; + msg += "'"; + } + + auto first_item = true; + size_t i = 0; + while (i < expected_tokens.size()) { + auto [error_literal, error_rule] = expected_tokens[i]; + + // Skip rules start with '_' + if (!(error_rule && error_rule->name[0] == '_')) { + msg += (first_item ? ", expecting " : ", "); + if (error_literal) { + msg += "'"; + msg += error_literal; + msg += "'"; + } else { + msg += "<" + error_rule->name + ">"; + if (label.empty()) { label = error_rule->name; } + } + first_item = false; + } + + i++; + } + msg += "."; + } + log(line.first, line.second, msg, label); + } + } +} + +inline size_t Context::skip_whitespace(const char *a_s, size_t n, + SemanticValues &vs, std::any &dt) { + if (in_token_boundary_count || !whitespaceOpe) { return 0; } + auto save = ignore_trace_state; + ignore_trace_state = !verbose_trace; + auto se = scope_exit([&]() { ignore_trace_state = save; }); + return whitespaceOpe->parse(a_s, n, vs, *this, dt); } inline void Context::set_error_pos(const char *a_s, const char *literal) { - if (log) { - if (error_info.error_pos <= a_s) { - if (error_info.error_pos < a_s || !error_info.keep_previous_token) { - error_info.error_pos = a_s; - error_info.expected_tokens.clear(); - } + if (log) { + if (error_info.error_pos <= a_s) { + if (error_info.error_pos < a_s || !error_info.keep_previous_token) { + error_info.error_pos = a_s; + error_info.expected_tokens.clear(); + } - const char *error_literal = nullptr; - const Definition *error_rule = nullptr; + const char *error_literal = nullptr; + const Definition *error_rule = nullptr; - if (literal) { - error_literal = literal; - } else if (!rule_stack.empty()) { - auto rule = rule_stack.back(); - auto ope = rule->get_core_operator(); - if (auto token = FindLiteralToken::token(*ope); - token && token[0] != '\0') { - error_literal = token; - } - } - - for (auto r : rule_stack) { - error_rule = r; - if (r->is_token()) { break; } - } - - if (error_literal || error_rule) { - error_info.add(error_literal, error_rule); - } + if (literal) { + error_literal = literal; + } else if (!rule_stack.empty()) { + auto rule = rule_stack.back(); + auto ope = rule->get_core_operator(); + if (auto token = FindLiteralToken::token(*ope); + token && token[0] != '\0') { + error_literal = token; } + } + + for (auto r : rule_stack) { + error_rule = r; + if (r->is_token()) { break; } + } + + if (error_literal || error_rule) { + error_info.add(error_literal, error_rule); + } } + } } inline void Context::trace_enter(const Ope &ope, const char *a_s, size_t n, const SemanticValues &vs, std::any &dt) { - trace_ids.push_back(next_trace_id++); - tracer_enter(ope, a_s, n, vs, *this, dt, trace_data); + trace_ids.push_back(next_trace_id++); + tracer_enter(ope, a_s, n, vs, *this, dt, trace_data); } inline void Context::trace_leave(const Ope &ope, const char *a_s, size_t n, const SemanticValues &vs, std::any &dt, size_t len) { - tracer_leave(ope, a_s, n, vs, *this, dt, len, trace_data); - trace_ids.pop_back(); + tracer_leave(ope, a_s, n, vs, *this, dt, len, trace_data); + trace_ids.pop_back(); } inline bool Context::is_traceable(const Ope &ope) const { - if (tracer_enter && tracer_leave) { - if (ignore_trace_state) { return false; } - return !dynamic_cast(&ope); - } - return false; + if (tracer_enter && tracer_leave) { + if (ignore_trace_state) { return false; } + return !dynamic_cast(&ope); + } + return false; } inline size_t Ope::parse(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt) const { - if (c.is_traceable(*this)) { - c.trace_enter(*this, s, n, vs, dt); - auto len = parse_core(s, n, vs, c, dt); - c.trace_leave(*this, s, n, vs, dt, len); - return len; - } - return parse_core(s, n, vs, c, dt); + if (c.is_traceable(*this)) { + c.trace_enter(*this, s, n, vs, dt); + auto len = parse_core(s, n, vs, c, dt); + c.trace_leave(*this, s, n, vs, dt, len); + return len; + } + return parse_core(s, n, vs, c, dt); } inline size_t Dictionary::parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt) const { - size_t id; - auto i = trie_.match(s, n, id); + size_t id; + auto i = trie_.match(s, n, id); - if (i == 0) { + if (i == 0) { + c.set_error_pos(s); + return static_cast(-1); + } + + vs.choice_count_ = trie_.items_count(); + vs.choice_ = id; + + // Word check + if (c.wordOpe) { + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + { + SemanticValues dummy_vs; + Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, + nullptr, nullptr, false, nullptr); + std::any dummy_dt; + + NotPredicate ope(c.wordOpe); + auto len = ope.parse(s + i, n - i, dummy_vs, dummy_c, dummy_dt); + if (fail(len)) { c.set_error_pos(s); - return static_cast(-1); + return len; + } + i += len; } + } - vs.choice_count_ = trie_.size(); - vs.choice_ = id; + // Skip whitespace + auto wl = c.skip_whitespace(s + i, n - i, vs, dt); + if (fail(wl)) { return wl; } + i += wl; - // Word check - if (c.wordOpe) { - auto save_ignore_trace_state = c.ignore_trace_state; - c.ignore_trace_state = !c.verbose_trace; - auto se = - scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); - - { - SemanticValues dummy_vs; - Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, - nullptr, nullptr, false, nullptr); - std::any dummy_dt; - - NotPredicate ope(c.wordOpe); - auto len = ope.parse(s + i, n - i, dummy_vs, dummy_c, dummy_dt); - if (fail(len)) { - c.set_error_pos(s); - return len; - } - i += len; - } - } - - // Skip whitespace - if (!c.in_token_boundary_count && c.whitespaceOpe) { - auto save_ignore_trace_state = c.ignore_trace_state; - c.ignore_trace_state = !c.verbose_trace; - auto se = - scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); - - auto len = c.whitespaceOpe->parse(s + i, n - i, vs, c, dt); - if (fail(len)) { return len; } - i += len; - } - - return i; + return i; } inline size_t LiteralString::parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt) const { - return parse_literal(s, n, vs, c, dt, lit_, init_is_word_, is_word_, - ignore_case_); + return parse_literal(s, n, vs, c, dt, lit_, init_is_word_, is_word_, + ignore_case_, lower_lit_); } inline size_t TokenBoundary::parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt) const { - auto save_ignore_trace_state = c.ignore_trace_state; - c.ignore_trace_state = !c.verbose_trace; - auto se1 = - scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); - size_t len; - { - c.in_token_boundary_count++; - auto se2 = scope_exit([&]() { c.in_token_boundary_count--; }); - len = ope_->parse(s, n, vs, c, dt); - } + size_t len; + { + c.in_token_boundary_count++; + auto se = scope_exit([&]() { c.in_token_boundary_count--; }); + len = ope_->parse(s, n, vs, c, dt); + } - if (success(len)) { - vs.tokens.emplace_back(std::string_view(s, len)); + if (success(len)) { + vs.tokens.emplace_back(std::string_view(s, len)); - if (!c.in_token_boundary_count) { - if (c.whitespaceOpe) { - auto l = c.whitespaceOpe->parse(s + len, n - len, vs, c, dt); - if (fail(l)) { return l; } - len += l; - } - } - } - return len; + auto wl = c.skip_whitespace(s + len, n - len, vs, dt); + if (fail(wl)) { return wl; } + len += wl; + } + return len; } inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt) const { - if (!ope_) { - throw std::logic_error("Uninitialized definition ope was used..."); - } + if (!ope_) { + throw std::logic_error("Uninitialized definition ope was used..."); + } - // Macro reference - if (outer_->is_macro) { - c.rule_stack.push_back(outer_); - auto len = ope_->parse(s, n, vs, c, dt); - c.rule_stack.pop_back(); - return len; - } + // Macro reference + if (outer_->is_macro) { + c.rule_stack.push_back(outer_); + auto len = ope_->parse(s, n, vs, c, dt); + c.rule_stack.pop_back(); + return len; + } - size_t len; - std::any val; + size_t len; + std::any val; - c.packrat(s, outer_->id, len, val, [&](std::any &a_val) { - if (outer_->enter) { outer_->enter(c, s, n, dt); } - auto &chvs = c.push_semantic_values_scope(); - auto se = scope_exit([&]() { - c.pop_semantic_values_scope(); - if (outer_->leave) { outer_->leave(c, s, n, len, a_val, dt); } - }); + // Shared parse body: invokes enter/leave callbacks, parses the rule's + // operator, handles actions/predicates/errors, and calls reduce. + // Returns {parse_len, parse_val}. + auto do_parse = [&]() { + size_t parse_len; + std::any parse_val; - c.rule_stack.push_back(outer_); - len = ope_->parse(s, n, chvs, c, dt); - c.rule_stack.pop_back(); - - // Invoke action - if (success(len)) { - chvs.sv_ = std::string_view(s, len); - chvs.name_ = outer_->name; - - auto ope_ptr = ope_.get(); - { - auto tok_ptr = dynamic_cast(ope_ptr); - if (tok_ptr) { ope_ptr = tok_ptr->ope_.get(); } - } - if (!dynamic_cast(ope_ptr) && - !dynamic_cast(ope_ptr)) { - chvs.choice_count_ = 0; - chvs.choice_ = 0; - } - - std::string msg; - if (outer_->predicate && !outer_->predicate(chvs, dt, msg)) { - if (c.log && !msg.empty() && c.error_info.message_pos < s) { - c.error_info.message_pos = s; - c.error_info.message = msg; - c.error_info.label = outer_->name; - } - len = static_cast(-1); - } - - if (success(len)) { - if (!c.recovered) { a_val = reduce(chvs, dt); } - } else { - if (c.log && !msg.empty() && c.error_info.message_pos < s) { - c.error_info.message_pos = s; - c.error_info.message = msg; - c.error_info.label = outer_->name; - } - } - } else { - if (c.log && !outer_->error_message.empty() && - c.error_info.message_pos < s) { - c.error_info.message_pos = s; - c.error_info.message = outer_->error_message; - c.error_info.label = outer_->name; - } - } + if (outer_->enter) { outer_->enter(c, s, n, dt); } + auto &chvs = c.push_semantic_values_scope(); + auto se = scope_exit([&]() { + c.pop_semantic_values_scope(); + if (outer_->leave) { outer_->leave(c, s, n, parse_len, parse_val, dt); } }); - if (success(len)) { - if (!outer_->ignoreSemanticValue) { - vs.emplace_back(std::move(val)); - vs.tags.emplace_back(str2tag(outer_->name)); + c.rule_stack.push_back(outer_); + parse_len = ope_->parse(s, n, chvs, c, dt); + c.rule_stack.pop_back(); + + if (success(parse_len)) { + chvs.sv_ = std::string_view(s, parse_len); + chvs.name_ = outer_->name; + + auto ope_ptr = ope_.get(); + if (ope_ptr->is_token_boundary) { + ope_ptr = static_cast(ope_ptr)->ope_.get(); + } + if (!ope_ptr->is_choice_like) { + chvs.choice_count_ = 0; + chvs.choice_ = 0; + } + + std::string msg; + std::any predicate_data; + if (outer_->predicate) { + if (!outer_->predicate(chvs, dt, msg, predicate_data)) { + if (c.log && !msg.empty() && c.error_info.message_pos < s) { + c.error_info.message_pos = s; + c.error_info.message = msg; + c.error_info.label = outer_->name; + } + parse_len = static_cast(-1); } + } + + if (success(parse_len)) { + if (!c.recovered) { parse_val = reduce(chvs, dt, predicate_data); } + } else { + if (c.log && !msg.empty() && c.error_info.message_pos < s) { + c.error_info.message_pos = s; + c.error_info.message = msg; + c.error_info.label = outer_->name; + } + } + } else { + if (c.log && !outer_->error_message.empty() && + c.error_info.message_pos < s) { + c.error_info.message_pos = s; + c.error_info.message = outer_->error_message; + c.error_info.label = outer_->name; + } } - return len; + return std::make_pair(parse_len, std::move(parse_val)); + }; + + if (outer_->is_left_recursive) { + auto lr_key = std::make_pair(outer_, s); + + // Check LR memo first + auto it = c.lr_memo.find(lr_key); + if (it != c.lr_memo.end()) { + if (success(it->second.len)) { + len = it->second.len; + val = it->second.val; + } else { + len = static_cast(-1); + } + // Record that this rule's lr_memo was accessed. + // Any LR rule currently seeding will know we're in its cycle. + c.lr_refs_hit.insert(outer_); + } else { + // Seed with FAIL + c.lr_memo[lr_key] = {static_cast(-1), {}}; + + // Mark as active seed (protects our lr_memo from inner growers) + c.lr_active_seeds.insert(lr_key); + auto seed_guard = scope_exit([&]() { c.lr_active_seeds.erase(lr_key); }); + + // Track which LR rules are referenced during our parse + // to identify cycle members + auto saved_refs = std::move(c.lr_refs_hit); + c.lr_refs_hit.clear(); + + // Initial parse (self-references will hit the FAIL seed) + auto [initial_len, initial_val] = do_parse(); + + // Rules whose lr_memo was hit during our parse are in our cycle. + // If we detected cycle members, we ourselves are also part of + // the cycle, so add self — this lets parent seeders see us as + // a transitive cycle member. + auto cycle_rules = c.lr_refs_hit; + if (!cycle_rules.empty()) { cycle_rules.insert(outer_); } + + // Restore parent's refs and propagate cycle info upward + c.lr_refs_hit = std::move(saved_refs); + c.lr_refs_hit.insert(cycle_rules.begin(), cycle_rules.end()); + + if (!success(initial_len)) { + // Keep FAIL in lr_memo so we don't re-seed + len = static_cast(-1); + } else { + // Got initial seed, now grow + len = initial_len; + val = std::move(initial_val); + c.lr_memo[lr_key] = {len, val}; + + while (true) { + // Clear this rule's packrat cache + c.clear_packrat_cache(s, outer_->id); + + // Clear lr_memo for cycle-dependent rules at this position, + // but NOT for rules currently in their own seeding phase + // (lr_active_seeds) — those are outer growers we must not + // interfere with. + for (auto memo_it = c.lr_memo.begin(); memo_it != c.lr_memo.end();) { + if (memo_it->first.second == s && memo_it->first.first != outer_ && + cycle_rules.count(memo_it->first.first) && + !c.lr_active_seeds.count(memo_it->first)) { + memo_it = c.lr_memo.erase(memo_it); + } else { + ++memo_it; + } + } + + auto [new_len, new_val] = do_parse(); + + if (!success(new_len) || new_len <= len) { + break; // No improvement, done growing + } + + len = new_len; + val = std::move(new_val); + c.lr_memo[lr_key] = {len, val}; + } + } + + // Write final result to packrat cache (lr_memo entry is kept as + // the primary lookup for LR rules at this position) + if (success(len)) { c.write_packrat_cache(s, outer_->id, len, val); } + } + } else { + if (c.enablePackratParsing) { + // Packrat cache acts as re-entry guard (pre-registered as + // failure before fn is called). + c.packrat(s, outer_->id, len, val, [&](std::any &a_val) { + auto [parse_len, parse_val] = do_parse(); + len = parse_len; + if (success(len)) { a_val = std::move(parse_val); } + }); + } else { + // Without packrat, use lr_memo as re-entry guard to prevent + // stack overflow from undetected left recursion. + auto guard_key = std::make_pair(outer_, s); + if (c.lr_memo.count(guard_key)) { + len = static_cast(-1); + } else { + c.lr_memo[guard_key] = {static_cast(-1), {}}; + auto [parse_len, parse_val] = do_parse(); + len = parse_len; + val = std::move(parse_val); + c.lr_memo.erase(guard_key); + } + } + } + + if (success(len)) { + if (!outer_->ignoreSemanticValue) { + vs.emplace_back(std::move(val)); + vs.tags.emplace_back(str2tag(outer_->name)); + } + } + + return len; } -inline std::any Holder::reduce(SemanticValues &vs, std::any &dt) const { - if (outer_->action && !outer_->disable_action) { - return outer_->action(vs, dt); - } else if (vs.empty()) { - return std::any(); - } else { - return std::move(vs.front()); - } +inline std::any Holder::reduce(SemanticValues &vs, std::any &dt, + const std::any &predicate_data) const { + if (outer_->action && !outer_->disable_action) { + return outer_->action(vs, dt, predicate_data); + } else if (vs.empty()) { + return std::any(); + } else { + return std::move(vs.front()); + } } inline const std::string &Holder::name() const { return outer_->name; } inline const std::string &Holder::trace_name() const { - std::call_once(trace_name_init_, - [this]() { trace_name_ = "[" + outer_->name + "]"; }); - return trace_name_; + std::call_once(trace_name_init_, + [this]() { trace_name_ = "[" + outer_->name + "]"; }); + return trace_name_; } inline size_t Reference::parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt) const { - auto save_ignore_trace_state = c.ignore_trace_state; - if (rule_ && rule_->ignoreSemanticValue) { - c.ignore_trace_state = !c.verbose_trace; - } - auto se1 = - scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + auto save_ignore_trace_state = c.ignore_trace_state; + if (rule_ && rule_->ignoreSemanticValue) { + c.ignore_trace_state = !c.verbose_trace; + } + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); - if (rule_) { - // Reference rule - if (rule_->is_macro) { - // Macro - FindReference vis(c.top_args(), c.rule_stack.back()->params); + if (rule_) { + // Reference rule + if (rule_->is_macro) { + // Macro + FindReference vis(c.top_args(), c.rule_stack.back()->params); - // Collect arguments - std::vector> args; - for (auto arg : args_) { - arg->accept(vis); - args.emplace_back(std::move(vis.found_ope)); - } + // Collect arguments + std::vector> args; + for (const auto &arg : args_) { + arg->accept(vis); + args.emplace_back(std::move(vis.found_ope)); + } - c.push_args(std::move(args)); - auto se2 = scope_exit([&]() { c.pop_args(); }); - auto ope = get_core_operator(); - return ope->parse(s, n, vs, c, dt); - } else { - // Definition - c.push_args(std::vector>()); - auto se3 = scope_exit([&]() { c.pop_args(); }); - auto ope = get_core_operator(); - return ope->parse(s, n, vs, c, dt); - } + c.push_args(std::move(args)); + auto se = scope_exit([&]() { c.pop_args(); }); + return rule_->holder_->parse(s, n, vs, c, dt); } else { - // Reference parameter in macro - const auto &args = c.top_args(); - return args[iarg_]->parse(s, n, vs, c, dt); + // Definition + c.push_args(std::vector>()); + auto se2 = scope_exit([&]() { c.pop_args(); }); + return rule_->holder_->parse(s, n, vs, c, dt); } + } else { + // Reference parameter in macro + const auto &args = c.top_args(); + return args[iarg_]->parse(s, n, vs, c, dt); + } } inline std::shared_ptr Reference::get_core_operator() const { - return rule_->holder_; + return rule_->holder_; } inline size_t BackReference::parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt) const { - auto size = static_cast(c.capture_scope_stack_size); - for (auto i = size - 1; i >= 0; i--) { - auto index = static_cast(i); - const auto &cs = c.capture_scope_stack[index]; - if (cs.find(name_) != cs.end()) { - const auto &lit = cs.at(name_); - std::once_flag init_is_word; - auto is_word = false; - return parse_literal(s, n, vs, c, dt, lit, init_is_word, is_word, false); - } + for (auto it = c.capture_entries.rbegin(); it != c.capture_entries.rend(); + ++it) { + if (it->first == name_) { + const auto &lit = it->second; + std::once_flag init_is_word; + auto is_word = false; + static const std::string empty; + return parse_literal(s, n, vs, c, dt, lit, init_is_word, is_word, false, + empty); } + } - c.error_info.message_pos = s; - c.error_info.message = "undefined back reference '$" + name_ + "'..."; - return static_cast(-1); + c.error_info.message_pos = s; + c.error_info.message = "undefined back reference '$" + name_ + "'..."; + return static_cast(-1); } inline Definition & PrecedenceClimbing::get_reference_for_binop(Context &c) const { - if (rule_.is_macro) { - // Reference parameter in macro - const auto &args = c.top_args(); - auto iarg = dynamic_cast(*binop_).iarg_; - auto arg = args[iarg]; - return *dynamic_cast(*arg).rule_; - } + if (rule_.is_macro) { + // Reference parameter in macro + const auto &args = c.top_args(); + auto iarg = dynamic_cast(*binop_).iarg_; + auto arg = args[iarg]; + return *dynamic_cast(*arg).rule_; + } - return *dynamic_cast(*binop_).rule_; + return *dynamic_cast(*binop_).rule_; } inline size_t PrecedenceClimbing::parse_expression(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt, size_t min_prec) const { - auto len = atom_->parse(s, n, vs, c, dt); - if (fail(len)) { return len; } + auto len = atom_->parse(s, n, vs, c, dt); + if (fail(len)) { return len; } - std::string tok; - auto &rule = get_reference_for_binop(c); - auto action = std::move(rule.action); + std::string tok; + auto &rule = get_reference_for_binop(c); + auto action = std::move(rule.action); - rule.action = [&](SemanticValues &vs2, std::any &dt2) { - tok = vs2.token(); - if (action) { - return action(vs2, dt2); - } else if (!vs2.empty()) { - return vs2[0]; - } - return std::any(); - }; - auto action_se = scope_exit([&]() { rule.action = std::move(action); }); + rule.action = [&](SemanticValues &vs2, std::any &dt2, + const std::any &predicate_data2) { + tok = vs2.token(); + if (action) { + return action(vs2, dt2, predicate_data2); + } else if (!vs2.empty()) { + return vs2[0]; + } + return std::any(); + }; + auto action_se = scope_exit([&]() { rule.action = std::move(action); }); - auto i = len; - while (i < n) { - std::vector save_values(vs.begin(), vs.end()); - auto save_tokens = vs.tokens; + auto i = len; + while (i < n) { + std::vector save_values(vs.begin(), vs.end()); + auto save_tokens = vs.tokens; - auto chvs = c.push_semantic_values_scope(); - auto chlen = binop_->parse(s + i, n - i, chvs, c, dt); - c.pop_semantic_values_scope(); + auto chvs = c.push_semantic_values_scope(); + auto chlen = binop_->parse(s + i, n - i, chvs, c, dt); + c.pop_semantic_values_scope(); - if (fail(chlen)) { break; } + if (fail(chlen)) { break; } - auto it = info_.find(tok); - if (it == info_.end()) { break; } + auto it = info_.find(tok); + if (it == info_.end()) { break; } - auto level = std::get<0>(it->second); - auto assoc = std::get<1>(it->second); + auto level = std::get<0>(it->second); + auto assoc = std::get<1>(it->second); - if (level < min_prec) { break; } + if (level < min_prec) { break; } - vs.emplace_back(std::move(chvs[0])); - i += chlen; + vs.emplace_back(std::move(chvs[0])); + i += chlen; - auto next_min_prec = level; - if (assoc == 'L') { next_min_prec = level + 1; } + auto next_min_prec = level; + if (assoc == 'L') { next_min_prec = level + 1; } - chvs = c.push_semantic_values_scope(); - chlen = parse_expression(s + i, n - i, chvs, c, dt, next_min_prec); - c.pop_semantic_values_scope(); + chvs = c.push_semantic_values_scope(); + chlen = parse_expression(s + i, n - i, chvs, c, dt, next_min_prec); + c.pop_semantic_values_scope(); - if (fail(chlen)) { - vs.assign(save_values.begin(), save_values.end()); - vs.tokens = save_tokens; - i = chlen; - break; - } - - vs.emplace_back(std::move(chvs[0])); - i += chlen; - - std::any val; - if (rule_.action) { - vs.sv_ = std::string_view(s, i); - val = rule_.action(vs, dt); - } else if (!vs.empty()) { - val = vs[0]; - } - vs.clear(); - vs.emplace_back(std::move(val)); + if (fail(chlen)) { + vs.assign(save_values.begin(), save_values.end()); + vs.tokens = save_tokens; + i = chlen; + break; } - return i; + vs.emplace_back(std::move(chvs[0])); + i += chlen; + + std::any val; + if (rule_.action) { + vs.sv_ = std::string_view(s, i); + static const std::any empty_predicate_data; + val = rule_.action(vs, dt, empty_predicate_data); + } else if (!vs.empty()) { + val = vs[0]; + } + vs.clear(); + vs.emplace_back(std::move(val)); + } + + return i; } inline size_t Recovery::parse_core(const char *s, size_t n, SemanticValues & /*vs*/, Context &c, std::any & /*dt*/) const { - const auto &rule = dynamic_cast(*ope_); + const auto &rule = dynamic_cast(*ope_); + + // Custom error message + if (c.log) { + auto label = dynamic_cast(rule.args_[0].get()); + if (label && !label->rule_->error_message.empty()) { + c.error_info.message_pos = s; + c.error_info.message = label->rule_->error_message; + c.error_info.label = label->rule_->name; + } + } + + // Recovery + auto len = static_cast(-1); + { + auto save_log = c.log; + c.log = nullptr; + auto se = scope_exit([&]() { c.log = save_log; }); + + SemanticValues dummy_vs; + std::any dummy_dt; + + len = rule.parse(s, n, dummy_vs, c, dummy_dt); + } + + if (success(len)) { + c.recovered = true; - // Custom error message if (c.log) { - auto label = dynamic_cast(rule.args_[0].get()); - if (label && !label->rule_->error_message.empty()) { - c.error_info.message_pos = s; - c.error_info.message = label->rule_->error_message; - c.error_info.label = label->rule_->name; - } + c.error_info.output_log(c.log, c.s, c.l); + c.error_info.clear(); } + } - // Recovery - auto len = static_cast(-1); - { - auto save_log = c.log; - c.log = nullptr; - auto se = scope_exit([&]() { c.log = save_log; }); + // Cut + if (!c.cut_stack.empty()) { + c.cut_stack.back() = true; - SemanticValues dummy_vs; - std::any dummy_dt; - - len = rule.parse(s, n, dummy_vs, c, dummy_dt); + if (c.cut_stack.size() == 1) { + // TODO: Remove unneeded entries in packrat memoise table } + } - if (success(len)) { - c.recovered = true; - - if (c.log) { - c.error_info.output_log(c.log, c.s, c.l); - c.error_info.clear(); - } - } - - // Cut - if (!c.cut_stack.empty()) { - c.cut_stack.back() = true; - - if (c.cut_stack.size() == 1) { - //! \todo Remove unneeded entries in packrat memoise table - } - } - - return len; + return len; } inline void Sequence::accept(Visitor &v) { v.visit(*this); } @@ -3100,194 +3601,452 @@ inline void Recovery::accept(Visitor &v) { v.visit(*this); } inline void Cut::accept(Visitor &v) { v.visit(*this); } inline void AssignIDToDefinition::visit(Holder &ope) { - auto p = static_cast(ope.outer_); - if (ids.count(p)) { return; } - auto id = ids.size(); - ids[p] = id; - ope.outer_->id = id; - ope.ope_->accept(*this); + auto p = static_cast(ope.outer_); + if (ids.count(p)) { return; } + auto id = ids.size(); + ids[p] = id; + ope.outer_->id = id; + ope.ope_->accept(*this); } inline void AssignIDToDefinition::visit(Reference &ope) { - if (ope.rule_) { - for (auto arg : ope.args_) { - arg->accept(*this); - } - ope.rule_->accept(*this); + if (ope.rule_) { + for (const auto &arg : ope.args_) { + arg->accept(*this); } + ope.rule_->accept(*this); + } } inline void AssignIDToDefinition::visit(PrecedenceClimbing &ope) { - ope.atom_->accept(*this); - ope.binop_->accept(*this); + ope.atom_->accept(*this); + ope.binop_->accept(*this); } inline void TokenChecker::visit(Reference &ope) { - if (ope.is_macro_) { - for (auto arg : ope.args_) { - arg->accept(*this); - } - } else { - has_rule_ = true; + if (ope.is_macro_) { + for (const auto &arg : ope.args_) { + arg->accept(*this); } + } else { + has_rule_ = true; + } } inline void FindLiteralToken::visit(Reference &ope) { - if (ope.is_macro_) { - ope.rule_->accept(*this); - for (auto arg : ope.args_) { - arg->accept(*this); - } + if (ope.is_macro_) { + ope.rule_->accept(*this); + for (const auto &arg : ope.args_) { + arg->accept(*this); } + } +} + +inline void ComputeCanBeEmpty::visit(Reference &ope) { + result = ope.rule_ && ope.rule_->can_be_empty; } inline void DetectLeftRecursion::visit(Reference &ope) { - if (ope.name_ == name_) { - error_s = ope.s_; - } else if (!refs_.count(ope.name_)) { - refs_.insert(ope.name_); - if (ope.rule_) { - ope.rule_->accept(*this); - if (done_ == false) { return; } - } + if (ope.name_ == name_) { + error_s = ope.s_; + } else if (!ope.rule_ && !macro_args_stack_.empty()) { + // Macro parameter reference: resolve through nested macro arg + // stacks (e.g. B(X) <- C(X) where X is itself a param ref). + auto resolved = resolve_macro_arg(ope.iarg_); + if (resolved) { + resolved->accept(*this); + if (done_ == false) { return; } } - done_ = true; + } else if (!refs_.count(ope.name_)) { + refs_.insert(ope.name_); + if (ope.rule_) { + if (ope.is_macro_) { macro_args_stack_.push_back(&ope.args_); } + ope.rule_->accept(*this); + if (ope.is_macro_) { macro_args_stack_.pop_back(); } + if (done_ == false) { return; } + } + } + // If the referenced rule can match empty, don't mark as done — + // the sequence may continue past this element to find LR. + if (!ope.rule_ && !macro_args_stack_.empty()) { + auto resolved = resolve_macro_arg(ope.iarg_); + if (resolved) { + ComputeCanBeEmpty cbe; + resolved->accept(cbe); + done_ = !cbe.result; + } else { + done_ = true; + } + } else { + done_ = !(ope.rule_ && ope.rule_->can_be_empty); + } +} + +inline std::shared_ptr +DetectLeftRecursion::resolve_macro_arg(size_t iarg) const { + for (int i = static_cast(macro_args_stack_.size()) - 1; i >= 0; i--) { + auto &args = *macro_args_stack_[i]; + if (iarg >= args.size()) { return nullptr; } + auto ref = dynamic_cast(args[iarg].get()); + if (ref && !ref->rule_) { + // Another param ref — resolve using parent level's args + iarg = ref->iarg_; + continue; + } + return args[iarg]; + } + return nullptr; } inline void HasEmptyElement::visit(Sequence &ope) { - auto save_is_empty = false; - const char *save_error_s = nullptr; - std::string save_error_name; + auto save_is_empty = false; + const char *save_error_s = nullptr; + std::string save_error_name; - auto it = ope.opes_.begin(); - while (it != ope.opes_.end()) { - (*it)->accept(*this); - if (!is_empty) { - ++it; - while (it != ope.opes_.end()) { - DetectInfiniteLoop vis(refs_, has_error_cache_); - (*it)->accept(vis); - if (vis.has_error) { - is_empty = true; - error_s = vis.error_s; - error_name = vis.error_name; - } - ++it; - } - return; + auto it = ope.opes_.begin(); + while (it != ope.opes_.end()) { + (*it)->accept(*this); + if (!is_empty) { + ++it; + while (it != ope.opes_.end()) { + DetectInfiniteLoop vis(refs_, has_error_cache_); + (*it)->accept(vis); + if (vis.has_error) { + is_empty = true; + error_s = vis.error_s; + error_name = vis.error_name; } - - save_is_empty = is_empty; - save_error_s = error_s; - save_error_name = error_name; - - is_empty = false; - error_name.clear(); ++it; + } + return; } - is_empty = save_is_empty; - error_s = save_error_s; - error_name = save_error_name; + save_is_empty = is_empty; + save_error_s = error_s; + save_error_name = error_name; + + is_empty = false; + error_name.clear(); + ++it; + } + + is_empty = save_is_empty; + error_s = save_error_s; + error_name = save_error_name; } inline void HasEmptyElement::visit(Reference &ope) { - auto it = std::find_if(refs_.begin(), refs_.end(), - [&](const std::pair &ref) { - return ope.name_ == ref.second; - }); - if (it != refs_.end()) { return; } + auto it = std::find_if(refs_.begin(), refs_.end(), + [&](const std::pair &ref) { + return ope.name_ == ref.second; + }); + if (it != refs_.end()) { return; } - if (ope.rule_) { - refs_.emplace_back(ope.s_, ope.name_); - ope.rule_->accept(*this); - refs_.pop_back(); - } + if (ope.rule_) { + refs_.emplace_back(ope.s_, ope.name_); + ope.rule_->accept(*this); + refs_.pop_back(); + } } inline void DetectInfiniteLoop::visit(Reference &ope) { - auto it1 = std::find_if(refs_.begin(), refs_.end(), - [&](const std::pair &ref) { - return ope.name_ == ref.second; - }); - if (it1 != refs_.end()) { return; } + auto it = std::find_if(refs_.begin(), refs_.end(), + [&](const std::pair &ref) { + return ope.name_ == ref.second; + }); + if (it != refs_.end()) { return; } - if (ope.rule_) { - auto it = has_error_cache_.find(ope.name_); - if (it != has_error_cache_.end()) { - has_error = it->second; - } else { - refs_.emplace_back(ope.s_, ope.name_); - ope.rule_->accept(*this); - refs_.pop_back(); - has_error_cache_[ope.name_] = has_error; - } + if (ope.rule_) { + auto it = has_error_cache_.find(ope.name_); + if (it != has_error_cache_.end()) { + has_error = it->second; + } else { + refs_.emplace_back(ope.s_, ope.name_); + ope.rule_->accept(*this); + refs_.pop_back(); + has_error_cache_[ope.name_] = has_error; } + } - if (ope.is_macro_) { - for (auto arg : ope.args_) { - arg->accept(*this); - } + if (ope.is_macro_) { + for (const auto &arg : ope.args_) { + arg->accept(*this); } + } } inline void ReferenceChecker::visit(Reference &ope) { - auto it = std::find(params_.begin(), params_.end(), ope.name_); - if (it != params_.end()) { return; } + auto it = std::find(params_.begin(), params_.end(), ope.name_); + if (it != params_.end()) { return; } - if (!grammar_.count(ope.name_)) { + if (!grammar_.count(ope.name_)) { + error_s[ope.name_] = ope.s_; + error_message[ope.name_] = "'" + ope.name_ + "' is not defined."; + } else { + if (!referenced.count(ope.name_)) { referenced.insert(ope.name_); } + const auto &rule = grammar_.at(ope.name_); + if (rule.is_macro) { + if (!ope.is_macro_ || ope.args_.size() != rule.params.size()) { error_s[ope.name_] = ope.s_; - error_message[ope.name_] = "'" + ope.name_ + "' is not defined."; - } else { - if (!referenced.count(ope.name_)) { referenced.insert(ope.name_); } - const auto &rule = grammar_.at(ope.name_); - if (rule.is_macro) { - if (!ope.is_macro_ || ope.args_.size() != rule.params.size()) { - error_s[ope.name_] = ope.s_; - error_message[ope.name_] = "incorrect number of arguments."; - } - } else if (ope.is_macro_) { - error_s[ope.name_] = ope.s_; - error_message[ope.name_] = "'" + ope.name_ + "' is not macro."; - } - for (auto arg : ope.args_) { - arg->accept(*this); - } + error_message[ope.name_] = "incorrect number of arguments."; + } + } else if (ope.is_macro_) { + error_s[ope.name_] = ope.s_; + error_message[ope.name_] = "'" + ope.name_ + "' is not macro."; } + for (const auto &arg : ope.args_) { + arg->accept(*this); + } + } +} + +inline void ComputeFirstSet::visit(Reference &ope) { + if (!ope.rule_) { + // Macro parameter reference — can't predict what it will match + result_.any_char = true; + return; + } + if (refs_.count(ope.name_)) { return; } + refs_.insert(ope.name_); + ope.rule_->accept(*this); + if (!result_.first_rule && ope.rule_->is_token()) { + result_.first_rule = ope.rule_; + } + refs_.erase(ope.name_); +} + +inline void SetupFirstSets::visit(Reference &ope) { + if (!ope.rule_ || refs_.count(ope.name_)) { return; } + refs_.insert(ope.name_); + ope.rule_->accept(*this); + refs_.erase(ope.name_); +} + +inline void SetupFirstSets::visit(Sequence &ope) { + ope.kw_guard_.reset(); + setup_keyword_guarded_identifier(ope); + for (const auto &op : ope.opes_) { + op->accept(*this); + } +} + +inline void SetupFirstSets::setup_keyword_guarded_identifier(Sequence &seq) { + // Detect pattern: NotPredicate(Reference→PrioritizedChoice) + // TokenBoundary(Sequence[CharacterClass, + // Repetition(CharacterClass)]) + // This is the pattern used by: PlainIdentifier <- !ReservedKeyword + // <[a-z_]i[a-z0-9_]i*> + if (seq.opes_.size() != 2) { return; } + + // Child 0 must be NotPredicate + auto *not_pred = dynamic_cast(seq.opes_[0].get()); + if (!not_pred) { return; } + + // NotPredicate's child must be Reference to a rule + auto *ref = dynamic_cast(not_pred->ope_.get()); + if (!ref || !ref->rule_) { return; } + + // The referenced rule's inner operator (Holder) must contain + // PrioritizedChoice + auto *holder = dynamic_cast(ref->get_core_operator().get()); + if (!holder) { return; } + auto *choice = dynamic_cast(holder->ope_.get()); + if (!choice) { return; } + + // Extract keywords from PrioritizedChoice alternatives + std::vector exact_keywords; + std::vector prefix_keywords; + + for (const auto &alt : choice->opes_) { + auto *lit = dynamic_cast(alt.get()); + if (lit) { + if (!lit->ignore_case_) { return; } + exact_keywords.push_back(to_lower(lit->lit_)); + continue; + } + // Check for compound keyword (Sequence of LiteralStrings) + auto *sub_seq = dynamic_cast(alt.get()); + if (sub_seq && !sub_seq->opes_.empty()) { + auto *first_lit = dynamic_cast(sub_seq->opes_[0].get()); + if (first_lit) { + auto all_ignore_case_lits = + std::all_of(sub_seq->opes_.begin(), sub_seq->opes_.end(), + [](const auto &child) { + auto *l = dynamic_cast(child.get()); + return l && l->ignore_case_; + }); + if (all_ignore_case_lits) { + prefix_keywords.push_back(to_lower(first_lit->lit_)); + continue; + } + } + } + // Unrecognized alternative — bail out + return; + } + + if (exact_keywords.empty()) { return; } + + // Child 1 must be TokenBoundary + auto *tb = dynamic_cast(seq.opes_[1].get()); + if (!tb) { return; } + + // TokenBoundary content: Sequence[CharacterClass, Repetition(CharacterClass)] + // or just CharacterClass (single char identifier) + CharacterClass *first_cc = nullptr; + CharacterClass *rest_cc = nullptr; + + auto *inner_seq = dynamic_cast(tb->ope_.get()); + if (inner_seq && inner_seq->opes_.size() == 2) { + first_cc = dynamic_cast(inner_seq->opes_[0].get()); + auto *rep = dynamic_cast(inner_seq->opes_[1].get()); + if (rep) { rest_cc = dynamic_cast(rep->ope_.get()); } + } + + if (!first_cc || !rest_cc) { return; } + if (!first_cc->is_ascii_only() || !rest_cc->is_ascii_only()) { return; } + + // All conditions met — set up the fast path + auto kw = std::make_unique(); + kw->identifier_first = first_cc->ascii_bitset(); + kw->identifier_rest = rest_cc->ascii_bitset(); + + // Compute keyword length range for early-out in hot path + size_t min_len = SIZE_MAX, max_len = 0; + for (const auto &k : exact_keywords) { + min_len = std::min(min_len, k.size()); + max_len = std::max(max_len, k.size()); + } + for (const auto &k : prefix_keywords) { + min_len = std::min(min_len, k.size()); + max_len = std::max(max_len, k.size()); + } + kw->min_keyword_len = min_len; + kw->max_keyword_len = max_len; + + kw->exact_keywords = std::move(exact_keywords); + kw->prefix_keywords = std::move(prefix_keywords); + seq.kw_guard_ = std::move(kw); +} + +// Compute which rules benefit from packrat memoization. +// A rule benefits if it's reachable from 2+ alternatives of the same +// PrioritizedChoice (backtracking will re-visit it at the same position). +inline void Definition::initialize_packrat_filter() const { + std::call_once(packrat_filter_init_, [&]() { + auto def_count = definition_ids_.size(); + if (def_count == 0) { return; } + + // Collect rule IDs reachable from an Ope subtree (bitvector indexed by + // def_id) + struct CollectReachableRules : public TraversalVisitor { + using TraversalVisitor::visit; + std::vector reachable; // indexed by def_id + + CollectReachableRules(size_t n) : reachable(n, false) {} + + void visit(Holder &ope) override { + auto id = ope.outer_->id; + if (id < reachable.size()) { reachable[id] = true; } + ope.ope_->accept(*this); + } + void visit(Reference &ope) override { + if (ope.rule_ && ope.rule_->id < reachable.size() && + !reachable[ope.rule_->id]) { + reachable[ope.rule_->id] = true; + ope.rule_->accept(*this); + } + } + }; + + // Find rules that benefit: reachable from 2+ alternatives of same choice + std::vector benefits(def_count, false); + + struct FindBacktrackRules : public TraversalVisitor { + using TraversalVisitor::visit; + std::vector &benefits; + size_t def_count; + std::vector visited_rules; // indexed by def_id + + FindBacktrackRules(std::vector &b, size_t n) + : benefits(b), def_count(n), visited_rules(n, false) {} + + void visit(PrioritizedChoice &ope) override { + // For each alternative, collect reachable rules as bitvectors + std::vector> alt_reachable; + for (auto &op : ope.opes_) { + CollectReachableRules crr(def_count); + op->accept(crr); + alt_reachable.push_back(std::move(crr.reachable)); + } + + // Mark rules reachable from 2+ alternatives + for (size_t id = 0; id < def_count; id++) { + size_t count = 0; + for (auto &alt : alt_reachable) { + if (alt[id]) { count++; } + } + if (count >= 2) { benefits[id] = true; } + } + + // Recurse into alternatives + for (auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(Holder &ope) override { + auto id = ope.outer_->id; + if (id < visited_rules.size() && !visited_rules[id]) { + visited_rules[id] = true; + ope.ope_->accept(*this); + } + } + void visit(Reference &ope) override { + if (ope.rule_) { ope.rule_->accept(*this); } + } + }; + + FindBacktrackRules finder(benefits, def_count); + holder_->accept(finder); + if (whitespaceOpe) { whitespaceOpe->accept(finder); } + if (wordOpe) { wordOpe->accept(finder); } + + packrat_filter_ = std::move(benefits); + }); } inline void LinkReferences::visit(Reference &ope) { - // Check if the reference is a macro parameter - auto found_param = false; - for (size_t i = 0; i < params_.size(); i++) { - const auto ¶m = params_[i]; - if (param == ope.name_) { - ope.iarg_ = i; - found_param = true; - break; - } + // Check if the reference is a macro parameter + auto found_param = false; + for (size_t i = 0; i < params_.size(); i++) { + const auto ¶m = params_[i]; + if (param == ope.name_) { + ope.iarg_ = i; + found_param = true; + break; } + } - // Check if the reference is a definition rule - if (!found_param && grammar_.count(ope.name_)) { - auto &rule = grammar_.at(ope.name_); - ope.rule_ = &rule; - } + // Check if the reference is a definition rule + if (!found_param && grammar_.count(ope.name_)) { + auto &rule = grammar_.at(ope.name_); + ope.rule_ = &rule; + } - for (auto arg : ope.args_) { - arg->accept(*this); - } + for (const auto &arg : ope.args_) { + arg->accept(*this); + } } inline void FindReference::visit(Reference &ope) { - for (size_t i = 0; i < args_.size(); i++) { - const auto &name = params_[i]; - if (name == ope.name_) { - found_ope = args_[i]; - return; - } + for (size_t i = 0; i < args_.size(); i++) { + const auto &name = params_[i]; + if (name == ope.name_) { + found_ope = args_[i]; + return; } - found_ope = ope.shared_from_this(); + } + found_ope = ope.shared_from_this(); } /*----------------------------------------------------------------------------- @@ -3298,955 +4057,1011 @@ using Rules = std::unordered_map>; class ParserGenerator { public: - struct ParserContext { - std::shared_ptr grammar; - std::string start; - bool enablePackratParsing = false; - }; + struct ParserContext { + std::shared_ptr grammar; + std::string start; + bool enablePackratParsing = false; + }; - static ParserContext parse(const char *s, size_t n, const Rules &rules, - Log log, std::string_view start) { - return get_instance().perform_core(s, n, rules, log, std::string(start)); - } + static ParserContext parse(const char *s, size_t n, const Rules &rules, + Log log, std::string_view start, + bool enable_left_recursion = true) { + return get_instance().perform_core(s, n, rules, log, std::string(start), + enable_left_recursion); + } - // For debugging purpose - static bool parse_test(const char *d, const char *s) { - Data data; - std::any dt = &data; + // For debugging purpose + static bool parse_test(const char *d, const char *s) { + Data data; + std::any dt = &data; - auto n = strlen(s); - auto r = get_instance().g[d].parse(s, n, dt); - return r.ret && r.len == n; - } + auto n = strlen(s); + auto r = get_instance().g[d].parse(s, n, dt); + return r.ret && r.len == n; + } #if defined(__cpp_lib_char8_t) - static bool parse_test(const char *d, const char8_t *s) { - return parse_test(d, reinterpret_cast(s)); - } + static bool parse_test(const char *d, const char8_t *s) { + return parse_test(d, reinterpret_cast(s)); + } #endif private: - static ParserGenerator &get_instance() { - static ParserGenerator instance; - return instance; - } + static ParserGenerator &get_instance() { + static ParserGenerator instance; + return instance; + } - ParserGenerator() { - make_grammar(); - setup_actions(); - } + ParserGenerator() { + make_grammar(); + setup_actions(); + } - struct Instruction { - std::string type; - std::any data; - std::string_view sv; + struct Instruction { + std::string type; + std::any data; + std::string_view sv; + }; + + struct Data { + std::shared_ptr grammar; + std::string start; + const char *start_pos = nullptr; + + std::vector> duplicates_of_definition; + + std::vector> duplicates_of_instruction; + std::map> instructions; + + std::vector> undefined_back_references; + std::vector> captures_stack{{}}; + + std::set captures_in_current_definition; + bool enablePackratParsing = true; + + Data() : grammar(std::make_shared()) {} + }; + + class SyntaxErrorException : public std::runtime_error { + public: + SyntaxErrorException(const char *what_arg, std::pair r) + : std::runtime_error(what_arg), r_(r) {} + + std::pair line_info() const { return r_; } + + private: + std::pair r_; + }; + + void make_grammar() { + // Setup PEG syntax parser + g["Grammar"] <= seq(g["Spacing"], oom(g["Definition"]), g["EndOfFile"]); + g["Definition"] <= + cho(seq(g["Ignore"], g["IdentCont"], g["Parameters"], g["LEFTARROW"], + g["Expression"], opt(g["Instruction"])), + seq(g["Ignore"], g["Identifier"], g["LEFTARROW"], g["Expression"], + opt(g["Instruction"]))); + g["Expression"] <= seq(g["Sequence"], zom(seq(g["SLASH"], g["Sequence"]))); + g["Sequence"] <= zom(cho(g["CUT"], g["Prefix"])); + g["Prefix"] <= seq(opt(cho(g["AND"], g["NOT"])), g["SuffixWithLabel"]); + g["SuffixWithLabel"] <= + seq(g["Suffix"], opt(seq(g["LABEL"], g["Identifier"]))); + g["Suffix"] <= seq(g["Primary"], opt(g["Loop"])); + g["Loop"] <= cho(g["QUESTION"], g["STAR"], g["PLUS"], g["Repetition"]); + g["Primary"] <= cho(seq(g["Ignore"], g["IdentCont"], g["Arguments"], + npd(g["LEFTARROW"])), + seq(g["Ignore"], g["Identifier"], + npd(seq(opt(g["Parameters"]), g["LEFTARROW"]))), + seq(g["OPEN"], g["Expression"], g["CLOSE"]), + seq(g["BeginTok"], g["Expression"], g["EndTok"]), + g["CapScope"], + seq(g["BeginCap"], g["Expression"], g["EndCap"]), + g["BackRef"], g["DictionaryI"], g["LiteralI"], + g["Dictionary"], g["Literal"], g["NegatedClassI"], + g["NegatedClass"], g["ClassI"], g["Class"], g["DOT"]); + + g["Identifier"] <= seq(g["IdentCont"], g["Spacing"]); + g["IdentCont"] <= tok(seq(g["IdentStart"], zom(g["IdentRest"]))); + + const static std::vector> range = { + {0x0080, 0xFFFF}}; + g["IdentStart"] <= seq(npd(lit(u8(u8"↑"))), npd(lit(u8(u8"⇑"))), + cho(cls("a-zA-Z_%"), cls(range))); + + g["IdentRest"] <= cho(g["IdentStart"], cls("0-9")); + + g["Dictionary"] <= seq(g["LiteralD"], oom(seq(g["PIPE"], g["LiteralD"]))); + + g["DictionaryI"] <= + seq(g["LiteralID"], oom(seq(g["PIPE"], g["LiteralID"]))); + + auto lit_ope = cho(seq(cls("'"), tok(zom(seq(npd(cls("'")), g["Char"]))), + cls("'"), g["Spacing"]), + seq(cls("\""), tok(zom(seq(npd(cls("\"")), g["Char"]))), + cls("\""), g["Spacing"])); + g["Literal"] <= lit_ope; + g["LiteralD"] <= lit_ope; + + auto lit_case_ignore_ope = + cho(seq(cls("'"), tok(zom(seq(npd(cls("'")), g["Char"]))), lit("'i"), + g["Spacing"]), + seq(cls("\""), tok(zom(seq(npd(cls("\"")), g["Char"]))), lit("\"i"), + g["Spacing"])); + g["LiteralI"] <= lit_case_ignore_ope; + g["LiteralID"] <= lit_case_ignore_ope; + + // NOTE: The original Brian Ford's paper uses 'zom' instead of 'oom'. + g["Class"] <= seq(chr('['), npd(chr('^')), + tok(oom(seq(npd(chr(']')), g["Range"]))), chr(']'), + g["Spacing"]); + g["ClassI"] <= seq(chr('['), npd(chr('^')), + tok(oom(seq(npd(chr(']')), g["Range"]))), lit("]i"), + g["Spacing"]); + + g["NegatedClass"] <= seq(lit("[^"), + tok(oom(seq(npd(chr(']')), g["Range"]))), chr(']'), + g["Spacing"]); + g["NegatedClassI"] <= seq(lit("[^"), + tok(oom(seq(npd(chr(']')), g["Range"]))), + lit("]i"), g["Spacing"]); + + // NOTE: This is different from The original Brian Ford's paper, and this + // modification allows us to specify `[+-]` as a valid char class. + g["Range"] <= + cho(seq(g["Char"], chr('-'), npd(chr(']')), g["Char"]), g["Char"]); + + g["Char"] <= + cho(seq(chr('\\'), cls("fnrtv'\"[]\\^-")), + seq(chr('\\'), cls("0-3"), cls("0-7"), cls("0-7")), + seq(chr('\\'), cls("0-7"), opt(cls("0-7"))), + seq(lit("\\x"), cls("0-9a-fA-F"), opt(cls("0-9a-fA-F"))), + seq(lit("\\u"), + cho(seq(cho(seq(chr('0'), cls("0-9a-fA-F")), lit("10")), + rep(cls("0-9a-fA-F"), 4, 4)), + rep(cls("0-9a-fA-F"), 4, 5))), + seq(npd(chr('\\')), dot())); + + g["Repetition"] <= + seq(g["BeginBracket"], g["RepetitionRange"], g["EndBracket"]); + g["RepetitionRange"] <= cho(seq(g["Number"], g["COMMA"], g["Number"]), + seq(g["Number"], g["COMMA"]), g["Number"], + seq(g["COMMA"], g["Number"])); + g["Number"] <= seq(oom(cls("0-9")), g["Spacing"]); + + g["CapScope"] <= seq(g["BeginCapScope"], g["Expression"], g["EndCapScope"]); + + g["LEFTARROW"] <= seq(cho(lit("<-"), lit(u8(u8"←"))), g["Spacing"]); + ~g["SLASH"] <= seq(chr('/'), g["Spacing"]); + ~g["PIPE"] <= seq(chr('|'), g["Spacing"]); + g["AND"] <= seq(chr('&'), g["Spacing"]); + g["NOT"] <= seq(chr('!'), g["Spacing"]); + g["QUESTION"] <= seq(chr('?'), g["Spacing"]); + g["STAR"] <= seq(chr('*'), g["Spacing"]); + g["PLUS"] <= seq(chr('+'), g["Spacing"]); + ~g["OPEN"] <= seq(chr('('), g["Spacing"]); + ~g["CLOSE"] <= seq(chr(')'), g["Spacing"]); + g["DOT"] <= seq(chr('.'), g["Spacing"]); + + g["CUT"] <= seq(lit(u8(u8"↑")), g["Spacing"]); + ~g["LABEL"] <= seq(cho(chr('^'), lit(u8(u8"⇑"))), g["Spacing"]); + + ~g["Spacing"] <= zom(cho(g["Space"], g["Comment"])); + g["Comment"] <= seq(chr('#'), zom(seq(npd(g["EndOfLine"]), dot())), + opt(g["EndOfLine"])); + g["Space"] <= cho(chr(' '), chr('\t'), g["EndOfLine"]); + g["EndOfLine"] <= cho(lit("\r\n"), chr('\n'), chr('\r')); + g["EndOfFile"] <= npd(dot()); + + ~g["BeginTok"] <= seq(chr('<'), g["Spacing"]); + ~g["EndTok"] <= seq(chr('>'), g["Spacing"]); + + ~g["BeginCapScope"] <= seq(chr('$'), chr('('), g["Spacing"]); + ~g["EndCapScope"] <= seq(chr(')'), g["Spacing"]); + + g["BeginCap"] <= seq(chr('$'), tok(g["IdentCont"]), chr('<'), g["Spacing"]); + ~g["EndCap"] <= seq(chr('>'), g["Spacing"]); + + g["BackRef"] <= seq(chr('$'), tok(g["IdentCont"]), g["Spacing"]); + + g["IGNORE"] <= chr('~'); + + g["Ignore"] <= opt(g["IGNORE"]); + g["Parameters"] <= seq(g["OPEN"], g["Identifier"], + zom(seq(g["COMMA"], g["Identifier"])), g["CLOSE"]); + g["Arguments"] <= seq(g["OPEN"], g["Expression"], + zom(seq(g["COMMA"], g["Expression"])), g["CLOSE"]); + ~g["COMMA"] <= seq(chr(','), g["Spacing"]); + + // Instruction grammars + g["Instruction"] <= + seq(g["BeginBracket"], + opt(seq(g["InstructionItem"], zom(seq(g["InstructionItemSeparator"], + g["InstructionItem"])))), + g["EndBracket"]); + g["InstructionItem"] <= + cho(g["PrecedenceClimbing"], g["ErrorMessage"], g["NoAstOpt"]); + ~g["InstructionItemSeparator"] <= seq(chr(';'), g["Spacing"]); + + ~g["SpacesZom"] <= zom(g["Space"]); + ~g["SpacesOom"] <= oom(g["Space"]); + ~g["BeginBracket"] <= seq(chr('{'), g["Spacing"]); + ~g["EndBracket"] <= seq(chr('}'), g["Spacing"]); + + // PrecedenceClimbing instruction + g["PrecedenceClimbing"] <= + seq(lit("precedence"), g["SpacesOom"], g["PrecedenceInfo"], + zom(seq(g["SpacesOom"], g["PrecedenceInfo"])), g["SpacesZom"]); + g["PrecedenceInfo"] <= + seq(g["PrecedenceAssoc"], + oom(seq(ign(g["SpacesOom"]), g["PrecedenceOpe"]))); + g["PrecedenceOpe"] <= + cho(seq(cls("'"), + tok(zom(seq(npd(cho(g["Space"], cls("'"))), g["Char"]))), + cls("'")), + seq(cls("\""), + tok(zom(seq(npd(cho(g["Space"], cls("\""))), g["Char"]))), + cls("\"")), + tok(oom(seq(npd(cho(g["PrecedenceAssoc"], g["Space"], chr('}'))), + dot())))); + g["PrecedenceAssoc"] <= cls("LR"); + + // Error message instruction + g["ErrorMessage"] <= seq(lit("error_message"), g["SpacesOom"], + g["LiteralD"], g["SpacesZom"]); + + // No Ast node optimization instruction + g["NoAstOpt"] <= seq(lit("no_ast_opt"), g["SpacesZom"]); + + // Set definition names + for (auto &x : g) { + x.second.name = x.first; + } + } + + void setup_actions() { + g["Definition"] = [&](const SemanticValues &vs, std::any &dt) { + auto &data = *std::any_cast(dt); + + auto is_macro = vs.choice() == 0; + auto ignore = std::any_cast(vs[0]); + auto name = std::any_cast(vs[1]); + + std::vector params; + std::shared_ptr ope; + auto has_instructions = false; + + if (is_macro) { + params = std::any_cast>(vs[2]); + ope = std::any_cast>(vs[4]); + if (vs.size() == 6) { has_instructions = true; } + } else { + ope = std::any_cast>(vs[3]); + if (vs.size() == 5) { has_instructions = true; } + } + + if (has_instructions) { + auto index = is_macro ? 5 : 4; + std::unordered_set types; + for (const auto &instruction : + std::any_cast>(vs[index])) { + const auto &type = instruction.type; + if (types.find(type) == types.end()) { + data.instructions[name].push_back(instruction); + types.insert(instruction.type); + if (type == "declare_symbol" || type == "check_symbol") { + if (!TokenChecker::is_token(*ope)) { ope = tok(ope); } + } + } else { + data.duplicates_of_instruction.emplace_back(type, + instruction.sv.data()); + } + } + } + + auto &grammar = *data.grammar; + if (!grammar.count(name)) { + auto &rule = grammar[name]; + rule <= ope; + rule.name = name; + rule.s_ = vs.sv().data(); + rule.line_ = line_info(vs.ss, rule.s_); + rule.ignoreSemanticValue = ignore; + rule.is_macro = is_macro; + rule.params = params; + + if (data.start.empty()) { + data.start = rule.name; + data.start_pos = rule.s_; + } + } else { + data.duplicates_of_definition.emplace_back(name, vs.sv().data()); + } }; - struct Data { - std::shared_ptr grammar; - std::string start; - const char *start_pos = nullptr; - - std::vector> duplicates_of_definition; - - std::vector> duplicates_of_instruction; - std::map> instructions; - - std::vector> undefined_back_references; - std::vector> captures_stack{{}}; - - std::set captures_in_current_definition; - bool enablePackratParsing = true; - - Data() : grammar(std::make_shared()) {} + g["Definition"].enter = [](const Context & /*c*/, const char * /*s*/, + size_t /*n*/, std::any &dt) { + auto &data = *std::any_cast(dt); + data.captures_in_current_definition.clear(); }; - void make_grammar() { - // Setup PEG syntax parser - g["Grammar"] <= seq(g["Spacing"], oom(g["Definition"]), g["EndOfFile"]); - g["Definition"] <= - cho(seq(g["Ignore"], g["IdentCont"], g["Parameters"], g["LEFTARROW"], - g["Expression"], opt(g["Instruction"])), - seq(g["Ignore"], g["Identifier"], g["LEFTARROW"], g["Expression"], - opt(g["Instruction"]))); - g["Expression"] <= seq(g["Sequence"], zom(seq(g["SLASH"], g["Sequence"]))); - g["Sequence"] <= zom(cho(g["CUT"], g["Prefix"])); - g["Prefix"] <= seq(opt(cho(g["AND"], g["NOT"])), g["SuffixWithLabel"]); - g["SuffixWithLabel"] <= - seq(g["Suffix"], opt(seq(g["LABEL"], g["Identifier"]))); - g["Suffix"] <= seq(g["Primary"], opt(g["Loop"])); - g["Loop"] <= cho(g["QUESTION"], g["STAR"], g["PLUS"], g["Repetition"]); - g["Primary"] <= cho(seq(g["Ignore"], g["IdentCont"], g["Arguments"], - npd(g["LEFTARROW"])), - seq(g["Ignore"], g["Identifier"], - npd(seq(opt(g["Parameters"]), g["LEFTARROW"]))), - seq(g["OPEN"], g["Expression"], g["CLOSE"]), - seq(g["BeginTok"], g["Expression"], g["EndTok"]), - g["CapScope"], - seq(g["BeginCap"], g["Expression"], g["EndCap"]), - g["BackRef"], g["DictionaryI"], g["LiteralI"], - g["Dictionary"], g["Literal"], g["NegatedClassI"], - g["NegatedClass"], g["ClassI"], g["Class"], g["DOT"]); - - g["Identifier"] <= seq(g["IdentCont"], g["Spacing"]); - g["IdentCont"] <= tok(seq(g["IdentStart"], zom(g["IdentRest"]))); - - const static std::vector> range = { - {0x0080, 0xFFFF}}; - g["IdentStart"] <= seq(npd(lit(u8(u8"↑"))), npd(lit(u8(u8"⇑"))), - cho(cls("a-zA-Z_%"), cls(range))); - - g["IdentRest"] <= cho(g["IdentStart"], cls("0-9")); - - g["Dictionary"] <= seq(g["LiteralD"], oom(seq(g["PIPE"], g["LiteralD"]))); - - g["DictionaryI"] <= - seq(g["LiteralID"], oom(seq(g["PIPE"], g["LiteralID"]))); - - auto lit_ope = cho(seq(cls("'"), tok(zom(seq(npd(cls("'")), g["Char"]))), - cls("'"), g["Spacing"]), - seq(cls("\""), tok(zom(seq(npd(cls("\"")), g["Char"]))), - cls("\""), g["Spacing"])); - g["Literal"] <= lit_ope; - g["LiteralD"] <= lit_ope; - - auto lit_case_ignore_ope = - cho(seq(cls("'"), tok(zom(seq(npd(cls("'")), g["Char"]))), lit("'i"), - g["Spacing"]), - seq(cls("\""), tok(zom(seq(npd(cls("\"")), g["Char"]))), lit("\"i"), - g["Spacing"])); - g["LiteralI"] <= lit_case_ignore_ope; - g["LiteralID"] <= lit_case_ignore_ope; - - // NOTE: The original Brian Ford's paper uses 'zom' instead of 'oom'. - g["Class"] <= seq(chr('['), npd(chr('^')), - tok(oom(seq(npd(chr(']')), g["Range"]))), chr(']'), - g["Spacing"]); - g["ClassI"] <= seq(chr('['), npd(chr('^')), - tok(oom(seq(npd(chr(']')), g["Range"]))), lit("]i"), - g["Spacing"]); - - g["NegatedClass"] <= seq(lit("[^"), - tok(oom(seq(npd(chr(']')), g["Range"]))), chr(']'), - g["Spacing"]); - g["NegatedClassI"] <= seq(lit("[^"), - tok(oom(seq(npd(chr(']')), g["Range"]))), - lit("]i"), g["Spacing"]); - - // NOTE: This is different from The original Brian Ford's paper, and this - // modification allows us to specify `[+-]` as a valid char class. - g["Range"] <= - cho(seq(g["Char"], chr('-'), npd(chr(']')), g["Char"]), g["Char"]); - - g["Char"] <= - cho(seq(chr('\\'), cls("fnrtv'\"[]\\^")), - seq(chr('\\'), cls("0-3"), cls("0-7"), cls("0-7")), - seq(chr('\\'), cls("0-7"), opt(cls("0-7"))), - seq(lit("\\x"), cls("0-9a-fA-F"), opt(cls("0-9a-fA-F"))), - seq(lit("\\u"), - cho(seq(cho(seq(chr('0'), cls("0-9a-fA-F")), lit("10")), - rep(cls("0-9a-fA-F"), 4, 4)), - rep(cls("0-9a-fA-F"), 4, 5))), - seq(npd(chr('\\')), dot())); - - g["Repetition"] <= - seq(g["BeginBracket"], g["RepetitionRange"], g["EndBracket"]); - g["RepetitionRange"] <= cho(seq(g["Number"], g["COMMA"], g["Number"]), - seq(g["Number"], g["COMMA"]), g["Number"], - seq(g["COMMA"], g["Number"])); - g["Number"] <= seq(oom(cls("0-9")), g["Spacing"]); - - g["CapScope"] <= seq(g["BeginCapScope"], g["Expression"], g["EndCapScope"]); - - g["LEFTARROW"] <= seq(cho(lit("<-"), lit(u8(u8"←"))), g["Spacing"]); - ~g["SLASH"] <= seq(chr('/'), g["Spacing"]); - ~g["PIPE"] <= seq(chr('|'), g["Spacing"]); - g["AND"] <= seq(chr('&'), g["Spacing"]); - g["NOT"] <= seq(chr('!'), g["Spacing"]); - g["QUESTION"] <= seq(chr('?'), g["Spacing"]); - g["STAR"] <= seq(chr('*'), g["Spacing"]); - g["PLUS"] <= seq(chr('+'), g["Spacing"]); - ~g["OPEN"] <= seq(chr('('), g["Spacing"]); - ~g["CLOSE"] <= seq(chr(')'), g["Spacing"]); - g["DOT"] <= seq(chr('.'), g["Spacing"]); - - g["CUT"] <= seq(lit(u8(u8"↑")), g["Spacing"]); - ~g["LABEL"] <= seq(cho(chr('^'), lit(u8(u8"⇑"))), g["Spacing"]); - - ~g["Spacing"] <= zom(cho(g["Space"], g["Comment"])); - g["Comment"] <= - seq(chr('#'), zom(seq(npd(g["EndOfLine"]), dot())), g["EndOfLine"]); - g["Space"] <= cho(chr(' '), chr('\t'), g["EndOfLine"]); - g["EndOfLine"] <= cho(lit("\r\n"), chr('\n'), chr('\r')); - g["EndOfFile"] <= npd(dot()); - - ~g["BeginTok"] <= seq(chr('<'), g["Spacing"]); - ~g["EndTok"] <= seq(chr('>'), g["Spacing"]); - - ~g["BeginCapScope"] <= seq(chr('$'), chr('('), g["Spacing"]); - ~g["EndCapScope"] <= seq(chr(')'), g["Spacing"]); - - g["BeginCap"] <= seq(chr('$'), tok(g["IdentCont"]), chr('<'), g["Spacing"]); - ~g["EndCap"] <= seq(chr('>'), g["Spacing"]); - - g["BackRef"] <= seq(chr('$'), tok(g["IdentCont"]), g["Spacing"]); - - g["IGNORE"] <= chr('~'); - - g["Ignore"] <= opt(g["IGNORE"]); - g["Parameters"] <= seq(g["OPEN"], g["Identifier"], - zom(seq(g["COMMA"], g["Identifier"])), g["CLOSE"]); - g["Arguments"] <= seq(g["OPEN"], g["Expression"], - zom(seq(g["COMMA"], g["Expression"])), g["CLOSE"]); - ~g["COMMA"] <= seq(chr(','), g["Spacing"]); - - // Instruction grammars - g["Instruction"] <= - seq(g["BeginBracket"], - opt(seq(g["InstructionItem"], zom(seq(g["InstructionItemSeparator"], - g["InstructionItem"])))), - g["EndBracket"]); - g["InstructionItem"] <= - cho(g["PrecedenceClimbing"], g["ErrorMessage"], g["NoAstOpt"]); - ~g["InstructionItemSeparator"] <= seq(chr(';'), g["Spacing"]); - - ~g["SpacesZom"] <= zom(g["Space"]); - ~g["SpacesOom"] <= oom(g["Space"]); - ~g["BeginBracket"] <= seq(chr('{'), g["Spacing"]); - ~g["EndBracket"] <= seq(chr('}'), g["Spacing"]); - - // PrecedenceClimbing instruction - g["PrecedenceClimbing"] <= - seq(lit("precedence"), g["SpacesOom"], g["PrecedenceInfo"], - zom(seq(g["SpacesOom"], g["PrecedenceInfo"])), g["SpacesZom"]); - g["PrecedenceInfo"] <= - seq(g["PrecedenceAssoc"], - oom(seq(ign(g["SpacesOom"]), g["PrecedenceOpe"]))); - g["PrecedenceOpe"] <= - cho(seq(cls("'"), - tok(zom(seq(npd(cho(g["Space"], cls("'"))), g["Char"]))), - cls("'")), - seq(cls("\""), - tok(zom(seq(npd(cho(g["Space"], cls("\""))), g["Char"]))), - cls("\"")), - tok(oom(seq(npd(cho(g["PrecedenceAssoc"], g["Space"], chr('}'))), - dot())))); - g["PrecedenceAssoc"] <= cls("LR"); - - // Error message instruction - g["ErrorMessage"] <= seq(lit("error_message"), g["SpacesOom"], - g["LiteralD"], g["SpacesZom"]); - - // No Ast node optimization instruction - g["NoAstOpt"] <= seq(lit("no_ast_opt"), g["SpacesZom"]); - - // Set definition names - for (auto &x : g) { - x.second.name = x.first; + g["Expression"] = [&](const SemanticValues &vs) { + if (vs.size() == 1) { + return std::any_cast>(vs[0]); + } else { + std::vector> opes; + for (auto i = 0u; i < vs.size(); i++) { + opes.emplace_back(std::any_cast>(vs[i])); } - } + const std::shared_ptr ope = + std::make_shared(opes); + return ope; + } + }; - void setup_actions() { - g["Definition"] = [&](const SemanticValues &vs, std::any &dt) { - auto &data = *std::any_cast(dt); - - auto is_macro = vs.choice() == 0; - auto ignore = std::any_cast(vs[0]); - auto name = std::any_cast(vs[1]); - - std::vector params; - std::shared_ptr ope; - auto has_instructions = false; - - if (is_macro) { - params = std::any_cast>(vs[2]); - ope = std::any_cast>(vs[4]); - if (vs.size() == 6) { has_instructions = true; } - } else { - ope = std::any_cast>(vs[3]); - if (vs.size() == 5) { has_instructions = true; } - } - - if (has_instructions) { - auto index = is_macro ? 5 : 4; - std::unordered_set types; - for (const auto &instruction : - std::any_cast>(vs[index])) { - const auto &type = instruction.type; - if (types.find(type) == types.end()) { - data.instructions[name].push_back(instruction); - types.insert(instruction.type); - if (type == "declare_symbol" || type == "check_symbol") { - if (!TokenChecker::is_token(*ope)) { ope = tok(ope); } - } - } else { - data.duplicates_of_instruction.emplace_back(type, - instruction.sv.data()); - } - } - } - - auto &grammar = *data.grammar; - if (!grammar.count(name)) { - auto &rule = grammar[name]; - rule <= ope; - rule.name = name; - rule.s_ = vs.sv().data(); - rule.line_ = line_info(vs.ss, rule.s_); - rule.ignoreSemanticValue = ignore; - rule.is_macro = is_macro; - rule.params = params; - - if (data.start.empty()) { - data.start = rule.name; - data.start_pos = rule.s_; - } - } else { - data.duplicates_of_definition.emplace_back(name, vs.sv().data()); - } - }; - - g["Definition"].enter = [](const Context & /*c*/, const char * /*s*/, - size_t /*n*/, std::any &dt) { - auto &data = *std::any_cast(dt); - data.captures_in_current_definition.clear(); - }; - - g["Expression"] = [&](const SemanticValues &vs) { - if (vs.size() == 1) { - return std::any_cast>(vs[0]); - } else { - std::vector> opes; - for (auto i = 0u; i < vs.size(); i++) { - opes.emplace_back(std::any_cast>(vs[i])); - } - const std::shared_ptr ope = - std::make_shared(opes); - return ope; - } - }; - - g["Sequence"] = [&](const SemanticValues &vs) { - if (vs.empty()) { - return npd(lit("")); - } else if (vs.size() == 1) { - return std::any_cast>(vs[0]); - } else { - std::vector> opes; - for (const auto &x : vs) { - opes.emplace_back(std::any_cast>(x)); - } - const std::shared_ptr ope = std::make_shared(opes); - return ope; - } - }; - - g["Prefix"] = [&](const SemanticValues &vs) { - std::shared_ptr ope; - if (vs.size() == 1) { - ope = std::any_cast>(vs[0]); - } else { - assert(vs.size() == 2); - auto tok = std::any_cast(vs[0]); - ope = std::any_cast>(vs[1]); - if (tok == '&') { - ope = apd(ope); - } else { // '!' - ope = npd(ope); - } - } - return ope; - }; - - g["SuffixWithLabel"] = [&](const SemanticValues &vs, std::any &dt) { - auto ope = std::any_cast>(vs[0]); - if (vs.size() == 1) { - return ope; - } else { - assert(vs.size() == 2); - auto &data = *std::any_cast(dt); - const auto &ident = std::any_cast(vs[1]); - auto label = ref(*data.grammar, ident, vs.sv().data(), false, {}); - auto recovery = rec(ref(*data.grammar, RECOVER_DEFINITION_NAME, - vs.sv().data(), true, {label})); - return cho4label_(ope, recovery); - } - }; - - struct Loop { - enum class Type { opt = 0, zom, oom, rep }; - Type type; - std::pair range; - }; - - g["Suffix"] = [&](const SemanticValues &vs) { - auto ope = std::any_cast>(vs[0]); - if (vs.size() == 1) { - return ope; - } else { - assert(vs.size() == 2); - auto loop = std::any_cast(vs[1]); - switch (loop.type) { - case Loop::Type::opt: return opt(ope); - case Loop::Type::zom: return zom(ope); - case Loop::Type::oom: return oom(ope); - default: // Regex-like repetition - return rep(ope, loop.range.first, loop.range.second); - } - } - }; - - g["Loop"] = [&](const SemanticValues &vs) { - switch (vs.choice()) { - case 0: // Option - return Loop{Loop::Type::opt, std::pair()}; - case 1: // Zero or More - return Loop{Loop::Type::zom, std::pair()}; - case 2: // One or More - return Loop{Loop::Type::oom, std::pair()}; - default: // Regex-like repetition - return Loop{Loop::Type::rep, - std::any_cast>(vs[0])}; - } - }; - - g["Primary"] = [&](const SemanticValues &vs, std::any &dt) { - auto &data = *std::any_cast(dt); - - switch (vs.choice()) { - case 0: // Macro Reference - case 1: { // Reference - auto is_macro = vs.choice() == 0; - auto ignore = std::any_cast(vs[0]); - const auto &ident = std::any_cast(vs[1]); - - std::vector> args; - if (is_macro) { - args = std::any_cast>>(vs[2]); - } - - auto ope = ref(*data.grammar, ident, vs.sv().data(), is_macro, args); - if (ident == RECOVER_DEFINITION_NAME) { ope = rec(ope); } - - if (ignore) { - return ign(ope); - } else { - return ope; - } - } - case 2: { // (Expression) - return std::any_cast>(vs[0]); - } - case 3: { // TokenBoundary - return tok(std::any_cast>(vs[0])); - } - case 4: { // CaptureScope - return csc(std::any_cast>(vs[0])); - } - case 5: { // Capture - const auto &name = std::any_cast(vs[0]); - auto ope = std::any_cast>(vs[1]); - - data.captures_stack.back().insert(name); - data.captures_in_current_definition.insert(name); - - return cap(ope, [name](const char *a_s, size_t a_n, Context &c) { - auto &cs = c.capture_scope_stack[c.capture_scope_stack_size - 1]; - cs[name] = std::string(a_s, a_n); - }); - } - default: { - return std::any_cast>(vs[0]); - } - } - }; - - g["IdentCont"] = [](const SemanticValues &vs) { - return std::string(vs.sv().data(), vs.sv().length()); - }; - - g["Dictionary"] = [](const SemanticValues &vs) { - auto items = vs.transform(); - return dic(items, false); - }; - g["DictionaryI"] = [](const SemanticValues &vs) { - auto items = vs.transform(); - return dic(items, true); - }; - - g["Literal"] = [](const SemanticValues &vs) { - const auto &tok = vs.tokens.front(); - return lit(resolve_escape_sequence(tok.data(), tok.size())); - }; - g["LiteralI"] = [](const SemanticValues &vs) { - const auto &tok = vs.tokens.front(); - return liti(resolve_escape_sequence(tok.data(), tok.size())); - }; - g["LiteralD"] = [](const SemanticValues &vs) { - auto &tok = vs.tokens.front(); - return resolve_escape_sequence(tok.data(), tok.size()); - }; - g["LiteralID"] = [](const SemanticValues &vs) { - auto &tok = vs.tokens.front(); - return resolve_escape_sequence(tok.data(), tok.size()); - }; - - g["Class"] = [](const SemanticValues &vs) { - auto ranges = vs.transform>(); - return cls(ranges); - }; - g["ClassI"] = [](const SemanticValues &vs) { - auto ranges = vs.transform>(); - return cls(ranges, true); - }; - g["NegatedClass"] = [](const SemanticValues &vs) { - auto ranges = vs.transform>(); - return ncls(ranges); - }; - g["NegatedClassI"] = [](const SemanticValues &vs) { - auto ranges = vs.transform>(); - return ncls(ranges, true); - }; - g["Range"] = [](const SemanticValues &vs) { - switch (vs.choice()) { - case 0: { - auto s1 = std::any_cast(vs[0]); - auto s2 = std::any_cast(vs[1]); - auto cp1 = decode_codepoint(s1.data(), s1.length()); - auto cp2 = decode_codepoint(s2.data(), s2.length()); - return std::pair(cp1, cp2); - } - case 1: { - auto s = std::any_cast(vs[0]); - auto cp = decode_codepoint(s.data(), s.length()); - return std::pair(cp, cp); - } - } - return std::pair(0, 0); - }; - g["Char"] = [](const SemanticValues &vs) { - return resolve_escape_sequence(vs.sv().data(), vs.sv().length()); - }; - - g["RepetitionRange"] = [&](const SemanticValues &vs) { - switch (vs.choice()) { - case 0: { // Number COMMA Number - auto min = std::any_cast(vs[0]); - auto max = std::any_cast(vs[1]); - return std::pair(min, max); - } - case 1: // Number COMMA - return std::pair(std::any_cast(vs[0]), - std::numeric_limits::max()); - case 2: { // Number - auto n = std::any_cast(vs[0]); - return std::pair(n, n); - } - default: // COMMA Number - return std::pair(std::numeric_limits::min(), - std::any_cast(vs[0])); - } - }; - g["Number"] = [&](const SemanticValues &vs) { - return vs.token_to_number(); - }; - - g["CapScope"].enter = [](const Context & /*c*/, const char * /*s*/, - size_t /*n*/, std::any &dt) { - auto &data = *std::any_cast(dt); - data.captures_stack.emplace_back(); - }; - g["CapScope"].leave = [](const Context & /*c*/, const char * /*s*/, - size_t /*n*/, size_t /*matchlen*/, - std::any & /*value*/, std::any &dt) { - auto &data = *std::any_cast(dt); - data.captures_stack.pop_back(); - }; - - g["AND"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; - g["NOT"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; - g["QUESTION"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; - g["STAR"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; - g["PLUS"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; - - g["DOT"] = [](const SemanticValues & /*vs*/) { return dot(); }; - - g["CUT"] = [](const SemanticValues & /*vs*/) { return cut(); }; - - g["BeginCap"] = [](const SemanticValues &vs) { return vs.token(); }; - - g["BackRef"] = [&](const SemanticValues &vs, std::any &dt) { - auto &data = *std::any_cast(dt); - - // Undefined back reference check - { - auto found = false; - auto it = data.captures_stack.rbegin(); - while (it != data.captures_stack.rend()) { - if (it->find(vs.token()) != it->end()) { - found = true; - break; - } - ++it; - } - if (!found) { - auto ptr = vs.token().data() - 1; // include '$' symbol - data.undefined_back_references.emplace_back(vs.token(), ptr); - } - } - - // NOTE: Disable packrat parsing if a back reference is not defined in - // captures in the current definition rule. - if (data.captures_in_current_definition.find(vs.token()) == - data.captures_in_current_definition.end()) { - data.enablePackratParsing = false; - } - - return bkr(vs.token_to_string()); - }; - - g["Ignore"] = [](const SemanticValues &vs) { return vs.size() > 0; }; - - g["Parameters"] = [](const SemanticValues &vs) { - return vs.transform(); - }; - - g["Arguments"] = [](const SemanticValues &vs) { - return vs.transform>(); - }; - - g["PrecedenceClimbing"] = [](const SemanticValues &vs) { - PrecedenceClimbing::BinOpeInfo binOpeInfo; - size_t level = 1; - for (auto v : vs) { - auto tokens = std::any_cast>(v); - auto assoc = tokens[0][0]; - for (size_t i = 1; i < tokens.size(); i++) { - binOpeInfo[tokens[i]] = std::pair(level, assoc); - } - level++; - } - Instruction instruction; - instruction.type = "precedence"; - instruction.data = binOpeInfo; - instruction.sv = vs.sv(); - return instruction; - }; - g["PrecedenceInfo"] = [](const SemanticValues &vs) { - return vs.transform(); - }; - g["PrecedenceOpe"] = [](const SemanticValues &vs) { return vs.token(); }; - g["PrecedenceAssoc"] = [](const SemanticValues &vs) { return vs.token(); }; - - g["ErrorMessage"] = [](const SemanticValues &vs) { - Instruction instruction; - instruction.type = "error_message"; - instruction.data = std::any_cast(vs[0]); - instruction.sv = vs.sv(); - return instruction; - }; - - g["NoAstOpt"] = [](const SemanticValues &vs) { - Instruction instruction; - instruction.type = "no_ast_opt"; - instruction.sv = vs.sv(); - return instruction; - }; - - g["Instruction"] = [](const SemanticValues &vs) { - return vs.transform(); - }; - } - - bool apply_precedence_instruction(Definition &rule, - const PrecedenceClimbing::BinOpeInfo &info, - const char *s, Log log) { - try { - auto &seq = dynamic_cast(*rule.get_core_operator()); - auto atom = seq.opes_[0]; - auto &rep = dynamic_cast(*seq.opes_[1]); - auto &seq1 = dynamic_cast(*rep.ope_); - auto binop = seq1.opes_[0]; - auto atom1 = seq1.opes_[1]; - - auto atom_name = dynamic_cast(*atom).name_; - auto binop_name = dynamic_cast(*binop).name_; - auto atom1_name = dynamic_cast(*atom1).name_; - - if (!rep.is_zom() || atom_name != atom1_name || atom_name == binop_name) { - if (log) { - auto line = line_info(s, rule.s_); - log(line.first, line.second, - "'precedence' instruction cannot be applied to '" + rule.name + - "'.", - ""); - } - return false; - } - - rule.holder_->ope_ = pre(atom, binop, info, rule); - rule.disable_action = true; - } catch (...) { - if (log) { - auto line = line_info(s, rule.s_); - log(line.first, line.second, - "'precedence' instruction cannot be applied to '" + rule.name + - "'.", - ""); - } - return false; + g["Sequence"] = [&](const SemanticValues &vs) { + if (vs.empty()) { + return npd(lit("")); + } else if (vs.size() == 1) { + return std::any_cast>(vs[0]); + } else { + std::vector> opes; + for (const auto &x : vs) { + opes.emplace_back(std::any_cast>(x)); } - return true; - } + const std::shared_ptr ope = std::make_shared(opes); + return ope; + } + }; - ParserContext perform_core(const char *s, size_t n, const Rules &rules, - Log log, std::string requested_start) { - Data data; - auto &grammar = *data.grammar; + g["Prefix"] = [&](const SemanticValues &vs) { + std::shared_ptr ope; + if (vs.size() == 1) { + ope = std::any_cast>(vs[0]); + } else { + assert(vs.size() == 2); + auto tok = std::any_cast(vs[0]); + ope = std::any_cast>(vs[1]); + if (tok == '&') { + ope = apd(ope); + } else { // '!' + ope = npd(ope); + } + } + return ope; + }; - // Built-in macros - { - // `%recover` - { - auto &rule = grammar[RECOVER_DEFINITION_NAME]; - rule <= ref(grammar, "x", "", false, {}); - rule.name = RECOVER_DEFINITION_NAME; - rule.s_ = "[native]"; - rule.ignoreSemanticValue = true; - rule.is_macro = true; - rule.params = {"x"}; - } + g["SuffixWithLabel"] = [&](const SemanticValues &vs, std::any &dt) { + auto ope = std::any_cast>(vs[0]); + if (vs.size() == 1) { + return ope; + } else { + assert(vs.size() == 2); + auto &data = *std::any_cast(dt); + const auto &ident = std::any_cast(vs[1]); + auto label = ref(*data.grammar, ident, vs.sv().data(), false, {}); + auto recovery = rec(ref(*data.grammar, RECOVER_DEFINITION_NAME, + vs.sv().data(), true, {label})); + return cho4label_(ope, recovery); + } + }; + + struct Loop { + enum class Type { opt = 0, zom, oom, rep }; + Type type; + std::pair range; + }; + + g["Suffix"] = [&](const SemanticValues &vs) { + auto ope = std::any_cast>(vs[0]); + if (vs.size() == 1) { + return ope; + } else { + assert(vs.size() == 2); + auto loop = std::any_cast(vs[1]); + switch (loop.type) { + case Loop::Type::opt: return opt(ope); + case Loop::Type::zom: return zom(ope); + case Loop::Type::oom: return oom(ope); + default: // Regex-like repetition + return rep(ope, loop.range.first, loop.range.second); + } + } + }; + + g["Loop"] = [&](const SemanticValues &vs) { + switch (vs.choice()) { + case 0: // Option + return Loop{Loop::Type::opt, std::pair()}; + case 1: // Zero or More + return Loop{Loop::Type::zom, std::pair()}; + case 2: // One or More + return Loop{Loop::Type::oom, std::pair()}; + default: // Regex-like repetition + return Loop{Loop::Type::rep, + std::any_cast>(vs[0])}; + } + }; + + g["Primary"] = [&](const SemanticValues &vs, std::any &dt) { + auto &data = *std::any_cast(dt); + + switch (vs.choice()) { + case 0: // Macro Reference + case 1: { // Reference + auto is_macro = vs.choice() == 0; + auto ignore = std::any_cast(vs[0]); + const auto &ident = std::any_cast(vs[1]); + + std::vector> args; + if (is_macro) { + args = std::any_cast>>(vs[2]); } - std::any dt = &data; - auto r = g["Grammar"].parse(s, n, dt, nullptr, log); + auto ope = ref(*data.grammar, ident, vs.sv().data(), is_macro, args); + if (ident == RECOVER_DEFINITION_NAME) { ope = rec(ope); } - if (!r.ret) { - if (log) { - if (r.error_info.message_pos) { - auto line = line_info(s, r.error_info.message_pos); - log(line.first, line.second, r.error_info.message, - r.error_info.label); - } else { - auto line = line_info(s, r.error_info.error_pos); - log(line.first, line.second, "syntax error", r.error_info.label); - } - } - return {}; + if (ignore) { + return ign(ope); + } else { + return ope; } + } + case 2: { // (Expression) + return std::any_cast>(vs[0]); + } + case 3: { // TokenBoundary + return tok(std::any_cast>(vs[0])); + } + case 4: { // CaptureScope + return csc(std::any_cast>(vs[0])); + } + case 5: { // Capture + const auto &name = std::any_cast(vs[0]); + auto ope = std::any_cast>(vs[1]); - // User provided rules - for (auto [user_name, user_rule] : rules) { - auto name = user_name; - auto ignore = false; - if (!name.empty() && name[0] == '~') { - ignore = true; - name.erase(0, 1); - } - if (!name.empty()) { - auto &rule = grammar[name]; - rule <= user_rule; - rule.name = name; - rule.ignoreSemanticValue = ignore; - } + data.captures_stack.back().insert(name); + data.captures_in_current_definition.insert(name); + + return cap(ope, [name](const char *a_s, size_t a_n, Context &c) { + c.capture_entries.emplace_back(name, std::string(a_s, a_n)); + }); + } + default: { + return std::any_cast>(vs[0]); + } + } + }; + + g["IdentCont"] = [](const SemanticValues &vs) { + return std::string(vs.sv().data(), vs.sv().length()); + }; + + g["Dictionary"] = [](const SemanticValues &vs) { + auto items = vs.transform(); + return dic(items, false); + }; + g["DictionaryI"] = [](const SemanticValues &vs) { + auto items = vs.transform(); + return dic(items, true); + }; + + g["Literal"] = [](const SemanticValues &vs) { + const auto &tok = vs.tokens.front(); + return lit(resolve_escape_sequence(tok.data(), tok.size())); + }; + g["LiteralI"] = [](const SemanticValues &vs) { + const auto &tok = vs.tokens.front(); + return liti(resolve_escape_sequence(tok.data(), tok.size())); + }; + g["LiteralD"] = [](const SemanticValues &vs) { + auto &tok = vs.tokens.front(); + return resolve_escape_sequence(tok.data(), tok.size()); + }; + g["LiteralID"] = [](const SemanticValues &vs) { + auto &tok = vs.tokens.front(); + return resolve_escape_sequence(tok.data(), tok.size()); + }; + + g["Class"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return cls(ranges); + }; + g["ClassI"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return cls(ranges, true); + }; + g["NegatedClass"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return ncls(ranges); + }; + g["NegatedClassI"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return ncls(ranges, true); + }; + g["Range"] = [](const SemanticValues &vs) { + switch (vs.choice()) { + case 0: { + auto s1 = std::any_cast(vs[0]); + auto s2 = std::any_cast(vs[1]); + auto cp1 = decode_codepoint(s1.data(), s1.length()); + auto cp2 = decode_codepoint(s2.data(), s2.length()); + if (cp1 > cp2) { + throw SyntaxErrorException("characer range is out of order...", + vs.line_info()); } + return std::pair(cp1, cp2); + } + case 1: { + auto s = std::any_cast(vs[0]); + auto cp = decode_codepoint(s.data(), s.length()); + return std::pair(cp, cp); + } + } + return std::pair(0, 0); + }; + g["Char"] = [](const SemanticValues &vs) { + return resolve_escape_sequence(vs.sv().data(), vs.sv().length()); + }; - // Check duplicated definitions - auto ret = true; + g["RepetitionRange"] = [&](const SemanticValues &vs) { + switch (vs.choice()) { + case 0: { // Number COMMA Number + auto min = std::any_cast(vs[0]); + auto max = std::any_cast(vs[1]); + return std::pair(min, max); + } + case 1: // Number COMMA + return std::pair(std::any_cast(vs[0]), + std::numeric_limits::max()); + case 2: { // Number + auto n = std::any_cast(vs[0]); + return std::pair(n, n); + } + default: // COMMA Number + return std::pair(std::numeric_limits::min(), + std::any_cast(vs[0])); + } + }; + g["Number"] = [&](const SemanticValues &vs) { + return vs.token_to_number(); + }; - if (!data.duplicates_of_definition.empty()) { - for (const auto &[name, ptr] : data.duplicates_of_definition) { - if (log) { - auto line = line_info(s, ptr); - log(line.first, line.second, - "The definition '" + name + "' is already defined.", ""); - } - } - ret = false; + g["CapScope"].enter = [](const Context & /*c*/, const char * /*s*/, + size_t /*n*/, std::any &dt) { + auto &data = *std::any_cast(dt); + data.captures_stack.emplace_back(); + }; + g["CapScope"].leave = [](const Context & /*c*/, const char * /*s*/, + size_t /*n*/, size_t /*matchlen*/, + std::any & /*value*/, std::any &dt) { + auto &data = *std::any_cast(dt); + data.captures_stack.pop_back(); + }; + + g["AND"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["NOT"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["QUESTION"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["STAR"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["PLUS"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + + g["DOT"] = [](const SemanticValues & /*vs*/) { return dot(); }; + + g["CUT"] = [](const SemanticValues & /*vs*/) { return cut(); }; + + g["BeginCap"] = [](const SemanticValues &vs) { return vs.token(); }; + + g["BackRef"] = [&](const SemanticValues &vs, std::any &dt) { + auto &data = *std::any_cast(dt); + + // Undefined back reference check + { + auto found = false; + auto it = data.captures_stack.rbegin(); + while (it != data.captures_stack.rend()) { + if (it->find(vs.token()) != it->end()) { + found = true; + break; + } + ++it; } - - // Check duplicated instructions - if (!data.duplicates_of_instruction.empty()) { - for (const auto &[type, ptr] : data.duplicates_of_instruction) { - if (log) { - auto line = line_info(s, ptr); - log(line.first, line.second, - "The instruction '" + type + "' is already defined.", ""); - } - } - ret = false; + if (!found) { + auto ptr = vs.token().data() - 1; // include '$' symbol + data.undefined_back_references.emplace_back(vs.token(), ptr); } + } - // Check undefined back references - if (!data.undefined_back_references.empty()) { - for (const auto &[name, ptr] : data.undefined_back_references) { - if (log) { - auto line = line_info(s, ptr); - log(line.first, line.second, - "The back reference '" + name + "' is undefined.", ""); - } - } - ret = false; + // NOTE: Disable packrat parsing if a back reference is not defined in + // captures in the current definition rule. + if (data.captures_in_current_definition.find(vs.token()) == + data.captures_in_current_definition.end()) { + data.enablePackratParsing = false; + } + + return bkr(vs.token_to_string()); + }; + + g["Ignore"] = [](const SemanticValues &vs) { return vs.size() > 0; }; + + g["Parameters"] = [](const SemanticValues &vs) { + return vs.transform(); + }; + + g["Arguments"] = [](const SemanticValues &vs) { + return vs.transform>(); + }; + + g["PrecedenceClimbing"] = [](const SemanticValues &vs) { + PrecedenceClimbing::BinOpeInfo binOpeInfo; + size_t level = 1; + for (const auto &v : vs) { + auto tokens = std::any_cast>(v); + auto assoc = tokens[0][0]; + for (size_t i = 1; i < tokens.size(); i++) { + binOpeInfo[tokens[i]] = std::pair(level, assoc); } + level++; + } + Instruction instruction; + instruction.type = "precedence"; + instruction.data = binOpeInfo; + instruction.sv = vs.sv(); + return instruction; + }; + g["PrecedenceInfo"] = [](const SemanticValues &vs) { + return vs.transform(); + }; + g["PrecedenceOpe"] = [](const SemanticValues &vs) { return vs.token(); }; + g["PrecedenceAssoc"] = [](const SemanticValues &vs) { return vs.token(); }; - // Set root definition - auto start = data.start; + g["ErrorMessage"] = [](const SemanticValues &vs) { + Instruction instruction; + instruction.type = "error_message"; + instruction.data = std::any_cast(vs[0]); + instruction.sv = vs.sv(); + return instruction; + }; - if (!requested_start.empty()) { - if (grammar.count(requested_start)) { - start = requested_start; - } else { - if (log) { - auto line = line_info(s, s); - log(line.first, line.second, - "The specified start rule '" + requested_start + - "' is undefined.", - ""); - } - ret = false; - } - } + g["NoAstOpt"] = [](const SemanticValues &vs) { + Instruction instruction; + instruction.type = "no_ast_opt"; + instruction.sv = vs.sv(); + return instruction; + }; - if (!ret) { return {}; } + g["Instruction"] = [](const SemanticValues &vs) { + return vs.transform(); + }; + } - auto &start_rule = grammar[start]; + bool apply_precedence_instruction(Definition &rule, + const PrecedenceClimbing::BinOpeInfo &info, + const char *s, Log log) { + try { + auto &seq = dynamic_cast(*rule.get_core_operator()); + auto atom = seq.opes_[0]; + auto &rep = dynamic_cast(*seq.opes_[1]); + auto &seq1 = dynamic_cast(*rep.ope_); + auto binop = seq1.opes_[0]; + auto atom1 = seq1.opes_[1]; - // Check if the start rule has ignore operator - { - if (start_rule.ignoreSemanticValue) { - if (log) { - auto line = line_info(s, start_rule.s_); - log(line.first, line.second, - "Ignore operator cannot be applied to '" + start_rule.name + "'.", - ""); - } - ret = false; - } - } + auto atom_name = dynamic_cast(*atom).name_; + auto binop_name = dynamic_cast(*binop).name_; + auto atom1_name = dynamic_cast(*atom1).name_; - if (!ret) { return {}; } - - // Check missing definitions - auto referenced = std::unordered_set{ - WHITESPACE_DEFINITION_NAME, - WORD_DEFINITION_NAME, - RECOVER_DEFINITION_NAME, - start_rule.name, - }; - - for (auto &[_, rule] : grammar) { - ReferenceChecker vis(grammar, rule.params); - rule.accept(vis); - referenced.insert(vis.referenced.begin(), vis.referenced.end()); - for (const auto &[name, ptr] : vis.error_s) { - if (log) { - auto line = line_info(s, ptr); - log(line.first, line.second, vis.error_message[name], ""); - } - ret = false; - } - } - - for (auto &[name, rule] : grammar) { - if (!referenced.count(name)) { - if (log) { - auto line = line_info(s, rule.s_); - auto msg = "'" + name + "' is not referenced."; - log(line.first, line.second, msg, ""); - } - } - } - - if (!ret) { return {}; } - - // Link references - for (auto &x : grammar) { - auto &rule = x.second; - LinkReferences vis(grammar, rule.params); - rule.accept(vis); - } - - // Check left recursion - ret = true; - - for (auto &[name, rule] : grammar) { - DetectLeftRecursion vis(name); - rule.accept(vis); - if (vis.error_s) { - if (log) { - auto line = line_info(s, vis.error_s); - log(line.first, line.second, "'" + name + "' is left recursive.", ""); - } - ret = false; - } - } - - if (!ret) { return {}; } - - // Check infinite loop - if (detect_infiniteLoop(data, start_rule, log, s)) { return {}; } - - // Automatic whitespace skipping - if (grammar.count(WHITESPACE_DEFINITION_NAME)) { - for (auto &x : grammar) { - auto &rule = x.second; - auto ope = rule.get_core_operator(); - if (IsLiteralToken::check(*ope)) { rule <= tok(ope); } - } - - auto &rule = grammar[WHITESPACE_DEFINITION_NAME]; - start_rule.whitespaceOpe = wsp(rule.get_core_operator()); - - if (detect_infiniteLoop(data, rule, log, s)) { return {}; } - } - - // Word expression - if (grammar.count(WORD_DEFINITION_NAME)) { - auto &rule = grammar[WORD_DEFINITION_NAME]; - start_rule.wordOpe = rule.get_core_operator(); - - if (detect_infiniteLoop(data, rule, log, s)) { return {}; } - } - - // Apply instructions - for (const auto &[name, instructions] : data.instructions) { - auto &rule = grammar[name]; - - for (const auto &instruction : instructions) { - if (instruction.type == "precedence") { - const auto &info = - std::any_cast(instruction.data); - - if (!apply_precedence_instruction(rule, info, s, log)) { return {}; } - } else if (instruction.type == "error_message") { - rule.error_message = std::any_cast(instruction.data); - } else if (instruction.type == "no_ast_opt") { - rule.no_ast_opt = true; - } - } - } - - return {data.grammar, start, data.enablePackratParsing}; - } - - bool detect_infiniteLoop(const Data &data, Definition &rule, const Log &log, - const char *s) const { - std::vector> refs; - std::unordered_map has_error_cache; - DetectInfiniteLoop vis(data.start_pos, rule.name, refs, has_error_cache); - rule.accept(vis); - if (vis.has_error) { - if (log) { - auto line = line_info(s, vis.error_s); - log(line.first, line.second, - "infinite loop is detected in '" + vis.error_name + "'.", ""); - } - return true; + if (!rep.is_zom() || atom_name != atom1_name || atom_name == binop_name) { + if (log) { + auto line = line_info(s, rule.s_); + log(line.first, line.second, + "'precedence' instruction cannot be applied to '" + rule.name + + "'.", + ""); } return false; + } + + rule.holder_->ope_ = pre(atom, binop, info, rule); + rule.disable_action = true; + } catch (...) { + if (log) { + auto line = line_info(s, rule.s_); + log(line.first, line.second, + "'precedence' instruction cannot be applied to '" + rule.name + + "'.", + ""); + } + return false; + } + return true; + } + + ParserContext perform_core(const char *s, size_t n, const Rules &rules, + Log log, std::string requested_start, + bool enable_left_recursion = true) { + Data data; + auto &grammar = *data.grammar; + + // Built-in macros + { + // `%recover` + { + auto &rule = grammar[RECOVER_DEFINITION_NAME]; + rule <= ref(grammar, "x", "", false, {}); + rule.name = RECOVER_DEFINITION_NAME; + rule.s_ = "[native]"; + rule.ignoreSemanticValue = true; + rule.is_macro = true; + rule.params = {"x"}; + } } - Grammar g; + try { + std::any dt = &data; + auto r = g["Grammar"].parse(s, n, dt, nullptr, log); + + if (!r.ret) { + if (log) { + if (r.error_info.message_pos) { + auto line = line_info(s, r.error_info.message_pos); + log(line.first, line.second, r.error_info.message, + r.error_info.label); + } else { + auto line = line_info(s, r.error_info.error_pos); + log(line.first, line.second, "syntax error", r.error_info.label); + } + } + return {}; + } + } catch (const SyntaxErrorException &e) { + if (log) { + auto line = e.line_info(); + log(line.first, line.second, e.what(), ""); + } + return {}; + } + + // User provided rules + for (auto [user_name, user_rule] : rules) { + auto name = user_name; + auto ignore = false; + if (!name.empty() && name[0] == '~') { + ignore = true; + name.erase(0, 1); + } + if (!name.empty()) { + auto &rule = grammar[name]; + rule <= user_rule; + rule.name = name; + rule.ignoreSemanticValue = ignore; + } + } + + // Check duplicated definitions + auto ret = true; + + if (!data.duplicates_of_definition.empty()) { + for (const auto &[name, ptr] : data.duplicates_of_definition) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, + "the definition '" + name + "' is already defined.", ""); + } + } + ret = false; + } + + // Check duplicated instructions + if (!data.duplicates_of_instruction.empty()) { + for (const auto &[type, ptr] : data.duplicates_of_instruction) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, + "the instruction '" + type + "' is already defined.", ""); + } + } + ret = false; + } + + // Check undefined back references + if (!data.undefined_back_references.empty()) { + for (const auto &[name, ptr] : data.undefined_back_references) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, + "the back reference '" + name + "' is undefined.", ""); + } + } + ret = false; + } + + // Set root definition + auto start = data.start; + + if (!requested_start.empty()) { + if (grammar.count(requested_start)) { + start = requested_start; + } else { + if (log) { + auto line = line_info(s, s); + log(line.first, line.second, + "the specified start rule '" + requested_start + + "' is undefined.", + ""); + } + ret = false; + } + } + + if (!ret) { return {}; } + + auto &start_rule = grammar[start]; + + // Check if the start rule has ignore operator + { + if (start_rule.ignoreSemanticValue) { + if (log) { + auto line = line_info(s, start_rule.s_); + log(line.first, line.second, + "ignore operator cannot be applied to '" + start_rule.name + "'.", + ""); + } + ret = false; + } + } + + if (!ret) { return {}; } + + // Check missing definitions + auto referenced = std::unordered_set{ + WHITESPACE_DEFINITION_NAME, + WORD_DEFINITION_NAME, + RECOVER_DEFINITION_NAME, + start_rule.name, + }; + + for (auto &[_, rule] : grammar) { + ReferenceChecker vis(grammar, rule.params); + rule.accept(vis); + referenced.insert(vis.referenced.begin(), vis.referenced.end()); + for (const auto &[name, ptr] : vis.error_s) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, vis.error_message[name], ""); + } + ret = false; + } + } + + for (auto &[name, rule] : grammar) { + if (!referenced.count(name)) { + if (log) { + auto line = line_info(s, rule.s_); + auto msg = "'" + name + "' is not referenced."; + log(line.first, line.second, msg, ""); + } + } + } + + if (!ret) { return {}; } + + // Link references + for (auto &x : grammar) { + auto &rule = x.second; + LinkReferences vis(grammar, rule.params); + rule.accept(vis); + } + + // Compute can_be_empty for each rule (fixed-point iteration) + { + bool changed = true; + while (changed) { + changed = false; + for (auto &[name, rule] : grammar) { + ComputeCanBeEmpty vis; + rule.accept(vis); + if (vis.result != rule.can_be_empty) { + rule.can_be_empty = vis.result; + changed = true; + } + } + } + } + + // Check left recursion + if (enable_left_recursion) { + for (auto &[name, rule] : grammar) { + DetectLeftRecursion vis(name); + rule.accept(vis); + if (vis.error_s) { rule.is_left_recursive = true; } + } + } else { + ret = true; + + for (auto &[name, rule] : grammar) { + DetectLeftRecursion vis(name); + rule.accept(vis); + if (vis.error_s) { + if (log) { + auto line = line_info(s, vis.error_s); + log(line.first, line.second, "'" + name + "' is left recursive.", + ""); + } + ret = false; + } + } + + if (!ret) { return {}; } + } + + // Check infinite loop + if (detect_infiniteLoop(data, start_rule, log, s)) { return {}; } + + // Automatic whitespace skipping + if (grammar.count(WHITESPACE_DEFINITION_NAME)) { + for (auto &x : grammar) { + auto &rule = x.second; + auto ope = rule.get_core_operator(); + if (IsLiteralToken::check(*ope)) { rule <= tok(ope); } + } + + auto &rule = grammar[WHITESPACE_DEFINITION_NAME]; + start_rule.whitespaceOpe = wsp(rule.get_core_operator()); + + if (detect_infiniteLoop(data, rule, log, s)) { return {}; } + } + + // Word expression + if (grammar.count(WORD_DEFINITION_NAME)) { + auto &rule = grammar[WORD_DEFINITION_NAME]; + start_rule.wordOpe = rule.get_core_operator(); + + if (detect_infiniteLoop(data, rule, log, s)) { return {}; } + } + + // Apply instructions + for (const auto &[name, instructions] : data.instructions) { + auto &rule = grammar[name]; + + for (const auto &instruction : instructions) { + if (instruction.type == "precedence") { + const auto &info = + std::any_cast(instruction.data); + + if (!apply_precedence_instruction(rule, info, s, log)) { return {}; } + } else if (instruction.type == "error_message") { + rule.error_message = std::any_cast(instruction.data); + } else if (instruction.type == "no_ast_opt") { + rule.no_ast_opt = true; + } + } + } + + // Setup First-Set and ISpan optimizations + for (auto &x : grammar) { + SetupFirstSets vis; + x.second.accept(vis); + } + + return {data.grammar, start, data.enablePackratParsing}; + } + + bool detect_infiniteLoop(const Data &data, Definition &rule, const Log &log, + const char *s) const { + std::vector> refs; + std::unordered_map has_error_cache; + DetectInfiniteLoop vis(data.start_pos, rule.name, refs, has_error_cache); + rule.accept(vis); + if (vis.has_error) { + if (log) { + auto line = line_info(s, vis.error_s); + log(line.first, line.second, + "infinite loop is detected in '" + vis.error_name + "'.", ""); + } + return true; + } + return false; + } + + Grammar g; }; /*----------------------------------------------------------------------------- @@ -4254,163 +5069,163 @@ private: *---------------------------------------------------------------------------*/ template struct AstBase : public Annotation { - AstBase(const char *path, size_t line, size_t column, const char *name, - const std::vector> &nodes, - size_t position = 0, size_t length = 0, size_t choice_count = 0, - size_t choice = 0) - : path(path ? path : ""), line(line), column(column), name(name), - position(position), length(length), choice_count(choice_count), - choice(choice), original_name(name), - original_choice_count(choice_count), original_choice(choice), - tag(str2tag(name)), original_tag(tag), is_token(false), nodes(nodes) {} + AstBase(const char *path, size_t line, size_t column, const char *name, + const std::vector> &nodes, + size_t position = 0, size_t length = 0, size_t choice_count = 0, + size_t choice = 0) + : path(path ? path : ""), line(line), column(column), name(name), + position(position), length(length), choice_count(choice_count), + choice(choice), original_name(name), + original_choice_count(choice_count), original_choice(choice), + tag(str2tag(name)), original_tag(tag), is_token(false), nodes(nodes) {} - AstBase(const char *path, size_t line, size_t column, const char *name, - const std::string_view &token, size_t position = 0, size_t length = 0, - size_t choice_count = 0, size_t choice = 0) - : path(path ? path : ""), line(line), column(column), name(name), - position(position), length(length), choice_count(choice_count), - choice(choice), original_name(name), - original_choice_count(choice_count), original_choice(choice), - tag(str2tag(name)), original_tag(tag), is_token(true), token(token) {} + AstBase(const char *path, size_t line, size_t column, const char *name, + const std::string_view &token, size_t position = 0, size_t length = 0, + size_t choice_count = 0, size_t choice = 0) + : path(path ? path : ""), line(line), column(column), name(name), + position(position), length(length), choice_count(choice_count), + choice(choice), original_name(name), + original_choice_count(choice_count), original_choice(choice), + tag(str2tag(name)), original_tag(tag), is_token(true), token(token) {} - AstBase(const AstBase &ast, const char *original_name, size_t position = 0, - size_t length = 0, size_t original_choice_count = 0, - size_t original_choice = 0) - : path(ast.path), line(ast.line), column(ast.column), name(ast.name), - position(position), length(length), choice_count(ast.choice_count), - choice(ast.choice), original_name(original_name), - original_choice_count(original_choice_count), - original_choice(original_choice), tag(ast.tag), - original_tag(str2tag(original_name)), is_token(ast.is_token), - token(ast.token), nodes(ast.nodes), parent(ast.parent) {} + AstBase(const AstBase &ast, const char *original_name, size_t position = 0, + size_t length = 0, size_t original_choice_count = 0, + size_t original_choice = 0) + : path(ast.path), line(ast.line), column(ast.column), name(ast.name), + position(position), length(length), choice_count(ast.choice_count), + choice(ast.choice), original_name(original_name), + original_choice_count(original_choice_count), + original_choice(original_choice), tag(ast.tag), + original_tag(str2tag(original_name)), is_token(ast.is_token), + token(ast.token), nodes(ast.nodes), parent(ast.parent) {} - const std::string path; - const size_t line = 1; - const size_t column = 1; + const std::string path; + const size_t line = 1; + const size_t column = 1; - const std::string name; - size_t position; - size_t length; - const size_t choice_count; - const size_t choice; - const std::string original_name; - const size_t original_choice_count; - const size_t original_choice; - const unsigned int tag; - const unsigned int original_tag; + const std::string name; + size_t position; + size_t length; + const size_t choice_count; + const size_t choice; + const std::string original_name; + const size_t original_choice_count; + const size_t original_choice; + const unsigned int tag; + const unsigned int original_tag; - const bool is_token; - const std::string_view token; + const bool is_token; + const std::string_view token; - std::vector>> nodes; - std::weak_ptr> parent; + std::vector>> nodes; + std::weak_ptr> parent; - std::string token_to_string() const { - assert(is_token); - return std::string(token); - } + std::string token_to_string() const { + assert(is_token); + return std::string(token); + } - template T token_to_number() const { - return token_to_number_(token); - } + template T token_to_number() const { + return token_to_number_(token); + } }; template void ast_to_s_core(const std::shared_ptr &ptr, std::string &s, int level, std::function fn) { - const auto &ast = *ptr; - for (auto i = 0; i < level; i++) { - s += " "; - } - auto name = ast.original_name; - if (ast.original_choice_count > 0) { - name += "/" + std::to_string(ast.original_choice); - } - if (ast.name != ast.original_name) { name += "[" + ast.name + "]"; } - if (ast.is_token) { - s += "- " + name + " ("; - s += ast.token; - s += ")\n"; - } else { - s += "+ " + name + "\n"; - } - if (fn) { s += fn(ast, level + 1); } - for (auto node : ast.nodes) { - ast_to_s_core(node, s, level + 1, fn); - } + const auto &ast = *ptr; + for (auto i = 0; i < level; i++) { + s += " "; + } + auto name = ast.original_name; + if (ast.original_choice_count > 0) { + name += "/" + std::to_string(ast.original_choice); + } + if (ast.name != ast.original_name) { name += "[" + ast.name + "]"; } + if (ast.is_token) { + s += "- " + name + " ("; + s += ast.token; + s += ")\n"; + } else { + s += "+ " + name + "\n"; + } + if (fn) { s += fn(ast, level + 1); } + for (const auto &node : ast.nodes) { + ast_to_s_core(node, s, level + 1, fn); + } } template std::string ast_to_s(const std::shared_ptr &ptr, std::function fn = nullptr) { - std::string s; - ast_to_s_core(ptr, s, 0, fn); - return s; + std::string s; + ast_to_s_core(ptr, s, 0, fn); + return s; } struct AstOptimizer { - AstOptimizer(bool mode, const std::vector &rules = {}) - : mode_(mode), rules_(rules) {} + AstOptimizer(bool mode, const std::vector &rules = {}) + : mode_(mode), rules_(rules) {} - template - std::shared_ptr optimize(std::shared_ptr original, - std::shared_ptr parent = nullptr) { - auto found = - std::find(rules_.begin(), rules_.end(), original->name) != rules_.end(); - auto opt = mode_ ? !found : found; + template + std::shared_ptr optimize(std::shared_ptr original, + std::shared_ptr parent = nullptr) { + auto found = + std::find(rules_.begin(), rules_.end(), original->name) != rules_.end(); + auto opt = mode_ ? !found : found; - if (opt && original->nodes.size() == 1) { - auto child = optimize(original->nodes[0], parent); - auto ast = std::make_shared(*child, original->name.data(), - original->choice_count, original->position, - original->length, original->choice); - for (auto node : ast->nodes) { - node->parent = ast; - } - return ast; - } - - auto ast = std::make_shared(*original); - ast->parent = parent; - ast->nodes.clear(); - for (auto node : original->nodes) { - auto child = optimize(node, ast); - ast->nodes.push_back(child); - } - return ast; + if (opt && original->nodes.size() == 1) { + auto child = optimize(original->nodes[0], parent); + auto ast = std::make_shared(*child, original->name.data(), + original->position, original->length, + original->choice_count, original->choice); + for (auto &node : ast->nodes) { + node->parent = ast; + } + return ast; } + auto ast = std::make_shared(*original); + ast->parent = parent; + ast->nodes.clear(); + for (const auto &node : original->nodes) { + auto child = optimize(node, ast); + ast->nodes.push_back(child); + } + return ast; + } + private: - const bool mode_; - const std::vector rules_; + const bool mode_; + const std::vector rules_; }; struct EmptyType {}; using Ast = AstBase; template void add_ast_action(Definition &rule) { - rule.action = [&](const SemanticValues &vs) { - auto line = vs.line_info(); + rule.action = [&](const SemanticValues &vs) { + auto line = vs.line_info(); - if (rule.is_token()) { - return std::make_shared( - vs.path, line.first, line.second, rule.name.data(), vs.token(), - std::distance(vs.ss, vs.sv().data()), vs.sv().length(), - vs.choice_count(), vs.choice()); - } + if (rule.is_token()) { + return std::make_shared( + vs.path, line.first, line.second, rule.name.data(), vs.token(), + std::distance(vs.ss, vs.sv().data()), vs.sv().length(), + vs.choice_count(), vs.choice()); + } - auto ast = - std::make_shared(vs.path, line.first, line.second, rule.name.data(), - vs.transform>(), - std::distance(vs.ss, vs.sv().data()), - vs.sv().length(), vs.choice_count(), vs.choice()); + auto ast = + std::make_shared(vs.path, line.first, line.second, rule.name.data(), + vs.transform>(), + std::distance(vs.ss, vs.sv().data()), + vs.sv().length(), vs.choice_count(), vs.choice()); - for (auto node : ast->nodes) { - node->parent = ast; - } - return ast; - }; + for (auto &node : ast->nodes) { + node->parent = ast; + } + return ast; + }; } #define PEG_EXPAND(...) __VA_ARGS__ @@ -4550,229 +5365,235 @@ template void add_ast_action(Definition &rule) { class parser { public: - parser() = default; + parser() = default; - parser(const char *s, size_t n, const Rules &rules, - std::string_view start = {}) { - load_grammar(s, n, rules, start); - } + parser(const char *s, size_t n, const Rules &rules, + std::string_view start = {}) { + load_grammar(s, n, rules, start); + } - parser(const char *s, size_t n, std::string_view start = {}) - : parser(s, n, Rules(), start) {} + parser(const char *s, size_t n, std::string_view start = {}) + : parser(s, n, Rules(), start) {} - parser(std::string_view sv, const Rules &rules, std::string_view start = {}) - : parser(sv.data(), sv.size(), rules, start) {} + parser(std::string_view sv, const Rules &rules, std::string_view start = {}) + : parser(sv.data(), sv.size(), rules, start) {} - parser(std::string_view sv, std::string_view start = {}) - : parser(sv.data(), sv.size(), Rules(), start) {} + parser(std::string_view sv, std::string_view start = {}) + : parser(sv.data(), sv.size(), Rules(), start) {} #if defined(__cpp_lib_char8_t) - parser(std::u8string_view sv, const Rules &rules, std::string_view start = {}) - : parser(reinterpret_cast(sv.data()), sv.size(), rules, - start) {} + parser(std::u8string_view sv, const Rules &rules, std::string_view start = {}) + : parser(reinterpret_cast(sv.data()), sv.size(), rules, + start) {} - parser(std::u8string_view sv, std::string_view start = {}) - : parser(reinterpret_cast(sv.data()), sv.size(), Rules(), - start) {} + parser(std::u8string_view sv, std::string_view start = {}) + : parser(reinterpret_cast(sv.data()), sv.size(), Rules(), + start) {} #endif - operator bool() { return grammar_ != nullptr; } + operator bool() const { return grammar_ != nullptr; } - bool load_grammar(const char *s, size_t n, const Rules &rules, - std::string_view start = {}) { - auto cxt = ParserGenerator::parse(s, n, rules, log_, start); - grammar_ = cxt.grammar; - start_ = cxt.start; - enablePackratParsing_ = cxt.enablePackratParsing; - return grammar_ != nullptr; + bool load_grammar(const char *s, size_t n, const Rules &rules, + std::string_view start = {}) { + auto cxt = + ParserGenerator::parse(s, n, rules, log_, start, enableLeftRecursion_); + grammar_ = cxt.grammar; + start_ = cxt.start; + enablePackratParsing_ = cxt.enablePackratParsing; + return grammar_ != nullptr; + } + + bool load_grammar(const char *s, size_t n, std::string_view start = {}) { + return load_grammar(s, n, Rules(), start); + } + + bool load_grammar(std::string_view sv, const Rules &rules, + std::string_view start = {}) { + return load_grammar(sv.data(), sv.size(), rules, start); + } + + bool load_grammar(std::string_view sv, std::string_view start = {}) { + return load_grammar(sv.data(), sv.size(), Rules(), start); + } + + bool parse_n(const char *s, size_t n, const char *path = nullptr) const { + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse(s, n, path, log_); + return post_process(s, n, result); } + return false; + } - bool load_grammar(const char *s, size_t n, std::string_view start = {}) { - return load_grammar(s, n, Rules(), start); - } - - bool load_grammar(std::string_view sv, const Rules &rules, - std::string_view start = {}) { - return load_grammar(sv.data(), sv.size(), rules, start); - } - - bool load_grammar(std::string_view sv, std::string_view start = {}) { - return load_grammar(sv.data(), sv.size(), start); - } - - bool parse_n(const char *s, size_t n, const char *path = nullptr) const { - if (grammar_ != nullptr) { - const auto &rule = (*grammar_)[start_]; - auto result = rule.parse(s, n, path, log_); - return post_process(s, n, result); - } - return false; - } - - bool parse_n(const char *s, size_t n, std::any &dt, - const char *path = nullptr) const { - if (grammar_ != nullptr) { - const auto &rule = (*grammar_)[start_]; - auto result = rule.parse(s, n, dt, path, log_); - return post_process(s, n, result); - } - return false; - } - - template - bool parse_n(const char *s, size_t n, T &val, - const char *path = nullptr) const { - if (grammar_ != nullptr) { - const auto &rule = (*grammar_)[start_]; - auto result = rule.parse_and_get_value(s, n, val, path, log_); - return post_process(s, n, result); - } - return false; - } - - template - bool parse_n(const char *s, size_t n, std::any &dt, T &val, - const char *path = nullptr) const { - if (grammar_ != nullptr) { - const auto &rule = (*grammar_)[start_]; - auto result = rule.parse_and_get_value(s, n, dt, val, path, log_); - return post_process(s, n, result); - } - return false; - } - - bool parse(std::string_view sv, const char *path = nullptr) const { - return parse_n(sv.data(), sv.size(), path); - } - - bool parse(std::string_view sv, std::any &dt, + bool parse_n(const char *s, size_t n, std::any &dt, const char *path = nullptr) const { - return parse_n(sv.data(), sv.size(), dt, path); + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse(s, n, dt, path, log_); + return post_process(s, n, result); } + return false; + } - template - bool parse(std::string_view sv, T &val, const char *path = nullptr) const { - return parse_n(sv.data(), sv.size(), val, path); - } - - template - bool parse(std::string_view sv, std::any &dt, T &val, + template + bool parse_n(const char *s, size_t n, T &val, const char *path = nullptr) const { - return parse_n(sv.data(), sv.size(), dt, val, path); + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse_and_get_value(s, n, val, path, log_); + return post_process(s, n, result); } + return false; + } + + template + bool parse_n(const char *s, size_t n, std::any &dt, T &val, + const char *path = nullptr) const { + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse_and_get_value(s, n, dt, val, path, log_); + return post_process(s, n, result); + } + return false; + } + + bool parse(std::string_view sv, const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), path); + } + + bool parse(std::string_view sv, std::any &dt, + const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), dt, path); + } + + template + bool parse(std::string_view sv, T &val, const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), val, path); + } + + template + bool parse(std::string_view sv, std::any &dt, T &val, + const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), dt, val, path); + } #if defined(__cpp_lib_char8_t) - bool parse(std::u8string_view sv, const char *path = nullptr) const { - return parse_n(reinterpret_cast(sv.data()), sv.size(), path); - } + bool parse(std::u8string_view sv, const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), path); + } - bool parse(std::u8string_view sv, std::any &dt, - const char *path = nullptr) const { - return parse_n(reinterpret_cast(sv.data()), sv.size(), dt, - path); - } + bool parse(std::u8string_view sv, std::any &dt, + const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), dt, + path); + } - template - bool parse(std::u8string_view sv, T &val, const char *path = nullptr) const { - return parse_n(reinterpret_cast(sv.data()), sv.size(), val, - path); - } + template + bool parse(std::u8string_view sv, T &val, const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), val, + path); + } - template - bool parse(std::u8string_view sv, std::any &dt, T &val, - const char *path = nullptr) const { - return parse_n(reinterpret_cast(sv.data()), sv.size(), dt, - val, path); - } + template + bool parse(std::u8string_view sv, std::any &dt, T &val, + const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), dt, + val, path); + } #endif - Definition &operator[](const char *s) { return (*grammar_)[s]; } + Definition &operator[](const char *s) { return (*grammar_)[s]; } - const Definition &operator[](const char *s) const { return (*grammar_)[s]; } + const Definition &operator[](const char *s) const { return (*grammar_)[s]; } - const Grammar &get_grammar() const { return *grammar_; } + const Grammar &get_grammar() const { return *grammar_; } - void disable_eoi_check() { - if (grammar_ != nullptr) { - auto &rule = (*grammar_)[start_]; - rule.eoi_check = false; - } + void disable_eoi_check() { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.eoi_check = false; } + } - void enable_packrat_parsing() { - if (grammar_ != nullptr) { - auto &rule = (*grammar_)[start_]; - rule.enablePackratParsing = enablePackratParsing_; - } + void enable_left_recursion(bool enable = true) { + enableLeftRecursion_ = enable; + } + + void enable_packrat_parsing() { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.enablePackratParsing = enablePackratParsing_; } + } - void enable_trace(TracerEnter tracer_enter, TracerLeave tracer_leave) { - if (grammar_ != nullptr) { - auto &rule = (*grammar_)[start_]; - rule.tracer_enter = tracer_enter; - rule.tracer_leave = tracer_leave; - } + void enable_trace(TracerEnter tracer_enter, TracerLeave tracer_leave) { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.tracer_enter = tracer_enter; + rule.tracer_leave = tracer_leave; } + } - void enable_trace(TracerEnter tracer_enter, TracerLeave tracer_leave, - TracerStartOrEnd tracer_start, - TracerStartOrEnd tracer_end) { - if (grammar_ != nullptr) { - auto &rule = (*grammar_)[start_]; - rule.tracer_enter = tracer_enter; - rule.tracer_leave = tracer_leave; - rule.tracer_start = tracer_start; - rule.tracer_end = tracer_end; - } + void enable_trace(TracerEnter tracer_enter, TracerLeave tracer_leave, + TracerStartOrEnd tracer_start, + TracerStartOrEnd tracer_end) { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.tracer_enter = tracer_enter; + rule.tracer_leave = tracer_leave; + rule.tracer_start = tracer_start; + rule.tracer_end = tracer_end; } + } - void set_verbose_trace(bool verbose_trace) { - if (grammar_ != nullptr) { - auto &rule = (*grammar_)[start_]; - rule.verbose_trace = verbose_trace; - } + void set_verbose_trace(bool verbose_trace) { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.verbose_trace = verbose_trace; } + } - template parser &enable_ast() { - for (auto &[_, rule] : *grammar_) { - if (!rule.action) { add_ast_action(rule); } - } - return *this; + template parser &enable_ast() { + for (auto &[_, rule] : *grammar_) { + if (!rule.action) { add_ast_action(rule); } } + return *this; + } - template - std::shared_ptr optimize_ast(std::shared_ptr ast, - bool opt_mode = true) const { - return AstOptimizer(opt_mode, get_no_ast_opt_rules()).optimize(ast); - } + template + std::shared_ptr optimize_ast(std::shared_ptr ast, + bool opt_mode = true) const { + return AstOptimizer(opt_mode, get_no_ast_opt_rules()).optimize(ast); + } - void set_logger(Log log) { log_ = log; } + void set_logger(Log log) { log_ = log; } - void set_logger( - std::function - log) { - log_ = [log](size_t line, size_t col, const std::string &msg, - const std::string & /*rule*/) { log(line, col, msg); }; - } + void set_logger( + std::function + log) { + log_ = [log](size_t line, size_t col, const std::string &msg, + const std::string & /*rule*/) { log(line, col, msg); }; + } private: - bool post_process(const char *s, size_t n, Definition::Result &r) const { - if (log_ && !r.ret) { r.error_info.output_log(log_, s, n); } - return r.ret && !r.recovered; - } + bool post_process(const char *s, size_t n, Definition::Result &r) const { + if (log_ && !r.ret) { r.error_info.output_log(log_, s, n); } + return r.ret && !r.recovered; + } - std::vector get_no_ast_opt_rules() const { - std::vector rules; - for (auto &[name, rule] : *grammar_) { - if (rule.no_ast_opt) { rules.push_back(name); } - } - return rules; + std::vector get_no_ast_opt_rules() const { + std::vector rules; + for (auto &[name, rule] : *grammar_) { + if (rule.no_ast_opt) { rules.push_back(name); } } + return rules; + } - std::shared_ptr grammar_; - std::string start_; - bool enablePackratParsing_ = false; - Log log_; + std::shared_ptr grammar_; + std::string start_; + bool enableLeftRecursion_ = true; + bool enablePackratParsing_ = false; + Log log_; }; /*----------------------------------------------------------------------------- @@ -4780,59 +5601,59 @@ private: *---------------------------------------------------------------------------*/ inline void enable_tracing(parser &parser, std::ostream &os) { - parser.enable_trace( - [&](auto &ope, auto s, auto, auto &, auto &c, auto &, auto &trace_data) { - auto prev_pos = std::any_cast(trace_data); - auto pos = static_cast(s - c.s); - auto backtrack = (pos < prev_pos ? "*" : ""); - std::string indent; - auto level = c.trace_ids.size() - 1; - while (level--) { - indent += "│"; - } - std::string name; - { - name = peg::TraceOpeName::get(const_cast(ope)); + parser.enable_trace( + [&](auto &ope, auto s, auto, auto &, auto &c, auto &, auto &trace_data) { + auto prev_pos = std::any_cast(trace_data); + auto pos = static_cast(s - c.s); + auto backtrack = (pos < prev_pos ? "*" : ""); + std::string indent; + auto level = c.trace_ids.size() - 1; + while (level--) { + indent += "│"; + } + std::string name; + { + name = peg::TraceOpeName::get(const_cast(ope)); - auto lit = dynamic_cast(&ope); - if (lit) { name += " '" + peg::escape_characters(lit->lit_) + "'"; } - } - os << "E " << pos + 1 << backtrack << "\t" << indent << "┌" << name - << " #" << c.trace_ids.back() << std::endl; - trace_data = static_cast(pos); - }, - [&](auto &ope, auto s, auto, auto &sv, auto &c, auto &, auto len, - auto &) { - auto pos = static_cast(s - c.s); - if (len != static_cast(-1)) { pos += len; } - std::string indent; - auto level = c.trace_ids.size() - 1; - while (level--) { - indent += "│"; - } - auto ret = len != static_cast(-1) ? "└o " : "└x "; - auto name = peg::TraceOpeName::get(const_cast(ope)); - std::stringstream choice; - if (sv.choice_count() > 0) { - choice << " " << sv.choice() << "/" << sv.choice_count(); - } - std::string token; - if (!sv.tokens.empty()) { - token += ", token '"; - token += sv.tokens[0]; - token += "'"; - } - std::string matched; - if (peg::success(len) && - peg::TokenChecker::is_token(const_cast(ope))) { - matched = ", match '" + peg::escape_characters(s, len) + "'"; - } - os << "L " << pos + 1 << "\t" << indent << ret << name << " #" - << c.trace_ids.back() << choice.str() << token << matched - << std::endl; - }, - [&](auto &trace_data) { trace_data = static_cast(0); }, - [&](auto &) {}); + auto lit = dynamic_cast(&ope); + if (lit) { name += " '" + peg::escape_characters(lit->lit_) + "'"; } + } + os << "E " << pos + 1 << backtrack << "\t" << indent << "┌" << name + << " #" << c.trace_ids.back() << std::endl; + trace_data = static_cast(pos); + }, + [&](auto &ope, auto s, auto, auto &sv, auto &c, auto &, auto len, + auto &) { + auto pos = static_cast(s - c.s); + if (len != static_cast(-1)) { pos += len; } + std::string indent; + auto level = c.trace_ids.size() - 1; + while (level--) { + indent += "│"; + } + auto ret = len != static_cast(-1) ? "└o " : "└x "; + auto name = peg::TraceOpeName::get(const_cast(ope)); + std::stringstream choice; + if (sv.choice_count() > 0) { + choice << " " << sv.choice() << "/" << sv.choice_count(); + } + std::string token; + if (!sv.tokens.empty()) { + token += ", token '"; + token += sv.tokens[0]; + token += "'"; + } + std::string matched; + if (peg::success(len) && + peg::TokenChecker::is_token(const_cast(ope))) { + matched = ", match '" + peg::escape_characters(s, len) + "'"; + } + os << "L " << pos + 1 << "\t" << indent << ret << name << " #" + << c.trace_ids.back() << choice.str() << token << matched + << std::endl; + }, + [&](auto &trace_data) { trace_data = static_cast(0); }, + [&](auto &) {}); } /*----------------------------------------------------------------------------- @@ -4840,98 +5661,98 @@ inline void enable_tracing(parser &parser, std::ostream &os) { *---------------------------------------------------------------------------*/ inline void enable_profiling(parser &parser, std::ostream &os) { - struct Stats { - struct Item { - std::string name; - size_t success; - size_t fail; - }; - std::vector items; - std::map index; - size_t total = 0; - std::chrono::steady_clock::time_point start; + struct Stats { + struct Item { + std::string name; + size_t success; + size_t fail; }; + std::vector items; + std::map index; + size_t total = 0; + std::chrono::steady_clock::time_point start; + }; - parser.enable_trace( - [&](auto &ope, auto, auto, auto &, auto &, auto &, std::any &trace_data) { - if (auto holder = dynamic_cast(&ope)) { - auto &stats = *std::any_cast(trace_data); + parser.enable_trace( + [&](auto &ope, auto, auto, auto &, auto &, auto &, std::any &trace_data) { + if (auto holder = dynamic_cast(&ope)) { + auto &stats = *std::any_cast(trace_data); - auto &name = holder->name(); - if (stats.index.find(name) == stats.index.end()) { - stats.index[name] = stats.index.size(); - stats.items.push_back({name, 0, 0}); - } - stats.total++; + auto &name = holder->name(); + if (stats.index.find(name) == stats.index.end()) { + stats.index[name] = stats.index.size(); + stats.items.push_back({name, 0, 0}); + } + stats.total++; + } + }, + [&](auto &ope, auto, auto, auto &, auto &, auto &, auto len, + std::any &trace_data) { + if (auto holder = dynamic_cast(&ope)) { + auto &stats = *std::any_cast(trace_data); + + auto &name = holder->name(); + auto index = stats.index[name]; + auto &stat = stats.items[index]; + if (len != static_cast(-1)) { + stat.success++; + } else { + stat.fail++; + } + + if (index == 0) { + auto end = std::chrono::steady_clock::now(); + auto nano = std::chrono::duration_cast( + end - stats.start) + .count(); + auto sec = nano / 1000000.0; + os << "duration: " << sec << "s (" << nano << "µs)" << std::endl + << std::endl; + + char buff[BUFSIZ]; + size_t total_success = 0; + size_t total_fail = 0; + for (auto &[name, success, fail] : stats.items) { + total_success += success; + total_fail += fail; } - }, - [&](auto &ope, auto, auto, auto &, auto &, auto &, auto len, - std::any &trace_data) { - if (auto holder = dynamic_cast(&ope)) { - auto &stats = *std::any_cast(trace_data); - auto &name = holder->name(); - auto index = stats.index[name]; - auto &stat = stats.items[index]; - if (len != static_cast(-1)) { - stat.success++; - } else { - stat.fail++; - } + os << " id total % success fail " + "definition" + << std::endl; - if (index == 0) { - auto end = std::chrono::steady_clock::now(); - auto nano = std::chrono::duration_cast( - end - stats.start) - .count(); - auto sec = nano / 1000000.0; - os << "duration: " << sec << "s (" << nano << "µs)" << std::endl - << std::endl; + auto grand_total = total_success + total_fail; + snprintf(buff, BUFSIZ, "%4s %10zu %5s %10zu %10zu %s", "", + grand_total, "", total_success, total_fail, + "Total counters"); + os << buff << std::endl; - char buff[BUFSIZ]; - size_t total_success = 0; - size_t total_fail = 0; - for (auto &[name_, success, fail] : stats.items) { - total_success += success; - total_fail += fail; - } + snprintf(buff, BUFSIZ, "%4s %10s %5s %10.2f %10.2f %s", "", "", + "", total_success * 100.0 / grand_total, + total_fail * 100.0 / grand_total, "% success/fail"); + os << buff << std::endl << std::endl; + ; - os << " id total % success fail " - "definition" - << std::endl; - - auto grand_total = total_success + total_fail; - snprintf(buff, BUFSIZ, "%4s %10zu %5s %10zu %10zu %s", "", - grand_total, "", total_success, total_fail, - "Total counters"); - os << buff << std::endl; - - snprintf(buff, BUFSIZ, "%4s %10s %5s %10.2f %10.2f %s", "", "", - "", total_success * 100.0 / grand_total, - total_fail * 100.0 / grand_total, "% success/fail"); - os << buff << std::endl << std::endl; - ; - - size_t id = 0; - for (auto &[name_, success, fail] : stats.items) { - auto total = success + fail; - auto ratio = total * 100.0 / stats.total; - snprintf(buff, BUFSIZ, "%4zu %10zu %5.2f %10zu %10zu %s", id, - total, ratio, success, fail, name.c_str()); - os << buff << std::endl; - id++; - } - } + size_t id = 0; + for (auto &[name, success, fail] : stats.items) { + auto total = success + fail; + auto ratio = total * 100.0 / stats.total; + snprintf(buff, BUFSIZ, "%4zu %10zu %5.2f %10zu %10zu %s", id, + total, ratio, success, fail, name.c_str()); + os << buff << std::endl; + id++; } - }, - [&](auto &trace_data) { - auto stats = new Stats{}; - stats->start = std::chrono::steady_clock::now(); - trace_data = stats; - }, - [&](auto &trace_data) { - auto stats = std::any_cast(trace_data); - delete stats; - }); + } + } + }, + [&](auto &trace_data) { + auto stats = new Stats{}; + stats->start = std::chrono::steady_clock::now(); + trace_data = stats; + }, + [&](auto &trace_data) { + auto stats = std::any_cast(trace_data); + delete stats; + }); } } // namespace peg From f3370a4f52b3a419ac47ea0b848fbb0d3cbf05e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 07:05:24 +0100 Subject: [PATCH 076/116] Bump doc/doxygen/theme from `1f36200` to `d52eafe` (#6751) Bumps [doc/doxygen/theme](https://github.com/jothepro/doxygen-awesome-css) from `1f36200` to `d52eafe`. - [Release notes](https://github.com/jothepro/doxygen-awesome-css/releases) - [Commits](https://github.com/jothepro/doxygen-awesome-css/compare/1f3620084ff75734ed192101acf40e9dff01d848...d52eafe3e9303399fda15661f3d7bb8fe3d7eabc) --- updated-dependencies: - dependency-name: doc/doxygen/theme dependency-version: d52eafe3e9303399fda15661f3d7bb8fe3d7eabc dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- doc/doxygen/theme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/doxygen/theme b/doc/doxygen/theme index 1f3620084..d52eafe3e 160000 --- a/doc/doxygen/theme +++ b/doc/doxygen/theme @@ -1 +1 @@ -Subproject commit 1f3620084ff75734ed192101acf40e9dff01d848 +Subproject commit d52eafe3e9303399fda15661f3d7bb8fe3d7eabc From 9dfac77ba28ad9a91fb5df683fd671e7e9c6ee08 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 05:49:17 +0200 Subject: [PATCH 077/116] Update translation source strings (#6762) --- cockatrice/cockatrice_en@source.ts | 2447 ++++++++++++++++------------ oracle/oracle_en@source.ts | 4 +- 2 files changed, 1362 insertions(+), 1089 deletions(-) diff --git a/cockatrice/cockatrice_en@source.ts b/cockatrice/cockatrice_en@source.ts index 9a2f56f07..810b345df 100644 --- a/cockatrice/cockatrice_en@source.ts +++ b/cockatrice/cockatrice_en@source.ts @@ -25,12 +25,12 @@ AbstractDlgDeckTextEdit - + &Refresh - + Parse Set Name and Number (if available) @@ -38,60 +38,60 @@ AbstractTabDeckEditor - + Open in new tab - + Are you sure? - + The decklist has been modified. Do you want to save the changes? - - - - - - + + + + + + Error - + Could not open deck at %1 - + Could not save remote deck - - + + The deck could not be saved. Please check that the directory is writable and try again. - + Save deck - + The deck could not be saved. - + There are no cards in your deck to be exported @@ -133,190 +133,168 @@ Please check that the directory is writable and try again. AppearanceSettingsPage - + seconds - + Error - + Could not create themes directory at '%1'. - - Enabling this feature will disable the use of the Printing Selector. - -You will not be able to manage printing preferences on a per-deck basis, or see printings other people have selected for their decks. - -You will have to use the Set Manager, available through Card Database -> Manage Sets. - -Are you sure you would like to enable this feature? - - - - - Disabling this feature will enable the Printing Selector. - -You can now choose printings on a per-deck basis in the Deck Editor and configure which printing gets added to a deck by default by pinning it in the Printing Selector. - -You can also use the Set Manager to adjust custom sort order for printings in the Printing Selector (other sort orders like alphabetical or release date are available). - -Are you sure you would like to disable this feature? - - - - - Confirm Change - - - - + Theme settings - + Current theme: - + Open themes folder - + Home tab background source: - + Home tab background shuffle frequency: - + Disabled - + + Display card name of background in bottom right: + + + + Menu settings - + Show keyboard shortcuts in right-click menus - + Show game filter toolbar above list in room tab - + Card rendering - + Display card names on cards having a picture - + Auto-Rotate cards with sideways layout - + Override all card art with personal set preference (Pre-ProviderID change behavior) - + Bump sets that the deck contains cards from to the top in the printing selector - + Scale cards on mouse over - + Use rounded card corners - + Minimum overlap percentage of cards on the stack and in vertical hand - + Maximum initial height for card view window: - - + + rows - + Maximum expanded height for card view window: - + Card counters - + Counter %1 - + Hand layout - + Display hand horizontally (wastes space) - + Enable left justification - + Table grid layout - + Invert vertical coordinate - + Minimum player count for multi-column layout: - + Maximum font size for information displayed on cards: @@ -324,7 +302,12 @@ Are you sure you would like to disable this feature? ArchidektApiResponseDeckDisplayWidget - + + Back to results + + + + Open Deck in Deck Editor @@ -643,22 +626,22 @@ This is only saved for moderators and cannot be seen by the banned person. CardInfoPictureWidget - + View related cards - + Add card to deck - + Mainboard - + Sideboard @@ -694,124 +677,124 @@ This is only saved for moderators and cannot be seen by the banned person. CardMenu - + Re&veal to... - + &All players - + View related cards - + Token: - + All tokens - + &Select All - + S&elect Row - + S&elect Column - + &Play - + &Hide - + Play &Face Down - + &Tap / Untap Turn sideways or back again - - Toggle &normal untapping + + Skip &untapping - + T&urn Over Turn face up/face down - + &Peek at card face - + &Clone - + Attac&h to card... - + Unattac&h - + &Draw arrow... - + &Set annotation... - + Ca&rd counters - + &Add counter (%1) - + &Remove counter (%1) - + &Set counters (%1)... @@ -827,133 +810,133 @@ This is only saved for moderators and cannot be seen by the banned person. CardZoneLogic - + their hand nominative - + %1's hand nominative - + their library look at zone - + %1's library look at zone - + of their library top cards of zone, - + of %1's library top cards of zone - + their library reveal zone - + %1's library reveal zone - + their library shuffle - + %1's library shuffle - + their library nominative - - - %1's library - nominative - - + %1's library + nominative + + + + their graveyard nominative - + %1's graveyard nominative - + their exile nominative - + %1's exile nominative - - - their sideboard - look at zone - - - - - %1's sideboard - look at zone - - their sideboard - nominative + look at zone %1's sideboard + look at zone + + + + + their sideboard nominative - + + %1's sideboard + nominative + + + + their custom zone '%1' nominative - + %1's custom zone '%2' nominative @@ -1015,7 +998,7 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorCardDatabaseDockWidget - + Card Database @@ -1023,7 +1006,7 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorCardInfoDockWidget - + Card Info @@ -1046,32 +1029,32 @@ This is only saved for moderators and cannot be seen by the banned person. - + Select Printing - + Show on EDHRec (Commander) - + Show on EDHRec (Card) - + Show Related cards - + Add card to &maindeck - + Add card to &sideboard @@ -1079,32 +1062,32 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorDeckDockWidget - + Loading Database... - + Banner Card - + Main Type - + Mana Cost - + Colors - + Select Printing @@ -1177,17 +1160,17 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorFilterDockWidget - + Filters - + &Clear all filters - + Delete selected @@ -1310,7 +1293,7 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorPrintingSelectorDockWidget - + Printing Selector @@ -1318,166 +1301,166 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorSettingsPage - - + + Update Spoilers - - + + Success - + Download URLs have been reset. - + Downloaded card pictures have been reset. - + Error - + One or more downloaded card pictures could not be cleared. - + Add URL - - + + URL: - - + + Edit URL - + Network Cache Size: - + Redirect Cache TTL: - + How long cached redirects for urls are valid for. - + Picture Cache Size: - + Add New URL - + Remove URL - + Day(s) - + Updating... - + Choose path - + URL Download Priority - + Spoilers - + Download Spoilers Automatically - + Spoiler Location: - + Last Change - + Spoilers download automatically on launch - + Press the button to manually update without relaunching - + Do not close settings until manual update is complete - + Download card pictures on the fly - + How to add a custom URL - + Delete Downloaded Images - + Reset Download URLs - + On-disk cache for downloaded pictures - + In-memory cache for pictures not currently on screen @@ -1485,32 +1468,32 @@ This is only saved for moderators and cannot be seen by the banned person. DeckListHistoryManagerWidget - + Undo - + Redo - + Undo/Redo history - + Click on an entry to revert to that point in the history. - + [redo] - + [undo] @@ -1518,27 +1501,27 @@ This is only saved for moderators and cannot be seen by the banned person. DeckListModel - + Count - + Set - + Number - + Provider ID - + Card @@ -1546,12 +1529,12 @@ This is only saved for moderators and cannot be seen by the banned person. DeckLoader - + Common deck formats (%1) - + All files (*.*) @@ -1648,94 +1631,94 @@ This is only saved for moderators and cannot be seen by the banned person. DeckPreviewWidget - + Banner Card - + Open in deck editor - + Edit Tags - + Rename Deck - + Save Deck to Clipboard - + Annotated - + Annotated (No set info) - + Not Annotated - + Not Annotated (No set info) - + Rename File - + Delete File - + Set Banner Card - - + + New name: - - + + Error - + Rename failed - + Delete file - + Are you sure you want to delete the selected file? - + Delete failed @@ -1768,32 +1751,32 @@ This is only saved for moderators and cannot be seen by the banned person. - + Added (%1): %2 (%3) %4 - + Moved to %1 1 × "%2" (%3) - + Removed "%1" (all copies) - + %1 1 × "%2" (%3) - + Added - + Removed @@ -1860,29 +1843,29 @@ This is only saved for moderators and cannot be seen by the banned person. - - + + Error - + The selected file could not be loaded. - + Deck is greater than maximum file size. - + Are you sure you want to force start? This will kick all non-ready players from the game. - + Cockatrice @@ -2374,17 +2357,17 @@ To remove your current avatar, confirm without choosing a new image. DlgEditDeckInClipboard - + Edit deck in clipboard - + Error - + Invalid deck list. @@ -2884,17 +2867,17 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgLoadDeckFromClipboard - + Load deck from clipboard - + Error - + Invalid deck list. @@ -2902,43 +2885,43 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgLoadDeckFromWebsite - + Paste a link to a decklist site here to import it. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) - - - - - + + + + + Load Deck from Website - + No parser available for this deck provider. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) - + Network error: %1 - + Received empty deck data. - + Failed to parse deck data: %1 - + The provided URL is not recognized as a valid deck URL. Valid deck URLs look like this: @@ -2957,40 +2940,73 @@ https://tappedout.net/mtg-decks/your-deck-name/ + + DlgLocalGameOptions + + + Players: + + + + + General + + + + + Starting life total: + + + + + Game setup options + + + + + Remember settings + + + + + Local game options + + + DlgMoveTopCardsUntil - + Card name (or search expressions): - + Number of hits: - + Auto play hits - + Put top cards on stack until... - + No cards matching the search expression exists in the card database. Proceed anyways? - + Cockatrice - + Invalid filter @@ -3151,12 +3167,12 @@ Your email will be used to verify your account. DlgSettings - + Unknown Error loading card database - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -3167,7 +3183,7 @@ Would you like to change your database location setting? - + Your card database version is too old. This can cause problems loading card information or images @@ -3178,7 +3194,7 @@ Would you like to change your database location setting? - + Your card database did not finish loading Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -3187,21 +3203,21 @@ Would you like to change your database location setting? - + File Error loading your card database. Would you like to change your database location setting? - + Your card database was loaded but contains no cards. Would you like to change your database location setting? - + Unknown card database load status Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues @@ -3210,59 +3226,59 @@ Would you like to change your database location setting? - - - + + + Error - + The path to your deck directory is invalid. Would you like to go back and set the correct path? - + The path to your card pictures directory is invalid. Would you like to go back and set the correct path? - + Settings - + General - + Appearance - + User Interface - + Card Sources - + Chat - + Sound - + Shortcuts @@ -3575,67 +3591,67 @@ You may have to manually download the new version. DrawProbabilityWidget - + Draw Probability - + Probability of drawing - + Card Name - + Type - + Subtype - + Mana Value - + At least - + Exactly - + card(s) having drawn at least - + cards - + Category - + Qty - + Odds (%) @@ -4089,143 +4105,143 @@ You may have to manually download the new version. GeneralSettingsPage - + Reset all paths - + All paths have been reset - - - - - - - + + + + + + + Choose path - + Personal settings - + Language: - + Paths (editing disabled in portable mode) - + Paths - + How to help with translations - + Decks directory: - + Filters directory: - + Replays directory: - + Pictures directory: - + Card database: - + Custom database directory: - + Token database: - + Update channel - + Check for client updates on startup - + Check for card database updates on startup - + Don't check - + Prompt for update - + Always update in the background - + Check for card database updates every - + days - + Notify if a feature supported by the server is missing in my client - + Automatically run Oracle when running a new version of Cockatrice - + Show tips on startup - + Last update check on %1 (%2 days ago) @@ -4233,47 +4249,47 @@ You may have to manually download the new version. GraveyardMenu - + &Graveyard - + &View graveyard - + &Move graveyard to... - + &Top of library - + &Bottom of library - + &All players - + &Hand - + &Exile - + Reveal random card to... @@ -4281,88 +4297,88 @@ You may have to manually download the new version. HandMenu - + &Hand - + &View hand - + Sort hand by... - + Name - + Type - + Mana Value - + Take &mulligan (Choose hand size) - + Take mulligan (Same hand size) - + Take mulligan (Hand size - 1) - + &Move hand to... - + &Top of library - + &Bottom of library - + &Graveyard - + &Exile - + &Reveal hand to... - - + + All players - + Reveal r&andom card to... @@ -4370,52 +4386,52 @@ You may have to manually download the new version. HomeWidget - + Create New Deck - + Browse Decks - + Browse Card Database - + Browse EDHRec - + Browse Archidekt - + View Replays - + Quit - + Connecting... - + Connect - + Play @@ -4423,193 +4439,213 @@ You may have to manually download the new version. LibraryMenu - + &Library - + &View library - + View &top cards of library... - + View bottom cards of library... - + Reveal &library to... - + Lend library to... - + Reveal &top cards to... - + &Top of library... - + &Bottom of library... - + &Always reveal top card - + &Always look at top card - + &Open deck in deck editor - + &Draw card - + D&raw cards... - + &Undo last draw - + Shuffle - + &Play top card - + Play top card &face down - + Put top card on &bottom - + Move top card to grave&yard - + Move top card to e&xile - + Move top cards to &graveyard... - + + Move top cards to graveyard face down... + + + + Move top cards to &exile... - + + Move top cards to exile face down... + + + + Put top cards on stack &until... - + Shuffle top cards... - + &Draw bottom card - + D&raw bottom cards... - + &Play bottom card - + Play bottom card &face down - + Move bottom card to grave&yard - + Move bottom card to e&xile - + Move bottom cards to &graveyard... - + + Move bottom cards to graveyard face down... + + + + Move bottom cards to &exile... - + + Move bottom cards to exile face down... + + + + Put bottom card on &top - + Shuffle bottom cards... - - + + &All players - + Reveal top cards of library - + Number of cards: (max. %1) @@ -4703,18 +4739,8 @@ Will now login. - - Number of players - - - - - Please enter the number of players. - - - - - + + Player %1 @@ -4817,8 +4843,8 @@ Will now login. - - + + Error @@ -5219,63 +5245,63 @@ Local version is %1, remote version is %2. - + New Version - + Congratulations on updating to Cockatrice %1! Oracle will now launch to update your card database. - + Cockatrice installed - + Congratulations on installing Cockatrice %1! Oracle will now launch to install the initial card database. - + Card database - + Cockatrice is unable to load the card database. Do you want to update your card database now? If unsure or first time user, choose "Yes" - - + + Yes - - + + No - + Open settings - + New sets found - + %n new set(s) found in the card database Set code(s): %1 Do you want to enable it/them? @@ -5285,81 +5311,81 @@ Do you want to enable it/them? - + View sets - + Welcome - + Hi! It seems like you're running this version of Cockatrice for the first time. All the sets in the card database have been enabled. Read more about changing the set order or disabling specific sets and consequent effects in the "Manage Sets" dialog. - - + + Information - + A card database update is already running. - + Unable to run the card database updater: - + Card database update running. - + Failed to start. The file might be missing, or permissions might be incorrect. - + The process crashed some time after starting successfully. - + Timed out. The process took too long to respond. The last waitFor...() function timed out. - + An error occurred when attempting to write to the process. For example, the process may not be running, or it may have closed its input channel. - + An error occurred when attempting to read from the process. For example, the process may not be running. - + Unknown error occurred. - + The card database updater exited with an error: %1 - + This server supports additional features that your client doesn't have. This is most likely not a problem, but this message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version. @@ -5367,54 +5393,54 @@ To update your client, go to Help -> Check for Updates. - - - - - + + + + + Load sets/cards - + Selected file cannot be found. - + You can only import XML databases at this time. - + The new sets/cards have been added successfully. Cockatrice will now reload the card database. - + Sets/cards failed to import. - - - + + + Reset Password - + Your password has been reset successfully, you can now log in using the new credentials. - + Failed to reset user account password, please contact the server operator to reset your password. - + Activation request received, please check your email for an activation token. @@ -5465,7 +5491,7 @@ Cockatrice will now reload the card database. ManaBaseWidget - + Mana Base @@ -5619,277 +5645,297 @@ Cockatrice will now reload the card database. MessageLogWidget - + from play - + from their graveyard - + from exile - + from their hand - + the top card of %1's library - + the top card of their library - + from the top of %1's library - + from the top of their library - + the bottom card of %1's library - + the bottom card of their library - + from the bottom of %1's library - + from the bottom of their library - + from %1's library - + from their library - + from sideboard - + from the stack - + from custom zone '%1' - + %1 is now keeping the top card %2 revealed. - + %1 is not revealing the top card %2 any longer. - + %1 can now look at top card %2 at any time. - + %1 no longer can look at top card %2 at any time. - + %1 attaches %2 to %3's %4. - + %1 has conceded the game. - + %1 has unconceded the game. - + %1 has restored connection to the game. - + %1 has lost connection to the game. - + %1 points from their %2 to themselves. - + %1 points from their %2 to %3. - + %1 points from %2's %3 to themselves. - + %1 points from %2's %3 to %4. - + %1 points from their %2 to their %3. - + %1 points from their %2 to %3's %4. - + %1 points from %2's %3 to their own %4. - + %1 points from %2's %3 to %4's %5. - + %1 creates a face down token. - + %1 creates token: %2%3. - + %1 has loaded a deck (%2). - + %1 has loaded a deck with %2 sideboard cards (%3). - + %1 destroys %2. - + a card - + %1 gives %2 control over %3. - + %1 puts %2 into play%3 face down. - + %1 puts %2 into play%3. - + + %1 puts %2%3 into their graveyard face down. + + + + %1 puts %2%3 into their graveyard. + + + %1 exiles %2%3 face down. + + %1 exiles %2%3. - + %1 moves %2%3 to their hand. - + %1 puts %2%3 into their library. - + %1 puts %2%3 onto the bottom of their library. - + %1 puts %2%3 on top of their library. - + %1 puts %2%3 into their library %4 cards from the top. - + %1 moves %2%3 to sideboard. - + + %1 plays %2%3 face down. + + + + %1 plays %2%3. - + + %1 moves %2%3 to custom zone '%4' face down. + + + + %1 moves %2%3 to custom zone '%4'. - + %1 tries to draw from an empty library - + %1 draws %2 card(s). @@ -5897,12 +5943,12 @@ Cockatrice will now reload the card database. - + %1 is looking at %2. - + %1 is looking at the %4 %3 card(s) %2. top card for singular, top %3 cards for plural @@ -5911,72 +5957,72 @@ Cockatrice will now reload the card database. - + bottom - + top - + %1 turns %2 face-down. - + %1 turns %2 face-up. - + The game has been closed. - + The game has started. - + You are flooding the game. Please wait a couple of seconds. - + %1 has joined the game. - + %1 is now watching the game. - + You have been kicked out of the game. - + %1 has left the game (%2). - + %1 is not watching the game any more (%2). - + %1 is not ready to start the game any more. - + %1 shuffles their deck and draws a new hand of %2 card(s). @@ -5984,28 +6030,28 @@ Cockatrice will now reload the card database. - + %1 shuffles their deck and draws a new hand. - + You are watching a replay of game #%1. - + %1 is ready to start the game. - + cards an unknown amount of cards - + %1 card(s) a card for singular, %1 cards for plural @@ -6014,107 +6060,107 @@ Cockatrice will now reload the card database. - + %1 lends %2 to %3. - + %1 reveals %2 to %3. - + %1 reveals %2. - + %1 randomly reveals %2%3 to %4. - + %1 randomly reveals %2%3. - + %1 peeks at face down card #%2. - + %1 peeks at face down card #%2: %3. - + %1 reveals %2%3 to %4. - + %1 reveals %2%3. - + %1 reversed turn order, now it's %2. - + reversed - + normal - + Heads - + Tails - + %1 flipped a coin. It landed as %2. - + %1 rolls a %2 with a %3-sided die. - + %1 flips %2 coins. There are %3 heads and %4 tails. - + %1 rolls a %2-sided dice %3 times: %4. - + %1's turn. - + %1 sets annotation of %2 to %3. - + %1 places %2 "%3" counter(s) on %4 (now %5). @@ -6122,7 +6168,7 @@ Cockatrice will now reload the card database. - + %1 removes %2 "%3" counter(s) from %4 (now %5). @@ -6130,97 +6176,97 @@ Cockatrice will now reload the card database. - + %1 sets counter %2 to %3 (%4%5). - + %1 sets %2 to not untap normally. - + %1 sets %2 to untap normally. - + %1 removes the PT of %2. - + %1 changes the PT of %2 from nothing to %4. - + %1 changes the PT of %2 from %3 to %4. - + %1 has locked their sideboard. - + %1 has unlocked their sideboard. - + %1 taps their permanents. - + %1 untaps their permanents. - + %1 taps %2. - + %1 untaps %2. - + %1 shuffles %2. - + %1 shuffles the bottom %3 cards of %2. - + %1 shuffles the top %3 cards of %2. - + %1 shuffles cards %3 - %4 of %2. - + %1 unattaches %2. - + %1 undoes their last draw. - + %1 undoes their last draw (%2). @@ -6228,110 +6274,110 @@ Cockatrice will now reload the card database. MessagesSettingsPage - + Word1 Word2 Word3 - + Add New Message - + Edit Message - + Remove Message - + Add message - - + + Message: - + Edit message - + Chat settings - + Custom alert words - + Enable chat mentions - + Enable mention completer - + In-game message macros - + How to use in-game message macros - + Ignore chat room messages sent by unregistered users - + Ignore private messages sent by unregistered users - - + + Invert text color - + Enable desktop notifications for private messages - + Enable desktop notification for mentions - + Enable room message history on join - - + + (Color is hexadecimal) - + Separate words with a space, alphanumeric characters only @@ -6344,32 +6390,37 @@ Cockatrice will now reload the card database. - + &Top of library in random order - + X cards from the top of library... - + &Bottom of library in random order - + + T&able + + + + &Hand - + &Graveyard - + &Exile @@ -6511,57 +6562,57 @@ Cockatrice will now reload the card database. PhasesToolbar - + Untap step - + Upkeep step - + Draw step - + First main phase - + Beginning of combat step - + Declare attackers step - + Declare blockers step - + Combat damage step - + End of combat step - + Second main phase - + End of turn step @@ -6578,134 +6629,138 @@ Cockatrice will now reload the card database. PlayerActions - + View top cards of library - - - - - - - - - - - + + + + + + + + + Number of cards: (max. %1) - + View bottom cards of library - + Shuffle top cards of library - + Shuffle bottom cards of library - + Draw hand - + 0 and lower are in comparison to current hand size - + Draw cards + + + + + + grave + + - Move top cards to grave + + + + exile - - Move top cards to exile + + Move top cards to %1 - - Move bottom cards to grave + + Move bottom cards to %1 - - Move bottom cards to exile - - - - + Draw bottom cards - - + + C&reate another %1 token - + Create tokens - - + + Number: - + Place card X cards from top of library - + Which position should this card be placed: - + (max. %1) - + Change power/toughness - + Change stats to: - + Set annotation - + Please enter the new annotation: - + Set counters @@ -6713,48 +6768,65 @@ Cockatrice will now reload the card database. PlayerMenu - + Player "%1" - + &Counters + + + PrintingDisabledInfoWidget - - S&ay + + The Printing Selector is disabled because you have currently enabled the setting to override all selected printings with personal set preferences. + +This setting means you'll only see the default printing for each card, instead of being able to select a printing, and will not see the printings other people have selected. + + + + + + + Enable printings again PrintingSelector - + Display Navigation Buttons + + + Printing Selector + + PrintingSelectorCardOverlayWidget - + Preference - + Pin Printing - + Unpin Printing - + Show Related cards @@ -6803,17 +6875,25 @@ Cockatrice will now reload the card database. - - + + Descending - + Ascending + + PrintingSelectorPlaceholderWidget + + + Select a card to view its available printings + + + PtMenu @@ -6952,6 +7032,33 @@ Cockatrice will now reload the card database. A .cod version of this deck already exists. Overwrite it? + + + Enabling this feature will disable the use of the Printing Selector. + +You will not be able to manage printing preferences on a per-deck basis, or see printings other people have selected for their decks. + +You will have to use the Set Manager, available through Card Database -> Manage Sets. + +Are you sure you would like to enable this feature? + + + + + Disabling this feature will enable the Printing Selector. + +You can now choose printings on a per-deck basis in the Deck Editor and configure which printing gets added to a deck by default by pinning it in the Printing Selector. + +You can also use the Set Manager to adjust custom sort order for printings in the Printing Selector (other sort orders like alphabetical or release date are available). + +Are you sure you would like to disable this feature? + + + + + Confirm Change + + QPlatformTheme @@ -7100,37 +7207,37 @@ Cockatrice will now reload the card database. RfgMenu - + &Exile - + &View exile - + &Move exile to... - + &Top of library - + &Bottom of library - + &Hand - + &Graveyard @@ -7173,6 +7280,14 @@ Cockatrice will now reload the card database. + + SayMenu + + + S&ay + + + SequenceEdit @@ -7237,53 +7352,53 @@ Cockatrice will now reload the card database. ShortcutSettingsPage - - + + Restore all default shortcuts - + Do you really want to restore all default shortcuts? - + Clear all default shortcuts - + Do you really want to clear all shortcuts? - + Section: - + Action: - + Shortcut: - + How to set custom shortcuts - + Clear all shortcuts - + Search by shortcut name @@ -7350,27 +7465,27 @@ Please check your shortcut settings! SoundSettingsPage - + Enable &sounds - + Current sounds theme: - + Test system sound engine - + Sound settings - + Master volume @@ -7586,59 +7701,123 @@ Please check your shortcut settings! TabArchidekt - - + + + Desc. - - Asc. + + + AND - - - Any Bracket + + + Require ALL selected colors - - Deck name contains... + + + Deck name... - - Owner name contains... + + + Owner... + + + + + + Packages + + + + + + Advanced Filters - Disabled + Bracket: - + + + Any + + + + + + Contains card... + + + + + + Commander... + + + + + + Tag... + + + + + + Deck Size + + + + + Cards: + + + + + + Asc. + + + + + Sort by: + + + + + Filter by: + + + + + Display Settings + + + + + Search - + + Formats - - Min. # of Cards: - - - - - Page: - - - - + Archidekt: @@ -7646,60 +7825,52 @@ Please check your shortcut settings! TabDeckEditor - + Card Info - + Deck - + Filters - + &View - + Card Database - + Printing - - - - - + Visible - - - - - + Floating - + Reset layout - + Deck: %1 @@ -7707,61 +7878,55 @@ Please check your shortcut settings! TabDeckEditorVisual - + Visual Deck: %1 - + &Visual Deck Editor - - + + Card Info - - + + Deck - - + + Filters - + &View - + Printing - - - - + Visible - - - - + Floating - + Reset layout @@ -7826,7 +7991,7 @@ Please check your shortcut settings! - + New folder @@ -7907,18 +8072,18 @@ Please enter a name: - + Delete remote decks - + Are you sure you want to delete the selected decks? - + Name of new folder: @@ -7936,12 +8101,12 @@ Please enter a name: - + Error - + Could not open deck at %1 @@ -7990,197 +8155,191 @@ Please enter a name: TabGame - - - + + + Replay - - + + Game - - + + Player List - - + + Card Info - - + + Messages - - + + Replay Timeline - + &Phases - + &Game - + Next &phase - + Next phase with &action - + Next &turn - + Reverse turn order - + &Remove all local arrows - + Rotate View Cl&ockwise - + Rotate View Co&unterclockwise - + Game &information - + Un&concede - - - + + + &Concede - + &Leave game - + C&lose replay - + &Focus Chat - + &Say: - + Selected cards - + &View - - - - + Visible - - - - + Floating - + Reset layout - + Concede - + Are you sure you want to concede this game? - + Unconcede - + You have already conceded. Do you want to return to this game? - + Leave game - + Are you sure you want to leave this game? - + A player has joined game #%1 - + %1 has joined the game - + You have been kicked out of the game. @@ -9280,142 +9439,152 @@ Please refrain from engaging in this activity or further actions may be taken ag UserInterfaceSettingsPage - + General interface settings - + &Double-click cards to play them (instead of single-click) - + &Clicking plays all selected cards (instead of just the clicked card) - + &Play all nonlands onto the stack (not the battlefield) by default - + Do not delete &arrows inside of subphases - + Close card view window when last card is removed - + Auto focus search bar when card view window is opened - + Annotate card text on tokens - - - Use tear-off menus, allowing right click menus to persist on screen - - - - - Notifications settings - - - - - Enable notifications in taskbar - - - - - Notify in the taskbar for game events while you are spectating - - - - - Notify in the taskbar when users in your buddy list connect - - - - - Animation settings - - - - - &Tap/untap animation - - - - - Deck editor/storage settings - - - - - Open deck in new tab by default - - - Use visual deck storage in game lobby + Show selection counter during drag selection - Use selection animation for Visual Deck Storage + Show total selection counter + + + + + Use tear-off menus, allowing right click menus to persist on screen - When adding a tag in the visual deck storage to a .txt deck: + Notifications settings + + + + + Enable notifications in taskbar - do nothing + Notify in the taskbar for game events while you are spectating + + + + + Notify in the taskbar when users in your buddy list connect - ask to convert to .cod + Animation settings + + + + + &Tap/untap animation - always convert to .cod + Deck editor/storage settings - Default deck editor type + Open deck in new tab by default - Classic Deck Editor + Use visual deck storage in game lobby - Visual Deck Editor - - - - - Replay settings + Use selection animation for Visual Deck Storage + When adding a tag in the visual deck storage to a .txt deck: + + + + + do nothing + + + + + ask to convert to .cod + + + + + always convert to .cod + + + + + Default deck editor type + + + + + Classic Deck Editor + + + + + Visual Deck Editor + + + + + Replay settings + + + + Buffer time for backwards skip via shortcut: @@ -9479,23 +9648,24 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayColorFilterWidget - - Mode: Exact Match + + Exact match + + + + + Includes - Mode: Includes + Include / Exclude + Mode: Includes - - Mode: Include/Exclude - - - - - Filter mode (AND/OR/NOT conjunctions of filters) + + How selected and unselected colors are combined in the filter @@ -9522,25 +9692,108 @@ Please refrain from engaging in this activity or further actions may be taken ag + + VisualDatabaseDisplayFilterToolbarWidget + + + Sort by + + + + + Filter by + + + + + Save and load filters + + + + + Filter by exact card name + + + + + Filter by card main-type + + + + + Filter by card sub-type + + + + + Filter by set + + + + + Filter by format legality + + + + + Save/Load + + + + + Name + + + + + Main Type + + + + + Sub Type + + + + + Sets + + + + + Formats + + + VisualDatabaseDisplayFormatLegalityFilterWidget - + + Show formats with at least: + + + + + cards + + + + Do not display formats with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match - + Mode: Includes @@ -9548,22 +9801,32 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayMainTypeFilterWidget - + + Show main types with at least: + + + + + cards + + + + Do not display card main-types with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match - + Mode: Includes @@ -9599,7 +9862,7 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayRecentSetFilterSettingsWidget - + Filter to most recent sets @@ -9607,19 +9870,19 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplaySetFilterWidget - + Search sets... - - + + Mode: Exact Match - - + + Mode: Includes @@ -9627,27 +9890,37 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplaySubTypeFilterWidget - + Search subtypes... - + + Show sub types with at least: + + + + + cards + + + + Do not display card sub-types with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match - + Mode: Includes @@ -9661,52 +9934,22 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Visual - + Loading database ... - + Clear all filters - - Sort by: - - - - - Filter by: - - - - - Save and load filters - - - - - Filter by exact card name - - - - - Filter by card sub-type - - - - - Filter by set - - - - + Table @@ -9714,56 +9957,64 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckDisplayOptionsWidget - + Group by: - + Change how cards are divided into categories/groups. - + Sort by: - + Click and drag to change the sort order within the groups - + Configure how cards are sorted within their groups - - + + Toggle Layout: Overlap - + Change how cards are displayed within zones (i.e. overlapped or fully visible.) - + Toggle Layout: Flat + + VisualDeckEditorPlaceholderWidget + + + Add cards using the search bar or database tab to have them appear here + + + VisualDeckEditorSampleHandWidget - + Draw a new sample hand - + Sample hand size @@ -9771,17 +10022,17 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckEditorWidget - + Type a card name here for suggestions from the database... - + Quick search and add card - + Search for closest match in the database (with auto-suggestions) and add preferred printing to the deck on pressing enter @@ -9797,47 +10048,52 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckStorageQuickSettingsWidget - + Show Folders - + Show Tag Filter - + + Show Color Identity + + + + Show Tags On Deck Previews - + Show Banner Card Selection Option - + Draw unused Color Identities - + Unused Color Identities Opacity - + Deck tooltip: - + None - + Filepath @@ -9938,133 +10194,133 @@ Please refrain from engaging in this activity or further actions may be taken ag WndSets - + Move selected set to the top - + Move selected set up - + Move selected set down - + Move selected set to the bottom - + Search by set name, code, or type - + Default order - + Restore original art priority order - + Enable all sets - + Disable all sets - + Enable selected set(s) - + Disable selected set(s) - + Deck Editor - + Use CTRL+A to select all sets in the view. - + Only cards in enabled sets will appear in the card list of the deck editor. - + Image priority is decided in the following order: - + first the CUSTOM Folder (%1), then the Enabled Sets in this dialog (Top to Bottom) %1 is a link to the wiki - + Include cards rebalanced for Alchemy [requires restart] - + Card Art - + How to use custom card art - + Hints - + Note - + Sorting by column allows you to find a set while not changing set priority. - + To enable ordering again, click the column header until this message disappears. - + Use the current sorting as the set priority instead - + Sorts the set priority using the same column - + Manage sets @@ -10072,72 +10328,72 @@ Please refrain from engaging in this activity or further actions may be taken ag ZoneViewWidget - + Search by card name (or search expressions) - + Ungrouped - + Group by Type - + Group by Mana Value - + Group by Color - + Unsorted - + Sort by Name - + Sort by Type - + Sort by Mana Cost - + Sort by Colors - + Sort by P/T - + Sort by Set - + shuffle when closing - + pile view @@ -10172,7 +10428,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Deck Editor @@ -10253,7 +10509,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Replays @@ -10405,7 +10661,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Reset Layout @@ -10826,7 +11082,8 @@ Please refrain from engaging in this activity or further actions may be taken ag - Toggle Untap + Toggle Skip Untapping + Toggle Untap @@ -10846,98 +11103,102 @@ Please refrain from engaging in this activity or further actions may be taken ag - Attach Card... + Play Card, Face Down - Unattach Card + Attach Card... - Clone Card + Unattach Card - Create Token... + Clone Card - Create All Related Tokens + Create Token... - Create Another Token + Create All Related Tokens - Set Annotation... + Create Another Token - Select All Cards in Zone + Set Annotation... - Select All Cards in Row + Select All Cards in Zone - Select All Cards in Column + Select All Cards in Row - Reveal Selected Cards to All Players + Select All Cards in Column - - Bottom of Library + Reveal Selected Cards to All Players - - - - Exile + + Bottom of Library + + + + Exile + + + + - + Graveyard - + Hand - - + + Top of Library - - + Battlefield, Face Down @@ -10973,234 +11234,246 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Stack - + Graveyard (Multiple) - - + + + Graveyard (Multiple), Face Down + + + + + Exile (Multiple) - + + + Exile (Multiple), Face Down + + + + Stack Until Found - + Draw Bottom Card - + Draw Multiple Cards from Bottom... - + Draw Arrow... - + Remove Local Arrows - + Leave Game - + Concede - + Roll Dice... - + Shuffle Library - + Shuffle Top Cards of Library - + Shuffle Bottom Cards of Library - + Mulligan - + Mulligan (Same hand size) - + Mulligan (Hand size - 1) - + Draw a Card - + Draw Multiple Cards... - + Undo Draw - + Always Reveal Top Card - + Always Look At Top Card - + Sort Hand by Name - + Sort Hand by Type - + Sort Hand by Mana Value - + Reveal Hand to All Players - + Reveal Random Card to All Players - + Rotate View Clockwise - + Rotate View Counterclockwise - + Unfocus Text Box - + Focus Chat - + Clear Chat - + Refresh - + Skip Forward - + Skip Backward - + Skip Forward by a lot - + Skip Backward by a lot - + Play/Pause - + Toggle Fast Forward - + Home - + Visual Deck Storage - + Deck Storage - + Server - + Account - + Administration - + Logs diff --git a/oracle/oracle_en@source.ts b/oracle/oracle_en@source.ts index 53ad27b4d..943b44a97 100644 --- a/oracle/oracle_en@source.ts +++ b/oracle/oracle_en@source.ts @@ -278,7 +278,7 @@ OracleImporter - + Dummy set containing tokens @@ -286,7 +286,7 @@ OracleWizard - + Oracle Importer From ba786a0289a8dbfcaf833d1452c018ff1575956a Mon Sep 17 00:00:00 2001 From: tooomm Date: Wed, 1 Apr 2026 14:54:27 +0200 Subject: [PATCH 078/116] Update dependabot.yml (#6756) --- .github/dependabot.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fc25af67f..88bed3663 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,9 @@ updates: - package-ecosystem: "gitsubmodule" # Look for `.gitmodules` in the `root` directory directory: "/" + ignore: + # Ignore updates for vcpkg (Bump to latest tag not working (no SemVer used) & macOS Intel triplet broken with newer releases) + - dependency-name: "vcpkg" # Check for updates once a month schedule: interval: "monthly" From 690a00aa6c0779a8ffe67323bf21f6c793689920 Mon Sep 17 00:00:00 2001 From: SlightlyCircuitous <71394296+SlightlyCircuitous@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:40:42 -0400 Subject: [PATCH 079/116] Add Ubuntu 26.04 "Resolute Racoon" build (#6766) * Create 26.04 Dockerfile * Update desktop-build.yml * Update release_template.md * Add ca-certificates package to build --- .ci/Ubuntu26.04/Dockerfile | 29 +++++++++++++++++++++++++++++ .ci/release_template.md | 1 + .github/workflows/desktop-build.yml | 5 +++++ 3 files changed, 35 insertions(+) create mode 100644 .ci/Ubuntu26.04/Dockerfile diff --git a/.ci/Ubuntu26.04/Dockerfile b/.ci/Ubuntu26.04/Dockerfile new file mode 100644 index 000000000..7b0cd389f --- /dev/null +++ b/.ci/Ubuntu26.04/Dockerfile @@ -0,0 +1,29 @@ +FROM ubuntu:26.04 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + libgl-dev \ + liblzma-dev \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt6multimedia6 \ + libqt6sql6-mysql \ + ninja-build \ + protobuf-compiler \ + qt6-image-formats-plugins \ + qt6-l10n-tools \ + qt6-multimedia-dev \ + qt6-svg-dev \ + qt6-tools-dev \ + qt6-tools-dev-tools \ + qt6-websockets-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/release_template.md b/.ci/release_template.md index b0924b92a..1ce9f4bf7 100644 --- a/.ci/release_template.md +++ b/.ci/release_template.md @@ -17,6 +17,7 @@ Available pre-compiled binaries for installation: • macOS 13+ Ventura Intel Linux + • Ubuntu 26.04 LTS Resolute RacoonUbuntu 24.04 LTS Noble NumbatUbuntu 22.04 LTS Jammy JellyfishDebian 13 Trixie diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 820044059..7bd84eb21 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -142,11 +142,16 @@ jobs: - distro: Ubuntu version: 22.04 package: DEB + test: skip # Running tests on all distros is superfluous - distro: Ubuntu version: 24.04 package: DEB + - distro: Ubuntu + version: 26.04 + package: DEB + name: ${{matrix.distro}} ${{matrix.version}} needs: configure runs-on: ubuntu-latest From a46ab5cd68dbbead94379778a0b6dca3dfaa89dc Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sat, 4 Apr 2026 02:40:38 -0700 Subject: [PATCH 080/116] [Game] Allow uppercase X in expressions (#6767) --- libcockatrice_utility/libcockatrice/utility/expression.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libcockatrice_utility/libcockatrice/utility/expression.cpp b/libcockatrice_utility/libcockatrice/utility/expression.cpp index 3b2e815d1..42073670c 100644 --- a/libcockatrice_utility/libcockatrice/utility/expression.cpp +++ b/libcockatrice_utility/libcockatrice/utility/expression.cpp @@ -20,7 +20,7 @@ peg::parser math(R"( NUMBER <- < '-'? [0-9]+ > NAME <- < [a-z][a-z0-9]* > - VARIABLE <- < [x] > + VARIABLE <- < [xX] > FUNCTION <- NAME '(' EXPRESSION ( [,\n] EXPRESSION )* ')' %whitespace <- [ \t\r]* From 3ec9ae97723252b3d4d7d821968d775a739893af Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Sun, 5 Apr 2026 21:52:46 +0200 Subject: [PATCH 081/116] fix compiling on arch (#6768) * fix compiling on arch * redo all the logging in affected files --- .../interface/widgets/tabs/tab_replays.cpp | 17 ++- cockatrice/src/interface/window_main.cpp | 9 +- .../network/client/remote/remote_client.cpp | 46 +++--- .../protocol/debug_pb_message.cpp | 8 +- servatrice/src/isl_interface.cpp | 51 ++++--- .../src/servatrice_database_interface.cpp | 105 +++++++------- servatrice/src/serversocketinterface.cpp | 131 +++++++++++------- 7 files changed, 219 insertions(+), 148 deletions(-) diff --git a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp index 2db9e83c5..570677ada 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp @@ -30,6 +30,8 @@ #include #include +inline Q_LOGGING_CATEGORY(TabReplaysLog, "replays_tab"); + TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo) : Tab(_tabSupervisor), client(_client) { @@ -265,9 +267,11 @@ void TabReplays::actOpenLocalReplay() f.close(); GameReplay *replay = new GameReplay; - replay->ParseFromArray(_data.data(), _data.size()); - - emit openReplay(replay); + if (replay->ParseFromArray(_data.data(), _data.size())) { + emit openReplay(replay); + } else { + qCWarning(TabReplaysLog) << "could not parse replay!"; + } } } @@ -379,9 +383,12 @@ void TabReplays::openRemoteReplayFinished(const Response &r) const Response_ReplayDownload &resp = r.GetExtension(Response_ReplayDownload::ext); GameReplay *replay = new GameReplay; - replay->ParseFromString(resp.replay_data()); + if (replay->ParseFromString(resp.replay_data())) { - emit openReplay(replay); + emit openReplay(replay); + } else { + qCWarning(TabReplaysLog) << "could not parse remote replay!"; + } } void TabReplays::actDownload() diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index fbba6c3f5..86e6c1534 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -84,6 +84,7 @@ const QString MainWindow::appName = "Cockatrice"; const QStringList MainWindow::fileNameFilters = QStringList() << QObject::tr("Cockatrice card database (*.xml)") << QObject::tr("All files (*.*)"); +inline Q_LOGGING_CATEGORY(MainWindowLog, "main_window"); /** * Replaces the tab-specific menus that are shown in the menuBar. @@ -277,9 +278,11 @@ void MainWindow::actWatchReplay() file.close(); replay = new GameReplay; - replay->ParseFromArray(buf.data(), buf.size()); - - tabSupervisor->openReplay(replay); + if (replay->ParseFromArray(buf.data(), buf.size())) { + tabSupervisor->openReplay(replay); + } else { + qCWarning(MainWindowLog) << "failed to parse replay!"; + } } void MainWindow::localGameEnded() diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp index 4b5d3f1b8..3e3ec889d 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp @@ -390,14 +390,18 @@ void RemoteClient::readData() return; ServerMessage newServerMessage; - newServerMessage.ParseFromArray(inputBuffer.data(), messageLength); - - qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); + bool ok = newServerMessage.ParseFromArray(inputBuffer.data(), messageLength); inputBuffer.remove(0, messageLength); messageInProgress = false; - processProtocolItem(newServerMessage); + if (ok) { + qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); + + processProtocolItem(newServerMessage); + } else { + qCDebug(RemoteClientLog) << "parsing error!"; + } if (getStatus() == StatusDisconnecting) // use thread-safe getter doDisconnectFromServer(); @@ -408,11 +412,13 @@ void RemoteClient::websocketMessageReceived(const QByteArray &message) { lastDataReceived = timeRunning; ServerMessage newServerMessage; - newServerMessage.ParseFromArray(message.data(), message.length()); + if (newServerMessage.ParseFromArray(message.data(), message.length())) { + qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); - qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); - - processProtocolItem(newServerMessage); + processProtocolItem(newServerMessage); + } else { + qCDebug(RemoteClientLog) << "parsing error!"; + } } void RemoteClient::sendCommandContainer(const CommandContainer &cont) @@ -426,19 +432,27 @@ void RemoteClient::sendCommandContainer(const CommandContainer &cont) qCDebug(RemoteClientLog).noquote() << "OUT" << getSafeDebugString(cont); QByteArray buf; + bool ok; if (usingWebSocket) { buf.resize(size); - cont.SerializeToArray(buf.data(), size); - websocket->sendBinaryMessage(buf); + ok = cont.SerializeToArray(buf.data(), size); + if (ok) { + websocket->sendBinaryMessage(buf); + } } else { buf.resize(size + 4); - cont.SerializeToArray(buf.data() + 4, size); - buf.data()[3] = (unsigned char)size; - buf.data()[2] = (unsigned char)(size >> 8); - buf.data()[1] = (unsigned char)(size >> 16); - buf.data()[0] = (unsigned char)(size >> 24); + ok = cont.SerializeToArray(buf.data() + 4, size); + if (ok) { + buf.data()[3] = (unsigned char)size; + buf.data()[2] = (unsigned char)(size >> 8); + buf.data()[1] = (unsigned char)(size >> 16); + buf.data()[0] = (unsigned char)(size >> 24); - socket->write(buf); + socket->write(buf); + } + } + if (!ok) { + qCDebug(RemoteClientLog) << "transmit error!"; } } diff --git a/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp index f0c3448b5..718487c18 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp +++ b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp @@ -101,6 +101,10 @@ QString getSafeDebugString(const ::google::protobuf::Message &message) #endif // GOOGLE_PROTOBUF_VERSION > 3004000 std::string debug_string; - printer.PrintToString(message, &debug_string); - return QString::number(size) + " bytes " + QString::fromStdString(debug_string); + bool ok = printer.PrintToString(message, &debug_string); + if (ok) { + return QString::number(size) + " bytes " + QString::fromStdString(debug_string); + } else { + return "[could not convert message to string]"; + } } diff --git a/servatrice/src/isl_interface.cpp b/servatrice/src/isl_interface.cpp index bcf71a98a..692a0fdba 100644 --- a/servatrice/src/isl_interface.cpp +++ b/servatrice/src/isl_interface.cpp @@ -3,6 +3,7 @@ #include "main.h" #include "server_logger.h" +#include #include #include #include @@ -21,6 +22,8 @@ #include #include +inline Q_LOGGING_CATEGORY(IslInterfaceLog, "isl_interface"); + void IslInterface::sharedCtor(const QSslCertificate &cert, const QSslKey &privateKey) { socket = new QSslSocket(this); @@ -113,10 +116,11 @@ void IslInterface::initServer() socket->startServerEncryption(); if (!socket->waitForEncrypted(5000)) { QList sslErrors(socket->sslHandshakeErrors()); - if (sslErrors.isEmpty()) - qDebug() << "[ISL] SSL handshake timeout, terminating connection"; - else - qDebug() << "[ISL] SSL errors:" << sslErrors; + if (sslErrors.isEmpty()) { + qCDebug(IslInterfaceLog) << "SSL handshake timeout, terminating connection"; + } else { + qCWarning(IslInterfaceLog) << "SSL errors:" << sslErrors; + } deleteLater(); return; } @@ -157,7 +161,7 @@ void IslInterface::initServer() server->islLock.lockForWrite(); if (server->islConnectionExists(serverId)) { - qDebug() << "[ISL] Duplicate connection to #" << serverId << "terminating connection"; + qCDebug(IslInterfaceLog) << "Duplicate connection to #" << serverId << "terminating connection"; deleteLater(); } else { transmitMessage(message); @@ -180,27 +184,28 @@ void IslInterface::initClient() expectedErrors.append(QSslError(QSslError::SelfSignedCertificate, peerCert)); socket->ignoreSslErrors(expectedErrors); - qDebug() << "[ISL] Connecting to #" << serverId << ":" << peerAddress << ":" << peerPort; + qCDebug(IslInterfaceLog) << "Connecting to #" << serverId << ":" << peerAddress << ":" << peerPort; socket->connectToHostEncrypted(peerAddress, peerPort, peerHostName); if (!socket->waitForConnected(5000)) { - qDebug() << "[ISL] Socket error:" << socket->errorString(); + qCDebug(IslInterfaceLog) << "Socket error:" << socket->errorString(); deleteLater(); return; } if (!socket->waitForEncrypted(5000)) { QList sslErrors(socket->sslHandshakeErrors()); - if (sslErrors.isEmpty()) - qDebug() << "[ISL] SSL handshake timeout, terminating connection"; - else - qDebug() << "[ISL] SSL errors:" << sslErrors; + if (sslErrors.isEmpty()) { + qCDebug(IslInterfaceLog) << "SSL handshake timeout, terminating connection"; + } else { + qCWarning(IslInterfaceLog) << "SSL errors:" << sslErrors; + } deleteLater(); return; } server->islLock.lockForWrite(); if (server->islConnectionExists(serverId)) { - qDebug() << "[ISL] Duplicate connection to #" << serverId << "terminating connection"; + qCDebug(IslInterfaceLog) << "Duplicate connection to #" << serverId << "terminating connection"; deleteLater(); return; } @@ -242,17 +247,21 @@ void IslInterface::readClient() return; IslMessage newMessage; - newMessage.ParseFromArray(inputBuffer.data(), messageLength); + bool ok = newMessage.ParseFromArray(inputBuffer.data(), messageLength); inputBuffer.remove(0, messageLength); messageInProgress = false; - processMessage(newMessage); + if (ok) { + processMessage(newMessage); + } else { + qCWarning(IslInterfaceLog) << "parsing error!"; + } } while (!inputBuffer.isEmpty()); } void IslInterface::catchSocketError(QAbstractSocket::SocketError socketError) { - qDebug() << "[ISL] Socket error:" << socketError; + qCWarning(IslInterfaceLog) << "Socket error:" << socketError; server->islLock.lockForWrite(); server->removeIslInterface(serverId); @@ -270,7 +279,10 @@ void IslInterface::transmitMessage(const IslMessage &item) unsigned int size = static_cast(item.ByteSize()); #endif buf.resize(size + 4); - item.SerializeToArray(buf.data() + 4, size); + if (!item.SerializeToArray(buf.data() + 4, size)) { + qCWarning(IslInterfaceLog) << "transmit error!"; + return; + } buf.data()[3] = (unsigned char)size; buf.data()[2] = (unsigned char)(size >> 8); buf.data()[1] = (unsigned char)(size >> 16); @@ -368,7 +380,7 @@ void IslInterface::processSessionEvent(const SessionEvent &event, qint64 session QReadLocker clientsLocker(&server->clientsLock); Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId); if (!client) { - qDebug() << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; + qCDebug(IslInterfaceLog) << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; break; } const Event_GameJoined &gameJoined = event.GetExtension(Event_GameJoined::ext); @@ -382,7 +394,8 @@ void IslInterface::processSessionEvent(const SessionEvent &event, qint64 session QReadLocker clientsLocker(&server->clientsLock); Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId); if (!client) { - qDebug() << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; + qCWarning(IslInterfaceLog) + << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; break; } @@ -430,7 +443,7 @@ void IslInterface::processRoomCommand(const CommandContainer &cont, qint64 sessi void IslInterface::processMessage(const IslMessage &item) { - qDebug() << getSafeDebugString(item); + qCDebug(IslInterfaceLog) << getSafeDebugString(item); switch (item.message_type()) { case IslMessage::ROOM_COMMAND_CONTAINER: { diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index fa9b14f31..bce3542e8 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -7,12 +7,15 @@ #include #include #include +#include #include #include #include #include #include +inline Q_LOGGING_CATEGORY(DatabaseInterfaceLog, "database_interface"); + Servatrice_DatabaseInterface::Servatrice_DatabaseInterface(int _instanceId, Servatrice *_server) : instanceId(_instanceId), sqlDatabase(QSqlDatabase()), server(_server) { @@ -56,17 +59,16 @@ bool Servatrice_DatabaseInterface::openDatabase() sqlDatabase.close(); const QString poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); - qDebug().noquote() << QString("[%1] Opening database...").arg(poolStr); + qCDebug(DatabaseInterfaceLog).noquote() << poolStr << "Opening database..."; if (!sqlDatabase.open()) { - qCritical() << QString("[%1] Error opening database: %2").arg(poolStr).arg(sqlDatabase.lastError().text()); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database:" << sqlDatabase.lastError().text(); return false; } QSqlQuery *versionQuery = prepareQuery("select version from {prefix}_schema_version limit 1"); if (!execSqlQuery(versionQuery)) { - qCritical() << QString("[%1] Error opening database: unable to load database schema version (hint: ensure the " - "cockatrice_schema_version exists)") - .arg(poolStr); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database: unable to load database schema version" + << "(hint: ensure the cockatrice_schema_version exists)"; return false; } @@ -74,24 +76,21 @@ bool Servatrice_DatabaseInterface::openDatabase() const int dbversion = versionQuery->value(0).toInt(); const int expectedversion = DATABASE_SCHEMA_VERSION; if (dbversion < expectedversion) { - qCritical() << QString("[%1] Error opening database: the database schema version is too old, you need to " - "run the migrations to update it from version %2 to version %3") - .arg(poolStr) - .arg(dbversion) - .arg(expectedversion); + qCCritical(DatabaseInterfaceLog) << poolStr + << "Error opening database: the database schema version is too old, you " + "need to run the migrations to update it from version" + << dbversion << "to version" << expectedversion; return false; } else if (dbversion > expectedversion) { - qCritical() << QString("[%1] Error opening database: the database schema version %2 is too new, you need " - "to update servatrice (this servatrice actually uses version %3)") - .arg(poolStr) - .arg(dbversion) - .arg(expectedversion); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database: the database schema version" + << dbversion << "is too new, you need to update servatrice" + << "(this servatrice actually uses version" << expectedversion << ")"; return false; } } else { - qCritical() << QString("[%1] Error opening database: unable to load database schema version (hint: ensure the " - "cockatrice_schema_version contains a single record)") - .arg(poolStr); + qCCritical(DatabaseInterfaceLog) << poolStr + << "Error opening database: unable to load database schema version (hint: " + "ensure the cockatrice_schema_version contains a single record)"; return false; } @@ -114,9 +113,7 @@ bool Servatrice_DatabaseInterface::checkSql() if (query.lastError().isValid()) { const auto &poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); - qCritical() << QString("[%1] Error executing query: %2, resetting connection") - .arg(poolStr) - .arg(query.lastError().text()); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error executing query:" << query.lastError().text(); sqlDatabase.close(); return openDatabase(); @@ -145,7 +142,7 @@ bool Servatrice_DatabaseInterface::execSqlQuery(QSqlQuery *query) if (query->exec()) return true; const QString poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); - qCritical() << QString("[%1] Error executing query: %2").arg(poolStr).arg(query->lastError().text()); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error executing query:" << query->lastError().text(); sqlDatabase.close(); openDatabase(); return false; @@ -252,7 +249,8 @@ bool Servatrice_DatabaseInterface::registerUser(const QString &userName, query->bindValue(":token", token); if (!execSqlQuery(query)) { - qDebug() << "Failed to insert user: " << query->lastError() << " sql: " << query->lastQuery(); + qCWarning(DatabaseInterfaceLog) << "Failed to insert user: " << query->lastError() + << " sql: " << query->lastQuery(); return false; } @@ -270,8 +268,8 @@ bool Servatrice_DatabaseInterface::activateUser(const QString &userName, const Q activateQuery->bindValue(":username", userName); activateQuery->bindValue(":token", token); if (!execSqlQuery(activateQuery)) { - qDebug() << "Account activation failed: SQL error." << activateQuery->lastError() - << " sql: " << activateQuery->lastQuery(); + qCWarning(DatabaseInterfaceLog) << "Account activation failed: SQL error." << activateQuery->lastError() + << " sql: " << activateQuery->lastQuery(); return false; } @@ -284,7 +282,8 @@ bool Servatrice_DatabaseInterface::activateUser(const QString &userName, const Q query->bindValue(":userName", userName); if (!execSqlQuery(query)) { - qDebug() << "Failed to activate user: " << query->lastError() << " sql: " << query->lastQuery(); + qCWarning(DatabaseInterfaceLog) + << "Failed to activate user: " << query->lastError() << " sql: " << query->lastQuery(); return false; } @@ -326,7 +325,7 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot prepareQuery("select password_sha512, active from {prefix}_users where name = :name"); passwordQuery->bindValue(":name", user); if (!execSqlQuery(passwordQuery)) { - qDebug("Login denied: SQL error"); + qCWarning(DatabaseInterfaceLog) << "Login denied: SQL error"; return NotLoggedIn; } @@ -334,7 +333,7 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot const QString correctPasswordSha512 = passwordQuery->value(0).toString(); const bool userIsActive = passwordQuery->value(1).toBool(); if (!userIsActive) { - qDebug("Login denied: user not active"); + qCWarning(DatabaseInterfaceLog) << "Login denied: user not active"; return UserIsInactive; } QString hashedPassword; @@ -344,14 +343,14 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot hashedPassword = password; } if (correctPasswordSha512 == hashedPassword) { - qDebug("Login accepted: password right"); + qCDebug(DatabaseInterfaceLog) << "Login accepted: password right"; return PasswordRight; } else { - qDebug("Login denied: password wrong"); + qCDebug(DatabaseInterfaceLog) << "Login denied: password wrong"; return NotLoggedIn; } } else { - qDebug("Login accepted: unknown user"); + qCDebug(DatabaseInterfaceLog) << "Login accepted: unknown user"; return UnknownUser; } } @@ -369,7 +368,7 @@ bool Servatrice_DatabaseInterface::checkUserIsBanned(const QString &ipAddress, return false; if (!checkSql()) { - qDebug("Failed to check if user is banned. Database invalid."); + qCWarning(DatabaseInterfaceLog) << "Failed to check if user is banned. Database invalid."; return false; } @@ -400,7 +399,7 @@ bool Servatrice_DatabaseInterface::checkUserIsIdBanned(const QString &clientId, idBanQuery->bindValue(":id", clientId); idBanQuery->bindValue(":id2", clientId); if (!execSqlQuery(idBanQuery)) { - qDebug() << "Id ban check failed: SQL error." << idBanQuery->lastError(); + qCWarning(DatabaseInterfaceLog) << "Id ban check failed: SQL error." << idBanQuery->lastError(); return false; } @@ -410,7 +409,7 @@ bool Servatrice_DatabaseInterface::checkUserIsIdBanned(const QString &clientId, if ((secondsLeft > 0) || permanentBan) { banReason = idBanQuery->value(2).toString(); banSecondsRemaining = permanentBan ? 0 : secondsLeft; - qDebug() << "User is banned by client id" << clientId; + qCDebug(DatabaseInterfaceLog) << "User is banned by client id" << clientId; return true; } } @@ -428,7 +427,7 @@ bool Servatrice_DatabaseInterface::checkUserIsNameBanned(const QString &userName nameBanQuery->bindValue(":name1", userName); nameBanQuery->bindValue(":name2", userName); if (!execSqlQuery(nameBanQuery)) { - qDebug() << "Name ban check failed: SQL error" << nameBanQuery->lastError(); + qCWarning(DatabaseInterfaceLog) << "Name ban check failed: SQL error" << nameBanQuery->lastError(); return false; } @@ -438,7 +437,7 @@ bool Servatrice_DatabaseInterface::checkUserIsNameBanned(const QString &userName if ((secondsLeft > 0) || permanentBan) { banReason = nameBanQuery->value(2).toString(); banSecondsRemaining = permanentBan ? 0 : secondsLeft; - qDebug() << "Username" << userName << "is banned by name"; + qCDebug(DatabaseInterfaceLog) << "Username" << userName << "is banned by name"; return true; } } @@ -464,7 +463,7 @@ bool Servatrice_DatabaseInterface::checkUserIsIpBanned(const QString &ipAddress, ipBanQuery->bindValue(":address", ipAddress); ipBanQuery->bindValue(":address2", ipAddress); if (!execSqlQuery(ipBanQuery)) { - qDebug() << "IP ban check failed: SQL error." << ipBanQuery->lastError(); + qCWarning(DatabaseInterfaceLog) << "IP ban check failed: SQL error." << ipBanQuery->lastError(); return false; } @@ -474,7 +473,7 @@ bool Servatrice_DatabaseInterface::checkUserIsIpBanned(const QString &ipAddress, if ((secondsLeft > 0) || permanentBan) { banReason = ipBanQuery->value(2).toString(); banSecondsRemaining = permanentBan ? 0 : secondsLeft; - qDebug() << "User is banned by address" << ipAddress; + qCDebug(DatabaseInterfaceLog) << "User is banned by address" << ipAddress; return true; } } @@ -865,12 +864,17 @@ void Servatrice_DatabaseInterface::storeGameInformation(const QString &roomName, const unsigned int size = static_cast(replayList[i]->ByteSize()); #endif blob.resize(size); - replayList[i]->SerializeToArray(blob.data(), size); + qulonglong replayId = replayList[i]->replay_id(); + if (replayList[i]->SerializeToArray(blob.data(), size)) { - replayIds.append(QVariant((qulonglong)replayList[i]->replay_id())); - replayGameIds.append(gameInfo.game_id()); - replayDurations.append(replayList[i]->duration_seconds()); - replayBlobs.append(blob); + replayIds.append(QVariant(replayId)); + replayGameIds.append(gameInfo.game_id()); + replayDurations.append(replayList[i]->duration_seconds()); + replayBlobs.append(blob); + } else { + qCWarning(DatabaseInterfaceLog) + << "failed to serialise replay, id:" << replayId << "game:" << gameInfo.game_id(); + } } { @@ -1017,7 +1021,7 @@ bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user, passwordQuery->bindValue(":name", user); if (!execSqlQuery(passwordQuery)) { - qDebug("Change password denied: SQL error"); + qCWarning(DatabaseInterfaceLog) << "Change password denied: SQL error"; return false; } @@ -1084,7 +1088,7 @@ void Servatrice_DatabaseInterface::updateUsersLastLoginData(const QString &userN QSqlQuery *query = prepareQuery("select id from {prefix}_users where name = :user_name"); query->bindValue(":user_name", userName); if (!execSqlQuery(query)) { - qDebug("Failed to locate user id when updating users last login data: SQL Error"); + qCWarning(DatabaseInterfaceLog) << "Failed to locate user id when updating users last login data: SQL Error"; return; } @@ -1133,7 +1137,7 @@ QList Servatrice_DatabaseInterface::getUserBanHistory(const QStr query->bindValue(":user_name", userName); if (!execSqlQuery(query)) { - qDebug("Failed to collect ban history information: SQL Error"); + qCWarning(DatabaseInterfaceLog) << "Failed to collect ban history information: SQL Error"; return results; } @@ -1168,7 +1172,7 @@ bool Servatrice_DatabaseInterface::addWarning(const QString userName, query->bindValue(":warn_reason", warningReason); query->bindValue(":client_id", clientID); if (!execSqlQuery(query)) { - qDebug("Failed to collect create warning history information: SQL Error"); + qCWarning(DatabaseInterfaceLog) << "Failed to collect create warning history information: SQL Error"; return false; } @@ -1189,7 +1193,7 @@ QList Servatrice_DatabaseInterface::getUserWarnHistory(const query->bindValue(":user_id", userID); if (!execSqlQuery(query)) { - qDebug("Failed to collect warning history information: SQL Error"); + qCWarning(DatabaseInterfaceLog) << "Failed to collect warning history information: SQL Error"; return results; } @@ -1296,7 +1300,7 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory } if (!execSqlQuery(query)) { - qDebug("Failed to collect log history information: SQL Error"); + qCWarning(DatabaseInterfaceLog) << "Failed to collect log history information: SQL Error"; return results; } @@ -1324,7 +1328,8 @@ int Servatrice_DatabaseInterface::checkNumberOfUserAccounts(const QString &email query->bindValue(":user_email", email); if (!execSqlQuery(query)) { - qDebug("Failed to identify the number of users accounts for users email address: SQL Error"); + qCWarning(DatabaseInterfaceLog) + << "Failed to identify the number of users accounts for users email address: SQL Error"; return 0; } diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 619d36d3a..41e61ddec 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -83,6 +84,10 @@ #include #include +inline Q_LOGGING_CATEGORY(AbstractServerSocketInterfaceLog, "abstract_server_socket_interface"); +inline Q_LOGGING_CATEGORY(TcpServerSocketInterfaceLog, "tcp_server_socket_interface"); +inline Q_LOGGING_CATEGORY(WebsocketServerSocketInterfaceLog, "websocket_server_socket_interface"); + static const int protocolVersion = 14; AbstractServerSocketInterface::AbstractServerSocketInterface(Servatrice *_server, @@ -130,7 +135,7 @@ bool AbstractServerSocketInterface::initSession() void AbstractServerSocketInterface::catchSocketError(QAbstractSocket::SocketError socketError) { - qDebug() << "Socket error:" << socketError; + qCWarning(AbstractServerSocketInterfaceLog) << "Socket error:" << socketError; prepareDestroy(); } @@ -1100,7 +1105,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Com clientIdQuery->bindValue(":client_id", nameFromStdString(cmd.clientid())); sqlInterface->execSqlQuery(clientIdQuery); if (!sqlInterface->execSqlQuery(clientIdQuery)) { - qDebug("ClientID username ban lookup failed: SQL Error"); + qCWarning(AbstractServerSocketInterfaceLog) << "ClientID username ban lookup failed: SQL Error"; } else { while (clientIdQuery->next()) { userName = clientIdQuery->value(0).toString(); @@ -1152,7 +1157,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const C { QString userName = nameFromStdString(cmd.user_name()); QString clientId = nameFromStdString(cmd.clientid()); - qDebug() << "Got register command for user:" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Got register command for user:" << userName; bool registrationEnabled = settingsCache->value("registration/enabled", false).toBool(); if (!registrationEnabled) { @@ -1289,7 +1294,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const C country, !requireEmailActivation); if (regSucceeded) { - qDebug() << "Accepted register command for user:" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Accepted register command for user:" << userName; if (requireEmailActivation) { QSqlQuery *query = sqlInterface->prepareQuery("insert into {prefix}_activation_emails (name) values(:name)"); @@ -1337,7 +1342,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdActivateAccount(const C clientId = "UNKNOWN"; if (sqlInterface->activateUser(userName, token)) { - qDebug() << "Accepted activation for user" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Accepted activation for user" << userName; if (servatrice->getEnableRegistrationAudit()) sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), @@ -1345,7 +1350,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdActivateAccount(const C return Response::RespActivationAccepted; } else { - qDebug() << "Failed activation for user" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Failed activation for user" << userName; if (servatrice->getEnableRegistrationAudit()) sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), @@ -1493,7 +1498,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordRequest(c const QString userName = nameFromStdString(cmd.user_name()); const QString clientId = nameFromStdString(cmd.clientid()); - qDebug() << "Received reset password request from user:" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password request from user:" << userName; if (!servatrice->getEnableForgotPassword()) { if (servatrice->getEnableForgotPasswordAudit()) @@ -1575,7 +1580,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordReset(con Q_UNUSED(rc); QString userName = nameFromStdString(cmd.user_name()); QString clientId = nameFromStdString(cmd.clientid()); - qDebug() << "Received reset password reset from user:" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password reset from user:" << userName; if (!sqlInterface->doesForgotPasswordExist(userName)) { if (servatrice->getEnableForgotPasswordAudit()) @@ -1626,7 +1631,7 @@ AbstractServerSocketInterface::cmdForgotPasswordChallenge(const Command_ForgotPa const QString userName = nameFromStdString(cmd.user_name()); const QString clientId = nameFromStdString(cmd.clientid()); - qDebug() << "Received reset password challenge from user:" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password challenge from user:" << userName; if (!servatrice->getEnableForgotPasswordChallenge()) { if (servatrice->getEnableForgotPasswordAudit()) { @@ -1971,13 +1976,16 @@ void TcpServerSocketInterface::flushOutputQueue() unsigned int size = static_cast(item.ByteSize()); #endif buf.resize(size + 4); - item.SerializeToArray(buf.data() + 4, size); - buf.data()[3] = (unsigned char)size; - buf.data()[2] = (unsigned char)(size >> 8); - buf.data()[1] = (unsigned char)(size >> 16); - buf.data()[0] = (unsigned char)(size >> 24); - // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. - writeToSocket(buf); + if (item.SerializeToArray(buf.data() + 4, size)) { + buf.data()[3] = (unsigned char)size; + buf.data()[2] = (unsigned char)(size >> 8); + buf.data()[1] = (unsigned char)(size >> 16); + buf.data()[0] = (unsigned char)(size >> 24); + // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. + writeToSocket(buf); + } else { + qCWarning(TcpServerSocketInterfaceLog) << "serialisation error!"; + } totalBytes += size + 4; locker.relock(); @@ -2010,41 +2018,48 @@ void TcpServerSocketInterface::readClient() return; CommandContainer newCommandContainer; + bool ok; try { - newCommandContainer.ParseFromArray(inputBuffer.data(), messageLength); + ok = newCommandContainer.ParseFromArray(inputBuffer.data(), messageLength); } catch (std::exception &e) { - qDebug() << "Caught std::exception in" << __FILE__ << __LINE__ << + qCWarning(TcpServerSocketInterfaceLog) << "Caught std::exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio - __FUNCTION__; + __FUNCTION__ #else - __PRETTY_FUNCTION__; + __PRETTY_FUNCTION__ #endif - qDebug() << "Exception:" << e.what(); - qDebug() << "Message coming from:" << getAddress(); - qDebug() << "Message length:" << messageLength; - qDebug() << "Message content:" << inputBuffer.toHex(); + << Qt::endl + << "Exception:" << e.what() << Qt::endl + << "Message coming from:" << getAddress() << Qt::endl + << "Message length:" << messageLength << Qt::endl + << "Message content:" << inputBuffer.toHex(); } catch (...) { - qDebug() << "Unhandled exception in" << __FILE__ << __LINE__ << + qCWarning(TcpServerSocketInterfaceLog) << "Unhandled exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio - __FUNCTION__; + __FUNCTION__ #else - __PRETTY_FUNCTION__; + __PRETTY_FUNCTION__ #endif - qDebug() << "Message coming from:" << getAddress(); + << Qt::endl + << "Message coming from:" << getAddress(); } - inputBuffer.remove(0, messageLength); messageInProgress = false; - // dirty hack to make v13 client display the correct error message - if (handshakeStarted) - processCommandContainer(newCommandContainer); - else if (!newCommandContainer.has_cmd_id()) { - handshakeStarted = true; - if (!initTcpSession()) - prepareDestroy(); + if (ok) { + // dirty hack to make v13 client display the correct error message + if (handshakeStarted) + processCommandContainer(newCommandContainer); + else if (!newCommandContainer.has_cmd_id()) { + handshakeStarted = true; + if (!initTcpSession()) + prepareDestroy(); + } + // end of hack + } else { + qCWarning(TcpServerSocketInterfaceLog) << "parsing error!"; } - // end of hack + } while (!inputBuffer.isEmpty()); } @@ -2172,9 +2187,12 @@ void WebsocketServerSocketInterface::flushOutputQueue() unsigned int size = static_cast(item.ByteSize()); #endif buf.resize(size); - item.SerializeToArray(buf.data(), size); - // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. - writeToSocket(buf); + if (item.SerializeToArray(buf.data(), size)) { + // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. + writeToSocket(buf); + } else { + qCWarning(TcpServerSocketInterfaceLog) << "serialisation error!"; + } totalBytes += size; locker.relock(); @@ -2190,30 +2208,37 @@ void WebsocketServerSocketInterface::binaryMessageReceived(const QByteArray &mes servatrice->incRxBytes(message.size()); CommandContainer newCommandContainer; + bool ok; try { - newCommandContainer.ParseFromArray(message.data(), message.size()); + ok = newCommandContainer.ParseFromArray(message.data(), message.size()); } catch (std::exception &e) { - qDebug() << "Caught std::exception in" << __FILE__ << __LINE__ << + qCWarning(WebsocketServerSocketInterfaceLog) << "Caught std::exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio - __FUNCTION__; + __FUNCTION__ #else - __PRETTY_FUNCTION__; + __PRETTY_FUNCTION__ #endif - qDebug() << "Exception:" << e.what(); - qDebug() << "Message coming from:" << getAddress(); - qDebug() << "Message length:" << message.size(); - qDebug() << "Message content:" << message.toHex(); + << Qt::endl + << "Exception:" << e.what() << Qt::endl + << "Message coming from:" << getAddress() << Qt::endl + << "Message length:" << message.size() << Qt::endl + << "Message content:" << message.toHex(); } catch (...) { - qDebug() << "Unhandled exception in" << __FILE__ << __LINE__ << + qCWarning(WebsocketServerSocketInterfaceLog) << "Unhandled exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio - __FUNCTION__; + __FUNCTION__ #else - __PRETTY_FUNCTION__; + __PRETTY_FUNCTION__ #endif - qDebug() << "Message coming from:" << getAddress(); + << Qt::endl + << "Message coming from:" << getAddress(); } - processCommandContainer(newCommandContainer); + if (ok) { + processCommandContainer(newCommandContainer); + } else { + qCWarning(WebsocketServerSocketInterfaceLog) << "parsing error!"; + } } bool AbstractServerSocketInterface::isPasswordLongEnough(const int passwordLength) From dbed6890dac7b308ef228362e63cb67413c14fcd Mon Sep 17 00:00:00 2001 From: Bruno Alexandre Rosa <1791393+brunoalr@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:36:45 -0300 Subject: [PATCH 082/116] feat: support expressions when setting card counters (#6753) * feat: enable expressions in card counters * fix includes * fix multiple selection * cleanup useless conversions * const ref where possible * do not use const, be consistent with local patterns in the file --- cockatrice/src/game/player/player_actions.cpp | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index ca0967636..a44c28d40 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -3,6 +3,7 @@ #include "../../interface/widgets/tabs/tab_game.h" #include "../../interface/widgets/utility/get_text_with_max.h" #include "../board/card_item.h" +#include "../client/settings/card_counter_settings.h" #include "../dialogs/dlg_move_top_cards_until.h" #include "../dialogs/dlg_roll_dice.h" #include "../zones/hand_zone.h" @@ -27,6 +28,7 @@ #include #include #include +#include #include #include @@ -1572,23 +1574,34 @@ void PlayerActions::actCardCounterTrigger() break; } case 11: { // set counter with dialog - bool ok; player->setDialogSemaphore(true); - int oldValue = 0; - if (player->getGameScene()->selectedItems().size() == 1) { - auto *card = static_cast(player->getGameScene()->selectedItems().first()); - oldValue = card->getCounters().value(counterId, 0); + // If a single card is selected, we show the old value in the dialog. Otherwise, we show "x" + QList sel = player->getGameScene()->selectedItems(); + QString oldValueForDlg = "x"; + if (sel.size() == 1) { + auto *card = dynamic_cast(sel.first()); + oldValueForDlg = QString::number(card->getCounters().value(counterId, 0)); } - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Set counters"), tr("Number:"), oldValue, - 0, MAX_COUNTERS_ON_CARD, 1, &ok); + + auto &cardCounterSettings = SettingsCache::instance().cardCounters(); + QString counterName = cardCounterSettings.displayName(counterId); + + AbstractCounterDialog dialog(counterName, oldValueForDlg, player->getGame()->getTab()); + int ok = dialog.exec(); + player->setDialogSemaphore(false); if (player->clearCardsToDelete() || !ok) { return; } - for (const auto &item : player->getGameScene()->selectedItems()) { - auto *card = static_cast(item); + for (const auto &item : sel) { + auto *card = dynamic_cast(item); + + int oldValue = card->getCounters().value(counterId, 0); + Expression exp(oldValue); + int number = static_cast(exp.parse(dialog.textValue())); + auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); From 335022c4aa3ebda39eb0f42fbfc3728cc2b831aa Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:02:00 +0200 Subject: [PATCH 083/116] Include themes (#6781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Properly reset default theme. Took 9 minutes * Descend in preference on windows. Took 9 minutes * Also reset style for custom themes. Took 3 minutes * Try things Took 9 minutes * Add modern windows style. Took 8 minutes * Update theme_manager.cpp --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 1ca3c77c2..12733afe6 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -564,6 +564,9 @@ if(WIN32) PATTERN "styles/qopensslbackend.dll" PATTERN "styles/qschannelbackend.dll" PATTERN "styles/qwindowsvistastyle.dll" + PATTERN "styles/qwindows11style.dll" + PATTERN "styles/qmodernwindowsstyle.dll" + PATTERN "styles/qmodernwindowsstyled.dll" PATTERN "tls/qcertonlybackend.dll" PATTERN "tls/qopensslbackend.dll" PATTERN "tls/qschannelbackend.dll" From 635fae101d2d5c97a52d774b90ffa6ffebd6f244 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:57:03 +0200 Subject: [PATCH 084/116] Use palette colors for resizable panel stylesheets. (#6782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Took 7 minutes Took 5 seconds Co-authored-by: Lukas Brübach --- .../deck_analytics/resizable_panel.cpp | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp index a0c971a75..f7bb5ac35 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp @@ -20,7 +20,6 @@ ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWid frame = new QFrame(this); frame->setFrameShape(QFrame::Box); frame->setLineWidth(2); - frame->setStyleSheet("border: none;"); auto *frameLayout = new QVBoxLayout(frame); frameLayout->setContentsMargins(0, 0, 0, 0); @@ -30,15 +29,13 @@ ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWid frameLayout->addWidget(analyticsPanel); dropIndicator = new QFrame(frame); - dropIndicator->setStyleSheet("background-color: #3daee9;"); dropIndicator->setFixedHeight(3); dropIndicator->hide(); // hidden by default dropIndicator->raise(); // make sure it's above children selectionOverlay = new QFrame(frame); - selectionOverlay->setStyleSheet("background-color: rgba(61,174,233,50);"); // semi-transparent blue - selectionOverlay->hide(); // hidden by default - selectionOverlay->raise(); // make sure it is above children + selectionOverlay->hide(); // hidden by default + selectionOverlay->raise(); // make sure it is above children selectionOverlay->setAttribute(Qt::WA_TransparentForMouseEvents); // Bottom bar with drag button and resize handle @@ -51,24 +48,41 @@ ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWid dragButton = new QPushButton("☰", bottomBar); dragButton->setFixedSize(40, 8); dragButton->setCursor(Qt::OpenHandCursor); - dragButton->setStyleSheet("QPushButton { " - "background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4a4a4a, stop:1 #3a3a3a); " - "border: none; color: #888; font-size: 10px; }" - "QPushButton:hover { background: #5a5a5a; }"); bottomLayout->addWidget(dragButton); // Resize handle fills the rest resizeHandle = new QWidget(bottomBar); resizeHandle->setFixedHeight(8); resizeHandle->setCursor(Qt::SizeVerCursor); - resizeHandle->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:0, y2:1, " - "stop:0 #3a3a3a, stop:1 #2a2a2a);"); bottomLayout->addWidget(resizeHandle, 1); frameLayout->addWidget(bottomBar); mainLayout->addWidget(frame); + const QPalette &pal = QApplication::palette(); + QColor mid = pal.color(QPalette::Mid); + QColor dark = pal.color(QPalette::Dark); + QColor midLight = pal.color(QPalette::Midlight); + QColor shadow = pal.color(QPalette::Shadow); + QColor placeholderText = pal.color(QPalette::PlaceholderText); + + frame->setStyleSheet("QFrame { border: none; }"); + + dropIndicator->setStyleSheet("QFrame { background-color: #3daee9; }"); + + selectionOverlay->setStyleSheet("QFrame { background-color: rgba(61,174,233,50); }"); + + dragButton->setStyleSheet(QString("QPushButton { " + "background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 %1, stop:1 %2); " + "border: none; color: %3; font-size: 10px; }" + "QPushButton:hover { background: %4; }") + .arg(mid.name(), dark.name(), placeholderText.name(), midLight.name())); + + resizeHandle->setStyleSheet(QString("QWidget { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, " + "stop:0 %1, stop:1 %2); }") + .arg(dark.name(), shadow.name())); + // Set size policy setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); From d7b31f2f9d87ba8e9cfa563cd95229a922db1567 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:58:42 +0200 Subject: [PATCH 085/116] Set OracleWizard style to "Modern" instead of "Aero" (#6778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use fusions own palette. Took 6 minutes * Start from default palette always. Took 4 minutes * Add modern style. Took 24 seconds * Scope this fix to Windows. Took 4 minutes --------- Co-authored-by: Lukas Brübach --- cockatrice/src/interface/theme_manager.cpp | 14 ++++++++------ oracle/src/oraclewizard.cpp | 4 ++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 1dc61fdb7..5809ce350 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -179,7 +179,7 @@ QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush) static inline QPalette createDarkGreenFusionPalette() { - QPalette p; + QPalette p = QStyleFactory::create("Fusion")->standardPalette(); // ---------- Core backgrounds ---------- p.setColor(QPalette::Window, QColor(30, 30, 30)); // #ff1e1e1e @@ -248,7 +248,7 @@ static inline QPalette createDarkGreenFusionPalette() static inline QPalette createLightGreenFusionPalette() { - QPalette p; + QPalette p = QStyleFactory::create("Fusion")->standardPalette(); // ---------- Core backgrounds ---------- p.setColor(QPalette::Window, QColor(240, 240, 240)); // #fff0f0f0 @@ -332,13 +332,15 @@ void ThemeManager::themeChangedSlot() } if (themeName == FUSION_THEME_NAME) { - qApp->setStyle(QStyleFactory::create("Fusion")); + QStyle *fusionStyle = QStyleFactory::create("Fusion"); + qApp->setStyle(fusionStyle); #if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)) - QPalette palette; + // Start from Fusion's own palette so dark mode is handled correctly, + // then apply any tweaks on top of it. + QPalette palette = fusionStyle->standardPalette(); if (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark) { palette.setColor(QPalette::AlternateBase, QColor(53, 53, 53)); } - qApp->setPalette(palette); #endif } else if (themeName == FUSION_THEME_NAME_LIGHT) { @@ -348,7 +350,7 @@ void ThemeManager::themeChangedSlot() qApp->setStyle(QStyleFactory::create("Fusion")); qApp->setPalette(createDarkGreenFusionPalette()); } else { - qApp->setStyle(defaultStyleName); // setting the style also sets the palette + qApp->setStyle(QStyleFactory::create(defaultStyleName)); // setting the style also sets the palette } if (dirPath.isEmpty()) { diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index 9d32a993b..2edb9e561 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -21,6 +21,10 @@ OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent) // define a dummy context that will be used where needed QString dummy = QT_TRANSLATE_NOOP("i18n", "English"); +#ifdef Q_OS_WIN + setWizardStyle(QWizard::ModernStyle); +#endif + QString oracleSettingsFile = SettingsCache::instance().getSettingsPath() + "oracle.ini"; settings = new QSettings(oracleSettingsFile, QSettings::IniFormat, this); From ac06fb9d1c945a311c1824bdaafc1ee99525ed9a Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:02:00 -0700 Subject: [PATCH 086/116] [Game] Fix crash by properly parenting QObjects (#6788) --- cockatrice/src/game/abstract_game.cpp | 3 ++- cockatrice/src/game/player/menu/player_menu.cpp | 2 +- cockatrice/src/game/player/menu/player_menu.h | 2 +- cockatrice/src/game/player/player_actions.cpp | 3 ++- cockatrice/src/game/player/player_event_handler.cpp | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cockatrice/src/game/abstract_game.cpp b/cockatrice/src/game/abstract_game.cpp index 505cd5fde..9216f9174 100644 --- a/cockatrice/src/game/abstract_game.cpp +++ b/cockatrice/src/game/abstract_game.cpp @@ -1,8 +1,9 @@ #include "abstract_game.h" +#include "../interface/widgets/tabs/tab_game.h" #include "player/player.h" -AbstractGame::AbstractGame(TabGame *_tab) : tab(_tab) +AbstractGame::AbstractGame(TabGame *_tab) : QObject(_tab), tab(_tab) { gameMetaInfo = new GameMetaInfo(this); gameEventHandler = new GameEventHandler(this); diff --git a/cockatrice/src/game/player/menu/player_menu.cpp b/cockatrice/src/game/player/menu/player_menu.cpp index 7786ec3fc..0dc381e28 100644 --- a/cockatrice/src/game/player/menu/player_menu.cpp +++ b/cockatrice/src/game/player/menu/player_menu.cpp @@ -10,7 +10,7 @@ #include -PlayerMenu::PlayerMenu(Player *_player) : player(_player) +PlayerMenu::PlayerMenu(Player *_player) : QObject(_player), player(_player) { playerMenu = new TearOffMenu(); diff --git a/cockatrice/src/game/player/menu/player_menu.h b/cockatrice/src/game/player/menu/player_menu.h index 5fce27158..104c5a930 100644 --- a/cockatrice/src/game/player/menu/player_menu.h +++ b/cockatrice/src/game/player/menu/player_menu.h @@ -37,7 +37,7 @@ private slots: void refreshShortcuts(); public: - PlayerMenu(Player *player); + explicit PlayerMenu(Player *player); /// Lifecycle methods: delegate to all managedComponents, plus counters separately via player->getCounters(). void retranslateUi(); diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index a44c28d40..20034df16 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -35,7 +35,8 @@ // milliseconds in between triggers of the move top cards until action static constexpr int MOVE_TOP_CARD_UNTIL_INTERVAL = 100; -PlayerActions::PlayerActions(Player *_player) : player(_player), lastTokenTableRow(0), movingCardsUntil(false) +PlayerActions::PlayerActions(Player *_player) + : QObject(_player), player(_player), lastTokenTableRow(0), movingCardsUntil(false) { moveTopCardTimer = new QTimer(this); moveTopCardTimer->setInterval(MOVE_TOP_CARD_UNTIL_INTERVAL); diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp index f4c3840e0..5571010d1 100644 --- a/cockatrice/src/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -32,7 +32,7 @@ #include #include -PlayerEventHandler::PlayerEventHandler(Player *_player) : player(_player) +PlayerEventHandler::PlayerEventHandler(Player *_player) : QObject(_player), player(_player) { } From 2d412bfe5266bb8c5ae00ff11b5baf1718bd0767 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Thu, 9 Apr 2026 21:16:38 +0200 Subject: [PATCH 087/116] fix cardloading on qt5 (#6790) --- .../src/interface/card_picture_loader/card_picture_loader.cpp | 1 + libcockatrice_card/libcockatrice/card/printing/exact_card.h | 1 + 2 files changed, 2 insertions(+) diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp index 06a0476c9..dbd51b973 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -28,6 +28,7 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr) connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this, &CardPictureLoader::picDownloadChanged); + qRegisterMetaType(); connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded); statusBar = new CardPictureLoaderStatusBar(nullptr); diff --git a/libcockatrice_card/libcockatrice/card/printing/exact_card.h b/libcockatrice_card/libcockatrice/card/printing/exact_card.h index 01c61a3a1..74fc75da3 100644 --- a/libcockatrice_card/libcockatrice/card/printing/exact_card.h +++ b/libcockatrice_card/libcockatrice/card/printing/exact_card.h @@ -114,5 +114,6 @@ public: */ void emitPixmapUpdated() const; }; +Q_DECLARE_METATYPE(ExactCard) #endif // EXACT_CARD_H From 9aa5702e14bcf17f72faa5e24d3eb16ee4b83bec Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:27:14 +0200 Subject: [PATCH 088/116] Translate cockatrice_en@source.ts in en_US (#6783) 100% translated source file: 'cockatrice_en@source.ts' on 'en_US'. Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com> --- cockatrice/translations/cockatrice_en_US.ts | 2435 +++++++++++-------- 1 file changed, 1356 insertions(+), 1079 deletions(-) diff --git a/cockatrice/translations/cockatrice_en_US.ts b/cockatrice/translations/cockatrice_en_US.ts index c74e63114..254ead590 100644 --- a/cockatrice/translations/cockatrice_en_US.ts +++ b/cockatrice/translations/cockatrice_en_US.ts @@ -23,12 +23,12 @@ AbstractDlgDeckTextEdit - + &Refresh &Refresh - + Parse Set Name and Number (if available) Parse Set Name and Number (if available) @@ -36,62 +36,62 @@ AbstractTabDeckEditor - + Open in new tab Open in new tab - + Are you sure? Are you sure? - + The decklist has been modified. Do you want to save the changes? The decklist has been modified. Do you want to save the changes? - - - - - - + + + + + + Error Error - + Could not open deck at %1 Could not open deck at %1 - + Could not save remote deck Could not save remote deck - - + + The deck could not be saved. Please check that the directory is writable and try again. The deck could not be saved. Please check that the directory is writable and try again. - + Save deck Save deck - + The deck could not be saved. The deck could not be saved. - + There are no cards in your deck to be exported There are no cards in your deck to be exported @@ -133,202 +133,168 @@ Please check that the directory is writable and try again. AppearanceSettingsPage - + seconds seconds - + Error Error - + Could not create themes directory at '%1'. Could not create themes directory at '%1'. - - Enabling this feature will disable the use of the Printing Selector. - -You will not be able to manage printing preferences on a per-deck basis, or see printings other people have selected for their decks. - -You will have to use the Set Manager, available through Card Database -> Manage Sets. - -Are you sure you would like to enable this feature? - Enabling this feature will disable the use of the Printing Selector. - -You will not be able to manage printing preferences on a per-deck basis, or see printings other people have selected for their decks. - -You will have to use the Set Manager, available through Card Database -> Manage Sets. - -Are you sure you would like to enable this feature? - - - - Disabling this feature will enable the Printing Selector. - -You can now choose printings on a per-deck basis in the Deck Editor and configure which printing gets added to a deck by default by pinning it in the Printing Selector. - -You can also use the Set Manager to adjust custom sort order for printings in the Printing Selector (other sort orders like alphabetical or release date are available). - -Are you sure you would like to disable this feature? - Disabling this feature will enable the Printing Selector. - -You can now choose printings on a per-deck basis in the Deck Editor and configure which printing gets added to a deck by default by pinning it in the Printing Selector. - -You can also use the Set Manager to adjust custom sort order for printings in the Printing Selector (other sort orders like alphabetical or release date are available). - -Are you sure you would like to disable this feature? - - - - Confirm Change - Confirm Change - - - + Theme settings Theme settings - + Current theme: Current theme: - + Open themes folder Open themes folder - + Home tab background source: Home tab background source: - + Home tab background shuffle frequency: Home tab background shuffle frequency: - + Disabled Disabled - + + Display card name of background in bottom right: + Display card name of background in bottom right: + + + Menu settings Menu settings - + Show keyboard shortcuts in right-click menus Show keyboard shortcuts in right-click menus - + Show game filter toolbar above list in room tab Show game filter toolbar above list in room tab - + Card rendering Card rendering - + Display card names on cards having a picture Display card names on cards having a picture - + Auto-Rotate cards with sideways layout Auto-Rotate cards with sideways layout - + Override all card art with personal set preference (Pre-ProviderID change behavior) Override all card art with personal set preference (Pre-ProviderID change behavior) - + Bump sets that the deck contains cards from to the top in the printing selector Bump sets that the deck contains cards from to the top in the printing selector - + Scale cards on mouse over Scale cards on mouse over - + Use rounded card corners Use rounded card corners - + Minimum overlap percentage of cards on the stack and in vertical hand Minimum overlap percentage of cards on the stack and in vertical hand - + Maximum initial height for card view window: Maximum initial height for card view window: - - + + rows rows - + Maximum expanded height for card view window: Maximum expanded height for card view window: - + Card counters Card counters - + Counter %1 Counter %1 - + Hand layout Hand layout - + Display hand horizontally (wastes space) Display hand horizontally (wastes space) - + Enable left justification Enable left justification - + Table grid layout Table grid layout - + Invert vertical coordinate Invert vertical coordinate - + Minimum player count for multi-column layout: Minimum player count for multi-column layout: - + Maximum font size for information displayed on cards: Maximum font size for information displayed on cards: @@ -336,7 +302,12 @@ Are you sure you would like to disable this feature? ArchidektApiResponseDeckDisplayWidget - + + Back to results + Back to results + + + Open Deck in Deck Editor Open Deck in Deck Editor @@ -656,22 +627,22 @@ This is only saved for moderators and cannot be seen by the banned person. CardInfoPictureWidget - + View related cards View related cards - + Add card to deck Add card to deck - + Mainboard Mainboard - + Sideboard Sideboard @@ -707,124 +678,124 @@ This is only saved for moderators and cannot be seen by the banned person. CardMenu - + Re&veal to... Re&veal to... - + &All players &All players - + View related cards View related cards - + Token: Token: - + All tokens All tokens - + &Select All &Select All - + S&elect Row S&elect Row - + S&elect Column S&elect Column - + &Play &Play - + &Hide &Hide - + Play &Face Down Play &Face Down - + &Tap / Untap Turn sideways or back again &Tap / Untap - - Toggle &normal untapping - Toggle &normal untapping + + Skip &untapping + Skip &untapping - + T&urn Over Turn face up/face down T&urn Over - + &Peek at card face &Peek at card face - + &Clone &Clone - + Attac&h to card... Attac&h to card... - + Unattac&h Unattac&h - + &Draw arrow... &Draw arrow... - + &Set annotation... &Set annotation... - + Ca&rd counters Ca&rd counters - + &Add counter (%1) &Add counter (%1) - + &Remove counter (%1) &Remove counter (%1) - + &Set counters (%1)... &Set counters (%1)... @@ -840,133 +811,133 @@ This is only saved for moderators and cannot be seen by the banned person. CardZoneLogic - + their hand nominative their hand - + %1's hand nominative %1's hand - + their library look at zone their library - + %1's library look at zone %1's library - + of their library top cards of zone, of their library - + of %1's library top cards of zone of %1's library - + their library reveal zone their library - + %1's library reveal zone %1's library - + their library shuffle their library - + %1's library shuffle %1's library - - - their library - nominative - their library - - - - %1's library - nominative - %1's library - + their library + nominative + their library + + + + %1's library + nominative + %1's library + + + their graveyard nominative their graveyard - + %1's graveyard nominative %1's graveyard - + their exile nominative their exile - + %1's exile nominative %1's exile - - - their sideboard - look at zone - their sideboard - - - - %1's sideboard - look at zone - %1's sideboard - their sideboard - nominative + look at zone their sideboard %1's sideboard + look at zone + %1's sideboard + + + + their sideboard + nominative + their sideboard + + + + %1's sideboard nominative %1's sideboard - + their custom zone '%1' nominative their custom zone '%1' - + %1's custom zone '%2' nominative %1's custom zone '%2' @@ -1028,7 +999,7 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorCardDatabaseDockWidget - + Card Database Card Database @@ -1036,7 +1007,7 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorCardInfoDockWidget - + Card Info Card Info @@ -1059,32 +1030,32 @@ This is only saved for moderators and cannot be seen by the banned person.Add to Sideboard - + Select Printing Select Printing - + Show on EDHRec (Commander) Show on EDHRec (Commander) - + Show on EDHRec (Card) Show on EDHRec (Card) - + Show Related cards Show Related cards - + Add card to &maindeck Add card to &maindeck - + Add card to &sideboard Add card to &sideboard @@ -1092,32 +1063,32 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorDeckDockWidget - + Loading Database... Loading Database... - + Banner Card Banner Card - + Main Type Main Type - + Mana Cost Mana Cost - + Colors Colors - + Select Printing Select Printing @@ -1190,17 +1161,17 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorFilterDockWidget - + Filters Filters - + &Clear all filters &Clear all filters - + Delete selected Delete selected @@ -1323,7 +1294,7 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorPrintingSelectorDockWidget - + Printing Selector Printing Selector @@ -1331,166 +1302,166 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorSettingsPage - - + + Update Spoilers Update Spoilers - - + + Success Success - + Download URLs have been reset. Download URLs have been reset. - + Downloaded card pictures have been reset. Downloaded card pictures have been reset. - + Error Error - + One or more downloaded card pictures could not be cleared. One or more downloaded card pictures could not be cleared. - + Add URL Add URL - - + + URL: URL: - - + + Edit URL Edit URL - + Network Cache Size: Network Cache Size: - + Redirect Cache TTL: Redirect Cache TTL: - + How long cached redirects for urls are valid for. How long cached redirects for urls are valid for. - + Picture Cache Size: Picture Cache Size: - + Add New URL Add New URL - + Remove URL Remove URL - + Day(s) Day(s) - + Updating... Updating... - + Choose path Choose path - + URL Download Priority URL Download Priority - + Spoilers Spoilers - + Download Spoilers Automatically Download Spoilers Automatically - + Spoiler Location: Spoiler Location: - + Last Change Last Change - + Spoilers download automatically on launch Spoilers download automatically on launch - + Press the button to manually update without relaunching Press the button to manually update without relaunching - + Do not close settings until manual update is complete Do not close settings until manual update is complete - + Download card pictures on the fly Download card pictures on the fly - + How to add a custom URL How to add a custom URL - + Delete Downloaded Images Delete Downloaded Images - + Reset Download URLs Reset Download URLs - + On-disk cache for downloaded pictures On-disk cache for downloaded pictures - + In-memory cache for pictures not currently on screen In-memory cache for pictures not currently on screen @@ -1498,32 +1469,32 @@ This is only saved for moderators and cannot be seen by the banned person. DeckListHistoryManagerWidget - + Undo Undo - + Redo Redo - + Undo/Redo history Undo/Redo history - + Click on an entry to revert to that point in the history. Click on an entry to revert to that point in the history. - + [redo] [redo] - + [undo] [undo] @@ -1531,27 +1502,27 @@ This is only saved for moderators and cannot be seen by the banned person. DeckListModel - + Count Count - + Set Set - + Number Number - + Provider ID Provider ID - + Card Card @@ -1559,12 +1530,12 @@ This is only saved for moderators and cannot be seen by the banned person. DeckLoader - + Common deck formats (%1) Common deck formats (%1) - + All files (*.*) All files (*.*) @@ -1661,94 +1632,94 @@ This is only saved for moderators and cannot be seen by the banned person. DeckPreviewWidget - + Banner Card Banner Card - + Open in deck editor Open in deck editor - + Edit Tags Edit Tags - + Rename Deck Rename Deck - + Save Deck to Clipboard Save Deck to Clipboard - + Annotated Annotated - + Annotated (No set info) Annotated (No set info) - + Not Annotated Not Annotated - + Not Annotated (No set info) Not Annotated (No set info) - + Rename File Rename File - + Delete File Delete File - + Set Banner Card Set Banner Card - - + + New name: New name: - - + + Error Error - + Rename failed Rename failed - + Delete file Delete file - + Are you sure you want to delete the selected file? Are you sure you want to delete the selected file? - + Delete failed Delete failed @@ -1781,32 +1752,32 @@ This is only saved for moderators and cannot be seen by the banned person.Set format to %1 - + Added (%1): %2 (%3) %4 Added (%1): %2 (%3) %4 - + Moved to %1 1 × "%2" (%3) Moved to %1 1 × "%2" (%3) - + Removed "%1" (all copies) Removed "%1" (all copies) - + %1 1 × "%2" (%3) %1 1 × "%2" (%3) - + Added Added - + Removed Removed @@ -1873,30 +1844,30 @@ This is only saved for moderators and cannot be seen by the banned person.Sideboard locked - - + + Error Error - + The selected file could not be loaded. The selected file could not be loaded. - + Deck is greater than maximum file size. Deck is greater than maximum file size. - + Are you sure you want to force start? This will kick all non-ready players from the game. Are you sure you want to force start? This will kick all non-ready players from the game. - + Cockatrice Cockatrice @@ -2391,17 +2362,17 @@ To remove your current avatar, confirm without choosing a new image. DlgEditDeckInClipboard - + Edit deck in clipboard Edit deck in clipboard - + Error Error - + Invalid deck list. Invalid deck list. @@ -2902,17 +2873,17 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgLoadDeckFromClipboard - + Load deck from clipboard Load deck from clipboard - + Error Error - + Invalid deck list. Invalid deck list. @@ -2920,45 +2891,45 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgLoadDeckFromWebsite - + Paste a link to a decklist site here to import it. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) Paste a link to a decklist site here to import it. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) - - - - - + + + + + Load Deck from Website Load Deck from Website - + No parser available for this deck provider. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) No parser available for this deck provider. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) - + Network error: %1 Network error: %1 - + Received empty deck data. Received empty deck data. - + Failed to parse deck data: %1 Failed to parse deck data: %1 - + The provided URL is not recognized as a valid deck URL. Valid deck URLs look like this: @@ -2983,40 +2954,73 @@ https://tappedout.net/mtg-decks/your-deck-name/ Load deck + + DlgLocalGameOptions + + + Players: + Players: + + + + General + General + + + + Starting life total: + Starting life total: + + + + Game setup options + Game setup options + + + + Remember settings + Remember settings + + + + Local game options + Local game options + + DlgMoveTopCardsUntil - + Card name (or search expressions): Card name (or search expressions): - + Number of hits: Number of hits: - + Auto play hits Auto play hits - + Put top cards on stack until... Put top cards on stack until... - + No cards matching the search expression exists in the card database. Proceed anyways? No cards matching the search expression exists in the card database. Proceed anyways? - + Cockatrice Cockatrice - + Invalid filter Invalid filter @@ -3178,12 +3182,12 @@ Your email will be used to verify your account. DlgSettings - + Unknown Error loading card database Unknown Error loading card database - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -3200,7 +3204,7 @@ You may need to rerun oracle to update your card database. Would you like to change your database location setting? - + Your card database version is too old. This can cause problems loading card information or images @@ -3217,7 +3221,7 @@ Usually this can be fixed by rerunning oracle to to update your card database. Would you like to change your database location setting? - + Your card database did not finish loading Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -3230,7 +3234,7 @@ Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues with you Would you like to change your database location setting? - + File Error loading your card database. Would you like to change your database location setting? @@ -3239,7 +3243,7 @@ Would you like to change your database location setting? Would you like to change your database location setting? - + Your card database was loaded but contains no cards. Would you like to change your database location setting? @@ -3248,7 +3252,7 @@ Would you like to change your database location setting? Would you like to change your database location setting? - + Unknown card database load status Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues @@ -3261,59 +3265,59 @@ Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues Would you like to change your database location setting? - - - + + + Error Error - + The path to your deck directory is invalid. Would you like to go back and set the correct path? The path to your deck directory is invalid. Would you like to go back and set the correct path? - + The path to your card pictures directory is invalid. Would you like to go back and set the correct path? The path to your card pictures directory is invalid. Would you like to go back and set the correct path? - + Settings Settings - + General General - + Appearance Appearance - + User Interface User Interface - + Card Sources Card Sources - + Chat Chat - + Sound Sound - + Shortcuts Shortcuts @@ -3630,67 +3634,67 @@ You may have to manually download the new version. DrawProbabilityWidget - + Draw Probability Draw Probability - + Probability of drawing Probability of drawing - + Card Name Card Name - + Type Type - + Subtype Subtype - + Mana Value Mana Value - + At least At least - + Exactly Exactly - + card(s) having drawn at least card(s) having drawn at least - + cards cards - + Category Category - + Qty Qty - + Odds (%) Odds (%) @@ -4138,143 +4142,143 @@ You may have to manually download the new version. GeneralSettingsPage - + Reset all paths Reset all paths - + All paths have been reset All paths have been reset - - - - - - - + + + + + + + Choose path Choose path - + Personal settings Personal settings - + Language: Language: - + Paths (editing disabled in portable mode) Paths (editing disabled in portable mode) - + Paths Paths - + How to help with translations How to help with translations - + Decks directory: Decks directory: - + Filters directory: Filters directory: - + Replays directory: Replays directory: - + Pictures directory: Pictures directory: - + Card database: Card database: - + Custom database directory: Custom database directory: - + Token database: Token database: - + Update channel Update channel - + Check for client updates on startup Check for client updates on startup - + Check for card database updates on startup Check for card database updates on startup - + Don't check Don't check - + Prompt for update Prompt for update - + Always update in the background Always update in the background - + Check for card database updates every Check for card database updates every - + days days - + Notify if a feature supported by the server is missing in my client Notify if a feature supported by the server is missing in my client - + Automatically run Oracle when running a new version of Cockatrice Automatically run Oracle when running a new version of Cockatrice - + Show tips on startup Show tips on startup - + Last update check on %1 (%2 days ago) Last update check on %1 (%2 days ago) @@ -4282,47 +4286,47 @@ You may have to manually download the new version. GraveyardMenu - + &Graveyard &Graveyard - + &View graveyard &View graveyard - + &Move graveyard to... &Move graveyard to... - + &Top of library &Top of library - + &Bottom of library &Bottom of library - + &All players &All players - + &Hand &Hand - + &Exile &Exile - + Reveal random card to... Reveal random card to... @@ -4330,88 +4334,88 @@ You may have to manually download the new version. HandMenu - + &Hand &Hand - + &View hand &View hand - + Sort hand by... Sort hand by... - + Name Name - + Type Type - + Mana Value Mana Value - + Take &mulligan (Choose hand size) Take &mulligan (Choose hand size) - + Take mulligan (Same hand size) Take mulligan (Same hand size) - + Take mulligan (Hand size - 1) Take mulligan (Hand size - 1) - + &Move hand to... &Move hand to... - + &Top of library &Top of library - + &Bottom of library &Bottom of library - + &Graveyard &Graveyard - + &Exile &Exile - + &Reveal hand to... &Reveal hand to... - - + + All players All players - + Reveal r&andom card to... Reveal r&andom card to... @@ -4419,52 +4423,52 @@ You may have to manually download the new version. HomeWidget - + Create New Deck Create New Deck - + Browse Decks Browse Decks - + Browse Card Database Browse Card Database - + Browse EDHRec Browse EDHRec - + Browse Archidekt Browse Archidekt - + View Replays View Replays - + Quit Quit - + Connecting... Connecting... - + Connect Connect - + Play Play @@ -4472,193 +4476,213 @@ You may have to manually download the new version. LibraryMenu - + &Library &Library - + &View library &View library - + View &top cards of library... View &top cards of library... - + View bottom cards of library... View bottom cards of library... - + Reveal &library to... Reveal &library to... - + Lend library to... Lend library to... - + Reveal &top cards to... Reveal &top cards to... - + &Top of library... &Top of library... - + &Bottom of library... &Bottom of library... - + &Always reveal top card &Always reveal top card - + &Always look at top card &Always look at top card - + &Open deck in deck editor &Open deck in deck editor - + &Draw card &Draw card - + D&raw cards... D&raw cards... - + &Undo last draw &Undo last draw - + Shuffle Shuffle - + &Play top card &Play top card - + Play top card &face down Play top card &face down - + Put top card on &bottom Put top card on &bottom - + Move top card to grave&yard Move top card to grave&yard - + Move top card to e&xile Move top card to e&xile - + Move top cards to &graveyard... Move top cards to &graveyard... - + + Move top cards to graveyard face down... + Move top cards to graveyard face down... + + + Move top cards to &exile... Move top cards to &exile... - + + Move top cards to exile face down... + Move top cards to exile face down... + + + Put top cards on stack &until... Put top cards on stack &until... - + Shuffle top cards... Shuffle top cards... - + &Draw bottom card &Draw bottom card - + D&raw bottom cards... D&raw bottom cards... - + &Play bottom card &Play bottom card - + Play bottom card &face down Play bottom card &face down - + Move bottom card to grave&yard Move bottom card to grave&yard - + Move bottom card to e&xile Move bottom card to e&xile - + Move bottom cards to &graveyard... Move bottom cards to &graveyard... - + + Move bottom cards to graveyard face down... + Move bottom cards to graveyard face down... + + + Move bottom cards to &exile... Move bottom cards to &exile... - + + Move bottom cards to exile face down... + Move bottom cards to exile face down... + + + Put bottom card on &top Put bottom card on &top - + Shuffle bottom cards... Shuffle bottom cards... - - + + &All players &All players - + Reveal top cards of library Reveal top cards of library - + Number of cards: (max. %1) Number of cards: (max. %1) @@ -4756,18 +4780,8 @@ Will now login. Will now login. - - Number of players - Number of players - - - - Please enter the number of players. - Please enter the number of players. - - - - + + Player %1 Player %1 @@ -4870,8 +4884,8 @@ Will now login. - - + + Error Error @@ -5279,36 +5293,36 @@ Local version is %1, remote version is %2. Show/Hide - + New Version New Version - + Congratulations on updating to Cockatrice %1! Oracle will now launch to update your card database. Congratulations on updating to Cockatrice %1! Oracle will now launch to update your card database. - + Cockatrice installed Cockatrice installed - + Congratulations on installing Cockatrice %1! Oracle will now launch to install the initial card database. Congratulations on installing Cockatrice %1! Oracle will now launch to install the initial card database. - + Card database Card database - + Cockatrice is unable to load the card database. Do you want to update your card database now? If unsure or first time user, choose "Yes" @@ -5317,29 +5331,29 @@ Do you want to update your card database now? If unsure or first time user, choose "Yes" - - + + Yes Yes - - + + No No - + Open settings Open settings - + New sets found New sets found - + %n new set(s) found in the card database Set code(s): %1 Do you want to enable it/them? @@ -5350,17 +5364,17 @@ Set codes: %1 Do you want to enable them? - + View sets View sets - + Welcome Welcome - + Hi! It seems like you're running this version of Cockatrice for the first time. All the sets in the card database have been enabled. Read more about changing the set order or disabling specific sets and consequent effects in the "Manage Sets" dialog. @@ -5369,65 +5383,65 @@ All the sets in the card database have been enabled. Read more about changing the set order or disabling specific sets and consequent effects in the "Manage Sets" dialog. - - + + Information Information - + A card database update is already running. A card database update is already running. - + Unable to run the card database updater: Unable to run the card database updater: - + Card database update running. Card database update running. - + Failed to start. The file might be missing, or permissions might be incorrect. Failed to start. The file might be missing, or permissions might be incorrect. - + The process crashed some time after starting successfully. The process crashed some time after starting successfully. - + Timed out. The process took too long to respond. The last waitFor...() function timed out. Timed out. The process took too long to respond. The last waitFor...() function timed out. - + An error occurred when attempting to write to the process. For example, the process may not be running, or it may have closed its input channel. An error occurred when attempting to write to the process. For example, the process may not be running, or it may have closed its input channel. - + An error occurred when attempting to read from the process. For example, the process may not be running. An error occurred when attempting to read from the process. For example, the process may not be running. - + Unknown error occurred. Unknown error occurred. - + The card database updater exited with an error: %1 The card database updater exited with an error: %1 - + This server supports additional features that your client doesn't have. This is most likely not a problem, but this message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version. @@ -5438,55 +5452,55 @@ This is most likely not a problem, but this message might mean there is a new ve To update your client, go to Help -> Check for Updates. - - - - - + + + + + Load sets/cards Load sets/cards - + Selected file cannot be found. Selected file cannot be found. - + You can only import XML databases at this time. You can only import XML databases at this time. - + The new sets/cards have been added successfully. Cockatrice will now reload the card database. The new sets/cards have been added successfully. Cockatrice will now reload the card database. - + Sets/cards failed to import. Sets/cards failed to import. - - - + + + Reset Password Reset Password - + Your password has been reset successfully, you can now log in using the new credentials. Your password has been reset successfully, you can now log in using the new credentials. - + Failed to reset user account password, please contact the server operator to reset your password. Failed to reset user account password, please contact the server operator to reset your password. - + Activation request received, please check your email for an activation token. Activation request received, please check your email for an activation token. @@ -5537,7 +5551,7 @@ Cockatrice will now reload the card database. ManaBaseWidget - + Mana Base Mana Base @@ -5691,590 +5705,610 @@ Cockatrice will now reload the card database. MessageLogWidget - + from play from play - + from their graveyard from their graveyard - + from exile from exile - + from their hand from their hand - + the top card of %1's library the top card of %1's library - + the top card of their library the top card of their library - + from the top of %1's library from the top of %1's library - + from the top of their library from the top of their library - + the bottom card of %1's library the bottom card of %1's library - + the bottom card of their library the bottom card of their library - + from the bottom of %1's library from the bottom of %1's library - + from the bottom of their library from the bottom of their library - + from %1's library from %1's library - + from their library from their library - + from sideboard from sideboard - + from the stack from the stack - + from custom zone '%1' from custom zone '%1' - + %1 is now keeping the top card %2 revealed. %1 is now keeping the top card %2 revealed. - + %1 is not revealing the top card %2 any longer. %1 is not revealing the top card %2 any longer. - + %1 can now look at top card %2 at any time. %1 can now look at top card %2 at any time. - + %1 no longer can look at top card %2 at any time. %1 no longer can look at top card %2 at any time. - + %1 attaches %2 to %3's %4. %1 attaches %2 to %3's %4. - + %1 has conceded the game. %1 has conceded the game. - + %1 has unconceded the game. %1 has unconceded the game. - + %1 has restored connection to the game. %1 has restored connection to the game. - + %1 has lost connection to the game. %1 has lost connection to the game. - + %1 points from their %2 to themselves. %1 points from their %2 to themselves. - + %1 points from their %2 to %3. %1 points from their %2 to %3. - + %1 points from %2's %3 to themselves. %1 points from %2's %3 to themselves. - + %1 points from %2's %3 to %4. %1 points from %2's %3 to %4. - + %1 points from their %2 to their %3. %1 points from their %2 to their %3. - + %1 points from their %2 to %3's %4. %1 points from their %2 to %3's %4. - + %1 points from %2's %3 to their own %4. %1 points from %2's %3 to their own %4. - + %1 points from %2's %3 to %4's %5. %1 points from %2's %3 to %4's %5. - + %1 creates a face down token. %1 creates a face down token. - + %1 creates token: %2%3. %1 creates token: %2%3. - + %1 has loaded a deck (%2). %1 has loaded a deck (%2). - + %1 has loaded a deck with %2 sideboard cards (%3). %1 has loaded a deck with %2 sideboard cards (%3). - + %1 destroys %2. %1 destroys %2. - + a card a card - + %1 gives %2 control over %3. %1 gives %2 control over %3. - + %1 puts %2 into play%3 face down. %1 puts %2 into play%3 face down. - + %1 puts %2 into play%3. %1 puts %2 into play%3. - + + %1 puts %2%3 into their graveyard face down. + %1 puts %2%3 into their graveyard face down. + + + %1 puts %2%3 into their graveyard. %1 puts %2%3 into their graveyard. + + + %1 exiles %2%3 face down. + %1 exiles %2%3 face down. + %1 exiles %2%3. %1 exiles %2%3. - + %1 moves %2%3 to their hand. %1 moves %2%3 to their hand. - + %1 puts %2%3 into their library. %1 puts %2%3 into their library. - + %1 puts %2%3 onto the bottom of their library. %1 puts %2%3 onto the bottom of their library. - + %1 puts %2%3 on top of their library. %1 puts %2%3 on top of their library. - + %1 puts %2%3 into their library %4 cards from the top. %1 puts %2%3 into their library %4 cards from the top. - + %1 moves %2%3 to sideboard. %1 moves %2%3 to sideboard. - + + %1 plays %2%3 face down. + %1 plays %2%3 face down. + + + %1 plays %2%3. %1 plays %2%3. - + + %1 moves %2%3 to custom zone '%4' face down. + %1 moves %2%3 to custom zone '%4' face down. + + + %1 moves %2%3 to custom zone '%4'. %1 moves %2%3 to custom zone '%4'. - + %1 tries to draw from an empty library %1 tries to draw from an empty library - + %1 draws %2 card(s). %1 draws %2 card.%1 draws %2 cards. - + %1 is looking at %2. %1 is looking at %2. - + %1 is looking at the %4 %3 card(s) %2. top card for singular, top %3 cards for plural %1 is looking at the %4 %3 card %2.%1 is looking at the %4 %3 cards %2. - + bottom bottom - + top top - + %1 turns %2 face-down. %1 turns %2 face-down. - + %1 turns %2 face-up. %1 turns %2 face-up. - + The game has been closed. The game has been closed. - + The game has started. The game has started. - + You are flooding the game. Please wait a couple of seconds. You are flooding the game. Please wait a couple of seconds. - + %1 has joined the game. %1 has joined the game. - + %1 is now watching the game. %1 is now watching the game. - + You have been kicked out of the game. You have been kicked out of the game. - + %1 has left the game (%2). %1 has left the game (%2). - + %1 is not watching the game any more (%2). %1 is not watching the game any more (%2). - + %1 is not ready to start the game any more. %1 is not ready to start the game any more. - + %1 shuffles their deck and draws a new hand of %2 card(s). %1 shuffles their deck and draws a card.%1 shuffles their deck and draws a new hand of %2 cards. - + %1 shuffles their deck and draws a new hand. %1 shuffles their deck and draws a new hand. - + You are watching a replay of game #%1. You are watching a replay of game #%1. - + %1 is ready to start the game. %1 is ready to start the game. - + cards an unknown amount of cards cards - + %1 card(s) a card for singular, %1 cards for plural one card%1 cards - + %1 lends %2 to %3. %1 lends %2 to %3. - + %1 reveals %2 to %3. %1 reveals %2 to %3. - + %1 reveals %2. %1 reveals %2. - + %1 randomly reveals %2%3 to %4. %1 randomly reveals %2%3 to %4. - + %1 randomly reveals %2%3. %1 randomly reveals %2%3. - + %1 peeks at face down card #%2. %1 peeks at face down card #%2. - + %1 peeks at face down card #%2: %3. %1 peeks at face down card #%2: %3. - + %1 reveals %2%3 to %4. %1 reveals %2%3 to %4. - + %1 reveals %2%3. %1 reveals %2%3. - + %1 reversed turn order, now it's %2. %1 reversed turn order, now it's %2. - + reversed reversed - + normal normal - + Heads Heads - + Tails Tails - + %1 flipped a coin. It landed as %2. %1 flipped a coin. It landed as %2. - + %1 rolls a %2 with a %3-sided die. %1 rolls a %2 with a %3-sided die. - + %1 flips %2 coins. There are %3 heads and %4 tails. %1 flips %2 coins. There are %3 heads and %4 tails. - + %1 rolls a %2-sided dice %3 times: %4. %1 rolls a %2-sided dice %3 times: %4. - + %1's turn. %1's turn. - + %1 sets annotation of %2 to %3. %1 sets annotation of %2 to %3. - + %1 places %2 "%3" counter(s) on %4 (now %5). %1 places %2 "%3" counter on %4 (now %5).%1 places %2 "%3" counters on %4 (now %5). - + %1 removes %2 "%3" counter(s) from %4 (now %5). %1 removes %2 "%3" counter from %4 (now %5).%1 removes %2 "%3" counters from %4 (now %5). - + %1 sets counter %2 to %3 (%4%5). %1 sets counter %2 to %3 (%4%5). - + %1 sets %2 to not untap normally. %1 sets %2 to not untap normally. - + %1 sets %2 to untap normally. %1 sets %2 to untap normally. - + %1 removes the PT of %2. %1 removes the PT of %2. - + %1 changes the PT of %2 from nothing to %4. %1 changes the PT of %2 from nothing to %4. - + %1 changes the PT of %2 from %3 to %4. %1 changes the PT of %2 from %3 to %4. - + %1 has locked their sideboard. %1 has locked their sideboard. - + %1 has unlocked their sideboard. %1 has unlocked their sideboard. - + %1 taps their permanents. %1 taps their permanents. - + %1 untaps their permanents. %1 untaps their permanents. - + %1 taps %2. %1 taps %2. - + %1 untaps %2. %1 untaps %2. - + %1 shuffles %2. %1 shuffles %2. - + %1 shuffles the bottom %3 cards of %2. %1 shuffles the bottom %3 cards of %2. - + %1 shuffles the top %3 cards of %2. %1 shuffles the top %3 cards of %2. - + %1 shuffles cards %3 - %4 of %2. %1 shuffles cards %3 - %4 of %2. - + %1 unattaches %2. %1 unattaches %2. - + %1 undoes their last draw. %1 undoes their last draw. - + %1 undoes their last draw (%2). %1 undoes their last draw (%2). @@ -6282,110 +6316,110 @@ Cockatrice will now reload the card database. MessagesSettingsPage - + Word1 Word2 Word3 Word1 Word2 Word3 - + Add New Message Add New Message - + Edit Message Edit Message - + Remove Message Remove Message - + Add message Add message - - + + Message: Message: - + Edit message Edit message - + Chat settings Chat settings - + Custom alert words Custom alert words - + Enable chat mentions Enable chat mentions - + Enable mention completer Enable mention completer - + In-game message macros In-game message macros - + How to use in-game message macros How to use in-game message macros - + Ignore chat room messages sent by unregistered users Ignore chat room messages sent by unregistered users - + Ignore private messages sent by unregistered users Ignore private messages sent by unregistered users - - + + Invert text color Invert text color - + Enable desktop notifications for private messages Enable desktop notifications for private messages - + Enable desktop notification for mentions Enable desktop notification for mentions - + Enable room message history on join Enable room message history on join - - + + (Color is hexadecimal) (Color is hexadecimal) - + Separate words with a space, alphanumeric characters only Separate words with a space, alphanumeric characters only @@ -6398,32 +6432,37 @@ Cockatrice will now reload the card database. Move to - + &Top of library in random order &Top of library in random order - + X cards from the top of library... X cards from the top of library... - + &Bottom of library in random order &Bottom of library in random order - + + T&able + T&able + + + &Hand &Hand - + &Graveyard &Graveyard - + &Exile &Exile @@ -6565,57 +6604,57 @@ Cockatrice will now reload the card database. PhasesToolbar - + Untap step Untap step - + Upkeep step Upkeep step - + Draw step Draw step - + First main phase First main phase - + Beginning of combat step Beginning of combat step - + Declare attackers step Declare attackers step - + Declare blockers step Declare blockers step - + Combat damage step Combat damage step - + End of combat step End of combat step - + Second main phase Second main phase - + End of turn step End of turn step @@ -6632,134 +6671,138 @@ Cockatrice will now reload the card database. PlayerActions - + View top cards of library View top cards of library - - - - - - - - - - - + + + + + + + + + Number of cards: (max. %1) Number of cards: (max. %1) - + View bottom cards of library View bottom cards of library - + Shuffle top cards of library Shuffle top cards of library - + Shuffle bottom cards of library Shuffle bottom cards of library - + Draw hand Draw hand - + 0 and lower are in comparison to current hand size 0 and lower are in comparison to current hand size - + Draw cards Draw cards + + + + + + grave + grave + - Move top cards to grave - Move top cards to grave + + + + exile + exile - - Move top cards to exile - Move top cards to exile + + Move top cards to %1 + Move top cards to %1 - - Move bottom cards to grave - Move bottom cards to grave + + Move bottom cards to %1 + Move bottom cards to %1 - - Move bottom cards to exile - Move bottom cards to exile - - - + Draw bottom cards Draw bottom cards - - + + C&reate another %1 token C&reate another %1 token - + Create tokens Create tokens - - + + Number: Number: - + Place card X cards from top of library Place card X cards from top of library - + Which position should this card be placed: Which position should this card be placed: - + (max. %1) (max. %1) - + Change power/toughness Change power/toughness - + Change stats to: Change stats to: - + Set annotation Set annotation - + Please enter the new annotation: Please enter the new annotation: - + Set counters Set counters @@ -6767,48 +6810,69 @@ Cockatrice will now reload the card database. PlayerMenu - + Player "%1" Player "%1" - + &Counters &Counters + + + PrintingDisabledInfoWidget - - S&ay - S&ay + + The Printing Selector is disabled because you have currently enabled the setting to override all selected printings with personal set preferences. + +This setting means you'll only see the default printing for each card, instead of being able to select a printing, and will not see the printings other people have selected. + + + The Printing Selector is disabled because you have currently enabled the setting to override all selected printings with personal set preferences. + +This setting means you'll only see the default printing for each card, instead of being able to select a printing, and will not see the printings other people have selected. + + + + + + Enable printings again + Enable printings again PrintingSelector - + Display Navigation Buttons Display Navigation Buttons + + + Printing Selector + Printing Selector + PrintingSelectorCardOverlayWidget - + Preference Preference - + Pin Printing Pin Printing - + Unpin Printing Unpin Printing - + Show Related cards Show Related cards @@ -6857,17 +6921,25 @@ Cockatrice will now reload the card database. Release Date - - + + Descending Descending - + Ascending Ascending + + PrintingSelectorPlaceholderWidget + + + Select a card to view its available printings + Select a card to view its available printings + + PtMenu @@ -7006,6 +7078,45 @@ Cockatrice will now reload the card database. A .cod version of this deck already exists. Overwrite it? A .cod version of this deck already exists. Overwrite it? + + + Enabling this feature will disable the use of the Printing Selector. + +You will not be able to manage printing preferences on a per-deck basis, or see printings other people have selected for their decks. + +You will have to use the Set Manager, available through Card Database -> Manage Sets. + +Are you sure you would like to enable this feature? + Enabling this feature will disable the use of the Printing Selector. + +You will not be able to manage printing preferences on a per-deck basis, or see printings other people have selected for their decks. + +You will have to use the Set Manager, available through Card Database -> Manage Sets. + +Are you sure you would like to enable this feature? + + + + Disabling this feature will enable the Printing Selector. + +You can now choose printings on a per-deck basis in the Deck Editor and configure which printing gets added to a deck by default by pinning it in the Printing Selector. + +You can also use the Set Manager to adjust custom sort order for printings in the Printing Selector (other sort orders like alphabetical or release date are available). + +Are you sure you would like to disable this feature? + Disabling this feature will enable the Printing Selector. + +You can now choose printings on a per-deck basis in the Deck Editor and configure which printing gets added to a deck by default by pinning it in the Printing Selector. + +You can also use the Set Manager to adjust custom sort order for printings in the Printing Selector (other sort orders like alphabetical or release date are available). + +Are you sure you would like to disable this feature? + + + + Confirm Change + Confirm Change + QPlatformTheme @@ -7154,37 +7265,37 @@ Cockatrice will now reload the card database. RfgMenu - + &Exile &Exile - + &View exile &View exile - + &Move exile to... &Move exile to... - + &Top of library &Top of library - + &Bottom of library &Bottom of library - + &Hand &Hand - + &Graveyard &Graveyard @@ -7227,6 +7338,14 @@ Cockatrice will now reload the card database. Games + + SayMenu + + + S&ay + S&ay + + SequenceEdit @@ -7291,53 +7410,53 @@ Cockatrice will now reload the card database. ShortcutSettingsPage - - + + Restore all default shortcuts Restore all default shortcuts - + Do you really want to restore all default shortcuts? Do you really want to restore all default shortcuts? - + Clear all default shortcuts Clear all default shortcuts - + Do you really want to clear all shortcuts? Do you really want to clear all shortcuts? - + Section: Section: - + Action: Action: - + Shortcut: Shortcut: - + How to set custom shortcuts How to set custom shortcuts - + Clear all shortcuts Clear all shortcuts - + Search by shortcut name Search by shortcut name @@ -7406,27 +7525,27 @@ Please check your shortcut settings! SoundSettingsPage - + Enable &sounds Enable &sounds - + Current sounds theme: Current sounds theme: - + Test system sound engine Test system sound engine - + Sound settings Sound settings - + Master volume Master volume @@ -7642,59 +7761,123 @@ Please check your shortcut settings! TabArchidekt - - + + + Desc. Desc. - + + + AND + AND + + + + + Require ALL selected colors + Require ALL selected colors + + + + + Deck name... + Deck name... + + + + + Owner... + Owner... + + + + + Packages + Packages + + + + + Advanced Filters + Advanced Filters + + + + Bracket: + Bracket: + + + + + Any + Any + + + + + Contains card... + Contains card... + + + + + Commander... + Commander... + + + + + Tag... + Tag... + + + + + Deck Size + Deck Size + + + + Cards: + Cards: + + + + Asc. Asc. - - - Any Bracket - Any Bracket + + Sort by: + Sort by: - - Deck name contains... - Deck name contains... + + Filter by: + Filter by: - - Owner name contains... - Owner name contains... + + Display Settings + Display Settings - - Disabled - Disabled - - - + + Search Search - + + Formats Formats - - Min. # of Cards: - Min. # of Cards: - - - - Page: - Page: - - - + Archidekt: Archidekt: @@ -7702,60 +7885,52 @@ Please check your shortcut settings! TabDeckEditor - + Card Info Card Info - + Deck Deck - + Filters Filters - + &View &View - + Card Database Card Database - + Printing Printing - - - - - + Visible Visible - - - - - + Floating Floating - + Reset layout Reset layout - + Deck: %1 Deck: %1 @@ -7763,61 +7938,55 @@ Please check your shortcut settings! TabDeckEditorVisual - + Visual Deck: %1 Visual Deck: %1 - + &Visual Deck Editor &Visual Deck Editor - - + + Card Info Card Info - - + + Deck Deck - - + + Filters Filters - + &View &View - + Printing Printing - - - - + Visible Visible - - - - + Floating Floating - + Reset layout Reset layout @@ -7882,7 +8051,7 @@ Please check your shortcut settings! - + New folder New folder @@ -7964,18 +8133,18 @@ Please enter a name: Are you sure you want to delete the selected files? - + Delete remote decks Delete remote decks - + Are you sure you want to delete the selected decks? Are you sure you want to delete the selected decks? - + Name of new folder: Name of new folder: @@ -7993,12 +8162,12 @@ Please enter a name: Visual Deck Storage - + Error Error - + Could not open deck at %1 Could not open deck at %1 @@ -8047,197 +8216,191 @@ Please enter a name: TabGame - - - + + + Replay Replay - - + + Game Game - - + + Player List Player List - - + + Card Info Card Info - - + + Messages Messages - - + + Replay Timeline Replay Timeline - + &Phases &Phases - + &Game &Game - + Next &phase Next &phase - + Next phase with &action Next phase with &action - + Next &turn Next &turn - + Reverse turn order Reverse turn order - + &Remove all local arrows &Remove all local arrows - + Rotate View Cl&ockwise Rotate View Cl&ockwise - + Rotate View Co&unterclockwise Rotate View Co&unterclockwise - + Game &information Game &information - + Un&concede Un&concede - - - + + + &Concede &Concede - + &Leave game &Leave game - + C&lose replay C&lose replay - + &Focus Chat &Focus Chat - + &Say: &Say: - + Selected cards Selected cards - + &View &View - - - - + Visible Visible - - - - + Floating Floating - + Reset layout Reset layout - + Concede Concede - + Are you sure you want to concede this game? Are you sure you want to concede this game? - + Unconcede Unconcede - + You have already conceded. Do you want to return to this game? You have already conceded. Do you want to return to this game? - + Leave game Leave game - + Are you sure you want to leave this game? Are you sure you want to leave this game? - + A player has joined game #%1 A player has joined game #%1 - + %1 has joined the game %1 has joined the game - + You have been kicked out of the game. You have been kicked out of the game. @@ -9340,142 +9503,152 @@ Please refrain from engaging in this activity or further actions may be taken ag UserInterfaceSettingsPage - + General interface settings General interface settings - + &Double-click cards to play them (instead of single-click) &Double-click cards to play them (instead of single-click) - + &Clicking plays all selected cards (instead of just the clicked card) &Clicking plays all selected cards (instead of just the clicked card) - + &Play all nonlands onto the stack (not the battlefield) by default &Play all nonlands onto the stack (not the battlefield) by default - + Do not delete &arrows inside of subphases Do not delete &arrows inside of subphases - + Close card view window when last card is removed Close card view window when last card is removed - + Auto focus search bar when card view window is opened Auto focus search bar when card view window is opened - + Annotate card text on tokens Annotate card text on tokens - + + Show selection counter during drag selection + Show selection counter during drag selection + + + + Show total selection counter + Show total selection counter + + + Use tear-off menus, allowing right click menus to persist on screen Use tear-off menus, allowing right click menus to persist on screen - + Notifications settings Notifications settings - + Enable notifications in taskbar Enable notifications in taskbar - + Notify in the taskbar for game events while you are spectating Notify in the taskbar for game events while you are spectating - + Notify in the taskbar when users in your buddy list connect Notify in the taskbar when users in your buddy list connect - + Animation settings Animation settings - + &Tap/untap animation &Tap/untap animation - + Deck editor/storage settings Deck editor/storage settings - + Open deck in new tab by default Open deck in new tab by default - + Use visual deck storage in game lobby Use visual deck storage in game lobby - + Use selection animation for Visual Deck Storage Use selection animation for Visual Deck Storage - + When adding a tag in the visual deck storage to a .txt deck: When adding a tag in the visual deck storage to a .txt deck: - + do nothing do nothing - + ask to convert to .cod ask to convert to .cod - + always convert to .cod always convert to .cod - + Default deck editor type Default deck editor type - + Classic Deck Editor Classic Deck Editor - + Visual Deck Editor Visual Deck Editor - + Replay settings Replay settings - + Buffer time for backwards skip via shortcut: Buffer time for backwards skip via shortcut: @@ -9539,24 +9712,25 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayColorFilterWidget - - Mode: Exact Match - Mode: Exact Match + + Exact match + Exact match + + + + Includes + Includes - Mode: Includes - Mode: Includes + Include / Exclude + Mode: Includes + Include / Exclude - - Mode: Include/Exclude - Mode: Include/Exclude - - - - Filter mode (AND/OR/NOT conjunctions of filters) - Filter mode (AND/OR/NOT conjunctions of filters) + + How selected and unselected colors are combined in the filter + How selected and unselected colors are combined in the filter @@ -9582,25 +9756,108 @@ Please refrain from engaging in this activity or further actions may be taken ag Enter filename... + + VisualDatabaseDisplayFilterToolbarWidget + + + Sort by + Sort by + + + + Filter by + Filter by + + + + Save and load filters + Save and load filters + + + + Filter by exact card name + Filter by exact card name + + + + Filter by card main-type + Filter by card main-type + + + + Filter by card sub-type + Filter by card sub-type + + + + Filter by set + Filter by set + + + + Filter by format legality + Filter by format legality + + + + Save/Load + Save/Load + + + + Name + Name + + + + Main Type + Main Type + + + + Sub Type + Sub Type + + + + Sets + Sets + + + + Formats + Formats + + VisualDatabaseDisplayFormatLegalityFilterWidget - + + Show formats with at least: + Show formats with at least: + + + + cards + cards + + + Do not display formats with less than this amount of cards in the database Do not display formats with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match Mode: Exact Match - + Mode: Includes Mode: Includes @@ -9608,22 +9865,32 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayMainTypeFilterWidget - + + Show main types with at least: + Show main types with at least: + + + + cards + cards + + + Do not display card main-types with less than this amount of cards in the database Do not display card main-types with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match Mode: Exact Match - + Mode: Includes Mode: Includes @@ -9659,7 +9926,7 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayRecentSetFilterSettingsWidget - + Filter to most recent sets Filter to most recent sets @@ -9667,19 +9934,19 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplaySetFilterWidget - + Search sets... Search sets... - - + + Mode: Exact Match Mode: Exact Match - - + + Mode: Includes Mode: Includes @@ -9687,27 +9954,37 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplaySubTypeFilterWidget - + Search subtypes... Search subtypes... - + + Show sub types with at least: + Show sub types with at least: + + + + cards + cards + + + Do not display card sub-types with less than this amount of cards in the database Do not display card sub-types with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match Mode: Exact Match - + Mode: Includes Mode: Includes @@ -9721,52 +9998,22 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Visual Visual - + Loading database ... Loading database ... - + Clear all filters Clear all filters - - Sort by: - Sort by: - - - - Filter by: - Filter by: - - - - Save and load filters - Save and load filters - - - - Filter by exact card name - Filter by exact card name - - - - Filter by card sub-type - Filter by card sub-type - - - - Filter by set - Filter by set - - - + Table Table @@ -9774,56 +10021,64 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckDisplayOptionsWidget - + Group by: Group by: - + Change how cards are divided into categories/groups. Change how cards are divided into categories/groups. - + Sort by: Sort by: - + Click and drag to change the sort order within the groups Click and drag to change the sort order within the groups - + Configure how cards are sorted within their groups Configure how cards are sorted within their groups - - + + Toggle Layout: Overlap Toggle Layout: Overlap - + Change how cards are displayed within zones (i.e. overlapped or fully visible.) Change how cards are displayed within zones (i.e. overlapped or fully visible.) - + Toggle Layout: Flat Toggle Layout: Flat + + VisualDeckEditorPlaceholderWidget + + + Add cards using the search bar or database tab to have them appear here + Add cards using the search bar or database tab to have them appear here + + VisualDeckEditorSampleHandWidget - + Draw a new sample hand Draw a new sample hand - + Sample hand size Sample hand size @@ -9831,17 +10086,17 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckEditorWidget - + Type a card name here for suggestions from the database... Type a card name here for suggestions from the database... - + Quick search and add card Quick search and add card - + Search for closest match in the database (with auto-suggestions) and add preferred printing to the deck on pressing enter Search for closest match in the database (with auto-suggestions) and add preferred printing to the deck on pressing enter @@ -9857,47 +10112,52 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckStorageQuickSettingsWidget - + Show Folders Show Folders - + Show Tag Filter Show Tag Filter - + + Show Color Identity + Show Color Identity + + + Show Tags On Deck Previews Show Tags On Deck Previews - + Show Banner Card Selection Option Show Banner Card Selection Option - + Draw unused Color Identities Draw unused Color Identities - + Unused Color Identities Opacity Unused Color Identities Opacity - + Deck tooltip: Deck tooltip: - + None None - + Filepath Filepath @@ -9998,133 +10258,133 @@ Please refrain from engaging in this activity or further actions may be taken ag WndSets - + Move selected set to the top Move selected set to the top - + Move selected set up Move selected set up - + Move selected set down Move selected set down - + Move selected set to the bottom Move selected set to the bottom - + Search by set name, code, or type Search by set name, code, or type - + Default order Default order - + Restore original art priority order Restore original art priority order - + Enable all sets Enable all sets - + Disable all sets Disable all sets - + Enable selected set(s) Enable selected set(s) - + Disable selected set(s) Disable selected set(s) - + Deck Editor Deck Editor - + Use CTRL+A to select all sets in the view. Use CTRL+A to select all sets in the view. - + Only cards in enabled sets will appear in the card list of the deck editor. Only cards in enabled sets will appear in the card list of the deck editor. - + Image priority is decided in the following order: Image priority is decided in the following order: - + first the CUSTOM Folder (%1), then the Enabled Sets in this dialog (Top to Bottom) %1 is a link to the wiki first the CUSTOM Folder (%1), then the Enabled Sets in this dialog (Top to Bottom) - + Include cards rebalanced for Alchemy [requires restart] Include cards rebalanced for Alchemy [requires restart] - + Card Art Card Art - + How to use custom card art How to use custom card art - + Hints Hints - + Note Note - + Sorting by column allows you to find a set while not changing set priority. Sorting by column allows you to find a set while not changing set priority. - + To enable ordering again, click the column header until this message disappears. To enable ordering again, click the column header until this message disappears. - + Use the current sorting as the set priority instead Use the current sorting as the set priority instead - + Sorts the set priority using the same column Sorts the set priority using the same column - + Manage sets Manage sets @@ -10132,72 +10392,72 @@ Please refrain from engaging in this activity or further actions may be taken ag ZoneViewWidget - + Search by card name (or search expressions) Search by card name (or search expressions) - + Ungrouped Ungrouped - + Group by Type Group by Type - + Group by Mana Value Group by Mana Value - + Group by Color Group by Color - + Unsorted Unsorted - + Sort by Name Sort by Name - + Sort by Type Sort by Type - + Sort by Mana Cost Sort by Mana Cost - + Sort by Colors Sort by Colors - + Sort by P/T Sort by P/T - + Sort by Set Sort by Set - + shuffle when closing shuffle when closing - + pile view pile view @@ -10232,7 +10492,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Deck Editor Deck Editor @@ -10313,7 +10573,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Replays Replays @@ -10465,7 +10725,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Reset Layout Reset Layout @@ -10886,8 +11146,9 @@ Please refrain from engaging in this activity or further actions may be taken ag - Toggle Untap - Toggle Untap + Toggle Skip Untapping + Toggle Untap + Toggle Skip Untapping @@ -10906,98 +11167,102 @@ Please refrain from engaging in this activity or further actions may be taken ag + Play Card, Face Down + Play Card, Face Down + + + Attach Card... Attach Card... - + Unattach Card Unattach Card - + Clone Card Clone Card - + Create Token... Create Token... - + Create All Related Tokens Create All Related Tokens - + Create Another Token Create Another Token - + Set Annotation... Set Annotation... - + Select All Cards in Zone Select All Cards in Zone - + Select All Cards in Row Select All Cards in Row - + Select All Cards in Column Select All Cards in Column - + Reveal Selected Cards to All Players Reveal Selected Cards to All Players - - + + Bottom of Library Bottom of Library - + - - + + Exile Exile - + - + Graveyard Graveyard - + Hand Hand - - + + Top of Library Top of Library - - + Battlefield, Face Down Battlefield, Face Down @@ -11033,234 +11298,246 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Stack Stack - + Graveyard (Multiple) Graveyard (Multiple) - - + + + Graveyard (Multiple), Face Down + Graveyard (Multiple), Face Down + + + + Exile (Multiple) Exile (Multiple) - + + + Exile (Multiple), Face Down + Exile (Multiple), Face Down + + + Stack Until Found Stack Until Found - + Draw Bottom Card Draw Bottom Card - + Draw Multiple Cards from Bottom... Draw Multiple Cards from Bottom... - + Draw Arrow... Draw Arrow... - + Remove Local Arrows Remove Local Arrows - + Leave Game Leave Game - + Concede Concede - + Roll Dice... Roll Dice... - + Shuffle Library Shuffle Library - + Shuffle Top Cards of Library Shuffle Top Cards of Library - + Shuffle Bottom Cards of Library Shuffle Bottom Cards of Library - + Mulligan Mulligan - + Mulligan (Same hand size) Mulligan (Same hand size) - + Mulligan (Hand size - 1) Mulligan (Hand size - 1) - + Draw a Card Draw a Card - + Draw Multiple Cards... Draw Multiple Cards... - + Undo Draw Undo Draw - + Always Reveal Top Card Always Reveal Top Card - + Always Look At Top Card Always Look At Top Card - + Sort Hand by Name Sort Hand by Name - + Sort Hand by Type Sort Hand by Type - + Sort Hand by Mana Value Sort Hand by Mana Value - + Reveal Hand to All Players Reveal Hand to All Players - + Reveal Random Card to All Players Reveal Random Card to All Players - + Rotate View Clockwise Rotate View Clockwise - + Rotate View Counterclockwise Rotate View Counterclockwise - + Unfocus Text Box Unfocus Text Box - + Focus Chat Focus Chat - + Clear Chat Clear Chat - + Refresh Refresh - + Skip Forward Skip Forward - + Skip Backward Skip Backward - + Skip Forward by a lot Skip Forward by a lot - + Skip Backward by a lot Skip Backward by a lot - + Play/Pause Play/Pause - + Toggle Fast Forward Toggle Fast Forward - + Home Home - + Visual Deck Storage Visual Deck Storage - + Deck Storage Deck Storage - + Server Server - + Account Account - + Administration Administration - + Logs Logs From d2732ac742cd7acf19719bb60ce5007ae3d8d91b Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Fri, 10 Apr 2026 13:42:05 +0200 Subject: [PATCH 089/116] fix prepare cards (#6792) --- oracle/src/oracleimporter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index d585842f6..b5d7b9856 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -360,13 +360,13 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } // split cards are considered a single card, enqueue for later merging - if (layout == "split" || layout == "aftermath" || layout == "adventure") { + if (layout == "split" || layout == "aftermath" || layout == "adventure" || layout == "prepare") { auto _faceName = getStringPropertyFromMap(card, "faceName"); SplitCardPart split(_faceName, text, properties, printingInfo); auto found_iter = splitCards.find(name + numProperty); if (found_iter == splitCards.end()) { splitCards.insert(name + numProperty, {{split}, name}); - } else if (layout == "adventure") { + } else if (layout == "adventure" || layout == "prepare") { found_iter->first.insert(0, split); } else { found_iter->first.append(split); From f9fb03b26b796bdfd1ac298110224598dc506cdc Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Fri, 10 Apr 2026 18:17:43 +0200 Subject: [PATCH 090/116] update all runners to use qt6.11 (#6794) * update all runners to use qt6.11 * use aqt version directly from repo --- .github/workflows/desktop-build.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 7bd84eb21..c93666640 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -270,7 +270,7 @@ jobs: override_target: 13 make_package: 1 package_suffix: "-macOS13_Intel" - qt_version: 6.10.* + qt_version: 6.11.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets cmake_generator: Ninja @@ -284,7 +284,7 @@ jobs: type: Release make_package: 1 package_suffix: "-macOS14" - qt_version: 6.10.* + qt_version: 6.11.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets cmake_generator: Ninja @@ -298,7 +298,7 @@ jobs: type: Release make_package: 1 package_suffix: "-macOS15" - qt_version: 6.10.* + qt_version: 6.11.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets cmake_generator: Ninja @@ -310,7 +310,7 @@ jobs: soc: Apple xcode: "16.4" type: Debug - qt_version: 6.10.* + qt_version: 6.11.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets cmake_generator: Ninja @@ -322,7 +322,7 @@ jobs: type: Release make_package: 1 package_suffix: "-Win10" - qt_version: 6.10.* + qt_version: 6.11.* qt_arch: win64_msvc2022_64 qt_modules: qtimageformats qtmultimedia qtwebsockets cmake_generator: "Visual Studio 17 2022" @@ -409,6 +409,8 @@ jobs: if: matrix.os == 'Windows' uses: jurplel/install-qt-action@v4 with: + # qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released + aqtsource: git+https://github.com/miurahr/aqtinstall.git version: ${{ steps.resolve_qt_version.outputs.version }} arch: ${{matrix.qt_arch}} modules: ${{matrix.qt_modules}} From 36aba81b1b668fcfafcbd4b0966e35d0b43dca26 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Fri, 10 Apr 2026 18:18:08 +0200 Subject: [PATCH 091/116] adds eternal to prioritisation list (#6793) --- oracle/src/oracleimporter.h | 1 + 1 file changed, 1 insertion(+) diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index e83958d73..5bf352594 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -22,6 +22,7 @@ const QMap setTypePriorities{ {"archenemy", CardSet::PriorityReprint}, {"arsenal", CardSet::PriorityReprint}, {"box", CardSet::PriorityReprint}, + {"eternal", CardSet::PriorityReprint}, {"from_the_vault", CardSet::PriorityReprint}, {"masterpiece", CardSet::PriorityReprint}, {"masters", CardSet::PriorityReprint}, From 29c1d7f3e4eada9981da383e80728df9dff0e5f0 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sat, 11 Apr 2026 18:19:11 +0200 Subject: [PATCH 092/116] add timeout to job matrixes (#6797) --- .github/workflows/desktop-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index c93666640..653df4ba0 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -156,6 +156,7 @@ jobs: needs: configure runs-on: ubuntu-latest continue-on-error: ${{matrix.allow-failure == 'yes'}} + timeout-minutes: 70 env: NAME: ${{matrix.distro}}${{matrix.version}} CACHE: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}} # directory for caching docker image and ccache @@ -331,6 +332,7 @@ jobs: name: ${{matrix.os}} ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} needs: configure runs-on: ${{matrix.runner}} + timeout-minutes: 100 env: CCACHE_DIR: ${{github.workspace}}/.cache/ # Cache size over the entire repo is 10Gi: From f56b6723070fedbf3a31c63b195a341d61feff21 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sat, 11 Apr 2026 18:20:35 +0200 Subject: [PATCH 093/116] CI: Allow failing of ccache deletion step (#6799) * Don't fail cache delete step * Use `continue-on-error` * remove leftover --- .github/workflows/desktop-build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 653df4ba0..8c3fd01de 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -208,9 +208,10 @@ jobs: --ccache "$CCACHE_SIZE" $NO_CLIENT .ci/name_build.sh - # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342. + # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - name: Delete remote compiler cache (ccache) if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit + continue-on-error: true env: GH_TOKEN: ${{ github.token }} run: gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }} @@ -451,9 +452,10 @@ jobs: TARGET_MACOS_VERSION: ${{ matrix.override_target }} run: .ci/compile.sh --server --test --vcpkg - # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342. + # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - name: Delete remote compiler cache (ccache) if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 && steps.ccache_restore.outputs.cache-hit + continue-on-error: true env: GH_TOKEN: ${{ github.token }} run: gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }} From e977f123ce47d393cc308b4575fe7444d8a6272c Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Sat, 11 Apr 2026 19:18:12 +0200 Subject: [PATCH 094/116] add ccache eviction for files older than 7 days (#6795) * add ccache eviction for files older than 7 days also clean up some of the scripts to be more internally consistent * move name build into the docker container again * remove one extra empty line [skip ci] * allow canceling concurrent builds on master for both desktop and docker * add ccache eviction age to macos as well --- .ci/compile.sh | 29 +++++++++++++++++++++++- .ci/docker.sh | 29 +++++++++++++++++++----- .github/workflows/desktop-build.yml | 33 ++++++++++++++++++---------- .github/workflows/docker-release.yml | 5 +++++ 4 files changed, 78 insertions(+), 18 deletions(-) diff --git a/.ci/compile.sh b/.ci/compile.sh index 424527f96..7ebdd6e4e 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -10,9 +10,11 @@ # --test runs tests # --debug or --release sets the build type ie CMAKE_BUILD_TYPE # --ccache [] uses ccache and shows stats, optionally provide size +# --evict-ccache runs ccache eviction based on given age after build # --dir sets the name of the build dir, default is "build" +# --cmake-generator sets CMAKE_GENERATOR as used by cmake # --target-macos-version sets the min os version - only used for macOS builds -# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION +# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION # (correspond to args: --debug/--release --install --package --suffix --server --test --ccache --dir ) # exitcode: 1 for failure, 3 for invalid arguments @@ -71,6 +73,15 @@ while [[ $# != 0 ]]; do shift fi ;; + '--evict-ccache') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--evict-ccache expects an argument" + exit 3 + fi + CCACHE_EVICTION_AGE=$1 + shift + ;; '--vcpkg') USE_VCPKG=1 shift @@ -84,6 +95,15 @@ while [[ $# != 0 ]]; do BUILD_DIR="$1" shift ;; + '--cmake-generator') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--cmake-generator expects an argument" + exit 3 + fi + export CMAKE_GENERATOR=$1 + shift + ;; '--target-macos-version') shift if [[ $# == 0 ]]; then @@ -251,9 +271,16 @@ cmake --build . "${buildflags[@]}" echo "::endgroup::" if [[ $USE_CCACHE ]]; then + if [[ $CCACHE_EVICTION_AGE ]]; then + echo "::group::evict ccache files older than $CCACHE_EVICTION_AGE" + ccache --evict-older-than "$CCACHE_EVICTION_AGE" + echo "::endgroup::" + fi echo "::group::Show ccache stats again" ccachestatsverbose echo "::endgroup::" +elif [[ $CCACHE_EVICTION_AGE ]]; then + echo "::error file=$0::ccache eviction is enabled while ccache is disabled!" fi if [[ $RUNNER_OS == macOS ]]; then diff --git a/.ci/docker.sh b/.ci/docker.sh index 911488ecf..46112daaa 100644 --- a/.ci/docker.sh +++ b/.ci/docker.sh @@ -3,17 +3,28 @@ # This script is to be used by the ci environment from the project root directory, do not use it from somewhere else. # Creates or loads docker images to use in compilation, creates RUN function to start compilation on the docker image. -# sets the name of the docker image, these correspond to directories in .ci +# +# usage: source