Compare commits

..

22 Commits

Author SHA1 Message Date
dependabot[bot]
64bb5355ff Bump peter-evans/create-pull-request from 7 to 8 (#6423) 2025-12-15 23:38:49 +01:00
dependabot[bot]
1198db8891 Bump actions/cache from 4 to 5 (#6424) 2025-12-15 23:35:18 +01:00
tooomm
9471adb4f7 Add repo activity with top contributors acknowledgment (#6420) 2025-12-15 22:55:57 +01:00
RickyRister
b29909bdbe [DeckList] Refactor: Create class to RAII underlying tree (#6412)
* [DeckList] Create class to RAII underlying tree

* Update usages

* fixes after rebase

* update docs
2025-12-14 15:56:58 -08:00
RickyRister
589e9a15a6 [DeckFilterString] Add search query for format (#6414)
* [DeckFilterString] Rename file search expression

* [DeckFilterString] Add search query for format
2025-12-15 00:14:19 +01:00
RickyRister
c218a66bcd [DeckFilterString] Rename file search expression (#6413) 2025-12-15 00:14:11 +01:00
tooomm
8485bbe575 Docs: Use Doxygen \todo comments (#6421)
* format todo comments for doxygen

* Mention Todo List & link search
2025-12-15 00:13:16 +01:00
RickyRister
5d9d7d3aa5 [DeckLoader] remove unused private methods (#6417) 2025-12-14 14:26:06 -08:00
BruebachL
ccdda39e78 Deck format legality checker (#6166)
* Deck legality checker.

Took 51 seconds

Took 1 minute

Took 1 minute

Took 5 minutes

Took 3 minutes

* Adjust format parsing.

Took 8 minutes


Took 3 seconds

* toString() the xmlName

Took 4 minutes

* more toStrings()

Took 5 minutes

* Comments

Took 3 minutes

* Layout

Took 2 minutes

* Layout part 2: Electric boogaloo

Took 59 seconds

* Update cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>

* Move layout.

Took 4 minutes


Took 10 seconds

* Emit deckModified

Took 6 minutes

* Fix qOverloads

Took 4 minutes

* Fix qOverloads

Took 12 seconds

* Consider text and name in a special way.

Took 11 minutes

* Adjust "Any number of" oracle text

Took 5 minutes

* Store allowedCounts by format

Took 15 minutes

Took 6 seconds

* Only restrict vintage.

Took 2 minutes

* Adjust for DBConverter.

Took 6 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
2025-12-13 15:17:55 +01:00
RickyRister
2e2682aad4 [DeckList] Refactor and cleanup methods that iterate over nodes (#6407)
* remove helpers

* create getZoneNodes method

* replace direct calls to getRoot and forEachCard

* remove more non-const uses of forEachCard

* make node getter return const lists

* one more usage

* address comment

* address comment again

* fix hash

* fix hashes (for real this time)
2025-12-12 12:37:44 -08:00
BruebachL
a390c8ada7 [NetworkManager] Set Version string as user agent (#6411)
* [NetworkManager] Set Version string as user agent

Took 13 minutes

* Update in oracle.

Took 14 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-12-12 11:29:18 +01:00
BruebachL
da70344547 [VDD] Implement ExactMatch Name filter (#6409)
* [VDD] Implement ExactMatch Name filter

Took 7 minutes

Took 4 minutes

* Update cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
2025-12-12 03:57:18 +01:00
RickyRister
2b690f8c87 [DeckLoader] Extract cardNode functions to own file (#6408)
* [DeckLoader] Extract cardNode functions to own file

* update usages
2025-12-08 00:47:24 -08:00
RickyRister
c8b419888a [DeckLoader] Extract LoadedDeck struct (#6406)
* [DeckLoader] Extract LoadedDeck struct

* update usages

* Move enum to separate namespace

* format

* format

* format
2025-12-07 15:03:52 +01:00
Lily Huang
d3302d521f Fix flipped svg for donator/judge/vip (#6400) 2025-12-06 14:09:55 +01:00
tooomm
5c1bb27d5c README: Add code docs + flathub repo links (#6384)
* Add code docs + flathub repo links

* Update README.md
2025-12-05 23:28:25 +01:00
BruebachL
dde36183ce [VDE] Proper parent lookup syncs group-by box again (#6396)
* [VDE] Proper parent lookup syncs group-by box again

* [VDE] Proper lib inclusion.

* [VDE] Lint.
2025-12-05 23:27:27 +01:00
BruebachL
7c7f2dd8d5 [Doxygen] Logging (#6399)
* [Doxygen] Logging

Took 50 minutes

Took 36 seconds

* [Doxygen] Newline.

Took 2 minutes

* [Doxygen] Add another example.

Took 7 minutes

* [Doxygen] \note and \warning

Took 4 minutes

Took 32 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-12-05 18:42:45 +01:00
BruebachL
edb0a954e2 [GameInformation] Check for existence of room for create as judge checkbox (#6398) 2025-12-05 17:26:35 +01:00
BruebachL
0a239712dd [VDD] Add search bar for filters. (#6389)
* [VDD] Add search bar for filters.

* Update cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>

---------

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
2025-12-04 23:14:19 +01:00
RickyRister
95c3434205 [TagDisplayWidget] Refactor to just store tags and use signals (#6395) 2025-12-04 10:26:39 -08:00
RickyRister
f0be6972cc [TagsDisplayWidget] cleanup refactor (#6394)
* Make fields private

* Move method to static

* clean up code

* move code
2025-12-04 09:40:24 -08:00
110 changed files with 2153 additions and 942 deletions

View File

@@ -166,7 +166,7 @@ jobs:
- name: Restore compiler cache (ccache)
id: ccache_restore
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with:
@@ -205,7 +205,7 @@ jobs:
- name: Save compiler cache (ccache)
if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v4
uses: actions/cache/save@v5
with:
path: ${{env.CACHE}}
key: ${{ steps.ccache_restore.outputs.cache-primary-key }}

View File

@@ -33,7 +33,7 @@ jobs:
- name: Create pull request
if: github.event_name != 'pull_request'
id: create_pr
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@v8
with:
add-paths: |
cockatrice/translations/*.ts

View File

@@ -57,7 +57,7 @@ jobs:
- name: Create pull request
if: github.event_name != 'pull_request'
id: create_pr
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@v8
with:
add-paths: |
cockatrice/cockatrice_en@source.ts

View File

@@ -46,7 +46,8 @@ Latest <kbd>beta</kbd> version:
- [Magic-Token](https://github.com/Cockatrice/Magic-Token): MtG token data to use in Cockatrice
- [Magic-Spoiler](https://github.com/Cockatrice/Magic-Spoiler): Script to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) to use in Cockatrice
- [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official webpage of the Cockatrice project
- [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official Cockatrice webpage
- [Cockatrice @Flathub](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration for our Linux `flatpak` package
# Community Resources [![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](https://discord.gg/3Z9yzmA)
@@ -54,6 +55,7 @@ Latest <kbd>beta</kbd> version:
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.
- [Official Website](https://cockatrice.github.io)
- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki)
- [Official Code Documentation](https://cockatrice.github.io/docs)
- [Official Discord](https://discord.gg/3Z9yzmA)
- [reddit r/Cockatrice](https://reddit.com/r/cockatrice)
@@ -75,10 +77,15 @@ This tag is used for issues that we are looking for somebody to pick up. Often t
For both tags, we're willing to provide help to contributors in showing them where and how they can make changes, as well as code reviews for submitted changes.<br>
We'll happily advice on how best to implement a feature, or we can show you where the codebase is doing something similar before you get too far along - put a note on an issue you want to discuss more on!
You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code).
Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. We encourage you to improve the documentation by suggesting edits based on this guide.
#### Repository Activity
![Cockatrice Repo Analytics](https://repobeats.axiom.co/api/embed/c7cec938789a5bbaeb4182a028b4dbb96db8f181.svg "Cockatrice Repo Analytics by Repobeats")
<details>
<summary><b>Kudos to our amazing contributors ❤️</b></summary>
<summary><b>Kudos to all our amazing contributors ❤️</b></summary>
<br>
<a href="https://github.com/Cockatrice/Cockatrice/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Cockatrice/Cockatrice" />

View File

@@ -19,7 +19,10 @@ set(cockatrice_SOURCES
src/client/settings/card_counter_settings.cpp
src/client/settings/shortcut_treeview.cpp
src/client/settings/shortcuts_settings.cpp
src/interface/deck_loader/card_node_function.cpp
src/interface/deck_loader/deck_file_format.cpp
src/interface/deck_loader/deck_loader.cpp
src/interface/deck_loader/loaded_deck.cpp
src/interface/widgets/dialogs/dlg_connect.cpp
src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
src/interface/widgets/dialogs/dlg_create_game.cpp
@@ -199,6 +202,7 @@ set(cockatrice_SOURCES
src/interface/widgets/utility/sequence_edit.cpp
src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp

View File

@@ -1,6 +1,10 @@
[Rules]
# The default log level is info
*.debug = false
#*.info = true
#*.warning = true
#*.critical = true
#*.fatal = true
# Uncomment a rule to see debug level logs for that category,
# or set <category> = false to disable logging

View File

@@ -15,15 +15,18 @@ searches are case insensitive.
<dd>[n:red n:deck n:wins](#n:red n:deck n:wins) <small>(Any deck with a name containing the words red, deck, and wins)</small></dd>
<dd>[n:"red deck wins"](#n:%22red deck wins%22) <small>(Any deck with a name containing the exact phrase "red deck wins")</small></dd>
<dt><u>F</u>ile Name:</dt>
<dd>[f:aggro](#f:aggro) <small>(Any deck with a filename containing the word aggro)</small></dd>
<dd>[f:red f:deck f:wins](#f:red f:deck f:wins) <small>(Any deck with a filename containing the words red, deck, and wins)</small></dd>
<dd>[f:"red deck wins"](#f:%22red deck wins%22) <small>(Any deck with a filename containing the exact phrase "red deck wins")</small></dd>
<dt><u>F</u>ile <u>N</u>ame:</dt>
<dd>[fn:aggro](#fn:aggro) <small>(Any deck with a filename containing the word aggro)</small></dd>
<dd>[fn:red fn:deck fn:wins](#fn:red fn:deck fn:wins) <small>(Any deck with a filename containing the words red, deck, and wins)</small></dd>
<dd>[fn:"red deck wins"](#fn:%22red deck wins%22) <small>(Any deck with a filename containing the exact phrase "red deck wins")</small></dd>
<dt>Relative <u>P</u>ath (starting from the deck folder):</dt>
<dd>[p:aggro](#p:aggro) <small>(Any deck that has "aggro" somewhere in its relative path)</small></dd>
<dd>[p:edh/](#p:edh/) <small>(Any deck with "edh/" in its relative path, A.K.A. decks in the "edh" folder)</small></dd>
<dt><u>F</u>ormat:</dt>
<dd>[f:standard](#f:standard) <small>(Any deck with format set to standard)</small></dd>
<dt>Deck Contents (Uses [card search expressions](#cardSearchSyntaxHelp)):</dt>
<dd><a href="#[[plains]]">[[plains]]</a> <small>(Any deck that contains at least one card with "plains" in its name)</small></dd>
<dd><a href="#[[t:legendary]]">[[t:legendary]]</a> <small>(Any deck that contains at least one legendary)</small></dd>

View File

@@ -350,11 +350,11 @@
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
transform="translate(0,952.36218)"
id="right" />
id="left" />
<path
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
id="left"
id="right"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:url(#linearGradient3);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3.77952756;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -321,11 +321,11 @@
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
transform="translate(0,952.36218)"
id="right" />
id="left" />
<path
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
id="left"
id="right"
inkscape:connector-curvature="0" />
<path
d="m 46.656521,12.167234 18.055171,18.054184 a 6.6081919,6.6078288 0 0 1 -0.126303,9.352065 6.6804126,6.6800456 0 0 1 -8.233169,1.011048 l -7.944268,7.943843 6.463762,6.445343 a 6.9331851,6.9328042 0 0 1 5.741536,2.022073 l 28.057729,28.092294 a 6.9962797,6.9958953 0 0 1 -9.894222,9.893685 L 50.719018,66.907526 A 7.0595711,7.0591833 0 0 1 49.18433,59.270613 l -5.741527,-5.741238 -7.944298,7.943843 A 6.716523,6.7161541 0 0 1 25.134866,69.832263 L 7.079684,51.778091 a 6.716523,6.7161541 0 0 1 8.39566,-10.345064 L 36.31101,20.59853 a 6.716523,6.7161541 0 0 1 10.345612,-8.431329 z"

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -340,11 +340,11 @@
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
transform="translate(0,952.36218)"
id="right" />
id="left" />
<path
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
id="left"
id="right"
inkscape:connector-curvature="0" />
<path
sodipodi:type="star"

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -8,6 +8,7 @@
#include <QUrlQuery>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
#include <version_string.h>
DeckStatsInterface::DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent)
: QObject(parent), cardDatabase(_cardDatabase)
@@ -62,6 +63,7 @@ void DeckStatsInterface::analyzeDeck(DeckList *deck)
QNetworkRequest request(QUrl("https://deckstats.net/index.php"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
manager->post(request, data);
}

View File

@@ -8,6 +8,7 @@
#include <QUrlQuery>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
#include <version_string.h>
TappedOutInterface::TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent)
: QObject(parent), cardDatabase(_cardDatabase)
@@ -87,6 +88,7 @@ void TappedOutInterface::analyzeDeck(DeckList *deck)
QNetworkRequest request(QUrl("https://tappedout.net/mtg-decks/paste/"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
manager->post(request, data);
}

View File

@@ -6,6 +6,8 @@
#ifndef INTERFACE_JSON_DECK_PARSER_H
#define INTERFACE_JSON_DECK_PARSER_H
#include "../../../interface/deck_loader/card_node_function.h"
#include "../../../interface/deck_loader/deck_loader.h"
#include <QJsonArray>
@@ -48,7 +50,7 @@ public:
}
loader->getDeckList()->loadFromStream_Plain(outStream, false);
DeckLoader::resolveSetNameAndNumberToProviderID(loader->getDeckList());
loader->getDeckList()->forEachCard(CardNodeFunction::ResolveProviderId());
return loader;
}
@@ -95,7 +97,7 @@ public:
}
loader->getDeckList()->loadFromStream_Plain(outStream, false);
DeckLoader::resolveSetNameAndNumberToProviderID(loader->getDeckList());
loader->getDeckList()->forEachCard(CardNodeFunction::ResolveProviderId());
QJsonObject commandersObj = obj.value("commanders").toObject();
if (!commandersObj.isEmpty()) {

View File

@@ -14,6 +14,7 @@
#include <QtConcurrent>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <version_string.h>
#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"
@@ -39,7 +40,9 @@ void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool sav
void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
{
auto *nam = new QNetworkAccessManager(this);
QNetworkReply *reply = nam->get(QNetworkRequest(url));
auto request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
QNetworkReply *reply = nam->get(request);
if (saveResults) {
// This will write out to the file (used for spoiler.xml)

View File

@@ -13,7 +13,7 @@ QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart
SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / GenericQuery
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / FormatQuery / GenericQuery
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
@@ -22,8 +22,9 @@ CardSearch <- '[[' CardFilterString ']]'
CardFilterString <- (!']]'.)*
DeckNameQuery <- ([Dd] 'eck')? [Nn] 'ame'? [:] String
FileNameQuery <- [Ff] ('ile' 'name'?)? [:] String
FileNameQuery <- [Ff] ([Nn] / 'ile' ([Nn] 'ame')?) [:] String
PathQuery <- [Pp] 'ath'? [:] String
FormatQuery <- [Ff] 'ormat'? [:] String
GenericQuery <- String
@@ -118,12 +119,13 @@ static void setupParserRules()
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool {
int count = 0;
deck->deckLoader->getDeckList()->forEachCard([&](InnerDecklistNode *, const DecklistCardNode *node) {
auto cardNodes = deck->deckLoader->getDeckList()->getCardNodes();
for (auto node : cardNodes) {
auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName());
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
count += node->getNumber();
}
});
}
return numberMatcher(count);
};
};
@@ -156,6 +158,14 @@ static void setupParserRules()
};
};
search["FormatQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto format = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
auto gameFormat = deck->deckLoader->getDeckList()->getGameFormat();
return QString::compare(format, gameFormat, Qt::CaseInsensitive) == 0;
};
};
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {

View File

@@ -343,10 +343,7 @@ void DeckViewScene::rebuildTree()
if (!deck)
return;
InnerDecklistNode *listRoot = deck->getRoot();
for (int i = 0; i < listRoot->size(); i++) {
auto *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
for (auto *currentZone : deck->getZoneNodes()) {
DeckViewCardContainer *container = cardContainers.value(currentZone->getName(), 0);
if (!container) {
container = new DeckViewCardContainer(currentZone->getName());

View File

@@ -259,7 +259,7 @@ void DeckViewContainer::loadLocalDeck()
void DeckViewContainer::loadDeckFromFile(const QString &filePath)
{
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath);
DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(filePath);
DeckLoader deck(this);
bool success = deck.loadFromFile(filePath, fmt, true);

View File

@@ -5,6 +5,7 @@
#include "../player_actions.h"
#include "player_menu.h"
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
UtilityMenu::UtilityMenu(Player *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player)
@@ -65,15 +66,13 @@ void UtilityMenu::populatePredefinedTokensMenu()
return;
}
InnerDecklistNode *tokenZone =
dynamic_cast<InnerDecklistNode *>(_deck->getDeckList()->getRoot()->findChild(DECK_ZONE_TOKENS));
auto tokenCardNodes = _deck->getDeckList()->getCardNodes({DECK_ZONE_TOKENS});
if (tokenZone) {
if (!tokenZone->empty())
setEnabled(true);
if (!tokenCardNodes.isEmpty()) {
setEnabled(true);
for (int i = 0; i < tokenZone->size(); ++i) {
const QString tokenName = tokenZone->at(i)->getName();
for (int i = 0; i < tokenCardNodes.size(); ++i) {
const QString tokenName = tokenCardNodes[i]->getName();
predefinedTokens.append(tokenName);
QAction *a = addAction(tokenName);
if (i < 10) {

View File

@@ -263,7 +263,7 @@ void Player::deleteCard(CardItem *card)
}
}
// TODO: Does a player need a DeckLoader?
//! \todo Does a player need a DeckLoader?
void Player::setDeck(DeckLoader &_deck)
{
deck = new DeckLoader(this, _deck.getDeckList());

View File

@@ -10,6 +10,7 @@
#include <QNetworkReply>
#include <QThread>
#include <utility>
#include <version_string.h>
static constexpr int MAX_REQUESTS_PER_SEC = 10;
@@ -86,6 +87,7 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
}
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
if (!picDownload) {
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
}

View File

@@ -0,0 +1,40 @@
#include "card_node_function.h"
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
void CardNodeFunction::SetProviderIdToPreferred::operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
{
Q_UNUSED(node);
PrintingInfo preferredPrinting = CardDatabaseManager::query()->getPreferredPrinting(card->getName());
QString providerId = preferredPrinting.getUuid();
QString setShortName = preferredPrinting.getSet()->getShortName();
QString collectorNumber = preferredPrinting.getProperty("num");
card->setCardProviderId(providerId);
card->setCardCollectorNumber(collectorNumber);
card->setCardSetShortName(setShortName);
}
void CardNodeFunction::ClearPrintingData::operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
{
Q_UNUSED(node);
card->setCardSetShortName(nullptr);
card->setCardCollectorNumber(nullptr);
card->setCardProviderId(nullptr);
}
void CardNodeFunction::ResolveProviderId::operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
{
Q_UNUSED(node);
// Retrieve the providerId based on setName and collectorNumber
QString providerId =
CardDatabaseManager::getInstance()
->query()
->getSpecificPrinting(card->getName(), card->getCardSetShortName(), card->getCardCollectorNumber())
.getUuid();
// Set the providerId on the card
card->setCardProviderId(providerId);
}

View File

@@ -0,0 +1,39 @@
#ifndef COCKATRICE_DECK_FUNCTION_H
#define COCKATRICE_DECK_FUNCTION_H
class DecklistCardNode;
class InnerDecklistNode;
/**
* Functions to be used with DeckList::forEachCard
*/
namespace CardNodeFunction
{
/**
* @brief Sets the providerId of the card to the preferred printing.
*/
struct SetProviderIdToPreferred
{
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const;
};
/**
* @brief Clears all fields on the card related to the printing
*/
struct ClearPrintingData
{
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const;
};
/**
* @brief Sets the providerId of the card based on its set name and collector number.
*/
struct ResolveProviderId
{
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const;
};
} // namespace CardNodeFunction
#endif // COCKATRICE_DECK_FUNCTION_H

View File

@@ -0,0 +1,9 @@
#include "deck_file_format.h"
DeckFileFormat::Format DeckFileFormat::getFormatFromName(const QString &fileName)
{
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
return Cockatrice;
}
return PlainText;
}

View File

@@ -0,0 +1,36 @@
#ifndef COCKATRICE_DECK_FILE_FORMAT_H
#define COCKATRICE_DECK_FILE_FORMAT_H
#include <QString>
namespace DeckFileFormat
{
/**
* The deck file formats that Cockatrice supports.
*/
enum Format
{
/**
* Plaintext deck files, a format that is intended to be widely supported among different programs.
* This format does not support Cockatrice specific features such as banner cards or tags.
*/
PlainText,
/**
* This is cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
* Stored as .cod files.
*/
Cockatrice
};
/**
* Determines what deck file format the given filename corresponds to.
*
* @param fileName The filename
* @return The deck format
*/
Format getFormatFromName(const QString &fileName);
} // namespace DeckFileFormat
#endif // COCKATRICE_DECK_FILE_FORMAT_H

View File

@@ -33,7 +33,7 @@ DeckLoader::DeckLoader(QObject *parent, DeckList *_deckList) : QObject(parent),
{
}
bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest)
bool DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
@@ -42,17 +42,17 @@ bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool user
bool result = false;
switch (fmt) {
case PlainTextFormat:
case DeckFileFormat::PlainText:
result = deckList->loadFromFile_Plain(&file);
break;
case CockatriceFormat: {
case DeckFileFormat::Cockatrice: {
result = deckList->loadFromFile_Native(&file);
qCInfo(DeckLoaderLog) << "Loaded from" << fileName << "-" << result;
if (!result) {
qCInfo(DeckLoaderLog) << "Retrying as plain format";
file.seek(0);
result = deckList->loadFromFile_Plain(&file);
fmt = PlainTextFormat;
fmt = DeckFileFormat::PlainText;
}
break;
}
@@ -77,7 +77,7 @@ bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool user
return result;
}
bool DeckLoader::loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest)
bool DeckLoader::loadFromFileAsync(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest)
{
auto *watcher = new QFutureWatcher<bool>(this);
@@ -106,9 +106,9 @@ bool DeckLoader::loadFromFileAsync(const QString &fileName, FileFormat fmt, bool
}
switch (fmt) {
case PlainTextFormat:
case DeckFileFormat::PlainText:
return deckList->loadFromFile_Plain(&file);
case CockatriceFormat: {
case DeckFileFormat::Cockatrice: {
bool result = false;
result = deckList->loadFromFile_Native(&file);
if (!result) {
@@ -140,7 +140,7 @@ bool DeckLoader::loadFromRemote(const QString &nativeString, int remoteDeckId)
return result;
}
bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
bool DeckLoader::saveToFile(const QString &fileName, DeckFileFormat::Format fmt)
{
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
@@ -149,10 +149,10 @@ bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
bool result = false;
switch (fmt) {
case PlainTextFormat:
case DeckFileFormat::PlainText:
result = deckList->saveToFile_Plain(&file);
break;
case CockatriceFormat:
case DeckFileFormat::Cockatrice:
result = deckList->saveToFile_Native(&file);
qCInfo(DeckLoaderLog) << "Saving to " << fileName << "-" << result;
break;
@@ -172,7 +172,7 @@ bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
return result;
}
bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt)
bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, DeckFileFormat::Format fmt)
{
QFileInfo fileInfo(fileName);
if (!fileInfo.exists()) {
@@ -193,10 +193,10 @@ bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, FileFormat f
// Perform file modifications
switch (fmt) {
case PlainTextFormat:
case DeckFileFormat::PlainText:
result = deckList->saveToFile_Plain(&file);
break;
case CockatriceFormat:
case DeckFileFormat::Cockatrice:
deckList->setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
result = deckList->saveToFile_Native(&file);
break;
@@ -269,6 +269,20 @@ static QString toDecklistExportString(const DecklistCardNode *card)
return cardString;
}
/**
* Converts all cards in the list to their decklist export string and joins them into one string
*/
static QString toDecklistExportString(const QList<const DecklistCardNode *> &cardNodes)
{
QString result;
for (auto cardNode : cardNodes) {
result += toDecklistExportString(cardNode);
}
return result;
}
/**
* Export deck to decklist function, called to format the deck in a way to be sent to a server
*
@@ -279,29 +293,11 @@ QString DeckLoader::exportDeckToDecklist(const DeckList *deckList, DecklistWebsi
{
// Add the base url
QString deckString = "https://" + getDomainForWebsite(website) + "/?";
// Create two strings to pass to function
QString mainBoardCards, sideBoardCards;
// Set up the function to call
auto formatDeckListForExport = [&mainBoardCards, &sideBoardCards](const auto *node, const auto *card) {
// Get the card name
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
if (!dbCard || dbCard->getIsToken()) {
// If it's a token, we don't care about the card.
return;
}
// export all cards in zone
QString mainBoardCards = toDecklistExportString(deckList->getCardNodes({DECK_ZONE_MAIN}));
QString sideBoardCards = toDecklistExportString(deckList->getCardNodes({DECK_ZONE_SIDE}));
// Check if it's a sideboard card.
if (node->getName() == DECK_ZONE_SIDE) {
sideBoardCards += toDecklistExportString(card);
} else {
// If it's a mainboard card, do the same thing, but for the mainboard card string
mainBoardCards += toDecklistExportString(card);
}
};
// call our struct function for each card in the deck
deckList->forEachCard(formatDeckListForExport);
// Remove the extra return at the end of the last cards
mainBoardCards.chop(3);
sideBoardCards.chop(3);
@@ -316,112 +312,6 @@ QString DeckLoader::exportDeckToDecklist(const DeckList *deckList, DecklistWebsi
return deckString;
}
// This struct is here to support the forEachCard function call, defined in decklist.
// It requires a function to be called for each card, and it will set the providerId to the preferred printing.
struct SetProviderIdToPreferred
{
// Main operator for struct, allowing the foreachcard to work.
SetProviderIdToPreferred()
{
}
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
{
Q_UNUSED(node);
PrintingInfo preferredPrinting = CardDatabaseManager::query()->getPreferredPrinting(card->getName());
QString providerId = preferredPrinting.getUuid();
QString setShortName = preferredPrinting.getSet()->getShortName();
QString collectorNumber = preferredPrinting.getProperty("num");
card->setCardProviderId(providerId);
card->setCardCollectorNumber(collectorNumber);
card->setCardSetShortName(setShortName);
}
};
/**
* This function iterates through each card in the decklist and sets the providerId
* on each card based on its set name and collector number.
*
* @param deckList The decklist to modify
*/
void DeckLoader::setProviderIdToPreferredPrinting(const DeckList *deckList)
{
// Set up the struct to call.
SetProviderIdToPreferred setProviderIdToPreferred;
// Call the forEachCard method for each card in the deck
deckList->forEachCard(setProviderIdToPreferred);
}
/**
* Sets the providerId on each card in the decklist based on its set name and collector number.
*
* @param deckList The decklist to modify
*/
void DeckLoader::resolveSetNameAndNumberToProviderID(const DeckList *deckList)
{
auto setProviderId = [](const auto node, const auto card) {
Q_UNUSED(node);
// Retrieve the providerId based on setName and collectorNumber
QString providerId =
CardDatabaseManager::getInstance()
->query()
->getSpecificPrinting(card->getName(), card->getCardSetShortName(), card->getCardCollectorNumber())
.getUuid();
// Set the providerId on the card
card->setCardProviderId(providerId);
};
deckList->forEachCard(setProviderId);
}
// This struct is here to support the forEachCard function call, defined in decklist.
// It requires a function to be called for each card, and it will set the providerId.
struct ClearSetNameNumberAndProviderId
{
// Main operator for struct, allowing the foreachcard to work.
ClearSetNameNumberAndProviderId()
{
}
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
{
Q_UNUSED(node);
// Set the providerId on the card
card->setCardSetShortName(nullptr);
card->setCardCollectorNumber(nullptr);
card->setCardProviderId(nullptr);
}
};
/**
* Clears the set name and numbers on each card in the decklist.
*
* @param deckList The decklist to modify
*/
void DeckLoader::clearSetNamesAndNumbers(const DeckList *deckList)
{
auto clearSetNameAndNumber = [](const auto node, auto card) {
Q_UNUSED(node)
// Set the providerId on the card
card->setCardSetShortName(nullptr);
card->setCardCollectorNumber(nullptr);
card->setCardProviderId(nullptr);
};
deckList->forEachCard(clearSetNameAndNumber);
}
DeckLoader::FileFormat DeckLoader::getFormatFromName(const QString &fileName)
{
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
return CockatriceFormat;
}
return PlainTextFormat;
}
void DeckLoader::saveToClipboard(const DeckList *deckList, bool addComments, bool addSetNameAndNumber)
{
QString buffer;
@@ -441,9 +331,7 @@ bool DeckLoader::saveToStream_Plain(QTextStream &out,
}
// loop zones
for (int i = 0; i < deckList->getRoot()->size(); i++) {
const auto *zoneNode = dynamic_cast<InnerDecklistNode *>(deckList->getRoot()->at(i));
for (auto zoneNode : deckList->getZoneNodes()) {
saveToStream_DeckZone(out, zoneNode, addComments, addSetNameAndNumber);
// end of zone
@@ -564,12 +452,12 @@ bool DeckLoader::convertToCockatriceFormat(QString fileName)
bool result = false;
// Perform file modifications based on the detected format
switch (getFormatFromName(fileName)) {
case PlainTextFormat:
switch (DeckFileFormat::getFormatFromName(fileName)) {
case DeckFileFormat::PlainText:
// Save in Cockatrice's native format
result = deckList->saveToFile_Native(&file);
break;
case CockatriceFormat:
case DeckFileFormat::Cockatrice:
qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed.";
result = true;
break;
@@ -590,37 +478,14 @@ bool DeckLoader::convertToCockatriceFormat(QString fileName)
}
lastLoadInfo = {
.fileName = newFileName,
.fileFormat = CockatriceFormat,
.fileFormat = DeckFileFormat::Cockatrice,
};
}
return result;
}
QString DeckLoader::getCardZoneFromName(const QString &cardName, QString currentZoneName)
{
CardInfoPtr card = CardDatabaseManager::query()->getCardInfo(cardName);
if (card && card->getIsToken()) {
return DECK_ZONE_TOKENS;
}
return currentZoneName;
}
QString DeckLoader::getCompleteCardName(const QString &cardName)
{
if (CardDatabaseManager::getInstance()) {
ExactCard temp = CardDatabaseManager::query()->guessCard({cardName});
if (temp) {
return temp.getName();
}
}
return cardName;
}
void DeckLoader::printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node)
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)
{
const int totalColumns = 2;
@@ -703,12 +568,11 @@ void DeckLoader::printDeckList(QPrinter *printer, const DeckList *deckList)
cursor.insertText(deckList->getComments());
cursor.insertBlock(headerBlockFormat, headerCharFormat);
for (int i = 0; i < deckList->getRoot()->size(); i++) {
for (auto zoneNode : deckList->getZoneNodes()) {
cursor.insertHtml("<br><img src=theme:hr.jpg>");
// cursor.insertHtml("<hr>");
cursor.insertBlock(headerBlockFormat, headerCharFormat);
printDeckListNode(&cursor, dynamic_cast<InnerDecklistNode *>(deckList->getRoot()->at(i)));
printDeckListNode(&cursor, zoneNode);
}
doc.print(printer);

View File

@@ -7,14 +7,16 @@
#ifndef DECK_LOADER_H
#define DECK_LOADER_H
#include "loaded_deck.h"
#include <QLoggingCategory>
#include <QPrinter>
#include <QTextCursor>
#include <libcockatrice/deck_list/deck_list.h>
inline Q_LOGGING_CATEGORY(DeckLoaderLog, "deck_loader")
inline Q_LOGGING_CATEGORY(DeckLoaderLog, "deck_loader");
class DeckLoader : public QObject
class DeckLoader : public QObject
{
Q_OBJECT
signals:
@@ -22,27 +24,6 @@ signals:
void loadFinished(bool success);
public:
enum FileFormat
{
PlainTextFormat,
CockatriceFormat
};
/**
* @brief Information about where the deck was loaded from.
*
* For local decks, the remoteDeckId field will always be -1.
* For remote decks, fileName will be empty and fileFormat will always be CockatriceFormat
*/
struct LoadInfo
{
static constexpr int NON_REMOTE_ID = -1;
QString fileName = "";
FileFormat fileFormat = CockatriceFormat;
int remoteDeckId = NON_REMOTE_ID;
};
/**
* Supported file extensions for decklist files
*/
@@ -61,7 +42,7 @@ public:
private:
DeckList *deckList;
LoadInfo lastLoadInfo;
LoadedDeck::LoadInfo lastLoadInfo;
public:
DeckLoader(QObject *parent);
@@ -69,35 +50,29 @@ public:
DeckLoader(const DeckLoader &) = delete;
DeckLoader &operator=(const DeckLoader &) = delete;
const LoadInfo &getLastLoadInfo() const
const LoadedDeck::LoadInfo &getLastLoadInfo() const
{
return lastLoadInfo;
}
void setLastLoadInfo(const LoadInfo &info)
void setLastLoadInfo(const LoadedDeck::LoadInfo &info)
{
lastLoadInfo = info;
}
[[nodiscard]] bool hasNotBeenLoaded() const
{
return lastLoadInfo.fileName.isEmpty() && lastLoadInfo.remoteDeckId == LoadInfo::NON_REMOTE_ID;
return lastLoadInfo.isEmpty();
}
static void clearSetNamesAndNumbers(const DeckList *deckList);
static FileFormat getFormatFromName(const QString &fileName);
bool loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest = false);
bool loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest);
bool loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest = false);
bool loadFromFileAsync(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest);
bool loadFromRemote(const QString &nativeString, int remoteDeckId);
bool saveToFile(const QString &fileName, FileFormat fmt);
bool updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt);
bool saveToFile(const QString &fileName, DeckFileFormat::Format fmt);
bool updateLastLoadedTimestamp(const QString &fileName, DeckFileFormat::Format fmt);
static QString exportDeckToDecklist(const DeckList *deckList, DecklistWebsite website);
static void setProviderIdToPreferredPrinting(const DeckList *deckList);
static void resolveSetNameAndNumberToProviderID(const DeckList *deckList);
static void saveToClipboard(const DeckList *deckList, bool addComments = true, bool addSetNameAndNumber = true);
static bool saveToStream_Plain(QTextStream &out,
const DeckList *deckList,
@@ -119,7 +94,7 @@ public:
}
private:
static void printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node);
static void printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node);
static void saveToStream_DeckHeader(QTextStream &out, const DeckList *deckList);
static void saveToStream_DeckZone(QTextStream &out,
@@ -131,9 +106,6 @@ private:
QList<DecklistCardNode *> cards,
bool addComments = true,
bool addSetNameAndNumber = true);
[[nodiscard]] static QString getCardZoneFromName(const QString &cardName, QString currentZoneName);
[[nodiscard]] static QString getCompleteCardName(const QString &cardName);
};
#endif

View File

@@ -0,0 +1,11 @@
#include "loaded_deck.h"
bool LoadedDeck::LoadInfo::isEmpty() const
{
return fileName.isEmpty() && remoteDeckId == NON_REMOTE_ID;
}
bool LoadedDeck::isEmpty() const
{
return deckList.isEmpty() && lastLoadInfo.isEmpty();
}

View File

@@ -0,0 +1,39 @@
#ifndef COCKATRICE_LOADED_DECK_H
#define COCKATRICE_LOADED_DECK_H
#include "deck_file_format.h"
#include "libcockatrice/deck_list/deck_list.h"
#include <QString>
/**
* @brief Represents a deck that was loaded from somewhere.
* Contains the DeckList itself, as well as info about where it was loaded from.
*/
struct LoadedDeck
{
/**
* @brief Information about where the deck was loaded from.
*
* For local decks, the remoteDeckId field will always be -1.
* For remote decks, fileName will be empty and fileFormat will always be CockatriceFormat
*/
struct LoadInfo
{
static constexpr int NON_REMOTE_ID = -1;
QString fileName = "";
DeckFileFormat::Format fileFormat = DeckFileFormat::Cockatrice;
int remoteDeckId = NON_REMOTE_ID;
bool isEmpty() const;
};
DeckList deckList; ///< The decklist itself
LoadInfo lastLoadInfo; ///< info about where the deck was loaded from
bool isEmpty() const;
};
#endif // COCKATRICE_LOADED_DECK_H

View File

@@ -124,6 +124,12 @@ void DeckEditorDeckDockWidget::createDeckDock()
quickSettingsWidget->addSettingsWidget(showBannerCardCheckBox);
quickSettingsWidget->addSettingsWidget(showTagsWidgetCheckBox);
formatLabel = new QLabel(this);
formatComboBox = new QComboBox(this);
formatComboBox->addItem(tr("Loading Database..."));
formatComboBox->setEnabled(false); // Disable until loaded
commentsLabel = new QLabel();
commentsLabel->setObjectName("commentsLabel");
commentsEdit = new QTextEdit;
@@ -154,8 +160,10 @@ void DeckEditorDeckDockWidget::createDeckDock()
&DeckEditorDeckDockWidget::setBannerCard);
bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible());
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList());
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList()->getTags());
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible());
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this,
&DeckEditorDeckDockWidget::setTags);
activeGroupCriteriaLabel = new QLabel(this);
@@ -206,13 +214,16 @@ void DeckEditorDeckDockWidget::createDeckDock()
upperLayout->addWidget(commentsLabel, 1, 0);
upperLayout->addWidget(commentsEdit, 1, 1);
upperLayout->addWidget(bannerCardLabel, 2, 0);
upperLayout->addWidget(bannerCardComboBox, 2, 1);
upperLayout->addWidget(formatLabel, 2, 0);
upperLayout->addWidget(formatComboBox, 2, 1);
upperLayout->addWidget(deckTagsDisplayWidget, 3, 1);
upperLayout->addWidget(bannerCardLabel, 3, 0);
upperLayout->addWidget(bannerCardComboBox, 3, 1);
upperLayout->addWidget(activeGroupCriteriaLabel, 4, 0);
upperLayout->addWidget(activeGroupCriteriaComboBox, 4, 1);
upperLayout->addWidget(deckTagsDisplayWidget, 4, 1);
upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0);
upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1);
hashLabel1 = new QLabel();
hashLabel1->setObjectName("hashLabel1");
@@ -261,6 +272,46 @@ void DeckEditorDeckDockWidget::createDeckDock()
refreshShortcuts();
retranslateUi();
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&DeckEditorDeckDockWidget::initializeFormats);
if (CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok) {
initializeFormats();
}
}
void DeckEditorDeckDockWidget::initializeFormats()
{
QMap<QString, int> allFormats = CardDatabaseManager::query()->getAllFormatsWithCount();
formatComboBox->clear(); // Remove "Loading Database..."
formatComboBox->setEnabled(true);
// Populate with formats
formatComboBox->addItem("", "");
for (auto it = allFormats.constBegin(); it != allFormats.constEnd(); ++it) {
QString displayText = QString("%1").arg(it.key());
formatComboBox->addItem(displayText, it.key()); // store the raw key in itemData
}
if (!deckModel->getDeckList()->getGameFormat().isEmpty()) {
deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat());
formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat()));
} else {
// Ensure no selection is visible initially
formatComboBox->setCurrentIndex(-1);
}
connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
if (index >= 0) {
QString formatKey = formatComboBox->itemData(index).toString();
deckModel->setActiveFormat(formatKey);
} else {
deckModel->setActiveFormat(QString()); // clear format if deselected
}
emit deckModified();
});
}
ExactCard DeckEditorDeckDockWidget::getCurrentCard()
@@ -335,7 +386,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
// Collect unique (name, providerId) pairs
QSet<QPair<QString, QString>> bannerCardSet;
QList<DecklistCardNode *> cardsInDeck = deckModel->getDeckList()->getCardNodes();
QList<const DecklistCardNode *> cardsInDeck = deckModel->getDeckList()->getCardNodes();
for (auto currentCard : cardsInDeck) {
if (!CardDatabaseManager::query()->getCard(currentCard->toCardRef())) {
@@ -383,6 +434,13 @@ void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */)
emit deckModified();
}
void DeckEditorDeckDockWidget::setTags(const QStringList &tags)
{
deckModel->getDeckList()->setTags(tags);
deckEditor->setModified(true);
emit deckModified();
}
void DeckEditorDeckDockWidget::syncDeckListBannerCardWithComboBox()
{
auto [name, id] = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
@@ -451,12 +509,18 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
sortDeckModelToDeckView();
expandAll();
deckTagsDisplayWidget->setDeckList(deckModel->getDeckList());
deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags());
}
void DeckEditorDeckDockWidget::sortDeckModelToDeckView()
{
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat());
formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat()));
deckView->expandAll();
deckView->expandAll();
emit deckChanged();
}
DeckLoader *DeckEditorDeckDockWidget::getDeckLoader()
@@ -484,7 +548,7 @@ void DeckEditorDeckDockWidget::cleanDeck()
emit deckModified();
emit deckChanged();
updateBannerCardComboBox();
deckTagsDisplayWidget->setDeckList(deckModel->getDeckList());
deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags());
}
void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index)
@@ -715,6 +779,8 @@ void DeckEditorDeckDockWidget::retranslateUi()
showTagsWidgetCheckBox->setText(tr("Show tags selection menu"));
commentsLabel->setText(tr("&Comments:"));
activeGroupCriteriaLabel->setText(tr("Group by:"));
formatLabel->setText(tr("Format:"));
hashLabel1->setText(tr("Hash:"));
aIncrement->setText(tr("&Increment number"));

View File

@@ -69,6 +69,7 @@ public slots:
void actSwapCard();
void actRemoveCard();
void offsetCountAtIndex(const QModelIndex &idx, int offset);
void initializeFormats();
void expandAll();
signals:
@@ -100,6 +101,8 @@ private:
LineEditUnfocusable *hashLabel;
QLabel *activeGroupCriteriaLabel;
QComboBox *activeGroupCriteriaComboBox;
QLabel *formatLabel;
QComboBox *formatComboBox;
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
@@ -112,6 +115,7 @@ private slots:
void updateName(const QString &name);
void updateComments();
void setBannerCard(int);
void setTags(const QStringList &tags);
void syncDeckListBannerCardWithComboBox();
void updateHash();
void refreshShortcuts();

View File

@@ -23,8 +23,7 @@ QVariant DeckListStyleProxy::data(const QModelIndex &index, int role) const
if (role == Qt::BackgroundRole) {
if (isCard) {
const bool legal =
true; // TODO: Not implemented yet. QIdentityProxyModel::data(index, DeckRoles::IsLegalRole).toBool();
const bool legal = QIdentityProxyModel::data(index, DeckRoles::IsLegalRole).toBool();
int base = 255 - (index.row() % 2) * 30;
return legal ? QBrush(QColor(base, base, base)) : QBrush(QColor(255, base / 3, base / 3));
} else {

View File

@@ -109,7 +109,7 @@ void DlgCreateGame::sharedCtor()
gameSetupOptionsLayout->addWidget(startingLifeTotalLabel, 0, 0);
gameSetupOptionsLayout->addWidget(startingLifeTotalEdit, 0, 1);
gameSetupOptionsLayout->addWidget(shareDecklistsOnLoadCheckBox, 1, 0);
if (room->getUserInfo()->user_level() & ServerInfo_User::IsJudge) {
if (room && room->getUserInfo()->user_level() & ServerInfo_User::IsJudge) {
gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 2, 0);
} else {
createGameAsJudgeCheckBox->setChecked(false);

View File

@@ -59,7 +59,7 @@ DlgEditPassword::DlgEditPassword(QWidget *parent) : QDialog(parent)
void DlgEditPassword::actOk()
{
// TODO this stuff should be using qvalidators
//! \todo this stuff should be using qvalidators
if (newPasswordEdit->text().length() < 8) {
QMessageBox::critical(this, tr("Error"), tr("Your password is too short."));
return;

View File

@@ -121,7 +121,7 @@ void DlgForgotPasswordReset::actOk()
return;
}
// TODO this stuff should be using qvalidators
//! \todo this stuff should be using qvalidators
if (newpasswordEdit->text().length() < 8) {
QMessageBox::critical(this, tr("Error"), tr("Your password is too short."));
return;

View File

@@ -1,6 +1,7 @@
#include "dlg_load_deck_from_clipboard.h"
#include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/card_node_function.h"
#include "../../deck_loader/deck_loader.h"
#include "dlg_settings.h"
@@ -82,9 +83,9 @@ bool AbstractDlgDeckTextEdit::loadIntoDeck(DeckLoader *deckLoader) const
if (deckLoader->getDeckList()->loadFromStream_Plain(stream, true)) {
if (loadSetNameAndNumberCheckBox->isChecked()) {
DeckLoader::resolveSetNameAndNumberToProviderID(deckLoader->getDeckList());
deckLoader->getDeckList()->forEachCard(CardNodeFunction::ResolveProviderId());
} else {
DeckLoader::clearSetNamesAndNumbers(deckLoader->getDeckList());
deckLoader->getDeckList()->forEachCard(CardNodeFunction::ClearPrintingData());
}
return true;
}

View File

@@ -8,6 +8,7 @@
#include <QJsonObject>
#include <QMessageBox>
#include <QNetworkReply>
#include <version_string.h>
DlgLoadDeckFromWebsite::DlgLoadDeckFromWebsite(QWidget *parent) : QDialog(parent)
{
@@ -67,6 +68,7 @@ void DlgLoadDeckFromWebsite::accept()
}
QNetworkRequest request(QUrl(info.fullUrl));
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
QNetworkReply *reply = nam->get(request);
QEventLoop loop;
@@ -98,7 +100,7 @@ void DlgLoadDeckFromWebsite::accept()
DeckLoader *loader = new DeckLoader(this);
QTextStream stream(&deckText);
loader->getDeckList()->loadFromStream_Plain(stream, false);
DeckLoader::resolveSetNameAndNumberToProviderID(loader->getDeckList());
loader->getDeckList()->forEachCard(CardNodeFunction::ResolveProviderId());
deck = loader;
QDialog::accept();

View File

@@ -356,7 +356,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
void DlgRegister::actOk()
{
// TODO this stuff should be using qvalidators
//! \todo this stuff should be using qvalidators
if (passwordEdit->text().length() < 8) {
QMessageBox::critical(this, tr("Registration Warning"), tr("Your password is too short."));
return;

View File

@@ -1,5 +1,6 @@
#include "dlg_select_set_for_cards.h"
#include "../../deck_loader/card_node_function.h"
#include "../../deck_loader/deck_loader.h"
#include "../interface/widgets/cards/card_info_picture_widget.h"
#include "../interface/widgets/general/layout_containers/flow_widget.h"
@@ -177,7 +178,7 @@ void DlgSelectSetForCards::actOK()
void DlgSelectSetForCards::actClear()
{
emit deckAboutToBeModified(tr("Cleared all printing information."));
DeckLoader::clearSetNamesAndNumbers(model->getDeckList());
model->getDeckList()->forEachCard(CardNodeFunction::ClearPrintingData());
emit deckModified();
accept();
}
@@ -185,8 +186,8 @@ void DlgSelectSetForCards::actClear()
void DlgSelectSetForCards::actSetAllToPreferred()
{
emit deckAboutToBeModified(tr("Set all printings to preferred."));
DeckLoader::clearSetNamesAndNumbers(model->getDeckList());
DeckLoader::setProviderIdToPreferredPrinting(model->getDeckList());
model->getDeckList()->forEachCard(CardNodeFunction::ClearPrintingData());
model->getDeckList()->forEachCard(CardNodeFunction::SetProviderIdToPreferred());
emit deckModified();
accept();
}
@@ -227,7 +228,7 @@ QMap<QString, int> DlgSelectSetForCards::getSetsForCards()
if (!decklist)
return setCounts;
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
for (auto currentCard : cardsInDeck) {
CardInfoPtr infoPtr = CardDatabaseManager::query()->getCardInfo(currentCard->getName());
@@ -270,7 +271,7 @@ void DlgSelectSetForCards::updateCardLists()
if (!decklist)
return;
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
for (auto currentCard : cardsInDeck) {
bool found = false;
@@ -359,7 +360,7 @@ QMap<QString, QStringList> DlgSelectSetForCards::getCardsForSets()
if (!decklist)
return setCards;
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
for (auto currentCard : cardsInDeck) {
CardInfoPtr infoPtr = CardDatabaseManager::query()->getCardInfo(currentCard->getName());

View File

@@ -1913,7 +1913,7 @@ void DlgSettings::closeEvent(QCloseEvent *event)
}
if (!QDir(SettingsCache::instance().getDeckPath()).exists() || SettingsCache::instance().getDeckPath().isEmpty()) {
// TODO: Prompt to create it
//! \todo Prompt to create it
if (QMessageBox::critical(
this, tr("Error"),
tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"),
@@ -1924,7 +1924,7 @@ void DlgSettings::closeEvent(QCloseEvent *event)
}
if (!QDir(SettingsCache::instance().getPicsPath()).exists() || SettingsCache::instance().getPicsPath().isEmpty()) {
// TODO: Prompt to create it
//! \todo Prompt to create it
if (QMessageBox::critical(this, tr("Error"),
tr("The path to your card pictures directory is invalid. Would you like to go back "
"and set the correct path?"),

View File

@@ -23,7 +23,7 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
backgroundSourceDeck = new DeckLoader(this);
backgroundSourceDeck->loadFromFile(SettingsCache::instance().getDeckPath() + "background.cod",
DeckLoader::CockatriceFormat, false);
DeckFileFormat::Cockatrice, false);
gradientColors = extractDominantColors(background);
@@ -73,7 +73,7 @@ void HomeWidget::initializeBackgroundFromSource()
break;
case BackgroundSources::DeckFileArt:
backgroundSourceDeck->loadFromFile(SettingsCache::instance().getDeckPath() + "background.cod",
DeckLoader::CockatriceFormat, false);
DeckFileFormat::Cockatrice, false);
cardChangeTimer->start(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() * 1000);
break;
}

View File

@@ -311,7 +311,7 @@ int CardAmountWidget::countCardsInZone(const QString &deckZone)
return -1;
}
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes({deckZone});
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes({deckZone});
int count = 0;
for (auto currentCard : cardsInDeck) {

View File

@@ -206,7 +206,7 @@ void ChatView::appendMessage(QString message,
defaultFormat = QTextCharFormat();
if (!isUserMessage) {
if (messageType == Event_RoomSay::ChatHistory) {
defaultFormat.setForeground(Qt::gray); // FIXME : hardcoded color
defaultFormat.setForeground(Qt::gray); //! \todo hardcoded color
defaultFormat.setFontWeight(QFont::Light);
defaultFormat.setFontItalic(true);
static const QRegularExpression userNameRegex("^(\\[[^\\]]*\\]\\s)(\\S+):\\s");
@@ -229,7 +229,7 @@ void ChatView::appendMessage(QString message,
message.remove(0, pos.relativePosition - 2); // do not remove semicolon
}
} else {
defaultFormat.setForeground(Qt::darkGreen); // FIXME : hardcoded color
defaultFormat.setForeground(Qt::darkGreen); //! \todo hardcoded color
defaultFormat.setFontWeight(QFont::Bold);
}
}

View File

@@ -380,7 +380,7 @@ void AbstractTabDeckEditor::actOpenRecent(const QString &fileName)
*/
void AbstractTabDeckEditor::openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation)
{
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(fileName);
auto *l = new DeckLoader(this);
if (l->loadFromFile(fileName, fmt, true)) {
@@ -406,7 +406,7 @@ void AbstractTabDeckEditor::openDeckFromFile(const QString &fileName, DeckOpenLo
bool AbstractTabDeckEditor::actSaveDeck()
{
DeckLoader *const deck = getDeckLoader();
if (deck->getLastLoadInfo().remoteDeckId != DeckLoader::LoadInfo::NON_REMOTE_ID) {
if (deck->getLastLoadInfo().remoteDeckId != LoadedDeck::LoadInfo::NON_REMOTE_ID) {
QString deckString = deck->getDeckList()->writeToString_Native();
if (deckString.length() > MAX_FILE_LENGTH) {
QMessageBox::critical(this, tr("Error"), tr("Could not save remote deck"));
@@ -452,7 +452,7 @@ bool AbstractTabDeckEditor::actSaveDeckAs()
return false;
QString fileName = dialog.selectedFiles().at(0);
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(fileName);
if (!getDeckLoader()->saveToFile(fileName, fmt)) {
QMessageBox::critical(

View File

@@ -21,16 +21,16 @@ void ArchidektApiResponseCard::fromJson(const QJsonObject &json)
edition.fromJson(json.value("edition").toObject());
flavor = json.value("flavor").toString();
// TODO but not really important
// games = {""};
// options = {""};
//! \todo but not really important
//! \todo games = {""};
//! \todo options = {""};
scryfallImageHash = json.value("scryfallImageHash").toString();
oracleCard = json.value("oracleCard").toObject();
owned = json.value("owned").toInt();
pinnedStatus = json.value("pinnedStatus").toInt();
rarity = json.value("rarity").toString();
// TODO but not really important
// globalCategories = {""};
//! \todo but not really important
//! \todo globalCategories = {""};
}
void ArchidektApiResponseCard::debugPrint() const

View File

@@ -1,5 +1,6 @@
#include "archidekt_api_response_deck_display_widget.h"
#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"
@@ -68,7 +69,7 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset);
model->getDeckList()->loadFromStream_Plain(deckStream, false);
DeckLoader::resolveSetNameAndNumberToProviderID(model->getDeckList());
model->getDeckList()->forEachCard(CardNodeFunction::ResolveProviderId());
model->rebuildTree();

View File

@@ -12,6 +12,7 @@
#include <QNetworkReply>
#include <QPixmap>
#include <QWidget>
#include <version_string.h>
#define ARCHIDEKT_DEFAULT_IMAGE "https://storage.googleapis.com/topdekt-user/images/archidekt_deck_card_shadow.jpg"
@@ -82,6 +83,7 @@ ArchidektApiResponseDeckEntryDisplayWidget::ArchidektApiResponseDeckEntryDisplay
imageUrl = response.getFeatured().isEmpty() ? QUrl(ARCHIDEKT_DEFAULT_IMAGE) : QUrl(response.getFeatured());
QNetworkRequest req(imageUrl);
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
QNetworkReply *reply = imageNetworkManager->get(req);
// tag the reply with "this" so we know it belongs to us later

View File

@@ -22,6 +22,7 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
#include <libcockatrice/models/database/card/card_search_model.h>
#include <version_string.h>
TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
{
@@ -213,7 +214,7 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
minDeckSizeLogicCombo->addItems({"Exact", "", ""}); // Exact = unset, ≥ = GTE, ≤ = LTE
minDeckSizeLogicCombo->setCurrentIndex(1); // default GTE
connect(minDeckSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch);
connect(minDeckSizeSpin, qOverload<int>(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch);
connect(minDeckSizeLogicCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch);
// Page number
@@ -223,7 +224,7 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
pageSpin->setRange(1, 9999);
pageSpin->setValue(1);
connect(pageSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch);
connect(pageSpin, qOverload<int>(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch);
// Page display
currentPageDisplay = new QWidget(container);
@@ -426,18 +427,21 @@ void TabArchidekt::doSearchImmediate()
{
QString url = buildSearchUrl();
QNetworkRequest req{QUrl(url)};
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
networkManager->get(req);
}
void TabArchidekt::actNavigatePage(QString url)
{
QNetworkRequest request{QUrl(url)};
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
networkManager->get(request);
}
void TabArchidekt::getTopDecks()
{
QNetworkRequest request{QUrl(buildSearchUrl())};
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
networkManager->get(request);
}

View File

@@ -25,6 +25,7 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
#include <libcockatrice/models/database/card/card_search_model.h>
#include <version_string.h>
static bool canBeCommander(const CardInfoPtr &cardInfo)
{
@@ -166,6 +167,7 @@ void TabEdhRecMain::setCard(CardInfoPtr _cardToQuery, bool isCommander)
}
QNetworkRequest request{QUrl(url)};
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
networkManager->get(request);
}
@@ -173,6 +175,7 @@ void TabEdhRecMain::setCard(CardInfoPtr _cardToQuery, bool isCommander)
void TabEdhRecMain::actNavigatePage(QString url)
{
QNetworkRequest request{QUrl("https://json.edhrec.com/pages" + url + ".json")};
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
networkManager->get(request);
}
@@ -180,6 +183,7 @@ void TabEdhRecMain::actNavigatePage(QString url)
void TabEdhRecMain::getTopCards()
{
QNetworkRequest request{QUrl("https://json.edhrec.com/pages/top/year.json")};
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
networkManager->get(request);
}
@@ -187,6 +191,7 @@ void TabEdhRecMain::getTopCards()
void TabEdhRecMain::getTopCommanders()
{
QNetworkRequest request{QUrl("https://json.edhrec.com/pages/commanders/year.json")};
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
networkManager->get(request);
}
@@ -194,7 +199,7 @@ void TabEdhRecMain::getTopCommanders()
void TabEdhRecMain::getTopTags()
{
QNetworkRequest request{QUrl("https://json.edhrec.com/pages/tags.json")};
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
networkManager->get(request);
}

View File

@@ -242,7 +242,7 @@ void TabDeckStorage::actOpenLocalDeck()
QString filePath = localDirModel->filePath(curLeft);
auto deckLoader = new DeckLoader(this);
if (!deckLoader->loadFromFile(filePath, DeckLoader::CockatriceFormat, true))
if (!deckLoader->loadFromFile(filePath, DeckFileFormat::Cockatrice, true))
continue;
emit openDeckEditor(deckLoader);
@@ -308,7 +308,7 @@ void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPa
QFileInfo deckFileInfo(deckFile);
DeckLoader deck(this);
if (!deck.loadFromFile(filePath, DeckLoader::CockatriceFormat)) {
if (!deck.loadFromFile(filePath, DeckFileFormat::Cockatrice)) {
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
return;
}
@@ -493,7 +493,7 @@ void TabDeckStorage::downloadFinished(const Response &r,
QString filePath = extraData.toString();
DeckLoader deck(this, new DeckList(QString::fromStdString(resp.deck())));
deck.saveToFile(filePath, DeckLoader::CockatriceFormat);
deck.saveToFile(filePath, DeckFileFormat::Cockatrice);
}
void TabDeckStorage::actNewFolder()

View File

@@ -25,7 +25,7 @@ TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath)
{
auto deckLoader = new DeckLoader(this);
if (!deckLoader->loadFromFile(filePath, DeckLoader::getFormatFromName(filePath), true)) {
if (!deckLoader->loadFromFile(filePath, DeckFileFormat::getFormatFromName(filePath), true)) {
QMessageBox::critical(this, tr("Error"), tr("Could not open deck at %1").arg(filePath));
return;
}

View File

@@ -18,6 +18,16 @@ VisualDatabaseDisplayFilterSaveLoadWidget::VisualDatabaseDisplayFilterSaveLoadWi
layout = new QVBoxLayout(this);
setLayout(layout);
// Filter search input
searchInput = new QLineEdit(this);
layout->addWidget(searchInput);
connect(searchInput, &QLineEdit::textChanged, this, &VisualDatabaseDisplayFilterSaveLoadWidget::applySearchFilter);
// File list container
fileListWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
layout->addWidget(fileListWidget);
// Input for filter filename
filenameInput = new QLineEdit(this);
layout->addWidget(filenameInput);
@@ -25,11 +35,12 @@ VisualDatabaseDisplayFilterSaveLoadWidget::VisualDatabaseDisplayFilterSaveLoadWi
// Save button
saveButton = new QPushButton(this);
layout->addWidget(saveButton);
connect(saveButton, &QPushButton::clicked, this, &VisualDatabaseDisplayFilterSaveLoadWidget::saveFilter);
// Disable save if empty
saveButton->setEnabled(false);
connect(filenameInput, &QLineEdit::textChanged, this,
[this](const QString &text) { saveButton->setEnabled(!text.trimmed().isEmpty()); });
// File list container
fileListWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
layout->addWidget(fileListWidget);
connect(saveButton, &QPushButton::clicked, this, &VisualDatabaseDisplayFilterSaveLoadWidget::saveFilter);
refreshFilterList(); // Populate the file list on startup
retranslateUi();
@@ -37,6 +48,7 @@ VisualDatabaseDisplayFilterSaveLoadWidget::VisualDatabaseDisplayFilterSaveLoadWi
void VisualDatabaseDisplayFilterSaveLoadWidget::retranslateUi()
{
searchInput->setPlaceholderText(tr("Search filter..."));
saveButton->setText(tr("Save Filter"));
saveButton->setToolTip(tr("Save all currently applied filters to a file"));
filenameInput->setPlaceholderText(tr("Enter filename..."));
@@ -112,42 +124,36 @@ void VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter(const QString &filena
emit filterModel->layoutChanged();
}
void VisualDatabaseDisplayFilterSaveLoadWidget::applySearchFilter(const QString &text)
{
fileListWidget->clearLayout();
QString filter = text.trimmed();
QStringList filtered = allFilterFiles;
if (!filter.isEmpty()) {
filtered = filtered.filter(QRegularExpression(filter, QRegularExpression::CaseInsensitiveOption));
}
for (const QString &filename : filtered) {
FilterDisplayWidget *filterWidget = new FilterDisplayWidget(this, filename, filterModel);
fileListWidget->addWidget(filterWidget);
connect(filterWidget, &FilterDisplayWidget::filterLoadRequested, this,
&VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter);
connect(filterWidget, &FilterDisplayWidget::filterDeleted, this,
&VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList);
}
}
void VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList()
{
fileListWidget->clearLayout();
// Clear existing widgets
for (auto buttonPair : fileButtons) {
buttonPair.first->deleteLater();
buttonPair.second->deleteLater();
}
fileButtons.clear(); // Clear the list of buttons
fileButtons.clear();
// Refresh the filter file list
QDir dir(SettingsCache::instance().getFiltersPath());
QStringList filterFiles = dir.entryList(QStringList() << "*.json", QDir::Files, QDir::Name);
allFilterFiles = dir.entryList({"*.json"}, QDir::Files, QDir::Name);
// Loop through the filter files and create widgets for them
for (const QString &filename : filterFiles) {
bool alreadyAdded = false;
// Check if the widget for this filter file already exists to avoid duplicates
for (const auto &pair : fileButtons) {
if (pair.first->text() == filename) {
alreadyAdded = true;
break;
}
}
if (!alreadyAdded) {
// Create a new custom widget for the filter
FilterDisplayWidget *filterWidget = new FilterDisplayWidget(this, filename, filterModel);
fileListWidget->addWidget(filterWidget);
// Connect signals to handle loading and deletion
connect(filterWidget, &FilterDisplayWidget::filterLoadRequested, this,
&VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter);
connect(filterWidget, &FilterDisplayWidget::filterDeleted, this,
&VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList);
}
}
applySearchFilter(searchInput->text());
}

View File

@@ -27,6 +27,7 @@ public:
void saveFilter();
void loadFilter(const QString &filename);
void applySearchFilter(const QString &text);
void refreshFilterList();
void deleteFilter(const QString &filename, QPushButton *deleteButton);
@@ -37,9 +38,11 @@ private:
FilterTreeModel *filterModel;
QVBoxLayout *layout;
QLineEdit *searchInput;
FlowWidget *fileListWidget;
QLineEdit *filenameInput;
QPushButton *saveButton;
FlowWidget *fileListWidget;
QStringList allFilterFiles;
QMap<QString, QPair<QPushButton *, QPushButton *>> fileButtons;
};

View File

@@ -0,0 +1,205 @@
#include "visual_database_display_format_legality_filter_widget.h"
#include "../../../filters/filter_tree_model.h"
#include <QPushButton>
#include <QSpinBox>
#include <QTimer>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/filters/filter_tree.h>
VisualDatabaseDisplayFormatLegalityFilterWidget::VisualDatabaseDisplayFormatLegalityFilterWidget(
QWidget *parent,
FilterTreeModel *_filterModel)
: QWidget(parent), filterModel(_filterModel)
{
allFormatsWithCount = CardDatabaseManager::query()->getAllFormatsWithCount();
setMaximumHeight(75);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
layout = new QHBoxLayout(this);
setLayout(layout);
layout->setContentsMargins(0, 1, 0, 1);
layout->setSpacing(1);
layout->setAlignment(Qt::AlignTop);
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
layout->addWidget(flowWidget);
// Create the spinbox
spinBox = new QSpinBox(this);
spinBox->setMinimum(1);
spinBox->setMaximum(getMaxMainTypeCount()); // Set the max value dynamically
spinBox->setValue(150);
layout->addWidget(spinBox);
connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), this,
&VisualDatabaseDisplayFormatLegalityFilterWidget::updateFormatButtonsVisibility);
// Create the toggle button for Exact Match/Includes mode
toggleButton = new QPushButton(this);
toggleButton->setCheckable(true);
layout->addWidget(toggleButton);
connect(toggleButton, &QPushButton::toggled, this,
&VisualDatabaseDisplayFormatLegalityFilterWidget::updateFilterMode);
connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() {
QTimer::singleShot(100, this, &VisualDatabaseDisplayFormatLegalityFilterWidget::syncWithFilterModel);
});
createFormatButtons(); // Populate buttons initially
updateFilterMode(false); // Initialize toggle button text
retranslateUi();
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::retranslateUi()
{
spinBox->setToolTip(tr("Do not display formats with less than this amount of cards in the database"));
toggleButton->setToolTip(tr("Filter mode (AND/OR/NOT conjunctions of filters)"));
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::createFormatButtons()
{
// Iterate through main types and create buttons
for (auto it = allFormatsWithCount.begin(); it != allFormatsWithCount.end(); ++it) {
auto *button = new QPushButton(it.key(), flowWidget);
button->setCheckable(true);
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }"
"QPushButton:checked { background-color: green; color: white; }");
flowWidget->addWidget(button);
formatButtons[it.key()] = button;
// Connect toggle signal
connect(button, &QPushButton::toggled, this,
[this, mainType = it.key()](bool checked) { handleFormatToggled(mainType, checked); });
}
updateFormatButtonsVisibility(); // Ensure visibility is updated initially
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::updateFormatButtonsVisibility()
{
int threshold = spinBox->value(); // Get the current spinbox value
// Iterate through buttons and hide/disable those below the threshold
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
bool visible = allFormatsWithCount[it.key()] >= threshold;
it.value()->setVisible(visible);
it.value()->setEnabled(visible);
}
}
int VisualDatabaseDisplayFormatLegalityFilterWidget::getMaxMainTypeCount() const
{
int maxCount = 1;
for (auto it = allFormatsWithCount.begin(); it != allFormatsWithCount.end(); ++it) {
maxCount = qMax(maxCount, it.value());
}
return maxCount;
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::handleFormatToggled(const QString &format, bool active)
{
activeFormats[format] = active;
if (formatButtons.contains(format)) {
formatButtons[format]->setChecked(active);
}
updateFormatFilter();
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::updateFormatFilter()
{
// Clear existing filters related to main type
filterModel->blockSignals(true);
filterModel->filterTree()->blockSignals(true);
filterModel->clearFiltersOfType(CardFilter::Attr::AttrFormat);
if (exactMatchMode) {
// Exact Match: Only selected main types are allowed
QSet<QString> selectedTypes;
for (const auto &type : activeFormats.keys()) {
if (activeFormats[type]) {
selectedTypes.insert(type);
}
}
if (!selectedTypes.isEmpty()) {
// Require all selected types (TypeAnd)
for (const auto &type : selectedTypes) {
QString typeString = type;
filterModel->addFilter(
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrFormat));
}
// Exclude any other types (TypeAndNot)
for (const auto &type : formatButtons.keys()) {
if (!selectedTypes.contains(type)) {
QString typeString = type;
filterModel->addFilter(
new CardFilter(typeString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrFormat));
}
}
}
} else {
// Default Includes Mode (TypeOr) - match any selected main types
for (const auto &type : activeFormats.keys()) {
if (activeFormats[type]) {
QString typeString = type;
filterModel->addFilter(
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrFormat));
}
}
}
filterModel->blockSignals(false);
filterModel->filterTree()->blockSignals(false);
emit filterModel->filterTree()->changed();
emit filterModel->layoutChanged();
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::updateFilterMode(bool checked)
{
exactMatchMode = checked;
toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes"));
updateFormatFilter();
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::syncWithFilterModel()
{
// Temporarily block signals for each button to prevent toggling while updating button states
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
it.value()->blockSignals(true);
}
// Uncheck all buttons
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
it.value()->setChecked(false);
}
// Get active filters for main types
QSet<QString> activeTypes;
for (const auto &filter : filterModel->getFiltersOfType(CardFilter::AttrFormat)) {
if (filter->type() == CardFilter::Type::TypeAnd) {
activeTypes.insert(filter->term());
}
}
// Check the buttons for active types
for (const auto &type : activeTypes) {
activeFormats[type] = true;
if (formatButtons.contains(type)) {
formatButtons[type]->setChecked(true);
}
}
// Re-enable signal emissions for each button
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
it.value()->blockSignals(false);
}
// Update the visibility of buttons
updateFormatButtonsVisibility();
}

View File

@@ -0,0 +1,43 @@
#ifndef COCKATRICE_VISUAL_DATABASE_DISPLAY_FORMAT_LEGALITY_FILTER_WIDGET_H
#define COCKATRICE_VISUAL_DATABASE_DISPLAY_FORMAT_LEGALITY_FILTER_WIDGET_H
#include "../../../filters/filter_tree_model.h"
#include "../general/layout_containers/flow_widget.h"
#include <QMap>
#include <QPushButton>
#include <QSpinBox>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWidget>
class VisualDatabaseDisplayFormatLegalityFilterWidget : public QWidget
{
Q_OBJECT
public:
explicit VisualDatabaseDisplayFormatLegalityFilterWidget(QWidget *parent, FilterTreeModel *filterModel);
void retranslateUi();
void createFormatButtons();
void updateFormatButtonsVisibility();
int getMaxMainTypeCount() const;
void handleFormatToggled(const QString &format, bool active);
void updateFormatFilter();
void updateFilterMode(bool checked);
void syncWithFilterModel();
private:
FilterTreeModel *filterModel;
QMap<QString, int> allFormatsWithCount;
QSpinBox *spinBox;
QHBoxLayout *layout;
FlowWidget *flowWidget;
QPushButton *toggleButton; // Mode switch button
QMap<QString, bool> activeFormats; // Track active filters
QMap<QString, QPushButton *> formatButtons; // Store toggle buttons
bool exactMatchMode = false; // Toggle between "Exact Match" and "Includes"
};
#endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_FORMAT_LEGALITY_FILTER_WIDGET_H

View File

@@ -33,7 +33,7 @@ VisualDatabaseDisplayMainTypeFilterWidget::VisualDatabaseDisplayMainTypeFilterWi
spinBox->setMaximum(getMaxMainTypeCount()); // Set the max value dynamically
spinBox->setValue(150);
layout->addWidget(spinBox);
connect(spinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), this,
&VisualDatabaseDisplayMainTypeFilterWidget::updateMainTypeButtonsVisibility);
// Create the toggle button for Exact Match/Includes mode

View File

@@ -51,7 +51,7 @@ VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWi
void VisualDatabaseDisplayNameFilterWidget::retranslateUi()
{
searchBox->setPlaceholderText(tr("Filter by name..."));
searchBox->setPlaceholderText(tr("Filter by name... (Exact match)"));
loadFromDeckButton->setText(tr("Load from Deck"));
loadFromDeckButton->setToolTip(tr("Apply all card names in currently loaded deck as exact match name filters"));
loadFromClipboardButton->setText(tr("Load from Clipboard"));
@@ -68,7 +68,7 @@ void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
if (!decklist)
return;
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
for (auto currentCard : cardsInDeck) {
createNameFilter(currentCard->getName());
@@ -123,14 +123,14 @@ void VisualDatabaseDisplayNameFilterWidget::updateFilterModel()
{
// Clear existing name filters
emit filterModel->layoutAboutToBeChanged();
filterModel->clearFiltersOfType(CardFilter::Attr::AttrName);
filterModel->clearFiltersOfType(CardFilter::Attr::AttrNameExact);
filterModel->blockSignals(true);
filterModel->filterTree()->blockSignals(true);
for (const auto &name : activeFilters.keys()) {
QString nameString = name;
filterModel->addFilter(new CardFilter(nameString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrName));
filterModel->addFilter(new CardFilter(nameString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrNameExact));
}
filterModel->blockSignals(false);
@@ -146,7 +146,7 @@ void VisualDatabaseDisplayNameFilterWidget::updateFilterModel()
void VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel()
{
QStringList currentFilters;
for (const auto &filter : filterModel->getFiltersOfType(CardFilter::Attr::AttrName)) {
for (const auto &filter : filterModel->getFiltersOfType(CardFilter::Attr::AttrNameExact)) {
if (filter->type() == CardFilter::Type::TypeOr) {
currentFilters.append(filter->term());
}

View File

@@ -27,7 +27,7 @@ VisualDatabaseDisplayRecentSetFilterSettingsWidget::VisualDatabaseDisplayRecentS
filterToMostRecentSetsAmount->setMaximum(100);
filterToMostRecentSetsAmount->setValue(
SettingsCache::instance().getVisualDatabaseDisplayFilterToMostRecentSetsAmount());
connect(filterToMostRecentSetsAmount, QOverload<int>::of(&QSpinBox::valueChanged), &SettingsCache::instance(),
connect(filterToMostRecentSetsAmount, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance(),
&SettingsCache::setVisualDatabaseDisplayFilterToMostRecentSetsAmount);
layout->addWidget(filterToMostRecentSetsCheckBox);

View File

@@ -26,7 +26,7 @@ VisualDatabaseDisplaySubTypeFilterWidget::VisualDatabaseDisplaySubTypeFilterWidg
spinBox->setMaximum(getMaxSubTypeCount());
spinBox->setValue(150);
layout->addWidget(spinBox);
connect(spinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), this,
&VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeButtonsVisibility);
// Create search box

View File

@@ -206,6 +206,7 @@ void VisualDatabaseDisplayWidget::initialize()
saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel);
nameFilterWidget = new VisualDatabaseDisplayNameFilterWidget(this, deckEditor, filterModel);
mainTypeFilterWidget = new VisualDatabaseDisplayMainTypeFilterWidget(this, filterModel);
formatLegalityWidget = new VisualDatabaseDisplayFormatLegalityFilterWidget(this, filterModel);
subTypeFilterWidget = new VisualDatabaseDisplaySubTypeFilterWidget(this, filterModel);
setFilterWidget = new VisualDatabaseDisplaySetFilterWidget(this, filterModel);
@@ -223,6 +224,7 @@ void VisualDatabaseDisplayWidget::initialize()
filterContainerLayout->addWidget(quickFilterSubTypeWidget);
filterContainerLayout->addWidget(quickFilterSetWidget);
filterContainerLayout->addWidget(mainTypeFilterWidget);
filterContainerLayout->addWidget(formatLegalityWidget);
searchLayout->addWidget(colorFilterWidget);
searchLayout->addWidget(clearFilterWidget);

View File

@@ -17,6 +17,7 @@
#include "../utility/custom_line_edit.h"
#include "visual_database_display_color_filter_widget.h"
#include "visual_database_display_filter_save_load_widget.h"
#include "visual_database_display_format_legality_filter_widget.h"
#include "visual_database_display_main_type_filter_widget.h"
#include "visual_database_display_name_filter_widget.h"
#include "visual_database_display_set_filter_widget.h"
@@ -91,6 +92,7 @@ private:
SettingsButtonWidget *quickFilterNameWidget;
VisualDatabaseDisplayNameFilterWidget *nameFilterWidget;
VisualDatabaseDisplayMainTypeFilterWidget *mainTypeFilterWidget;
VisualDatabaseDisplayFormatLegalityFilterWidget *formatLegalityWidget;
SettingsButtonWidget *quickFilterSubTypeWidget;
VisualDatabaseDisplaySubTypeFilterWidget *subTypeFilterWidget;
SettingsButtonWidget *quickFilterSetWidget;

View File

@@ -2,7 +2,9 @@
#include "../tabs/visual_deck_editor/tab_deck_editor_visual.h"
VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent)
#include <libcockatrice/utility/qt_utils.h>
VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent) : QWidget(parent)
{
groupAndSortLayout = new QHBoxLayout(this);
groupAndSortLayout->setAlignment(Qt::AlignLeft);
@@ -11,23 +13,19 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent)
groupByLabel = new QLabel(this);
groupByComboBox = new QComboBox(this);
if (auto visualDeckEditorWidget = qobject_cast<VisualDeckEditorWidget *>(parent)) {
if (auto tabWidget = qobject_cast<TabDeckEditorVisualTabWidget *>(visualDeckEditorWidget)) {
// Inside a central widget QWidget container inside TabDeckEditorVisual
if (auto tab = qobject_cast<TabDeckEditorVisual *>(tabWidget->parent()->parent())) {
auto originalBox = tab->getDeckDockWidget()->getGroupByComboBox();
groupByComboBox->setModel(originalBox->model());
groupByComboBox->setModelColumn(originalBox->modelColumn());
// Original -> clone
connect(originalBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int index) { groupByComboBox->setCurrentIndex(index); });
if (auto tab = QtUtils::findParentOfType<TabDeckEditorVisual>(this)) {
auto originalBox = tab->getDeckDockWidget()->getGroupByComboBox();
groupByComboBox->setModel(originalBox->model());
groupByComboBox->setModelColumn(originalBox->modelColumn());
// Clone -> original
connect(groupByComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
[originalBox](int index) { originalBox->setCurrentIndex(index); });
}
}
// Original -> clone
connect(originalBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int index) { groupByComboBox->setCurrentIndex(index); });
// Clone -> original
connect(groupByComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
[originalBox](int index) { originalBox->setCurrentIndex(index); });
} else {
groupByComboBox->addItem(
tr(qPrintable(DeckListModelGroupCriteria::toString(DeckListModelGroupCriteria::MAIN_TYPE))),

View File

@@ -24,9 +24,9 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare
handSizeSpinBox = new QSpinBox(this);
handSizeSpinBox->setValue(SettingsCache::instance().getVisualDeckEditorSampleHandSize());
handSizeSpinBox->setMinimum(1);
connect(handSizeSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), &SettingsCache::instance(),
connect(handSizeSpinBox, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance(),
&SettingsCache::setVisualDeckEditorSampleHandSize);
connect(handSizeSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
connect(handSizeSpinBox, qOverload<int>(&QSpinBox::valueChanged), this,
&VisualDeckEditorSampleHandWidget::updateDisplay);
resetAndHandSizeLayout->addWidget(handSizeSpinBox);
@@ -84,7 +84,7 @@ QList<ExactCard> VisualDeckEditorSampleHandWidget::getRandomCards(int amountToGe
if (!decklist)
return randomCards;
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes({DECK_ZONE_MAIN});
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes({DECK_ZONE_MAIN});
// Collect all cards in the main deck, allowing duplicates based on their count
for (auto currentCard : cardsInDeck) {

View File

@@ -13,8 +13,8 @@
#include <QHBoxLayout>
#include <QMessageBox>
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList)
: QWidget(_parent), deckList(nullptr)
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags)
: QWidget(_parent), currentTags(_tags)
{
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
@@ -27,16 +27,14 @@ DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_par
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
if (_deckList) {
setDeckList(_deckList);
}
layout->addWidget(flowWidget);
refreshTags();
}
void DeckPreviewDeckTagsDisplayWidget::setDeckList(DeckList *_deckList)
void DeckPreviewDeckTagsDisplayWidget::setTags(const QStringList &_tags)
{
deckList = _deckList;
currentTags = _tags;
refreshTags();
}
@@ -44,7 +42,7 @@ void DeckPreviewDeckTagsDisplayWidget::refreshTags()
{
flowWidget->clearLayout();
for (const QString &tag : deckList->getTags()) {
for (const QString &tag : currentTags) {
flowWidget->addWidget(new DeckPreviewTagDisplayWidget(this, tag));
}
@@ -71,7 +69,45 @@ static QStringList getAllFiles(const QString &filePath)
return allFiles;
}
bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
/**
* Gets all tags that appear in the deck folder
*/
static QStringList findAllKnownTags()
{
QStringList allFiles = getAllFiles(SettingsCache::instance().getDeckPath());
QStringList knownTags;
auto loader = DeckLoader(nullptr);
for (const QString &file : allFiles) {
loader.loadFromFile(file, DeckFileFormat::getFormatFromName(file), false);
QStringList tags = loader.getDeckList()->getTags();
knownTags.append(tags);
knownTags.removeDuplicates();
}
return knownTags;
}
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
{
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
// If we're the child of a DeckPreviewWidget, then we need to handle conversion
auto *deckPreviewWidget = qobject_cast<DeckPreviewWidget *>(parentWidget());
bool canAddTags = promptFileConversionIfRequired(deckPreviewWidget);
if (canAddTags) {
QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags();
execTagDialog(knownTags);
}
} else {
// If we're the child of an AbstractTabDeckEditor, then we don't bother with conversion
QStringList knownTags = findAllKnownTags();
execTagDialog(knownTags);
}
}
static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
{
QFileInfo fileInfo(filePath);
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
@@ -86,98 +122,70 @@ bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
return true; // Safe to proceed
}
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
static void convertFileToCockatriceFormat(DeckPreviewWidget *deckPreviewWidget)
{
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
auto *deckPreviewWidget = qobject_cast<DeckPreviewWidget *>(parentWidget());
QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags();
QStringList activeTags = deckList->getTags();
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastLoadInfo().fileName;
deckPreviewWidget->refreshBannerCardText();
}
bool canAddTags = true;
/**
* Checks if the deck's file format supports tags.
* If not, then prompt the user for file conversion.
* @return whether the resulting file can support adding tags
*/
bool DeckPreviewDeckTagsDisplayWidget::promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget)
{
if (DeckFileFormat::getFormatFromName(deckPreviewWidget->filePath) == DeckFileFormat::Cockatrice) {
return true;
}
if (DeckLoader::getFormatFromName(deckPreviewWidget->filePath) != DeckLoader::CockatriceFormat) {
canAddTags = false;
// Retrieve saved preference if the prompt is disabled
if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) {
if (SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) {
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
return;
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastLoadInfo().fileName;
deckPreviewWidget->refreshBannerCardText();
canAddTags = true;
}
} else {
// Show the dialog to the user
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
if (conversionDialog.exec() == QDialog::Accepted) {
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
return;
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastLoadInfo().fileName;
deckPreviewWidget->refreshBannerCardText();
canAddTags = true;
if (conversionDialog.dontAskAgain()) {
SettingsCache::instance().setVisualDeckStoragePromptForConversion(false);
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(true);
}
} else {
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(false);
if (conversionDialog.dontAskAgain()) {
SettingsCache::instance().setVisualDeckStoragePromptForConversion(false);
} else {
SettingsCache::instance().setVisualDeckStoragePromptForConversion(true);
}
}
}
// Retrieve saved preference if the prompt is disabled
if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) {
if (!SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) {
return false;
}
if (canAddTags) {
DeckPreviewTagDialog dialog(knownTags, activeTags);
if (dialog.exec() == QDialog::Accepted) {
QStringList updatedTags = dialog.getActiveTags();
deckList->setTags(updatedTags);
deckPreviewWidget->deckLoader->saveToFile(deckPreviewWidget->filePath, DeckLoader::CockatriceFormat);
refreshTags();
}
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) {
return false;
}
} else if (parentWidget()) {
// If we're the child of an AbstractTabDeckEditor, we are buried under a ton of childWidgets in the
// DeckInfoDock.
QWidget *currentParent = parentWidget();
while (currentParent) {
if (qobject_cast<AbstractTabDeckEditor *>(currentParent)) {
break;
}
currentParent = currentParent->parentWidget();
}
if (qobject_cast<AbstractTabDeckEditor *>(currentParent)) {
auto *deckEditor = qobject_cast<AbstractTabDeckEditor *>(currentParent);
QStringList knownTags;
QStringList allFiles = getAllFiles(SettingsCache::instance().getDeckPath());
DeckLoader loader(this);
for (const QString &file : allFiles) {
loader.loadFromFile(file, DeckLoader::getFormatFromName(file), false);
QStringList tags = loader.getDeckList()->getTags();
knownTags.append(tags);
knownTags.removeDuplicates();
}
QStringList activeTags = deckList->getTags();
convertFileToCockatriceFormat(deckPreviewWidget);
return true;
}
DeckPreviewTagDialog dialog(knownTags, activeTags);
if (dialog.exec() == QDialog::Accepted) {
QStringList updatedTags = dialog.getActiveTags();
deckList->setTags(updatedTags);
deckEditor->setModified(true);
refreshTags();
}
// Show the dialog to the user
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
if (conversionDialog.exec() != QDialog::Accepted) {
SettingsCache::instance().setVisualDeckStoragePromptForConversion(!conversionDialog.dontAskAgain());
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(false);
return false;
}
// Try to convert file
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) {
return false;
}
convertFileToCockatriceFormat(deckPreviewWidget);
if (conversionDialog.dontAskAgain()) {
SettingsCache::instance().setVisualDeckStoragePromptForConversion(false);
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(true);
}
return true;
}
void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTags)
{
DeckPreviewTagDialog dialog(knownTags, currentTags);
if (dialog.exec() == QDialog::Accepted) {
QStringList updatedTags = dialog.getActiveTags();
if (updatedTags != currentTags) {
setTags(updatedTags);
emit tagsChanged(updatedTags);
}
}
}
}

View File

@@ -12,21 +12,31 @@
#include <QWidget>
inline bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath);
class DeckPreviewWidget;
class DeckPreviewDeckTagsDisplayWidget : public QWidget
{
Q_OBJECT
public:
explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList);
void setDeckList(DeckList *_deckList);
void refreshTags();
DeckList *deckList;
QStringList currentTags;
FlowWidget *flowWidget;
public:
explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags);
void setTags(const QStringList &_tags);
void refreshTags();
public slots:
void openTagEditDlg();
private:
bool promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget);
void execTagDialog(const QStringList &knownTags);
signals:
/**
* Emitted when the tags have changed due to user interaction.
* @param tags The new list of tags.
*/
void tagsChanged(const QStringList &tags);
};
#endif // DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H

View File

@@ -32,7 +32,7 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
many deck loads have finished already and if we've loaded all decks and THEN load all the tags at once. */
connect(deckLoader, &DeckLoader::loadFinished, visualDeckStorageWidget->tagFilterWidget,
&VisualDeckStorageTagFilterWidget::refreshTags);
deckLoader->loadFromFileAsync(filePath, DeckLoader::getFormatFromName(filePath), false);
deckLoader->loadFromFileAsync(filePath, DeckFileFormat::getFormatFromName(filePath), false);
bannerCardDisplayWidget =
new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled);
@@ -83,7 +83,8 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
setFilePath(deckLoader->getLastLoadInfo().fileName);
colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity());
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader->getDeckList());
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader->getDeckList()->getTags());
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, &DeckPreviewWidget::setTags);
bannerCardLabel = new QLabel(this);
bannerCardLabel->setObjectName("bannerCardLabel");
@@ -234,7 +235,7 @@ void DeckPreviewWidget::updateBannerCardComboBox()
// Prepare the new items with deduplication
QSet<QPair<QString, QString>> bannerCardSet;
QList<DecklistCardNode *> cardsInDeck = deckLoader->getDeckList()->getCardNodes();
QList<const DecklistCardNode *> cardsInDeck = deckLoader->getDeckList()->getCardNodes();
for (auto currentCard : cardsInDeck) {
for (int k = 0; k < currentCard->getNumber(); ++k) {
@@ -287,7 +288,7 @@ void DeckPreviewWidget::setBannerCard(int /* changedIndex */)
auto [name, id] = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
CardRef cardRef = {name, id};
deckLoader->getDeckList()->setBannerCard(cardRef);
deckLoader->saveToFile(filePath, DeckLoader::getFormatFromName(filePath));
deckLoader->saveToFile(filePath, DeckFileFormat::getFormatFromName(filePath));
bannerCardDisplayWidget->setCard(CardDatabaseManager::query()->getCard(cardRef));
}
@@ -307,6 +308,12 @@ void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewC
emit deckLoadRequested(filePath);
}
void DeckPreviewWidget::setTags(const QStringList &tags)
{
deckLoader->getDeckList()->setTags(tags);
deckLoader->saveToFile(filePath, DeckFileFormat::Cockatrice);
}
QMenu *DeckPreviewWidget::createRightClickMenu()
{
auto *menu = new QMenu(this);
@@ -379,7 +386,7 @@ void DeckPreviewWidget::actRenameDeck()
// write change
deckLoader->getDeckList()->setName(newName);
deckLoader->saveToFile(filePath, DeckLoader::getFormatFromName(filePath));
deckLoader->saveToFile(filePath, DeckFileFormat::getFormatFromName(filePath));
// update VDS
refreshBannerCardText();
@@ -409,7 +416,7 @@ void DeckPreviewWidget::actRenameFile()
return;
}
DeckLoader::LoadInfo lastLoadInfo = deckLoader->getLastLoadInfo();
LoadedDeck::LoadInfo lastLoadInfo = deckLoader->getLastLoadInfo();
lastLoadInfo.fileName = newFilePath;
deckLoader->setLastLoadInfo(lastLoadInfo);

View File

@@ -72,6 +72,8 @@ private:
void addSetBannerCardMenu(QMenu *menu);
private slots:
void setTags(const QStringList &tags);
void actRenameDeck();
void actRenameFile();
void actDeleteFile();

View File

@@ -61,10 +61,10 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
unusedColorIdentitiesOpacitySpinBox->setMaximum(100);
unusedColorIdentitiesOpacitySpinBox->setValue(
SettingsCache::instance().getVisualDeckStorageUnusedColorIdentitiesOpacity());
connect(unusedColorIdentitiesOpacitySpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
connect(unusedColorIdentitiesOpacitySpinBox, qOverload<int>(&QSpinBox::valueChanged), this,
&VisualDeckStorageQuickSettingsWidget::unusedColorIdentitiesOpacityChanged);
connect(unusedColorIdentitiesOpacitySpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
&SettingsCache::instance(), &SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity);
connect(unusedColorIdentitiesOpacitySpinBox, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance(),
&SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity);
unusedColorIdentitiesOpacityLabel->setBuddy(unusedColorIdentitiesOpacitySpinBox);

View File

@@ -5,6 +5,10 @@
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
static const QList<AllowedCount> kConstructedCounts = {{4, "legal"}, {0, "banned"}};
static const QList<AllowedCount> kSingletonCounts = {{1, "legal"}, {0, "banned"}};
class CardDatabaseConverter : public CardDatabase
{
public:
@@ -23,7 +27,73 @@ public:
bool saveCardDatabase(const QString &fileName)
{
CockatriceXml4Parser parser(new NoopCardPreferenceProvider());
return parser.saveToFile(sets, cards, fileName);
return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName);
}
FormatRulesNameMap createDefaultMagicFormats()
{
// Predefined common exceptions
CardCondition superTypeIsBasic;
superTypeIsBasic.field = "type";
superTypeIsBasic.matchType = "contains";
superTypeIsBasic.value = "Basic Land";
ExceptionRule basicLands;
basicLands.conditions.append(superTypeIsBasic);
CardCondition anyNumberAllowed;
anyNumberAllowed.field = "text";
anyNumberAllowed.matchType = "contains";
anyNumberAllowed.value = "A deck can have any number of";
ExceptionRule mayContainAnyNumber;
mayContainAnyNumber.conditions.append(anyNumberAllowed);
// Map to store default rules
FormatRulesNameMap defaultFormatRulesNameMap;
// ----------------- Helper lambda to create format -----------------
auto makeFormat = [&](const QString &name, int minDeck = 60, int maxDeck = -1, int maxSideboardSize = 15,
const QList<AllowedCount> &allowedCounts = kConstructedCounts) -> FormatRulesPtr {
FormatRulesPtr f(new FormatRules);
f->formatName = name;
f->allowedCounts = allowedCounts;
f->minDeckSize = minDeck;
f->maxDeckSize = maxDeck;
f->maxSideboardSize = maxSideboardSize;
f->exceptions.append(basicLands);
f->exceptions.append(mayContainAnyNumber);
defaultFormatRulesNameMap.insert(name.toLower(), f);
return f;
};
// ----------------- Standard formats -----------------
makeFormat("Standard");
makeFormat("Modern");
makeFormat("Legacy");
makeFormat("Pioneer");
makeFormat("Historic");
makeFormat("Timeless");
makeFormat("Future");
makeFormat("OldSchool");
makeFormat("Premodern");
makeFormat("Pauper");
makeFormat("Penny");
// ----------------- Singleton formats -----------------
makeFormat("Commander", 100, 100, 15, kSingletonCounts);
makeFormat("Duel", 100, 100, 15, kSingletonCounts);
makeFormat("Brawl", 60, 60, 15, kSingletonCounts);
makeFormat("StandardBrawl", 60, 60, 15, kSingletonCounts);
makeFormat("Oathbreaker", 60, 60, 15, kSingletonCounts);
makeFormat("PauperCommander", 100, 100, 15, kSingletonCounts);
makeFormat("Predh", 100, 100, 15, kSingletonCounts);
// ----------------- Restricted formats -----------------
makeFormat("Vintage", 60, -1, 15, {{4, "legal"}, {1, "restricted"}, {0, "banned"}});
return defaultFormatRulesNameMap;
}
};

View File

@@ -1,5 +1,7 @@
@page developer_reference Developer Reference
- @subpage logging
- @subpage primer_cards
- @subpage card_database_schema_and_parsing

View File

@@ -0,0 +1,184 @@
@page logging Logging
Cockatrice uses QtLogging from the QtCore module for its logging. See
the [official documentation](https://doc.qt.io/qt-6/qtlogging.html) for further details.
# Log Message Pattern
Any message logged through the QtLogging system automatically conforms to this message pattern:
Generic:
```
[<timestamp> <log_level>] [<class:function>] - <message> [<filename>:<line_no>]
```
Example:
```
[2025-12-05 14:48:25.908 I] [MainWindow::startupConfigCheck] - Startup: found config with current version [window_main.cpp:951]
```
For more information, see [Logging Setup](#logging-setup).
# Log Level and Categories
\note The default log level for the application is info.
This means that you should only use qInfo() in production-level code if you are truly sure that this message is
beneficial to end-users and other developers. As a general rule, if your functionality logs to info more than twice in
response to a user interaction, you are advised to consider moving some of these logs down to the debug level.
\warning You are strongly advised to avoid the use of the generic logging macros (e.g. qDebug(), qInfo(), qWarn()).
\note You should instead use the corresponding category logging macros (qCDebug(), qCInfo(), qCWarn()) and define
logging
categories for your log statements.
Example:
```c++
in .h
inline Q_LOGGING_CATEGORY(ExampleCategory, "cockatrice_example_category");
inline Q_LOGGING_CATEGORY(ExampleSubCategory, "cockatrice_example_category.sub_category");
in .cpp
qCInfo(ExampleCategory) << "Info level logs are usually sent through the main category"
qCDebug(ExampleSubCategory) << "Debug level logs are permitted their own category to allow selective silencing"
```
For more information on how to enable or disable logging categories,
see [Logging Configuration](#logging-configuration).
# Logging Configuration
For configuring our logging, we use the qtlogging.ini, located under cockatrice/resources/config/qtlogging.ini, which is
baked into the application in release version and set as the QT_LOGGING_CONF environment variable in main.cpp.
```c++
#ifdef Q_OS_APPLE
// <build>/cockatrice/cockatrice.app/Contents/MacOS/cockatrice
const QByteArray configPath = "../../../qtlogging.ini";
#elif defined(Q_OS_UNIX)
// <build>/cockatrice/cockatrice
const QByteArray configPath = "./qtlogging.ini";
#elif defined(Q_OS_WIN)
// <build>/cockatrice/Debug/cockatrice.exe
const QByteArray configPath = "../qtlogging.ini";
#else
const QByteArray configPath = "";
#endif
if (!qEnvironmentVariableIsSet(("QT_LOGGING_CONF"))) {
// Set the QT_LOGGING_CONF environment variable
qputenv("QT_LOGGING_CONF", configPath);
}
```
For more information on how to use this file and on how Qt evaluates which logging rules/file to use, please
see the [official Qt documentation](https://doc.qt.io/qt-6/qloggingcategory.html#configuring-categories).
Some examples:
```
# Turn off all logging except everything from card_picture_loader and all sub categories
[Rules]
# The default log level is info
*.debug = false
*.info = false
*.warning = false
*.critical = false
*.fatal = false
card_picture_loader.* = true
```
```
# Turn off all logging except info level logs from card_picture_loader and all sub categories
[Rules]
# The default log level is info
*.debug = false
*.info = false
*.warning = false
*.critical = false
*.fatal = false
card_picture_loader.*.info = true
```
```
[Rules]
# Turn on debug level logs for card_picture_loader but keep logging for sub categories suppressed
*.debug = false
card_picture_loader.debug = true
```
```
[Rules]
# Turn on all logs for worker subcategory of card_picture_loader
*.debug = false
card_picture_loader.worker = true
```
```
[Rules]
# Turn off some noisy and irrelevant startup logging for local development
*.debug = false
qt_translator = false
window_main.* = false
release_channel = false
spoiler_background_updater = false
theme_manager = false
sound_engine = false
tapped_out_interface = false
card_database = false
card_database.loading = false
card_database.loading.success_or_failure = true
cockatrice_xml.* = false
```
# Logging Setup
This is achieved through our logging setup in @ref main.cpp, where we set the message pattern and install a custom
logger which replaces the full file path at the end with just the file name (Qt shows the full and quite lengthy path by
default).
```c++
qSetMessagePattern(
"\033[0m[%{time yyyy-MM-dd h:mm:ss.zzz} "
"%{if-debug}\033[36mD%{endif}%{if-info}\033[32mI%{endif}%{if-warning}\033[33mW%{endif}%{if-critical}\033[31mC%{"
"endif}%{if-fatal}\033[1;31mF%{endif}\033[0m] [%{function}] - %{message} [%{file}:%{line}]");
QApplication app(argc, argv);
QObject::connect(&app, &QApplication::lastWindowClosed, &app, &QApplication::quit);
qInstallMessageHandler(CockatriceLogger);
```
```c++
static void CockatriceLogger(QtMsgType type, const QMessageLogContext &ctx, const QString &message)
{
QString logMessage = qFormatLogMessage(type, ctx, message);
// Regular expression to match the full path in the square brackets and extract only the filename and line number
QRegularExpression regex(R"(\[(?:.:)?[\/\\].*[\/\\]([^\/\\]+\:\d+)\])");
QRegularExpressionMatch match = regex.match(logMessage);
if (match.hasMatch()) {
// Extract the filename and line number (e.g., "main.cpp:211")
QString filenameLine = match.captured(1);
// Replace the full path in square brackets with just the filename and line number
logMessage.replace(match.captured(0), QString("[%1]").arg(filenameLine));
}
Logger::getInstance().log(type, ctx, logMessage);
}
```

View File

@@ -41,6 +41,8 @@ add_library(
libcockatrice/card/relation/card_relation.cpp
libcockatrice/card/set/card_set.cpp
libcockatrice/card/set/card_set_list.cpp
libcockatrice/card/format/format_legality_rules.cpp
libcockatrice/card/format/format_legality_rules.h
)
target_include_directories(

View File

@@ -1,6 +1,7 @@
#ifndef CARD_INFO_H
#define CARD_INFO_H
#include "format/format_legality_rules.h"
#include "printing/printing_info.h"
#include <QDate>
@@ -22,10 +23,12 @@ class ICardDatabaseParser;
typedef QSharedPointer<CardInfo> CardInfoPtr;
typedef QSharedPointer<CardSet> CardSetPtr;
typedef QSharedPointer<FormatRules> FormatRulesPtr;
typedef QMap<QString, QList<PrintingInfo>> SetToPrintingsMap;
typedef QHash<QString, CardInfoPtr> CardNameMap;
typedef QHash<QString, CardSetPtr> SetNameMap;
typedef QHash<QString, FormatRulesPtr> FormatRulesNameMap;
Q_DECLARE_METATYPE(CardInfoPtr)

View File

@@ -199,3 +199,8 @@ void CardDatabase::notifyEnabledSetsChanged()
// inform the carddatabasemodels that they need to re-check their list of cards
emit cardDatabaseEnabledSetsChanged();
}
void CardDatabase::addFormat(FormatRulesPtr format)
{
formats.insert(format->formatName.toLower(), format);
}

View File

@@ -42,6 +42,8 @@ protected:
/// Sets indexed by short name
SetNameMap sets;
FormatRulesNameMap formats;
/// Loader responsible for file discovery and parsing
CardDatabaseLoader *loader;
@@ -141,6 +143,8 @@ public slots:
*/
void addSet(CardSetPtr set);
void addFormat(FormatRulesPtr format);
/** @brief Loads card databases from configured paths. */
void loadCardDatabases();

View File

@@ -23,6 +23,7 @@ CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
// connect parser outputs to the database adders
connect(p, &ICardDatabaseParser::addCard, database, &CardDatabase::addCard, Qt::DirectConnection);
connect(p, &ICardDatabaseParser::addSet, database, &CardDatabase::addSet, Qt::DirectConnection);
connect(p, &ICardDatabaseParser::addFormat, database, &CardDatabase::addFormat, Qt::DirectConnection);
}
// when SettingsCache's path changes, trigger reloads
@@ -149,6 +150,6 @@ bool CardDatabaseLoader::saveCustomTokensToFile()
}
}
availableParsers.first()->saveToFile(tmpSets, tmpCards, fileName);
availableParsers.first()->saveToFile(FormatRulesNameMap(), tmpSets, tmpCards, fileName);
return true;
}

View File

@@ -341,4 +341,27 @@ QMap<QString, int> CardDatabaseQuerier::getAllSubCardTypesWithCount() const
}
return typeCounts;
}
FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const
{
return db->formats.value(formatName.toLower());
}
QMap<QString, int> CardDatabaseQuerier::getAllFormatsWithCount() const
{
QMap<QString, int> formatCounts;
for (const auto &card : db->cards.values()) {
QStringList allProps = card->getProperties();
for (const QString &prop : allProps) {
if (prop.startsWith("format-")) {
QString formatName = prop.mid(QStringLiteral("format-").size());
formatCounts[formatName]++;
}
}
}
return formatCounts;
}

View File

@@ -214,6 +214,8 @@ public:
* @return Map of subtype string to count.
*/
[[nodiscard]] QMap<QString, int> getAllSubCardTypesWithCount() const;
FormatRulesPtr getFormat(const QString &formatName) const;
QMap<QString, int> getAllFormatsWithCount() const;
private:
const CardDatabase *db; //!< Card database used for all lookups.

View File

@@ -38,6 +38,7 @@ public:
/**
* @brief Saves card and set data to a file.
* @param _formats
* @param sets Map of sets to save.
* @param cards Map of cards to save.
* @param fileName Target file path.
@@ -45,7 +46,8 @@ public:
* @param sourceVersion Optional version string of the source.
* @return true if save succeeded.
*/
virtual bool saveToFile(SetNameMap sets,
virtual bool saveToFile(FormatRulesNameMap _formats,
SetNameMap sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl = "unknown",
@@ -79,6 +81,8 @@ signals:
/** Emitted when a set is loaded from the database. */
void addSet(CardSetPtr set);
void addFormat(FormatRulesPtr format);
};
Q_DECLARE_INTERFACE(ICardDatabaseParser, "ICardDatabaseParser")

View File

@@ -438,12 +438,15 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in
return xml;
}
bool CockatriceXml3Parser::saveToFile(SetNameMap _sets,
bool CockatriceXml3Parser::saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl,
const QString &sourceVersion)
{
Q_UNUSED(_formats);
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
return false;

View File

@@ -46,7 +46,8 @@ public:
/**
* @brief Save sets and cards back to an XML3 file.
*/
bool saveToFile(SetNameMap _sets,
bool saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl = "unknown",

View File

@@ -6,6 +6,7 @@
#include <QDebug>
#include <QFile>
#include <QXmlStreamReader>
#include <libcockatrice/card/format/format_legality_rules.h>
#include <version_string.h>
#define COCKATRICE_XML4_TAGNAME "cockatrice_carddatabase"
@@ -60,7 +61,9 @@ void CockatriceXml4Parser::parseFile(QIODevice &device)
}
auto xmlName = xml.name().toString();
if (xmlName == "sets") {
if (xmlName == "formats") {
loadFormats(xml);
} else if (xmlName == "sets") {
loadSetsFromXml(xml);
} else if (xmlName == "cards") {
loadCardsFromXml(xml);
@@ -78,6 +81,116 @@ void CockatriceXml4Parser::parseFile(QIODevice &device)
}
}
static QSharedPointer<FormatRules> parseFormat(QXmlStreamReader &xml)
{
auto rulesPtr = FormatRulesPtr(new FormatRules());
if (xml.attributes().hasAttribute("formatName")) {
rulesPtr->formatName = xml.attributes().value("formatName").toString();
}
while (!xml.atEnd()) {
auto token = xml.readNext();
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "format") {
break;
}
if (token != QXmlStreamReader::StartElement) {
continue;
}
QString xmlName = xml.name().toString();
if (xmlName == "minDeckSize") {
rulesPtr->minDeckSize = xml.readElementText().toInt();
} else if (xmlName == "maxDeckSize") {
QString text = xml.readElementText();
rulesPtr->maxDeckSize = text.toInt();
} else if (xmlName == "maxSideboardSize") {
rulesPtr->maxSideboardSize = xml.readElementText().toInt();
} else if (xmlName == "allowedCounts") {
while (!xml.atEnd()) {
token = xml.readNext();
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "allowedCounts") {
break;
}
if (token == QXmlStreamReader::StartElement && xml.name().toString() == "count") {
AllowedCount c;
QString maxAttr = xml.attributes().value("max").toString();
c.max = (maxAttr == "unlimited") ? -1 : maxAttr.toInt();
c.label = xml.readElementText().trimmed();
rulesPtr->allowedCounts.append(c);
}
}
} else if (xmlName == "exceptions") {
while (!xml.atEnd()) {
token = xml.readNext();
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exceptions") {
break;
}
if (token == QXmlStreamReader::StartElement && xml.name().toString() == "exception") {
ExceptionRule ex;
while (!xml.atEnd()) {
token = xml.readNext();
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exception") {
break;
}
if (token == QXmlStreamReader::StartElement) {
QString ename = xml.name().toString();
if (ename == "maxCopies") {
QString text = xml.readElementText();
ex.maxCopies = (text == "unlimited") ? -1 : text.toInt();
} else if (ename == "cardCondition") {
CardCondition cond;
cond.field = xml.attributes().value("field").toString();
cond.matchType = xml.attributes().value("match").toString();
cond.value = xml.attributes().value("value").toString();
ex.conditions.append(cond);
xml.skipCurrentElement();
} else {
xml.skipCurrentElement();
}
}
}
rulesPtr->exceptions.append(ex);
}
}
} else {
xml.skipCurrentElement();
}
}
return rulesPtr;
}
void CockatriceXml4Parser::loadFormats(QXmlStreamReader &xml)
{
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
if (xml.name().toString() == "format") {
auto rulesPtr = parseFormat(xml);
emit addFormat(rulesPtr);
}
}
}
void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml)
{
while (!xml.atEnd()) {
@@ -273,6 +386,59 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
}
}
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const QSharedPointer<FormatRules> &rulesPtr)
{
if (rulesPtr.isNull()) {
qCWarning(CockatriceXml4Log) << "&operator<< FormatRules is nullptr";
return xml;
}
const FormatRules &rules = *rulesPtr;
xml.writeStartElement("format");
if (!rules.formatName.isEmpty()) {
xml.writeAttribute("formatName", rules.formatName);
}
xml.writeTextElement("minDeckSize", QString::number(rules.minDeckSize));
xml.writeTextElement("maxDeckSize", rules.maxDeckSize >= 0 ? QString::number(rules.maxDeckSize) : "0");
xml.writeTextElement("maxSideboardSize", QString::number(rules.maxSideboardSize));
if (!rules.allowedCounts.isEmpty()) {
xml.writeStartElement("allowedCounts");
for (const AllowedCount &c : rules.allowedCounts) {
xml.writeStartElement("count");
xml.writeAttribute("max", c.max == -1 ? "unlimited" : QString::number(c.max));
xml.writeCharacters(c.label);
xml.writeEndElement(); // count
}
xml.writeEndElement(); // allowedCounts
}
if (!rules.exceptions.isEmpty()) {
xml.writeStartElement("exceptions");
for (const ExceptionRule &ex : rules.exceptions) {
xml.writeStartElement("exception");
xml.writeTextElement("maxCopies", ex.maxCopies == -1 ? "unlimited" : QString::number(ex.maxCopies));
for (const CardCondition &cond : ex.conditions) {
xml.writeStartElement("cardCondition");
xml.writeAttribute("field", cond.field);
xml.writeAttribute("match", cond.matchType);
xml.writeAttribute("value", cond.value);
xml.writeEndElement(); // cardCondition
}
xml.writeEndElement(); // exception
}
xml.writeEndElement(); // exceptions
}
xml.writeEndElement(); // format
return xml;
}
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set)
{
if (set.isNull()) {
@@ -399,7 +565,8 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in
return xml;
}
bool CockatriceXml4Parser::saveToFile(SetNameMap _sets,
bool CockatriceXml4Parser::saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl,
@@ -426,6 +593,14 @@ bool CockatriceXml4Parser::saveToFile(SetNameMap _sets,
xml.writeTextElement("sourceVersion", sourceVersion);
xml.writeEndElement();
if (_formats.count() > 0) {
xml.writeStartElement("formats");
for (FormatRulesPtr format : _formats) {
xml << format;
}
xml.writeEndElement();
}
if (_sets.count() > 0) {
xml.writeStartElement("sets");
for (CardSetPtr set : _sets) {

View File

@@ -49,7 +49,8 @@ public:
/**
* @brief Save sets and cards back to an XML4 file.
*/
bool saveToFile(SetNameMap _sets,
bool saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl = "unknown",
@@ -72,6 +73,7 @@ private:
*/
void loadCardsFromXml(QXmlStreamReader &xml);
void loadFormats(QXmlStreamReader &xml);
/**
* @brief Load all <set> elements from the XML stream.
* @param xml The open QXmlStreamReader positioned at the <sets> element.

View File

@@ -0,0 +1,53 @@
#include "format_legality_rules.h"
#include <libcockatrice/card/card_info.h>
bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond)
{
CardMatchType type = matchTypeFromString(cond.matchType);
QString fieldValue;
if (cond.field == "name") {
fieldValue = card.getName();
} else if (cond.field == "text") {
fieldValue = card.getText();
} else {
fieldValue = card.getProperty(cond.field);
}
switch (type) {
case CardMatchType::Equals:
return fieldValue == cond.value;
case CardMatchType::NotEquals:
return fieldValue != cond.value;
case CardMatchType::Contains:
return fieldValue.contains(cond.value, Qt::CaseInsensitive);
case CardMatchType::NotContains:
return !fieldValue.contains(cond.value, Qt::CaseInsensitive);
case CardMatchType::Regex: {
QRegularExpression re(cond.value, QRegularExpression::CaseInsensitiveOption);
return re.match(fieldValue).hasMatch();
}
default:
return false;
}
}
bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule)
{
for (const CardCondition &cond : rule.conditions) {
if (!cardMatchesCondition(card, cond)) {
return false; // all conditions must match
}
}
return true;
}
bool cardHasAnyException(const CardInfo &card, const FormatRules &format)
{
for (const ExceptionRule &rule : format.exceptions) {
if (exceptionAppliesToCard(card, rule)) {
return true;
}
}
return false;
}

View File

@@ -0,0 +1,73 @@
#ifndef COCKATRICE_FORMAT_LEGALITY_RULES_H
#define COCKATRICE_FORMAT_LEGALITY_RULES_H
#include <QRegularExpression>
#include <QSharedPointer>
#include <QString>
class CardInfo;
using CardInfoPtr = QSharedPointer<CardInfo>;
struct CardCondition
{
QString field; // e.g. "type", "maintype", "text"
QString matchType; // "contains", "equals", "regex", "notContains", etc.
QString value; // e.g. "Basic Land"
};
struct AllowedCount
{
int max = 0; // 4, 1, 0, or -1 for unlimited
QString label; // "legal", "restricted", "banned"
};
struct ExceptionRule
{
QList<CardCondition> conditions; // All must match
int maxCopies = -1; // -1 = unlimited
};
struct FormatRules
{
QString formatName;
int minDeckSize = 60;
int maxDeckSize = -1; // -1 = unlimited
int maxSideboardSize = 15;
QList<AllowedCount> allowedCounts;
QList<ExceptionRule> exceptions; // Cards allowed to break maxCopies
};
enum class CardMatchType
{
Equals,
NotEquals,
Contains,
NotContains,
Regex
};
// convert string to enum
inline CardMatchType matchTypeFromString(const QString &str)
{
if (str == "equals")
return CardMatchType::Equals;
if (str == "notEquals")
return CardMatchType::NotEquals;
if (str == "contains")
return CardMatchType::Contains;
if (str == "notContains")
return CardMatchType::NotContains;
if (str == "regex")
return CardMatchType::Regex;
return CardMatchType::Equals; // fallback default
}
bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond);
bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule);
bool cardHasAnyException(const CardInfo &card, const FormatRules &format);
#endif // COCKATRICE_FORMAT_LEGALITY_RULES_H

View File

@@ -27,6 +27,8 @@ add_library(
libcockatrice/deck_list/tree/inner_deck_list_node.cpp
libcockatrice/deck_list/deck_list.cpp
libcockatrice/deck_list/deck_list_history_manager.cpp
libcockatrice/deck_list/deck_list_node_tree.cpp
libcockatrice/deck_list/deck_list_node_tree.h
)
add_dependencies(libcockatrice_deck_list libcockatrice_protocol)

View File

@@ -83,26 +83,23 @@ bool DeckList::Metadata::isEmpty() const
}
DeckList::DeckList()
{
root = new InnerDecklistNode;
}
DeckList::DeckList(const DeckList &other)
: metadata(other.metadata), sideboardPlans(other.sideboardPlans), root(new InnerDecklistNode(other.getRoot())),
cachedDeckHash(other.cachedDeckHash)
{
}
DeckList::DeckList(const QString &nativeString)
{
root = new InnerDecklistNode;
loadFromString_Native(nativeString);
}
DeckList::DeckList(const Metadata &metadata,
const DecklistNodeTree &tree,
const QMap<QString, SideboardPlan *> &sideboardPlans)
: metadata(metadata), sideboardPlans(sideboardPlans), tree(tree)
{
}
DeckList::~DeckList()
{
delete root;
QMapIterator<QString, SideboardPlan *> i(sideboardPlans);
while (i.hasNext())
delete i.next().value();
@@ -136,6 +133,8 @@ bool DeckList::readElement(QXmlStreamReader *xml)
metadata.lastLoadedTimestamp = xml->readElementText();
} else if (childName == "deckname") {
metadata.name = xml->readElementText();
} else if (childName == "format") {
metadata.gameFormat = xml->readElementText();
} else if (childName == "comments") {
metadata.comments = xml->readElementText();
} else if (childName == "bannerCard") {
@@ -150,8 +149,7 @@ bool DeckList::readElement(QXmlStreamReader *xml)
}
}
} else if (childName == "zone") {
InnerDecklistNode *newZone = getZoneObjFromName(xml->attributes().value("name").toString());
newZone->readElement(xml);
tree.readZoneElement(xml);
} else if (childName == "sideboard_plan") {
SideboardPlan *newSideboardPlan = new SideboardPlan;
if (newSideboardPlan->readElement(xml)) {
@@ -170,6 +168,7 @@ void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metadata)
{
xml->writeTextElement("lastLoadedTimestamp", metadata.lastLoadedTimestamp);
xml->writeTextElement("deckname", metadata.name);
xml->writeTextElement("format", metadata.gameFormat);
xml->writeStartElement("bannerCard");
xml->writeAttribute("providerId", metadata.bannerCard.providerId);
xml->writeCharacters(metadata.bannerCard.name);
@@ -192,9 +191,7 @@ void DeckList::write(QXmlStreamWriter *xml) const
writeMetadata(xml, metadata);
// Write zones
for (int i = 0; i < root->size(); i++) {
root->at(i)->writeElement(xml);
}
tree.write(xml);
// Write sideboard plans
QMapIterator<QString, SideboardPlan *> i(sideboardPlans);
@@ -453,25 +450,13 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata)
QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
// make new entry in decklist
new DecklistCardNode(cardName, amount, getZoneObjFromName(zoneName), -1, setCode, collectorNumber);
tree.addCard(cardName, amount, zoneName, -1, setCode, collectorNumber);
}
refreshDeckHash();
return true;
}
InnerDecklistNode *DeckList::getZoneObjFromName(const QString &zoneName)
{
for (int i = 0; i < root->size(); i++) {
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
if (node->getName() == zoneName) {
return node;
}
}
return new InnerDecklistNode(zoneName, root);
}
bool DeckList::loadFromFile_Plain(QIODevice *device)
{
QTextStream in(device);
@@ -516,184 +501,70 @@ QString DeckList::writeToString_Plain(bool prefixSideboardCards, bool slashTappe
*/
void DeckList::cleanList(bool preserveMetadata)
{
root->clearTree();
tree.clear();
if (!preserveMetadata) {
metadata = {};
}
refreshDeckHash();
}
void DeckList::getCardListHelper(InnerDecklistNode *item, QSet<QString> &result)
{
for (int i = 0; i < item->size(); ++i) {
auto *node = dynamic_cast<DecklistCardNode *>(item->at(i));
if (node) {
result.insert(node->getName());
} else {
getCardListHelper(dynamic_cast<InnerDecklistNode *>(item->at(i)), result);
}
}
}
void DeckList::getCardRefListHelper(InnerDecklistNode *item, QList<CardRef> &result)
{
for (int i = 0; i < item->size(); ++i) {
auto *node = dynamic_cast<DecklistCardNode *>(item->at(i));
if (node) {
result.append(node->toCardRef());
} else {
getCardRefListHelper(dynamic_cast<InnerDecklistNode *>(item->at(i)), result);
}
}
}
QStringList DeckList::getCardList() const
{
QSet<QString> result;
getCardListHelper(root, result);
return result.values();
auto nodes = tree.getCardNodes();
QStringList result;
std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(result), [](auto node) { return node->getName(); });
return result;
}
QList<CardRef> DeckList::getCardRefList() const
{
auto nodes = tree.getCardNodes();
QList<CardRef> result;
getCardRefListHelper(root, result);
std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(result),
[](auto node) { return node->toCardRef(); });
return result;
}
QList<DecklistCardNode *> DeckList::getCardNodes(const QStringList &restrictToZones) const
QList<const DecklistCardNode *> DeckList::getCardNodes(const QSet<QString> &restrictToZones) const
{
QList<DecklistCardNode *> result;
return tree.getCardNodes(restrictToZones);
}
for (auto *node : *root) {
auto *zoneNode = dynamic_cast<InnerDecklistNode *>(node);
if (zoneNode == nullptr) {
continue;
}
if (!restrictToZones.isEmpty() && !restrictToZones.contains(node->getName())) {
continue;
}
for (auto *cardNode : *zoneNode) {
auto *cardCardNode = dynamic_cast<DecklistCardNode *>(cardNode);
if (cardCardNode != nullptr) {
result.append(cardCardNode);
}
}
}
return result;
QList<const InnerDecklistNode *> DeckList::getZoneNodes() const
{
return tree.getZoneNodes();
}
int DeckList::getSideboardSize() const
{
int size = 0;
for (int i = 0; i < root->size(); ++i) {
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
if (node->getName() != DECK_ZONE_SIDE) {
continue;
}
auto cards = tree.getCardNodes({DECK_ZONE_SIDE});
for (int j = 0; j < node->size(); j++) {
auto *card = dynamic_cast<DecklistCardNode *>(node->at(j));
size += card->getNumber();
}
int size = 0;
for (auto card : cards) {
size += card->getNumber();
}
return size;
}
DecklistCardNode *DeckList::addCard(const QString &cardName,
const QString &zoneName,
const int position,
int position,
const QString &cardSetName,
const QString &cardSetCollectorNumber,
const QString &cardProviderId)
const QString &cardProviderId,
bool formatLegal)
{
auto *zoneNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName));
if (zoneNode == nullptr) {
zoneNode = new InnerDecklistNode(zoneName, root);
}
auto *node =
new DecklistCardNode(cardName, 1, zoneNode, position, cardSetName, cardSetCollectorNumber, cardProviderId);
auto node =
tree.addCard(cardName, 1, zoneName, position, cardSetName, cardSetCollectorNumber, cardProviderId, formatLegal);
refreshDeckHash();
return node;
}
bool DeckList::deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode)
{
if (node == root) {
return true;
}
bool updateHash = false;
if (rootNode == nullptr) {
rootNode = root;
updateHash = true;
}
int index = rootNode->indexOf(node);
if (index != -1) {
delete rootNode->takeAt(index);
if (rootNode->empty()) {
deleteNode(rootNode, rootNode->getParent());
}
if (updateHash) {
refreshDeckHash();
}
return true;
}
for (int i = 0; i < rootNode->size(); i++) {
auto *inner = dynamic_cast<InnerDecklistNode *>(rootNode->at(i));
if (inner) {
if (deleteNode(node, inner)) {
if (updateHash) {
refreshDeckHash();
}
return true;
}
}
}
return false;
}
static QString computeDeckHash(const InnerDecklistNode *root)
{
QStringList cardList;
QSet<QString> hashZones, optionalZones;
hashZones << DECK_ZONE_MAIN << DECK_ZONE_SIDE; // Zones in deck to be included in hashing process
optionalZones << DECK_ZONE_TOKENS; // Optional zones in deck not included in hashing process
for (int i = 0; i < root->size(); i++) {
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
for (int j = 0; j < node->size(); j++) {
if (hashZones.contains(node->getName())) // Mainboard or Sideboard
{
auto *card = dynamic_cast<DecklistCardNode *>(node->at(j));
for (int k = 0; k < card->getNumber(); ++k) {
cardList.append((node->getName() == DECK_ZONE_SIDE ? "SB:" : "") + card->getName().toLower());
}
}
}
}
cardList.sort();
QByteArray deckHashArray = QCryptographicHash::hash(cardList.join(";").toUtf8(), QCryptographicHash::Sha1);
quint64 number = (((quint64)(unsigned char)deckHashArray[0]) << 32) +
(((quint64)(unsigned char)deckHashArray[1]) << 24) +
(((quint64)(unsigned char)deckHashArray[2] << 16)) +
(((quint64)(unsigned char)deckHashArray[3]) << 8) + (quint64)(unsigned char)deckHashArray[4];
return QString::number(number, 32).rightJustified(8, '0');
}
/**
* Gets the deck hash.
* The hash is computed on the first call to this method, and is cached until the decklist is modified.
@@ -706,7 +577,7 @@ QString DeckList::getDeckHash() const
return cachedDeckHash;
}
cachedDeckHash = computeDeckHash(root);
cachedDeckHash = tree.computeDeckHash();
return cachedDeckHash;
}
@@ -723,15 +594,7 @@ void DeckList::refreshDeckHash()
*/
void DeckList::forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const
{
// Support for this is only possible if the internal structure
// doesn't get more complicated.
for (int i = 0; i < root->size(); i++) {
InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
for (int j = 0; j < node->size(); j++) {
DecklistCardNode *card = dynamic_cast<DecklistCardNode *>(node->at(j));
func(node, card);
}
}
tree.forEachCard(func);
}
DeckListMemento DeckList::createMemento(const QString &reason) const

View File

@@ -11,6 +11,7 @@
#define DECKLIST_H
#include "deck_list_memento.h"
#include "deck_list_node_tree.h"
#include "tree/inner_deck_list_node.h"
#include <QMap>
@@ -107,14 +108,14 @@ public:
* - Provide hashing for deck identity (deck hash).
*
* ### Ownership:
* - Owns the root `InnerDecklistNode` tree.
* - Owns the `DecklistNodeTree`.
* - Owns `SideboardPlan` instances stored in `sideboardPlans`.
*
* ### Example workflow:
* ```
* DeckList deck;
* deck.setName("Mono Red Aggro");
* deck.addCard("Lightning Bolt", "main", -1);
* deck.addCard("Lightning Bolt", "main");
* deck.addTag("Aggro");
* deck.saveToFile_Native(device);
* ```
@@ -126,6 +127,7 @@ public:
{
QString name; ///< User-defined deck name.
QString comments; ///< Free-form comments or notes.
QString gameFormat; ///< The name of the game format this deck contains legal cards for
CardRef bannerCard; ///< Optional representative card for the deck.
QStringList tags; ///< User-defined tags for deck classification.
QString lastLoadedTimestamp; ///< Timestamp string of last load.
@@ -139,7 +141,7 @@ public:
private:
Metadata metadata; ///< Deck metadata that is stored in the deck file
QMap<QString, SideboardPlan *> sideboardPlans; ///< Named sideboard plans.
InnerDecklistNode *root; ///< Root of the deck tree (zones + cards).
DecklistNodeTree tree; ///< The deck tree (zones + cards).
/**
* @brief Cached deck hash, recalculated lazily.
@@ -147,11 +149,6 @@ private:
*/
mutable QString cachedDeckHash;
// Helpers for traversing the tree
static void getCardListHelper(InnerDecklistNode *node, QSet<QString> &result);
static void getCardRefListHelper(InnerDecklistNode *item, QList<CardRef> &result);
InnerDecklistNode *getZoneObjFromName(const QString &zoneName);
public:
/// @name Metadata setters
///@{
@@ -183,20 +180,36 @@ public:
{
metadata.lastLoadedTimestamp = _lastLoadedTimestamp;
}
void setGameFormat(const QString &_gameFormat = QString())
{
metadata.gameFormat = _gameFormat;
}
///@}
/// @brief Construct an empty deck.
explicit DeckList();
/// @brief Copy constructor (deep copies the node tree)
DeckList(const DeckList &other);
/// @brief Construct from a serialized native-format string.
explicit DeckList(const QString &nativeString);
/// @brief Construct from components
DeckList(const Metadata &metadata,
const DecklistNodeTree &tree,
const QMap<QString, SideboardPlan *> &sideboardPlans = {});
virtual ~DeckList();
/**
* @brief Gets a pointer to the underlying node tree.
* Note: DO NOT call this method unless the object needs to have access to the underlying model.
* For now, only the DeckListModel should be calling this.
*/
DecklistNodeTree *getTree()
{
return &tree;
}
/// @name Metadata getters
/// The individual metadata getters still exist for backwards compatibility.
/// TODO: Figure out when we can remove them.
///@{
//! \todo Figure out when we can remove them.
const Metadata &getMetadata() const
{
return metadata;
@@ -221,6 +234,10 @@ public:
{
return metadata.lastLoadedTimestamp;
}
QString getGameFormat() const
{
return metadata.gameFormat;
}
///@}
bool isBlankDeck() const
@@ -263,23 +280,21 @@ public:
void cleanList(bool preserveMetadata = false);
bool isEmpty() const
{
return root->isEmpty() && metadata.isEmpty() && sideboardPlans.isEmpty();
return tree.isEmpty() && metadata.isEmpty() && sideboardPlans.isEmpty();
}
QStringList getCardList() const;
QList<CardRef> getCardRefList() const;
QList<DecklistCardNode *> getCardNodes(const QStringList &restrictToZones = QStringList()) const;
QList<const DecklistCardNode *> getCardNodes(const QSet<QString> &restrictToZones = {}) const;
QList<const InnerDecklistNode *> getZoneNodes() const;
int getSideboardSize() const;
InnerDecklistNode *getRoot() const
{
return root;
}
DecklistCardNode *addCard(const QString &cardName,
const QString &zoneName,
int position,
int position = -1,
const QString &cardSetName = QString(),
const QString &cardSetCollectorNumber = QString(),
const QString &cardProviderId = QString());
bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr);
const QString &cardProviderId = QString(),
const bool formatLegal = true);
///@}
/// @name Deck identity

View File

@@ -0,0 +1,186 @@
#include "deck_list_node_tree.h"
#include "tree/deck_list_card_node.h"
#include <QCryptographicHash>
#include <QSet>
DecklistNodeTree::DecklistNodeTree() : root(new InnerDecklistNode())
{
}
DecklistNodeTree::DecklistNodeTree(const DecklistNodeTree &other) : root(new InnerDecklistNode(other.root))
{
}
DecklistNodeTree &DecklistNodeTree::operator=(const DecklistNodeTree &other)
{
if (this != &other) {
delete root;
root = new InnerDecklistNode(other.root);
}
return *this;
}
DecklistNodeTree::~DecklistNodeTree()
{
delete root;
}
bool DecklistNodeTree::isEmpty() const
{
return root->isEmpty();
}
void DecklistNodeTree::clear()
{
root->clearTree();
}
QList<const DecklistCardNode *> DecklistNodeTree::getCardNodes(const QSet<QString> &restrictToZones) const
{
QList<const DecklistCardNode *> result;
for (auto *zoneNode : getZoneNodes()) {
if (!restrictToZones.isEmpty() && !restrictToZones.contains(zoneNode->getName())) {
continue;
}
for (auto *cardNode : *zoneNode) {
auto *cardCardNode = dynamic_cast<DecklistCardNode *>(cardNode);
if (cardCardNode) {
result.append(cardCardNode);
}
}
}
return result;
}
QList<const InnerDecklistNode *> DecklistNodeTree::getZoneNodes() const
{
QList<const InnerDecklistNode *> zones;
for (auto *node : *root) {
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(node);
if (!currentZone)
continue;
zones.append(currentZone);
}
return zones;
}
QString DecklistNodeTree::computeDeckHash() const
{
auto mainDeckNodes = getCardNodes({DECK_ZONE_MAIN});
auto sideDeckNodes = getCardNodes({DECK_ZONE_SIDE});
static auto nodesToCardList = [](const QList<const DecklistCardNode *> &nodes, const QString &prefix = {}) {
QStringList result;
for (auto node : nodes) {
for (int i = 0; i < node->getNumber(); ++i) {
result.append(prefix + node->getName().toLower());
}
}
return result;
};
QStringList cardList = nodesToCardList(mainDeckNodes) + nodesToCardList(sideDeckNodes, "SB:");
cardList.sort();
QByteArray deckHashArray = QCryptographicHash::hash(cardList.join(";").toUtf8(), QCryptographicHash::Sha1);
quint64 number = (((quint64)(unsigned char)deckHashArray[0]) << 32) +
(((quint64)(unsigned char)deckHashArray[1]) << 24) +
(((quint64)(unsigned char)deckHashArray[2] << 16)) +
(((quint64)(unsigned char)deckHashArray[3]) << 8) + (quint64)(unsigned char)deckHashArray[4];
return QString::number(number, 32).rightJustified(8, '0');
}
void DecklistNodeTree::write(QXmlStreamWriter *xml) const
{
for (int i = 0; i < root->size(); i++) {
root->at(i)->writeElement(xml);
}
}
void DecklistNodeTree::readZoneElement(QXmlStreamReader *xml)
{
QString zoneName = xml->attributes().value("name").toString();
InnerDecklistNode *newZone = getZoneObjFromName(zoneName);
newZone->readElement(xml);
}
DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName,
int amount,
const QString &zoneName,
int position,
const QString &cardSetName,
const QString &cardSetCollectorNumber,
const QString &cardProviderId,
const bool formatLegal)
{
auto *zoneNode = getZoneObjFromName(zoneName);
auto *node = new DecklistCardNode(cardName, amount, zoneNode, position, cardSetName, cardSetCollectorNumber,
cardProviderId, formatLegal);
return node;
}
bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode)
{
if (node == root) {
return true;
}
if (rootNode == nullptr) {
rootNode = root;
}
int index = rootNode->indexOf(node);
if (index != -1) {
delete rootNode->takeAt(index);
if (rootNode->empty()) {
deleteNode(rootNode, rootNode->getParent());
}
return true;
}
for (int i = 0; i < rootNode->size(); i++) {
auto *inner = dynamic_cast<InnerDecklistNode *>(rootNode->at(i));
if (inner) {
if (deleteNode(node, inner)) {
return true;
}
}
}
return false;
}
void DecklistNodeTree::forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const
{
// Support for this is only possible if the internal structure
// doesn't get more complicated.
for (int i = 0; i < root->size(); i++) {
InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
for (int j = 0; j < node->size(); j++) {
DecklistCardNode *card = dynamic_cast<DecklistCardNode *>(node->at(j));
func(node, card);
}
}
}
/**
* Gets the InnerDecklistNode that is the root node for the given zone, creating a new node if it doesn't exist.
*/
InnerDecklistNode *DecklistNodeTree::getZoneObjFromName(const QString &zoneName) const
{
for (int i = 0; i < root->size(); i++) {
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
if (node->getName() == zoneName) {
return node;
}
}
return new InnerDecklistNode(zoneName, root);
}

View File

@@ -0,0 +1,87 @@
#ifndef COCKATRICE_DECKLIST_NODE_TREE_H
#define COCKATRICE_DECKLIST_NODE_TREE_H
#include "libcockatrice/utility/card_ref.h"
#include "tree/deck_list_card_node.h"
#include "tree/inner_deck_list_node.h"
#include <QSet>
class DecklistNodeTree
{
InnerDecklistNode *root; ///< Root of the deck tree (zones + cards).
public:
/// @brief Constructs an empty DecklistNodeTree
explicit DecklistNodeTree();
/// @brief Copy constructor. Deep copies the tree
explicit DecklistNodeTree(const DecklistNodeTree &other);
/// @brief Copy-assignment operator. Deep copies the tree
DecklistNodeTree &operator=(const DecklistNodeTree &other);
virtual ~DecklistNodeTree();
/**
* @brief Gets a pointer to the underlying root node.
* Note: DO NOT call this method unless the object needs to have access to the underlying model.
* For now, only the DeckListModel should be calling this.
*/
InnerDecklistNode *getRoot() const
{
return root;
}
bool isEmpty() const;
/**
* @brief Deletes all nodes except the root.
*/
void clear();
/**
* Gets all card nodes in the tree
* @param restrictToZones Only get the nodes in these zones
* @return A QList containing all the card nodes in the zone.
*/
QList<const DecklistCardNode *> getCardNodes(const QSet<QString> &restrictToZones = {}) const;
QList<const InnerDecklistNode *> getZoneNodes() const;
/**
* @brief Computes the deck hash
*/
QString computeDeckHash() const;
/**
*@brief Writes the contents of the deck to xml
*/
void write(QXmlStreamWriter *xml) const;
/**
* @brief Reads a "zone" section of the xml to this tree
*/
void readZoneElement(QXmlStreamReader *xml);
DecklistCardNode *addCard(const QString &cardName,
int amount,
const QString &zoneName,
int position,
const QString &cardSetName = QString(),
const QString &cardSetCollectorNumber = QString(),
const QString &cardProviderId = QString(),
const bool formatLegal = true);
bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr);
/**
* @brief Apply a function to every card in the deck tree. This can modify the cards.
*
* @param func Function taking (zone node, card node).
*/
void forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const;
private:
// Helpers for traversing the tree
InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const;
};
#endif // COCKATRICE_DECKLIST_NODE_TREE_H

View File

@@ -88,6 +88,12 @@ public:
/// @param _cardSetNumber Set the collector number.
virtual void setCardCollectorNumber(const QString &_cardSetNumber) = 0;
/// @return The format legality of the card
virtual bool getFormatLegality() const = 0;
/// @param _formatLegal If the card is considered legal
virtual void setFormatLegality(const bool _formatLegal) = 0;
/**
* @brief Get the height of this node in the tree.
*

View File

@@ -51,6 +51,7 @@ class DecklistCardNode : public AbstractDecklistCardNode
QString cardSetShortName; ///< Short set code (e.g., "NEO").
QString cardSetNumber; ///< Collector number within the set.
QString cardProviderId; ///< External provider identifier (e.g., UUID).
bool formatLegal; ///< Format legality
public:
/**
@@ -63,6 +64,7 @@ public:
* @param _cardSetShortName Short set code (e.g., "NEO").
* @param _cardSetNumber Collector number within the set.
* @param _cardProviderId External provider ID (e.g., UUID).
* @param _formatLegality If the card is legal in the format
*
* On construction, if a parent is provided, this node is inserted into
* the parents children list automatically.
@@ -73,10 +75,11 @@ public:
int position = -1,
QString _cardSetShortName = QString(),
QString _cardSetNumber = QString(),
QString _cardProviderId = QString())
QString _cardProviderId = QString(),
bool _formatLegality = true)
: AbstractDecklistCardNode(_parent, position), name(std::move(_name)), number(_number),
cardSetShortName(std::move(_cardSetShortName)), cardSetNumber(std::move(_cardSetNumber)),
cardProviderId(std::move(_cardProviderId))
cardProviderId(std::move(_cardProviderId)), formatLegal(_formatLegality)
{
}
@@ -150,6 +153,18 @@ public:
cardSetNumber = _cardSetNumber;
}
/// @return The format legality of the card
[[nodiscard]] bool getFormatLegality() const override
{
return formatLegal;
}
/// @param _formatLegal If the card is considered legal
void setFormatLegality(const bool _formatLegal) override
{
formatLegal = _formatLegal;
}
/// @return Always false; card nodes are not deck headers.
[[nodiscard]] bool isDeckHeader() const override
{

View File

@@ -60,6 +60,8 @@ const QString CardFilter::attrName(Attr a)
switch (a) {
case AttrName:
return tr("Name");
case AttrNameExact:
return tr("Name (Exact)");
case AttrType:
return tr("Type");
case AttrColor:

View File

@@ -33,6 +33,7 @@ public:
AttrLoyalty,
AttrManaCost,
AttrName,
AttrNameExact,
AttrPow,
AttrRarity,
AttrSet,

View File

@@ -153,6 +153,11 @@ bool FilterItem::acceptName(const CardInfoPtr info) const
return info->getName().contains(term, Qt::CaseInsensitive);
}
bool FilterItem::acceptNameExact(const CardInfoPtr info) const
{
return info->getName() == term;
}
bool FilterItem::acceptType(const CardInfoPtr info) const
{
return info->getCardType().contains(term, Qt::CaseInsensitive);
@@ -401,6 +406,8 @@ bool FilterItem::acceptCardAttr(const CardInfoPtr info, CardFilter::Attr attr) c
switch (attr) {
case CardFilter::AttrName:
return acceptName(info);
case CardFilter::AttrNameExact:
return acceptNameExact(info);
case CardFilter::AttrType:
return acceptType(info);
case CardFilter::AttrColor:

View File

@@ -203,6 +203,7 @@ public:
}
[[nodiscard]] bool acceptName(CardInfoPtr info) const;
[[nodiscard]] bool acceptNameExact(CardInfoPtr info) const;
[[nodiscard]] bool acceptType(CardInfoPtr info) const;
[[nodiscard]] bool acceptMainType(CardInfoPtr info) const;
[[nodiscard]] bool acceptSubType(CardInfoPtr info) const;

View File

@@ -41,7 +41,7 @@ void DeckListModel::rebuildTree()
beginResetModel();
root->clearTree();
InnerDecklistNode *listRoot = deckList->getRoot();
InnerDecklistNode *listRoot = deckList->getTree()->getRoot();
for (int i = 0; i < listRoot->size(); i++) {
auto *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
@@ -168,6 +168,10 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const
return card->depth();
}
case DeckRoles::IsLegalRole: {
return card->getFormatLegality();
}
default: {
return {};
}
@@ -268,6 +272,7 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con
switch (index.column()) {
case DeckListModelColumns::CARD_AMOUNT:
node->setNumber(value.toInt());
refreshCardFormatLegalities();
break;
case DeckListModelColumns::CARD_NAME:
node->setName(value.toString());
@@ -308,7 +313,7 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent)
for (int i = 0; i < count; i++) {
AbstractDecklistNode *toDelete = node->takeAt(row);
if (auto *temp = dynamic_cast<DecklistModelCardNode *>(toDelete)) {
deckList->deleteNode(temp->getDataNode());
deckList->getTree()->deleteNode(temp->getDataNode());
}
delete toDelete;
}
@@ -414,8 +419,9 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam
// Determine the correct index
int insertRow = findSortedInsertRow(groupNode, cardInfo);
auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName,
printingInfo.getProperty("num"), printingInfo.getProperty("uuid"));
auto *decklistCard =
deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName, printingInfo.getProperty("num"),
printingInfo.getProperty("uuid"), isCardLegalForCurrentFormat(cardInfo));
beginInsertRows(parentIndex, insertRow, insertRow);
cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow);
@@ -532,6 +538,13 @@ void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria::Type newC
rebuildTree();
}
void DeckListModel::setActiveFormat(const QString &_format)
{
deckList->setGameFormat(_format);
refreshCardFormatLegalities();
emitBackgroundUpdates(QModelIndex()); // start from root
}
void DeckListModel::cleanList()
{
setDeckList(new DeckList);
@@ -550,86 +563,135 @@ void DeckListModel::setDeckList(DeckList *_deck)
QList<ExactCard> DeckListModel::getCards() const
{
QList<ExactCard> cards;
DeckList *decklist = getDeckList();
if (!decklist) {
return cards;
}
InnerDecklistNode *listRoot = decklist->getRoot();
if (!listRoot)
return cards;
auto nodes = deckList->getCardNodes();
for (int i = 0; i < listRoot->size(); i++) {
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
if (!currentZone)
continue;
for (int j = 0; j < currentZone->size(); j++) {
DecklistCardNode *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
if (!currentCard)
continue;
for (int k = 0; k < currentCard->getNumber(); ++k) {
ExactCard card = CardDatabaseManager::query()->getCard(currentCard->toCardRef());
if (card) {
cards.append(card);
} else {
qDebug() << "Card not found in database!";
}
QList<ExactCard> cards;
for (auto node : nodes) {
ExactCard card = CardDatabaseManager::query()->getCard(node->toCardRef());
if (card) {
for (int k = 0; k < node->getNumber(); ++k) {
cards.append(card);
}
} else {
qDebug() << "Card not found in database!";
}
}
return cards;
}
QList<ExactCard> DeckListModel::getCardsForZone(const QString &zoneName) const
{
QList<ExactCard> cards;
DeckList *decklist = getDeckList();
if (!decklist) {
return cards;
}
InnerDecklistNode *listRoot = decklist->getRoot();
if (!listRoot)
return cards;
auto nodes = deckList->getCardNodes({zoneName});
for (int i = 0; i < listRoot->size(); i++) {
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
if (!currentZone)
continue;
if (currentZone->getName() == zoneName) {
for (int j = 0; j < currentZone->size(); j++) {
DecklistCardNode *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
if (!currentCard)
continue;
for (int k = 0; k < currentCard->getNumber(); ++k) {
ExactCard card = CardDatabaseManager::query()->getCard(currentCard->toCardRef());
if (card) {
cards.append(card);
} else {
qDebug() << "Card not found in database!";
}
}
QList<ExactCard> cards;
for (auto node : nodes) {
ExactCard card = CardDatabaseManager::query()->getCard(node->toCardRef());
if (card) {
for (int k = 0; k < node->getNumber(); ++k) {
cards.append(card);
}
} else {
qDebug() << "Card not found in database!";
}
}
return cards;
}
QList<QString> *DeckListModel::getZones() const
QList<QString> DeckListModel::getZones() const
{
QList<QString> *zones = new QList<QString>();
DeckList *decklist = getDeckList();
if (!decklist) {
return zones;
auto zoneNodes = deckList->getZoneNodes();
QList<QString> zones;
std::transform(zoneNodes.cbegin(), zoneNodes.cend(), std::back_inserter(zones),
[](auto zoneNode) { return zoneNode->getName(); });
return zones;
}
bool DeckListModel::isCardLegalForCurrentFormat(const CardInfoPtr cardInfo)
{
if (!deckList->getGameFormat().isEmpty()) {
if (cardInfo->getProperties().contains("format-" + deckList->getGameFormat())) {
QString formatLegality = cardInfo->getProperty("format-" + deckList->getGameFormat());
return formatLegality == "legal" || formatLegality == "restricted";
}
return false;
}
InnerDecklistNode *listRoot = decklist->getRoot();
if (!listRoot)
return zones;
return true;
}
int maxAllowedForLegality(const FormatRules &format, const QString &legality)
{
for (const AllowedCount &c : format.allowedCounts) {
if (c.label == legality) {
return c.max;
}
}
return -1; // unknown legality → treat as illegal
}
bool DeckListModel::isCardQuantityLegalForCurrentFormat(const CardInfoPtr cardInfo, int quantity)
{
auto formatRules = CardDatabaseManager::query()->getFormat(deckList->getGameFormat());
if (!formatRules) {
return true;
}
// Exceptions always win
if (cardHasAnyException(*cardInfo, *formatRules)) {
return true;
}
const QString legalityProp = "format-" + deckList->getGameFormat();
if (!cardInfo->getProperties().contains(legalityProp)) {
return false;
}
const QString legality = cardInfo->getProperty(legalityProp);
int maxAllowed = maxAllowedForLegality(*formatRules, legality);
if (maxAllowed == -1) {
return false;
}
if (maxAllowed < 0) { // unlimited
return true;
}
return quantity <= maxAllowed;
}
void DeckListModel::refreshCardFormatLegalities()
{
InnerDecklistNode *listRoot = deckList->getTree()->getRoot();
for (int i = 0; i < listRoot->size(); i++) {
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
if (!currentZone)
continue;
zones->append(currentZone->getName());
auto *currentZone = static_cast<InnerDecklistNode *>(listRoot->at(i));
for (int j = 0; j < currentZone->size(); j++) {
auto *currentCard = static_cast<DecklistCardNode *>(currentZone->at(j));
// TODO: better sanity checking
if (currentCard == nullptr) {
continue;
}
ExactCard exactCard = CardDatabaseManager::query()->getCard(currentCard->toCardRef());
if (!exactCard) {
continue;
}
bool legal = isCardLegalForCurrentFormat(exactCard.getCardPtr());
if (legal) {
legal = isCardQuantityLegalForCurrentFormat(exactCard.getCardPtr(), currentCard->getNumber());
}
currentCard->setFormatLegality(legal);
}
}
return zones;
}
}

View File

@@ -164,6 +164,14 @@ public:
{
dataNode->setCardCollectorNumber(_cardSetNumber);
}
bool getFormatLegality() const override
{
return dataNode->getFormatLegality();
}
void setFormatLegality(const bool _formatLegal) override
{
dataNode->setFormatLegality(_formatLegal);
}
/**
* @brief Returns the underlying data node.
@@ -194,7 +202,7 @@ public:
* affects its hash.
*
* Slots:
* - rebuildTree(): rebuilds the model structure from the underlying DeckLoader.
* - rebuildTree(): rebuilds the model structure from the underlying node tree.
*/
class DeckListModel : public QAbstractItemModel
{
@@ -202,13 +210,16 @@ class DeckListModel : public QAbstractItemModel
public slots:
/**
* @brief Rebuilds the model tree from the underlying DeckLoader.
* @brief Rebuilds the model tree from the underlying node tree.
*
* This updates all indices and ensures the model reflects the current
* state of the deck.
*/
void rebuildTree();
public slots:
void setActiveFormat(const QString &_format);
signals:
/**
* @brief Emitted whenever the deck hash changes due to modifications in the model.
@@ -300,7 +311,10 @@ public:
[[nodiscard]] QList<ExactCard> getCards() const;
[[nodiscard]] QList<ExactCard> getCardsForZone(const QString &zoneName) const;
[[nodiscard]] QList<QString> *getZones() const;
[[nodiscard]] QList<QString> getZones() const;
bool isCardLegalForCurrentFormat(CardInfoPtr cardInfo);
bool isCardQuantityLegalForCurrentFormat(CardInfoPtr cardInfo, int quantity);
void refreshCardFormatLegalities();
/**
* @brief Sets the criteria used to group cards in the model.

View File

@@ -501,7 +501,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
allPlayersEver.insert(playerName);
// if the original creator of the game joins, give them host status back
// FIXME: transferring host to spectators has side effects
//! \todo transferring host to spectators has side effects
if (newParticipant->getUserInfo()->name() == creatorInfo->name()) {
hostId = newParticipant->getPlayerId();
sendGameEventContainer(prepareGameEvent(Event_GameHostChanged(), hostId));

View File

@@ -102,28 +102,16 @@ void Server_Player::setupZones()
// ------------------------------------------------------------------
// Assign card ids and create deck from deck list
InnerDecklistNode *listRoot = deck->getRoot();
for (int i = 0; i < listRoot->size(); ++i) {
auto *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
Server_CardZone *z;
if (currentZone->getName() == DECK_ZONE_MAIN) {
z = deckZone;
} else if (currentZone->getName() == DECK_ZONE_SIDE) {
z = sbZone;
} else {
continue;
auto insertCardsIntoZone = [this](auto cards, auto *zone) {
for (auto card : cards) {
for (int k = 0; k < card->getNumber(); ++k) {
zone->insertCard(new Server_Card(card->toCardRef(), nextCardId++, 0, 0, zone), -1, 0);
}
}
};
for (int j = 0; j < currentZone->size(); ++j) {
auto *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
if (!currentCard) {
continue;
}
for (int k = 0; k < currentCard->getNumber(); ++k) {
z->insertCard(new Server_Card(currentCard->toCardRef(), nextCardId++, 0, 0, z), -1, 0);
}
}
}
insertCardsIntoZone(deck->getCardNodes({DECK_ZONE_MAIN}), deckZone);
insertCardsIntoZone(deck->getCardNodes({DECK_ZONE_SIDE}), sbZone);
const QList<MoveCard_ToZone> &sideboardPlan = deck->getCurrentSideboardPlan();
for (const auto &m : sideboardPlan) {

View File

@@ -6,17 +6,12 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp
libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/system_memory_querier.cpp
libcockatrice/utility/passwordhasher.cpp
)
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/system_memory_querier.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
)
add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS})

Some files were not shown because too many files have changed in this diff Show More