Compare commits

..

10 commits

Author SHA1 Message Date
github-actions[bot]
baddbfae14
Update translation source strings (#7028)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
Co-authored-by: github-actions <github-actions@github.com>
2026-07-01 16:25:10 +02:00
Skagra42
9b0348240d
Search by date. (#7027)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
2026-06-30 23:46:37 -07:00
DawnFire42
18b23b19a7
Split trice_limits.h into dedicated headers (#7025)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 44 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Servatrice_Debian 12 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
* Split trice_limits.h into dedicated headers

* Updated docstrings
2026-06-29 14:37:52 -07:00
DawnFire42
4a384f2a75
fix theme manager return type warning (#7026)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
2026-06-29 08:03:30 -07:00
DawnFire42
05ae6f47a6
Unify counter clamp arithmetic into shared addClamped() helper (#7009)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* Unify counter clamp arithmetic into shared addClamped() helper

- Add addClamped() in new header clamped_arithmetic.h; uses a 64-bit
  intermediate so the addition cannot overflow int.
- Use it in Server_Card::incrementCounter() (clamps [0, MAX_COUNTERS_ON_CARD])
  and Server_Counter::incrementCount() (clamps [INT_MIN, INT_MAX]), removing
  the duplicated overflow-safe logic and its keep-in-sync TODO.
- Inline incrementCount() into server_counter.h; server_counter.cpp now holds
  only the constructor and getInfo().
- Clarify the card-counter bounds comment in trice_limits.h.

* Rename MAX_COUNTERS_ON_CARD to MAX_COUNTER_VALUE

The constant caps the counter's value, not how many counters can be on the card

* Add direct unit tests for addClamped() helper

* Harden offsetCardCounter() against signed-int overflow

Replace the raw oldValue + offset sum with addClamped(), clamping to [0, MAX_COUNTER_VALUE] without overflow.

* Comment update

* Remove class names from addClamped() docstring
2026-06-28 16:10:57 -07:00
RickyRister
fcac7493ad
[Card] Add facedown property to CardRelation (#6997)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* [Card] Add facedown property to CardRelation

* trailing newline

* fix comments

* update schema
2026-06-28 02:03:07 -07:00
DawnFire42
055ba9a16f
Add subtype breakdown counter for card selection (#6923)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* Add subtype breakdown counter for card selection

  Display a categorized count of creature subtypes (and other card type
  subtypes) when multiple cards are selected. The breakdown appears above
  the total selection counter in the bottom-right corner.

  Subtypes are grouped by main card type and sorted by frequency, with
  the most common subtypes positioned adjacent to the total count for
  quick reference. The feature can be toggled via a new checkbox in
  Settings > User Interface.

* Alignment fix

* Computation logic moved to helper funtction in separate file

* Rename SubtypeCounter to SubtypeTally

* Fix subtype tally alignment by using grid layout instead of character padding

* Rename count to tally in the subtype breakdown feature

* partial rename

* list position fixed

* Clean up code and documentation

* Rename subtypeCountLabelStyle to subtypeTallyLabelStyle and fix include ordering

* Fix include path for selection_subtype_tally.h after file relocation

* fixed count to tally rename inconsistencies
2026-06-27 15:53:21 -07:00
BruebachL
ad4922537d
[UserListDelegate] Supply providerid in cmd, position popup correctly (#7020)
* [UserListDelegate] Transmit providerId in cmd when setting user banner card

* [UserListDelegate] Position popup correctly

* Lint.

---------

Co-authored-by: Lukas Brübach <lukas.bruebach@bdosecurity.de>
2026-06-27 14:44:24 -04:00
dependabot[bot]
4cbc00b9c4
Bump actions/cache from 5 to 6 (#7019)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
2026-06-27 17:25:30 +02:00
BruebachL
6dc974a05d
[UserListDelegate] Consider providerId (#7018)
Co-authored-by: Lukas Brübach <lukas.bruebach@bdosecurity.de>
2026-06-27 11:23:55 -04:00
89 changed files with 3717 additions and 2345 deletions

View file

@ -162,7 +162,7 @@ jobs:
- name: "Restore compiler cache (ccache)" - name: "Restore compiler cache (ccache)"
id: ccache_restore id: ccache_restore
uses: actions/cache/restore@v5 uses: actions/cache/restore@v6
env: env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }} BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with: with:
@ -215,7 +215,7 @@ jobs:
- name: "Save updated compiler cache (ccache)" - name: "Save updated compiler cache (ccache)"
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v5 uses: actions/cache/save@v6
with: with:
key: ${{ steps.ccache_restore.outputs.cache-primary-key }} key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
path: ${{ env.CACHE }} path: ${{ env.CACHE }}
@ -365,7 +365,7 @@ jobs:
- name: "[macOS] Restore compiler cache (ccache)" - name: "[macOS] Restore compiler cache (ccache)"
if: matrix.os == 'macOS' && matrix.use_ccache == 1 if: matrix.os == 'macOS' && matrix.use_ccache == 1
id: ccache_restore id: ccache_restore
uses: actions/cache/restore@v5 uses: actions/cache/restore@v6
env: env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }} BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with: with:
@ -387,7 +387,7 @@ jobs:
- name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries" - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries"
if: matrix.os == 'macOS' if: matrix.os == 'macOS'
id: restore_qt id: restore_qt
uses: actions/cache/restore@v5 uses: actions/cache/restore@v6
with: with:
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
path: ${{ github.workspace }}/Qt path: ${{ github.workspace }}/Qt
@ -410,7 +410,7 @@ jobs:
- name: "[macOS] Cache thin Qt libraries" - name: "[macOS] Cache thin Qt libraries"
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: actions/cache/save@v5 uses: actions/cache/save@v6
with: with:
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
path: ${{ github.workspace }}/Qt path: ${{ github.workspace }}/Qt
@ -473,7 +473,7 @@ jobs:
- name: "[macOS] Save updated compiler cache (ccache)" - name: "[macOS] Save updated compiler cache (ccache)"
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master'
uses: actions/cache/save@v5 uses: actions/cache/save@v6
with: with:
key: ${{ steps.ccache_restore.outputs.cache-primary-key }} key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
path: ${{ env.CCACHE_DIR }} path: ${{ env.CCACHE_DIR }}

View file

@ -83,6 +83,7 @@ set(cockatrice_SOURCES
src/game/game_state.cpp src/game/game_state.cpp
src/game_graphics/game_view.cpp src/game_graphics/game_view.cpp
src/game_graphics/hand_counter.cpp src/game_graphics/hand_counter.cpp
src/game/selection_subtype_tally.cpp
src/game_graphics/log/message_log_widget.cpp src/game_graphics/log/message_log_widget.cpp
src/game/phase.cpp src/game/phase.cpp
src/game_graphics/phases_toolbar.cpp src/game_graphics/phases_toolbar.cpp

File diff suppressed because it is too large Load diff

View file

@ -313,6 +313,7 @@ SettingsCache::SettingsCache()
showDragSelectionCount = settings->value("interface/showlassoselectioncount", true).toBool(); showDragSelectionCount = settings->value("interface/showlassoselectioncount", true).toBool();
showTotalSelectionCount = settings->value("interface/showpersistentselectioncount", true).toBool(); showTotalSelectionCount = settings->value("interface/showpersistentselectioncount", true).toBool();
showSubtypeSelectionTally = settings->value("interface/showsubtypeselectiontally", true).toBool();
showShortcuts = settings->value("menu/showshortcuts", true).toBool(); showShortcuts = settings->value("menu/showshortcuts", true).toBool();
showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool(); showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool();
@ -1395,6 +1396,12 @@ void SettingsCache::setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSele
settings->setValue("interface/showpersistentselectioncount", showTotalSelectionCount); settings->setValue("interface/showpersistentselectioncount", showTotalSelectionCount);
} }
void SettingsCache::setShowSubtypeSelectionTally(QT_STATE_CHANGED_T _showSubtypeSelectionTally)
{
showSubtypeSelectionTally = static_cast<bool>(_showSubtypeSelectionTally);
settings->setValue("interface/showsubtypeselectiontally", showSubtypeSelectionTally);
}
void SettingsCache::loadPaths() void SettingsCache::loadPaths()
{ {
QString dataPath = getDataPath(); QString dataPath = getDataPath();

View file

@ -355,6 +355,7 @@ private:
bool showStatusBar; bool showStatusBar;
bool showDragSelectionCount; bool showDragSelectionCount;
bool showTotalSelectionCount; bool showTotalSelectionCount;
bool showSubtypeSelectionTally;
public: public:
SettingsCache(); SettingsCache();
@ -478,6 +479,10 @@ public:
{ {
return showTotalSelectionCount; return showTotalSelectionCount;
} }
[[nodiscard]] bool getShowSubtypeSelectionTally() const
{
return showSubtypeSelectionTally;
}
[[nodiscard]] bool getNotificationsEnabled() const [[nodiscard]] bool getNotificationsEnabled() const
{ {
return notificationsEnabled; return notificationsEnabled;
@ -1176,5 +1181,6 @@ public slots:
void setRoundCardCorners(bool _roundCardCorners); void setRoundCardCorners(bool _roundCardCorners);
void setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount); void setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount);
void setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSelectionCount); void setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSelectionCount);
void setShowSubtypeSelectionTally(QT_STATE_CHANGED_T _showSubtypeSelectionTally);
}; };
#endif #endif

View file

@ -27,8 +27,9 @@
#include <libcockatrice/protocol/pb/command_shuffle.pb.h> #include <libcockatrice/protocol/pb/command_shuffle.pb.h>
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h> #include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
#include <libcockatrice/protocol/pb/context_move_card.pb.h> #include <libcockatrice/protocol/pb/context_move_card.pb.h>
#include <libcockatrice/utility/clamped_arithmetic.h>
#include <libcockatrice/utility/counter_limits.h>
#include <libcockatrice/utility/expression.h> #include <libcockatrice/utility/expression.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/zone_names.h> #include <libcockatrice/utility/zone_names.h>
// milliseconds in between triggers of the move top cards until action // milliseconds in between triggers of the move top cards until action
@ -1018,8 +1019,9 @@ void PlayerActions::actCreateAllRelatedCards()
if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) { if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) {
dbName = cardRelationAll->getName(); dbName = cardRelationAll->getName();
bool persistent = cardRelationAll->getIsPersistent(); bool persistent = cardRelationAll->getIsPersistent();
bool faceDown = cardRelationAll->getIsFaceDown();
for (int i = 0; i < cardRelationAll->getDefaultCount(); ++i) { for (int i = 0; i < cardRelationAll->getDefaultCount(); ++i) {
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
} }
++tokensTypesCreated; ++tokensTypesCreated;
if (tokensTypesCreated == 1) { if (tokensTypesCreated == 1) {
@ -1034,8 +1036,9 @@ void PlayerActions::actCreateAllRelatedCards()
if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) { if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) {
dbName = cardRelationNotExcluded->getName(); dbName = cardRelationNotExcluded->getName();
bool persistent = cardRelationNotExcluded->getIsPersistent(); bool persistent = cardRelationNotExcluded->getIsPersistent();
bool faceDown = cardRelationNotExcluded->getIsFaceDown();
for (int i = 0; i < cardRelationNotExcluded->getDefaultCount(); ++i) { for (int i = 0; i < cardRelationNotExcluded->getDefaultCount(); ++i) {
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
} }
++tokensTypesCreated; ++tokensTypesCreated;
if (tokensTypesCreated == 1) { if (tokensTypesCreated == 1) {
@ -1073,6 +1076,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
const QString dbName = cardRelation->getName(); const QString dbName = cardRelation->getName();
const bool persistent = cardRelation->getIsPersistent(); const bool persistent = cardRelation->getIsPersistent();
const bool faceDown = cardRelation->getIsFaceDown();
// Variable relations always use DoesNotAttach, regardless of the count the user // Variable relations always use DoesNotAttach, regardless of the count the user
// entered. // entered.
@ -1081,7 +1085,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
return false; return false;
} }
for (int i = 0; i < variableCount; ++i) { for (int i = 0; i < variableCount; ++i) {
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
} }
return true; return true;
} }
@ -1090,7 +1094,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
if (count > 1) { if (count > 1) {
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown);
} }
return true; return true;
} }
@ -1110,7 +1114,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard,
playCardToTable(sourceCard, false); playCardToTable(sourceCard, false);
} }
createCard(sourceCard, dbName, attachType, persistent); createCard(sourceCard, dbName, attachType, persistent, faceDown);
return true; return true;
} }
@ -1137,7 +1141,8 @@ void PlayerActions::onRelatedCardCreated(const CardItem *sourceCard, const CardR
void PlayerActions::createCard(const CardItem *sourceCard, void PlayerActions::createCard(const CardItem *sourceCard,
const QString &dbCardName, const QString &dbCardName,
CardRelationType attachType, CardRelationType attachType,
bool persistent) bool persistent,
bool faceDown)
{ {
CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(dbCardName); CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(dbCardName);
@ -1172,6 +1177,7 @@ void PlayerActions::createCard(const CardItem *sourceCard,
cmd.set_destroy_on_zone_change(!persistent); cmd.set_destroy_on_zone_change(!persistent);
cmd.set_x(gridPoint.x()); cmd.set_x(gridPoint.x());
cmd.set_y(gridPoint.y()); cmd.set_y(gridPoint.y());
cmd.set_face_down(faceDown);
ExactCard relatedCard = ExactCard relatedCard =
CardDatabaseManager::query()->getCardFromSameSet(cardInfo->getName(), sourceCard->getCard().getPrinting()); CardDatabaseManager::query()->getCardFromSameSet(cardInfo->getName(), sourceCard->getCard().getPrinting());
@ -1525,12 +1531,15 @@ void PlayerActions::offsetCardCounter(QList<CardItem *> selectedCards, int count
QList<const ::google::protobuf::Message *> commandList; QList<const ::google::protobuf::Message *> commandList;
for (auto card : selectedCards) { for (auto card : selectedCards) {
int oldValue = card->getCounters().value(counterId, 0); int oldValue = card->getCounters().value(counterId, 0);
int newValue = oldValue + offset;
// Early exit optimization: server enforces [0, MAX_COUNTERS_ON_CARD]. // Overflow-safe clamp to the server-enforced range [0, MAX_COUNTER_VALUE];
// Compare clamped value to allow recovery from invalid states. // a result differing from oldValue also corrects an out-of-range cached value.
int clampedValue = qBound(0, newValue, MAX_COUNTERS_ON_CARD); // Callers only ever pass offset == ±1 (actAddCardCounter / actRemoveCardCounter).
if (clampedValue != oldValue) { // This client-side clamp is a defense-in-depth UX check, consistent with
// actSetCardCounter and actIncrementAllCardCounters; the server remains the
// authoritative enforcer of the bounds.
int newValue = addClamped(oldValue, offset, 0, MAX_COUNTER_VALUE);
if (newValue != oldValue) {
auto *cmd = new Command_SetCardCounter; auto *cmd = new Command_SetCardCounter;
cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_zone(card->getZone()->getName().toStdString());
cmd->set_card_id(card->getId()); cmd->set_card_id(card->getId());
@ -1563,7 +1572,7 @@ void PlayerActions::actSetCardCounter(QList<CardItem *> selectedCards, int count
Expression exp(oldValue); Expression exp(oldValue);
double parsed = exp.parse(counterValue); double parsed = exp.parse(counterValue);
// Clamp in double precision first to avoid UB, then cast // Clamp in double precision first to avoid UB, then cast
int number = static_cast<int>(qBound(0.0, parsed, static_cast<double>(MAX_COUNTERS_ON_CARD))); int number = static_cast<int>(qBound(0.0, parsed, static_cast<double>(MAX_COUNTER_VALUE)));
auto *cmd = new Command_SetCardCounter; auto *cmd = new Command_SetCardCounter;
cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_zone(card->getZone()->getName().toStdString());
@ -1593,7 +1602,7 @@ void PlayerActions::actIncrementAllCardCounters(QList<CardItem *> cardsToUpdate)
counterIterator.next(); counterIterator.next();
int counterId = counterIterator.key(); int counterId = counterIterator.key();
int currentValue = counterIterator.value(); int currentValue = counterIterator.value();
if (currentValue >= MAX_COUNTERS_ON_CARD) { if (currentValue >= MAX_COUNTER_VALUE) {
continue; continue;
} }

View file

@ -240,7 +240,8 @@ private:
void createCard(const CardItem *sourceCard, void createCard(const CardItem *sourceCard,
const QString &dbCardName, const QString &dbCardName,
CardRelationType attach = CardRelationType::DoesNotAttach, CardRelationType attach = CardRelationType::DoesNotAttach,
bool persistent = false); bool persistent = false,
bool faceDown = false);
void playSelectedCards(QList<CardItem *> selectedCards, bool faceDown = false); void playSelectedCards(QList<CardItem *> selectedCards, bool faceDown = false);

View file

@ -0,0 +1,64 @@
#include "selection_subtype_tally.h"
#include "../game_graphics/board/card_item.h"
#include <QMap>
#include <algorithm>
namespace
{
/** @brief Extracts subtypes from a single card face's type line. */
QStringList extractSubtypesFromFace(const QString &faceType)
{
// Card type format: "Creature — Goblin Warrior" or "Legendary Enchantment — Saga"
QStringList parts = faceType.split(QStringLiteral(""));
if (parts.size() > 1) {
return parts[1].split(QStringLiteral(" "), Qt::SkipEmptyParts);
}
return {};
}
} // anonymous namespace
namespace SelectionSubtypeTally
{
QList<SubtypeEntry> countSubtypes(const QList<CardItem *> &cards)
{
QMap<QString, int> subtypeCounts;
for (CardItem *card : cards) {
if (card->getFaceDown() || card->getCard().isEmpty()) {
continue;
}
QString cardType = card->getCardInfo().getCardType();
// Handle double-faced cards: "Creature — Human // Creature — Werewolf"
QStringList cardFaces = cardType.split(QStringLiteral(" // "));
for (const QString &face : cardFaces) {
QStringList subtypes = extractSubtypesFromFace(face);
for (const QString &subtype : subtypes) {
subtypeCounts[subtype]++;
}
}
}
QList<SubtypeEntry> entries;
for (auto it = subtypeCounts.constBegin(); it != subtypeCounts.constEnd(); ++it) {
entries.append({it.key(), it.value()});
}
// Sort by count ascending, then alphabetically (lowest counts at bottom of display)
std::sort(entries.begin(), entries.end(), [](const SubtypeEntry &a, const SubtypeEntry &b) {
if (a.count != b.count) {
return a.count < b.count;
}
return a.name < b.name;
});
return entries;
}
} // namespace SelectionSubtypeTally

View file

@ -0,0 +1,36 @@
#ifndef SELECTION_SUBTYPE_TALLY_H
#define SELECTION_SUBTYPE_TALLY_H
#include <QList>
#include <QString>
class CardItem;
/** @brief A single subtype (e.g., "Goblin", "Warrior") with its occurrence count. */
struct SubtypeEntry
{
QString name; ///< The subtype name
int count; ///< Number of selected cards with this subtype
bool operator==(const SubtypeEntry &other) const
{
return name == other.name && count == other.count;
}
};
/**
* @brief Extracts and tallies subtypes from selected cards.
*/
namespace SelectionSubtypeTally
{
/**
* @brief Parses card type lines and counts each subtype occurrence.
*
* Skips face-down cards and cards without type info.
* @param cards The list of selected card items to analyze.
* @return Entries sorted by count ascending, then alphabetically.
*/
QList<SubtypeEntry> countSubtypes(const QList<CardItem *> &cards);
} // namespace SelectionSubtypeTally
#endif

View file

@ -12,7 +12,6 @@
#include "abstract_card_item.h" #include "abstract_card_item.h"
#include <libcockatrice/network/server/remote/game/server_card.h> #include <libcockatrice/network/server/remote/game/server_card.h>
#include <libcockatrice/utility/trice_limits.h>
class CardDatabase; class CardDatabase;
class CardDragItem; class CardDragItem;

View file

@ -19,7 +19,7 @@
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h> #include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/response_deck_download.pb.h> #include <libcockatrice/protocol/pb/response_deck_download.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false) ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false)
{ {

View file

@ -20,7 +20,7 @@
#include <libcockatrice/deck_list/deck_list.h> #include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/database/card_database_model.h> #include <libcockatrice/models/database/card_database_model.h>
#include <libcockatrice/models/database/token/token_display_model.h> #include <libcockatrice/models/database/token/token_display_model.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent) DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent)
: QDialog(parent), predefinedTokens(_predefinedTokens) : QDialog(parent), predefinedTokens(_predefinedTokens)

View file

@ -5,7 +5,7 @@
#include <QSpinBox> #include <QSpinBox>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/dice_limits.h>
DlgRollDice::DlgRollDice(QWidget *parent) : QDialog(parent) DlgRollDice::DlgRollDice(QWidget *parent) : QDialog(parent)
{ {

View file

@ -1,12 +1,16 @@
#include "game_view.h" #include "game_view.h"
#include "../client/settings/cache_settings.h" #include "../client/settings/cache_settings.h"
#include "../game/selection_subtype_tally.h"
#include "game_scene.h" #include "game_scene.h"
#include <QAction> #include <QAction>
#include <QGridLayout>
#include <QLabel> #include <QLabel>
#include <QLayout>
#include <QResizeEvent> #include <QResizeEvent>
#include <QRubberBand> #include <QRubberBand>
#include <libcockatrice/utility/qt_utils.h>
// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings. // QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings.
// This subclass disables that behavior so dragCountLabel can appear above it. // This subclass disables that behavior so dragCountLabel can appear above it.
@ -55,31 +59,40 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
refreshShortcuts(); refreshShortcuts();
rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this); rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this);
const QString countLabelStyle = "color: white; " const QString baseProperties = "color: white; "
"font-size: 14px; " "font-family: monospace; "
"font-weight: bold; " "background-color: rgba(0, 0, 0, 160); "
"background-color: rgba(0, 0, 0, 160); " "border-radius: 3px; "
"border-radius: 3px; " "padding: 1px 2px; "
"padding: 1px 2px;"; "white-space: pre;";
const QString dragCountLabelStyle = baseProperties + "font-size: 14px; font-weight: bold;";
const QString totalCountLabelStyle = baseProperties + "font-size: 16px; font-weight: bold;";
const QString subtypeTallyLabelStyle = baseProperties + "font-size: 12px;";
dragCountLabel = new QLabel(this); dragCountLabel = new QLabel(this);
dragCountLabel->setStyleSheet(countLabelStyle); dragCountLabel->setStyleSheet(dragCountLabelStyle);
dragCountLabel->hide(); dragCountLabel->hide();
dragCountLabel->raise(); dragCountLabel->raise();
totalCountLabel = new QLabel(this); totalCountLabel = new QLabel(this);
totalCountLabel->setStyleSheet(countLabelStyle); totalCountLabel->setStyleSheet(totalCountLabelStyle);
totalCountLabel->hide(); totalCountLabel->hide();
subtypeTallyContainer = new QWidget(this);
subtypeTallyContainer->setStyleSheet(subtypeTallyLabelStyle);
subtypeTallyLayout = new QGridLayout(subtypeTallyContainer);
subtypeTallyLayout->setContentsMargins(2, 2, 2, 2);
subtypeTallyLayout->setSpacing(2);
subtypeTallyContainer->hide();
} }
void GameView::resizeEvent(QResizeEvent *event) void GameView::resizeEvent(QResizeEvent *event)
{ {
QGraphicsView::resizeEvent(event); QGraphicsView::resizeEvent(event);
GameScene *s = dynamic_cast<GameScene *>(scene()); GameScene *s = static_cast<GameScene *>(scene());
if (s) { s->processViewSizeChange(event->size());
s->processViewSizeChange(event->size());
}
updateSceneRect(scene()->sceneRect()); updateSceneRect(scene()->sceneRect());
updateTotalSelectionCount(event->size()); updateTotalSelectionCount(event->size());
@ -164,29 +177,114 @@ void GameView::refreshShortcuts()
SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView")); SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView"));
} }
void GameView::clearSubtypeLabels()
{
QtUtils::clearLayoutRec(subtypeTallyLayout);
}
QSize GameView::rebuildSubtypeLabels(const QList<SubtypeEntry> &entries)
{
clearSubtypeLabels();
const QString nameStyle = QStringLiteral("color: white; font-size: 12px; background: transparent;");
const QString countStyle =
QStringLiteral("color: white; font-size: 14px; font-weight: bold; background: transparent;");
int totalHeight = 0;
int maxNameWidth = 0;
int maxCountWidth = 0;
int row = 0;
for (const SubtypeEntry &entry : entries) {
auto *nameLabel = new QLabel(entry.name, subtypeTallyContainer);
nameLabel->setStyleSheet(nameStyle);
nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
subtypeTallyLayout->addWidget(nameLabel, row, 0);
auto *countLabel = new QLabel(QString::number(entry.count), subtypeTallyContainer);
countLabel->setStyleSheet(countStyle);
countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
subtypeTallyLayout->addWidget(countLabel, row, 1);
QSize nameSize = nameLabel->sizeHint();
QSize countSize = countLabel->sizeHint();
maxNameWidth = qMax(maxNameWidth, nameSize.width());
maxCountWidth = qMax(maxCountWidth, countSize.width());
totalHeight += qMax(nameSize.height(), countSize.height());
++row;
}
int spacing = subtypeTallyLayout->spacing();
int margins = subtypeTallyLayout->contentsMargins().left() + subtypeTallyLayout->contentsMargins().right();
int verticalMargins = subtypeTallyLayout->contentsMargins().top() + subtypeTallyLayout->contentsMargins().bottom();
int width = maxNameWidth + spacing + maxCountWidth + margins;
int height = totalHeight + (row - 1) * spacing + verticalMargins;
return QSize(width, height);
}
void GameView::updateTotalSelectionCount(const QSize &viewSize) void GameView::updateTotalSelectionCount(const QSize &viewSize)
{ {
if (!SettingsCache::instance().getShowTotalSelectionCount()) { constexpr int kMarginInPixels = 10;
totalCountLabel->hide(); constexpr int kSpacingBetweenLabels = 4;
return;
} int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width();
int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height();
int count = scene()->selectedItems().count(); int count = scene()->selectedItems().count();
if (count > 1) { if (!SettingsCache::instance().getShowTotalSelectionCount() || count <= 1) {
totalCountLabel->hide();
} else {
totalCountLabel->setText(QString::number(count)); totalCountLabel->setText(QString::number(count));
totalCountLabel->adjustSize(); totalCountLabel->adjustSize();
constexpr int kMarginInPixels = 10;
int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width();
int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height();
int x = availableWidth - totalCountLabel->width() - kMarginInPixels; int x = availableWidth - totalCountLabel->width() - kMarginInPixels;
int y = availableHeight - totalCountLabel->height() - kMarginInPixels; int y = availableHeight - totalCountLabel->height() - kMarginInPixels;
totalCountLabel->move(x, y); totalCountLabel->move(x, y);
totalCountLabel->show(); totalCountLabel->show();
} else {
totalCountLabel->hide();
} }
if (!SettingsCache::instance().getShowSubtypeSelectionTally() || count <= 1) {
subtypeTallyContainer->hide();
cachedSubtypeEntries.clear();
return;
}
GameScene *gameScene = static_cast<GameScene *>(scene());
QList<SubtypeEntry> entries = SelectionSubtypeTally::countSubtypes(gameScene->selectedCards());
if (entries.isEmpty()) {
subtypeTallyContainer->hide();
cachedSubtypeEntries.clear();
return;
}
// Only rebuild labels if entries changed
QSize containerSize;
if (entries != cachedSubtypeEntries) {
cachedSubtypeEntries = entries;
containerSize = rebuildSubtypeLabels(entries);
subtypeTallyContainer->resize(containerSize);
} else {
containerSize = subtypeTallyContainer->size();
}
int x = availableWidth - containerSize.width() - kMarginInPixels;
int y;
if (totalCountLabel->isVisible()) {
y = totalCountLabel->y() - containerSize.height() - kSpacingBetweenLabels;
} else {
y = availableHeight - containerSize.height() - kMarginInPixels;
}
y = qMax(kMarginInPixels, y);
subtypeTallyContainer->move(x, y);
subtypeTallyContainer->show();
} }
/** /**

View file

@ -7,9 +7,12 @@
#ifndef GAMEVIEW_H #ifndef GAMEVIEW_H
#define GAMEVIEW_H #define GAMEVIEW_H
#include "../game/selection_subtype_tally.h"
#include <QGraphicsView> #include <QGraphicsView>
class GameScene; class GameScene;
class QGridLayout;
class QLabel; class QLabel;
class QRubberBand; class QRubberBand;
@ -21,7 +24,13 @@ private:
QRubberBand *rubberBand; QRubberBand *rubberBand;
QLabel *dragCountLabel; QLabel *dragCountLabel;
QLabel *totalCountLabel; QLabel *totalCountLabel;
QWidget *subtypeTallyContainer;
QGridLayout *subtypeTallyLayout;
QPointF selectionOrigin; QPointF selectionOrigin;
QList<SubtypeEntry> cachedSubtypeEntries; ///< Cached entries to avoid redundant rebuilds
QSize rebuildSubtypeLabels(const QList<SubtypeEntry> &entries);
void clearSubtypeLabels();
protected: protected:
void resizeEvent(QResizeEvent *event) override; void resizeEvent(QResizeEvent *event) override;

View file

@ -8,6 +8,7 @@
#include <QInputDialog> #include <QInputDialog>
#include <libcockatrice/card/relation/card_relation.h> #include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/utility/string_limits.h>
PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions) PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions)
: QObject(_player), player(_player), playerActions(_playerActions) : QObject(_player), player(_player), playerActions(_playerActions)

View file

@ -271,6 +271,9 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
const PaletteConfig &palCfg, const PaletteConfig &palCfg,
const QString &activeScheme) const QString &activeScheme)
{ {
#if (QT_VERSION < QT_VERSION_CHECK(6, 5, 0))
Q_UNUSED(activeScheme)
#endif
QString styleName = themeCfg.styleName; QString styleName = themeCfg.styleName;
if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) { if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) {
if (themeName == FUSION_THEME_NAME) { if (themeName == FUSION_THEME_NAME) {
@ -396,6 +399,7 @@ static QString roleBgName(ThemeManager::Role role)
default: default:
Q_ASSERT(false); Q_ASSERT(false);
return {};
} }
} }

View file

@ -11,7 +11,7 @@
#include <QSplitter> #include <QSplitter>
#include <QTextEdit> #include <QTextEdit>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
static int findRestoreIndex(const CardRef &wanted, const QComboBox *combo) static int findRestoreIndex(const CardRef &wanted, const QComboBox *combo)
{ {

View file

@ -12,7 +12,7 @@
#include <QMessageBox> #include <QMessageBox>
#include <QPushButton> #include <QPushButton>
#include <QRadioButton> #include <QRadioButton>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
{ {

View file

@ -17,7 +17,7 @@
#include <QSpinBox> #include <QSpinBox>
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h> #include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
void DlgCreateGame::sharedCtor() void DlgCreateGame::sharedCtor()
{ {

View file

@ -8,7 +8,7 @@
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image() DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image()
{ {

View file

@ -7,7 +7,7 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
DlgEditPassword::DlgEditPassword(QWidget *parent) : QDialog(parent) DlgEditPassword::DlgEditPassword(QWidget *parent) : QDialog(parent)
{ {

View file

@ -19,7 +19,7 @@
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/models/database/card_database_model.h> #include <libcockatrice/models/database/card_database_model.h>
#include <libcockatrice/models/database/token/token_edit_model.h> #include <libcockatrice/models/database/token/token_edit_model.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nullptr) DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nullptr)
{ {

View file

@ -6,7 +6,7 @@
#include <QGridLayout> #include <QGridLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
DlgEditUser::DlgEditUser(QWidget *parent, QString email, QString country, QString realName) : QDialog(parent) DlgEditUser::DlgEditUser(QWidget *parent, QString email, QString country, QString realName) : QDialog(parent)
{ {

View file

@ -7,7 +7,7 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
DlgForgotPasswordChallenge::DlgForgotPasswordChallenge(QWidget *parent) : QDialog(parent) DlgForgotPasswordChallenge::DlgForgotPasswordChallenge(QWidget *parent) : QDialog(parent)
{ {

View file

@ -7,7 +7,7 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
DlgForgotPasswordRequest::DlgForgotPasswordRequest(QWidget *parent) : QDialog(parent) DlgForgotPasswordRequest::DlgForgotPasswordRequest(QWidget *parent) : QDialog(parent)
{ {

View file

@ -7,7 +7,7 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent) DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent)
{ {

View file

@ -62,7 +62,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
// search field // search field
searchField = new LineEditUnfocusable; searchField = new LineEditUnfocusable;
searchField->setObjectName("searchEdit"); searchField->setObjectName("searchEdit");
searchField->setPlaceholderText(tr("Search by set name, code, or type")); searchField->setPlaceholderText(tr("Search by set name, code, type, or release date"));
searchField->addAction(QPixmap("theme:icons/search"), LineEditUnfocusable::LeadingPosition); searchField->addAction(QPixmap("theme:icons/search"), LineEditUnfocusable::LeadingPosition);
searchField->setClearButtonEnabled(true); searchField->setClearButtonEnabled(true);
setFocusProxy(searchField); setFocusProxy(searchField);

View file

@ -8,7 +8,7 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
{ {

View file

@ -5,9 +5,9 @@
#include <QPointer> #include <QPointer>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
static QString makeKey(const QString &user, const QString &card) static QString makeKey(const QString &user, const QString &card, const QString &providerId)
{ {
return user + u'|' + card; return user + u'|' + card + u'|' + providerId;
} }
UserCardArtProvider::UserCardArtProvider(QObject *parent) : QObject(parent) UserCardArtProvider::UserCardArtProvider(QObject *parent) : QObject(parent)
@ -31,13 +31,13 @@ const QMap<QString, QPixmap> &UserCardArtProvider::cache() const
return cardArtCache; return cardArtCache;
} }
void UserCardArtProvider::requestCardArt(const QString &userName, const QString &cardName) void UserCardArtProvider::requestCardArt(const QString &userName, const QString &cardName, const QString &providerId)
{ {
if (cardName.isEmpty()) { if (cardName.isEmpty()) {
return; return;
} }
const QString key = makeKey(userName, cardName); const QString key = makeKey(userName, cardName, providerId);
if (cardArtCache.contains(key) || pending.contains(key)) { if (cardArtCache.contains(key) || pending.contains(key)) {
return; return;
@ -83,15 +83,16 @@ void UserCardArtProvider::processQueue()
const QString key = queue.dequeue(); const QString key = queue.dequeue();
const QStringList parts = key.split(u'|'); const QStringList parts = key.split(u'|');
if (parts.size() != 2) { if (parts.size() != 3) {
pending.remove(key); pending.remove(key);
continue; continue;
} }
const QString userName = parts.at(0); const QString userName = parts.at(0);
const QString cardName = parts.at(1); const QString cardName = parts.at(1);
const QString providerId = parts.at(2);
ExactCard card = CardDatabaseManager::query()->getCard({cardName}); ExactCard card = CardDatabaseManager::query()->getCard({cardName, providerId});
if (!card) { if (!card) {
pending.remove(key); pending.remove(key);

View file

@ -14,7 +14,7 @@ class UserCardArtProvider : public QObject
public: public:
explicit UserCardArtProvider(QObject *parent = nullptr); explicit UserCardArtProvider(QObject *parent = nullptr);
void requestCardArt(const QString &userName, const QString &cardName); void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId);
const QMap<QString, QPixmap> &cache() const; const QMap<QString, QPixmap> &cache() const;
static QPixmap cropCardArt(const QPixmap &fullRes); static QPixmap cropCardArt(const QPixmap &fullRes);

View file

@ -70,7 +70,7 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *)
QString(), // userName not needed for override path QString(), // userName not needed for override path
nullptr, // no cache nullptr, // no cache
params, params,
&sourcePixmap // 👈 direct pixmap &sourcePixmap // direct pixmap
); );
// Avatar placeholder so the left-margin interaction is visible // Avatar placeholder so the left-margin interaction is visible
@ -174,6 +174,13 @@ void UserCardArtSettingsDialog::setupUi()
{ {
initializeSearchBar(); initializeSearchBar();
providerComboBox = new QComboBox;
connect(providerComboBox, &QComboBox::currentIndexChanged, this, [this]() {
currentParams.cardProviderId = providerComboBox->currentData().toString();
reloadPreview();
onParamChanged();
});
marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01); marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01);
marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01); marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01);
verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01); verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01);
@ -181,6 +188,7 @@ void UserCardArtSettingsDialog::setupUi()
auto *form = new QFormLayout; auto *form = new QFormLayout;
form->addRow(tr("Card name:"), searchBar); form->addRow(tr("Card name:"), searchBar);
form->addRow(tr("Card ProviderId:"), providerComboBox);
form->addRow(tr("Left margin (%):"), marginLSpin); form->addRow(tr("Left margin (%):"), marginLSpin);
form->addRow(tr("Right margin (%):"), marginRSpin); form->addRow(tr("Right margin (%):"), marginRSpin);
form->addRow(tr("Vertical offset:"), verticalOffsetSpin); form->addRow(tr("Vertical offset:"), verticalOffsetSpin);
@ -219,6 +227,32 @@ void UserCardArtSettingsDialog::setupUi()
connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
} }
void UserCardArtSettingsDialog::populateProviderCombo(const QString &cardName)
{
providerComboBox->clear();
auto card = CardDatabaseManager::query()->getCard({cardName});
const auto &sets = card.getInfo().getSets();
for (const auto &printings : sets) {
for (const auto &p : printings) {
QString setName = p.getSet()->getLongName();
QString collector = p.getProperty("num");
QString uuid = p.getUuid();
QString label = setName;
if (!collector.isEmpty()) {
label += " #" + collector;
}
providerComboBox->addItem(label, uuid);
}
}
}
void UserCardArtSettingsDialog::onCardNameChanged(const QString &name) void UserCardArtSettingsDialog::onCardNameChanged(const QString &name)
{ {
if (name.isEmpty()) { if (name.isEmpty()) {
@ -231,27 +265,68 @@ void UserCardArtSettingsDialog::onCardNameChanged(const QString &name)
if (!card) { if (!card) {
currentPixmap = QPixmap(); currentPixmap = QPixmap();
preview->setPixmap(currentPixmap); preview->setPixmap(currentPixmap);
providerComboBox->clear();
return; return;
} }
currentParams.cardName = name; currentParams.cardName = name;
populateProviderCombo(name);
if (providerComboBox->count() == 0) {
// No printings found for this card; nothing to preview.
currentPixmap = QPixmap();
preview->setPixmap(currentPixmap);
currentParams.cardProviderId.clear();
return;
}
currentParams.cardProviderId = providerComboBox->currentData().toString();
reloadPreview();
}
void UserCardArtSettingsDialog::reloadPreview()
{
if (currentParams.cardName.isEmpty()) {
return;
}
ExactCard card = CardDatabaseManager::query()->getCard({currentParams.cardName, currentParams.cardProviderId});
if (!card) {
return;
}
// CardPictureLoader::getPixmap() is async on a cache miss: it enqueues a
// background download and returns a null pixmap immediately. When that
// download finishes, CardPictureLoader::imageLoaded() caches the result
// and calls card.emitPixmapUpdated(), which emits pixmapUpdated() on the
// underlying CardInfo (see exact_card.h). Listen for that, scoped to
// whichever CardInfo we just asked for, so the preview catches up once
// the image actually arrives instead of staying on the placeholder.
//
// Disconnect any previous listener first -- otherwise switching cards
// repeatedly stacks up connections to old CardInfo objects, each of
// which would still fire reloadPreview() (harmlessly, but wastefully)
// whenever ITS art finishes loading later.
disconnect(pixmapUpdatedConnection);
QPixmap fullRes; QPixmap fullRes;
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
if (fullRes.isNull()) { if (fullRes.isNull()) {
connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this, card](const PrintingInfo &) { // Not loaded yet -- wait for the signal instead of giving up.
disconnect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, nullptr); // card.getCardPtr() is a CardInfoPtr (QSharedPointer<CardInfo>);
QPixmap loaded; // .data() gives the raw QObject* needed for connect().
CardPictureLoader::getPixmap(loaded, card, QSize(745, 1040)); CardInfo *cardInfo = card.getCardPtr().data();
currentPixmap = UserCardArtProvider::cropCardArt(loaded); if (cardInfo) {
preview->setPixmap(currentPixmap); pixmapUpdatedConnection = connect(cardInfo, &CardInfo::pixmapUpdated, this, [this]() { reloadPreview(); });
}); }
return; return;
} }
currentPixmap = UserCardArtProvider::cropCardArt(fullRes); currentPixmap = UserCardArtProvider::cropCardArt(fullRes);
preview->setPixmap(currentPixmap); preview->setPixmap(currentPixmap);
preview->setParams(currentParams);
} }
void UserCardArtSettingsDialog::onParamChanged() void UserCardArtSettingsDialog::onParamChanged()

View file

@ -3,6 +3,7 @@
#include "user_list_painter.h" #include "user_list_painter.h"
#include <QComboBox>
#include <QDialog> #include <QDialog>
#include <QPixmap> #include <QPixmap>
@ -43,10 +44,12 @@ public:
private slots: private slots:
void onCardNameChanged(const QString &name); void onCardNameChanged(const QString &name);
void reloadPreview();
void onParamChanged(); void onParamChanged();
private: private:
void setupUi(); void setupUi();
void populateProviderCombo(const QString &cardName);
void initializeSearchBar(); void initializeSearchBar();
QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step); QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step);
@ -57,6 +60,10 @@ private:
CardSearchModel *searchModel; CardSearchModel *searchModel;
CardCompleterProxyModel *proxyModel; CardCompleterProxyModel *proxyModel;
QComboBox *providerComboBox;
QMetaObject::Connection pixmapUpdatedConnection;
QDoubleSpinBox *marginLSpin; QDoubleSpinBox *marginLSpin;
QDoubleSpinBox *marginRSpin; QDoubleSpinBox *marginRSpin;
QDoubleSpinBox *verticalOffsetSpin; QDoubleSpinBox *verticalOffsetSpin;

View file

@ -335,6 +335,7 @@ void UserInfoBox::actBannerCard()
Command_SetCardArtParams cmd; Command_SetCardArtParams cmd;
cmd.set_card_name(p.cardName.toStdString()); cmd.set_card_name(p.cardName.toStdString());
if (!p.cardName.isEmpty()) { if (!p.cardName.isEmpty()) {
cmd.set_card_provider_id(p.cardProviderId.toStdString());
cmd.set_margin_pct_l(p.marginPctL); cmd.set_margin_pct_l(p.marginPctL);
cmd.set_margin_pct_r(p.marginPctR); cmd.set_margin_pct_r(p.marginPctR);
cmd.set_vertical_offset(p.verticalOffset); cmd.set_vertical_offset(p.verticalOffset);

View file

@ -542,7 +542,7 @@ void UserInfoPopup::showForUser(const QString &userName,
const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(userName)) const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(userName))
? m_cardArtParamsMap->value(userName) ? m_cardArtParamsMap->value(userName)
: CardArtParams{}; : CardArtParams{};
const QString artKey = userName + u'|' + params.cardName; const QString artKey = userName + u'|' + params.cardName + u'|' + params.cardProviderId;
const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{}; const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{};
m_header->setUserData(userInfo, online, avatar, cardArt, params); m_header->setUserData(userInfo, online, avatar, cardArt, params);

View file

@ -73,9 +73,9 @@ void UserListPainter::drawBackground(QPainter *painter,
painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2); painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
} }
static QString makeKey(const QString &user, const QString &card) static QString makeKey(const QString &user, const QString &card, const QString &providerId)
{ {
return user + u'|' + card; return user + u'|' + card + u'|' + providerId;
} }
void UserListPainter::drawCardArt(QPainter *painter, void UserListPainter::drawCardArt(QPainter *painter,
@ -95,7 +95,7 @@ void UserListPainter::drawCardArt(QPainter *painter,
return; return;
} }
const QString key = makeKey(userName, params.cardName); const QString key = makeKey(userName, params.cardName, params.cardProviderId);
if (!cardArtCache->contains(key)) { if (!cardArtCache->contains(key)) {
return; return;

View file

@ -18,6 +18,7 @@ class ServerInfo_User;
struct CardArtParams struct CardArtParams
{ {
QString cardName = ""; QString cardName = "";
QString cardProviderId = "";
double marginPctL = 0.33; double marginPctL = 0.33;
double marginPctR = 0.02; double marginPctR = 0.02;
double verticalOffset = 0.35; double verticalOffset = 0.35;

View file

@ -30,7 +30,7 @@
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h> #include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h> #include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent)
{ {
@ -767,13 +767,17 @@ void UserListWidget::showPopupForUser(const QString &userName)
m_userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn); m_userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn);
positionPopup(userName); // Realize the native window at opacity 0 before positioning so that:
// 1) move() applies to an existing native handle (not overridden by Qt's
// default centering logic on first show)
// 2) adjustSize() inside positionPopup() can measure the final laid-out
// geometry correctly
m_userInfoPopup->setWindowOpacity(0.0);
m_userInfoPopup->show(); m_userInfoPopup->show();
m_userInfoPopup->raise(); m_userInfoPopup->raise();
// Fade in positionPopup(userName); // geometry is now accurate; move() sticks
m_userInfoPopup->setWindowOpacity(0.0);
auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup); auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup);
fade->setDuration(120); fade->setDuration(120);
fade->setStartValue(0.0); fade->setStartValue(0.0);
@ -790,11 +794,10 @@ void UserListWidget::positionPopup(const QString &userName)
QWidget *vp = userTree->viewport(); QWidget *vp = userTree->viewport();
const QRect itemR = userTree->visualItemRect(item); const QRect itemR = userTree->visualItemRect(item);
const QPoint itemBR = vp->mapToGlobal(itemR.bottomRight()); const QPoint itemTL = vp->mapToGlobal(itemR.topLeft());
const QPoint vpTL = vp->mapToGlobal(vp->rect().topLeft()); const QPoint vpTL = vp->mapToGlobal(vp->rect().topLeft());
const QPoint vpTR = vp->mapToGlobal(vp->rect().topRight()); const QPoint vpTR = vp->mapToGlobal(vp->rect().topRight());
// Force a fresh size calculation so popH is accurate
m_userInfoPopup->adjustSize(); m_userInfoPopup->adjustSize();
const int popW = m_userInfoPopup->width(); const int popW = m_userInfoPopup->width();
const int popH = m_userInfoPopup->height(); const int popH = m_userInfoPopup->height();
@ -802,19 +805,32 @@ void UserListWidget::positionPopup(const QString &userName)
const QRect screen = QGuiApplication::primaryScreen()->availableGeometry(); const QRect screen = QGuiApplication::primaryScreen()->availableGeometry();
// ── X: left of the list if there's room, otherwise right ───────────────── // ── X: prefer the side with more space ───────────────────────────────────
int x = (vpTL.x() >= popW + margin) ? vpTL.x() - popW - margin : vpTR.x() + margin; const int spaceLeft = vpTL.x() - screen.left() - margin;
const int spaceRight = screen.right() - vpTR.x() - margin;
int x;
if (spaceLeft >= spaceRight) {
x = (spaceLeft >= popW) ? (vpTL.x() - margin - popW) : (vpTR.x() + margin);
} else {
x = (spaceRight >= popW) ? (vpTR.x() + margin) : (vpTL.x() - margin - popW);
}
x = qBound(screen.left() + margin, x, screen.right() - popW - margin); x = qBound(screen.left() + margin, x, screen.right() - popW - margin);
// ── Y: bottom of popup aligns with bottom of hovered row, grows upward ─── // ── Y: grow down if there's room, otherwise grow up ───────────────────────
int y = itemBR.y() - popH; const int itemTopY = itemTL.y();
const int spaceBelow = screen.bottom() - itemTopY - margin;
const int spaceAbove = itemTopY - screen.top() - margin;
// Clamp: never above the screen top int y;
y = qMax(y, screen.top() + margin); if (spaceBelow >= popH) {
y = itemTopY; // top edges align, popup grows downward
// Clamp: never below the screen bottom (e.g. if the popup is taller } else if (spaceAbove >= popH) {
// than the space above the row, let it spill downward rather than clip) y = itemTopY - popH; // bottom of popup meets top of item, grows upward
y = qMin(y, screen.bottom() - popH - margin); } else {
// Neither side fits cleanly — pick the roomier side and let clamp handle the rest
y = (spaceBelow >= spaceAbove) ? itemTopY : (itemTopY - popH);
}
y = qBound(screen.top() + margin, y, screen.bottom() - popH - margin);
m_userInfoPopup->move(x, y); m_userInfoPopup->move(x, y);
} }
@ -904,12 +920,13 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
const auto &cap = user.card_art_params(); const auto &cap = user.card_art_params();
CardArtParams params; CardArtParams params;
params.cardName = QString::fromStdString(cap.card_name()); params.cardName = QString::fromStdString(cap.card_name());
params.cardProviderId = QString::fromStdString(cap.card_provider_id());
params.marginPctL = cap.margin_pct_l(); params.marginPctL = cap.margin_pct_l();
params.marginPctR = cap.margin_pct_r(); params.marginPctR = cap.margin_pct_r();
params.verticalOffset = cap.vertical_offset(); params.verticalOffset = cap.vertical_offset();
params.zoom = cap.zoom(); params.zoom = cap.zoom();
cardArtParamsMap.insert(userName, params); cardArtParamsMap.insert(userName, params);
cardArtProvider->requestCardArt(userName, params.cardName); cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId);
} else { } else {
cardArtParamsMap.remove(userName); // clear stale params on removal cardArtParamsMap.remove(userName); // clear stale params on removal
} }

View file

@ -6,6 +6,7 @@
#include <QGridLayout> #include <QGridLayout>
#include <QLineEdit> #include <QLineEdit>
#include <QToolBar> #include <QToolBar>
#include <libcockatrice/utility/string_limits.h>
MessagesSettingsPage::MessagesSettingsPage() MessagesSettingsPage::MessagesSettingsPage()
{ {

View file

@ -68,6 +68,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setShowTotalSelectionCount); &SettingsCache::setShowTotalSelectionCount);
showSubtypeSelectionTallyCheckBox.setChecked(SettingsCache::instance().getShowSubtypeSelectionTally());
connect(&showSubtypeSelectionTallyCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setShowSubtypeSelectionTally);
useTearOffMenusCheckBox.setChecked(SettingsCache::instance().getUseTearOffMenus()); useTearOffMenusCheckBox.setChecked(SettingsCache::instance().getUseTearOffMenus());
connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
[](const QT_STATE_CHANGED_T state) { SettingsCache::instance().setUseTearOffMenus(state == Qt::Checked); }); [](const QT_STATE_CHANGED_T state) { SettingsCache::instance().setUseTearOffMenus(state == Qt::Checked); });
@ -86,8 +90,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
generalGrid->addWidget(&annotateTokensCheckBox, 6, 0); generalGrid->addWidget(&annotateTokensCheckBox, 6, 0);
generalGrid->addWidget(&showDragSelectionCountCheckBox, 7, 0); generalGrid->addWidget(&showDragSelectionCountCheckBox, 7, 0);
generalGrid->addWidget(&showTotalSelectionCountCheckBox, 8, 0); generalGrid->addWidget(&showTotalSelectionCountCheckBox, 8, 0);
generalGrid->addWidget(&useTearOffMenusCheckBox, 9, 0); generalGrid->addWidget(&showSubtypeSelectionTallyCheckBox, 9, 0);
generalGrid->addWidget(&keepGameChatFocusCheckBox, 10, 0); generalGrid->addWidget(&useTearOffMenusCheckBox, 10, 0);
generalGrid->addWidget(&keepGameChatFocusCheckBox, 11, 0);
generalGroupBox = new QGroupBox; generalGroupBox = new QGroupBox;
generalGroupBox->setLayout(generalGrid); generalGroupBox->setLayout(generalGrid);
@ -209,8 +214,9 @@ void UserInterfaceSettingsPage::retranslateUi()
closeEmptyCardViewCheckBox.setText(tr("Close card view window when last card is removed")); closeEmptyCardViewCheckBox.setText(tr("Close card view window when last card is removed"));
focusCardViewSearchBarCheckBox.setText(tr("Auto focus search bar when card view window is opened")); focusCardViewSearchBarCheckBox.setText(tr("Auto focus search bar when card view window is opened"));
annotateTokensCheckBox.setText(tr("Annotate card text on tokens")); annotateTokensCheckBox.setText(tr("Annotate card text on tokens"));
showDragSelectionCountCheckBox.setText(tr("Show selection counter during drag selection")); showDragSelectionCountCheckBox.setText(tr("Show selection count during drag selection"));
showTotalSelectionCountCheckBox.setText(tr("Show total selection counter")); showTotalSelectionCountCheckBox.setText(tr("Show total selection count"));
showSubtypeSelectionTallyCheckBox.setText(tr("Show subtype breakdown in selection tally"));
useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen")); useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen"));
keepGameChatFocusCheckBox.setText( keepGameChatFocusCheckBox.setText(
tr("Keep game chat focused when clicking in game (Note: disables card view search bar)")); tr("Keep game chat focused when clicking in game (Note: disables card view search bar)"));

View file

@ -29,6 +29,7 @@ private:
QCheckBox annotateTokensCheckBox; QCheckBox annotateTokensCheckBox;
QCheckBox showDragSelectionCountCheckBox; QCheckBox showDragSelectionCountCheckBox;
QCheckBox showTotalSelectionCountCheckBox; QCheckBox showTotalSelectionCountCheckBox;
QCheckBox showSubtypeSelectionTallyCheckBox;
QCheckBox useTearOffMenusCheckBox; QCheckBox useTearOffMenusCheckBox;
QCheckBox keepGameChatFocusCheckBox; QCheckBox keepGameChatFocusCheckBox;
QCheckBox tapAnimationCheckBox; QCheckBox tapAnimationCheckBox;

View file

@ -43,7 +43,7 @@
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h> #include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
#include <libcockatrice/protocol/pb/response.pb.h> #include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
/** /**
* @brief Constructs the AbstractTabDeckEditor. * @brief Constructs the AbstractTabDeckEditor.

View file

@ -17,7 +17,7 @@
#include <libcockatrice/protocol/pb/response_list_users.pb.h> #include <libcockatrice/protocol/pb/response_list_users.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h> #include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo) TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo)
: Tab(_tabSupervisor), client(_client) : Tab(_tabSupervisor), client(_client)

View file

@ -13,7 +13,7 @@
#include <libcockatrice/protocol/pb/event_replay_added.pb.h> #include <libcockatrice/protocol/pb/event_replay_added.pb.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h> #include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent) ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent)
{ {

View file

@ -27,7 +27,7 @@ int CardArtRulesModel::rowCount(const QModelIndex &parent) const
int CardArtRulesModel::columnCount(const QModelIndex &parent) const int CardArtRulesModel::columnCount(const QModelIndex &parent) const
{ {
Q_UNUSED(parent); Q_UNUSED(parent);
return 3; return 4;
} }
QVariant CardArtRulesModel::data(const QModelIndex &index, int role) const QVariant CardArtRulesModel::data(const QModelIndex &index, int role) const
@ -43,8 +43,10 @@ QVariant CardArtRulesModel::data(const QModelIndex &index, int role) const
case 0: case 0:
return e.cardName; return e.cardName;
case 1: case 1:
return e.mode; return e.cardProviderId;
case 2: case 2:
return e.mode;
case 3:
return e.reason; return e.reason;
} }
} }
@ -62,8 +64,10 @@ QVariant CardArtRulesModel::headerData(int section, Qt::Orientation orientation,
case 0: case 0:
return tr("Card"); return tr("Card");
case 1: case 1:
return tr("Mode"); return tr("ProviderId");
case 2: case 2:
return tr("Mode");
case 3:
return tr("Reason"); return tr("Reason");
default: default:
return {}; return {};
@ -97,6 +101,15 @@ QString CardArtRulesModel::cardAt(int row) const
return entries[row].cardName; return entries[row].cardName;
} }
const CardArtRulesModel::Entry *CardArtRulesModel::entryAt(int row) const
{
if (row < 0 || row >= static_cast<int>(entries.size())) {
return nullptr;
}
return &entries[row];
}
void CardArtRulesModel::onRefreshFinished(const Response &r) void CardArtRulesModel::onRefreshFinished(const Response &r)
{ {
if (r.response_code() != Response::RespOk) { if (r.response_code() != Response::RespOk) {
@ -109,8 +122,8 @@ void CardArtRulesModel::onRefreshFinished(const Response &r)
entries.clear(); entries.clear();
for (const auto &e : resp.entries()) { for (const auto &e : resp.entries()) {
entries.push_back({QString::fromStdString(e.card_name()), QString::fromStdString(e.mode()), entries.push_back({QString::fromStdString(e.card_name()), QString::fromStdString(e.card_provider_id()),
QString::fromStdString(e.reason())}); QString::fromStdString(e.mode()), QString::fromStdString(e.reason())});
} }
endResetModel(); endResetModel();
@ -128,6 +141,7 @@ void TabCardArtRules::setupUi()
initSearchBar(); initSearchBar();
providerComboBox = new QComboBox;
modeBox = new QComboBox; modeBox = new QComboBox;
reasonEdit = new QLineEdit; reasonEdit = new QLineEdit;
@ -146,6 +160,7 @@ void TabCardArtRules::setupUi()
auto *form = new QFormLayout; auto *form = new QFormLayout;
form->addRow(tr("Card:"), searchEdit); form->addRow(tr("Card:"), searchEdit);
form->addRow(tr("ProviderId:"), providerComboBox);
form->addRow(tr("Mode:"), modeBox); form->addRow(tr("Mode:"), modeBox);
form->addRow(tr("Reason:"), reasonEdit); form->addRow(tr("Reason:"), reasonEdit);
@ -204,6 +219,34 @@ void TabCardArtRules::initSearchBar()
}); });
connect(searchCompleter, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this, connect(searchCompleter, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
[this](const QString &name) { searchEdit->setText(name); }); [this](const QString &name) { searchEdit->setText(name); });
connect(searchEdit, &QLineEdit::editingFinished, this,
[this]() { populateProviderCombo(searchEdit->text().trimmed()); });
}
void TabCardArtRules::populateProviderCombo(const QString &cardName)
{
providerComboBox->clear();
auto card = CardDatabaseManager::query()->getCard({cardName});
const auto &sets = card.getInfo().getSets();
for (const auto &printings : sets) {
for (const auto &p : printings) {
QString setName = p.getSet()->getLongName();
QString collector = p.getProperty("num");
QString uuid = p.getUuid();
QString label = setName;
if (!collector.isEmpty()) {
label += " #" + collector;
}
providerComboBox->addItem(label, uuid);
}
}
} }
void TabCardArtRules::retranslateUi() void TabCardArtRules::retranslateUi()
@ -222,6 +265,7 @@ void TabCardArtRules::addRule()
{ {
Command_AddCardArtRule cmd; Command_AddCardArtRule cmd;
cmd.set_card_name(searchEdit->text().toStdString()); cmd.set_card_name(searchEdit->text().toStdString());
cmd.set_card_provider_id(providerComboBox->currentData().toString().toStdString());
cmd.set_mode(modeBox->currentText().toStdString()); cmd.set_mode(modeBox->currentText().toStdString());
cmd.set_reason(reasonEdit->text().toStdString()); cmd.set_reason(reasonEdit->text().toStdString());
@ -238,7 +282,10 @@ void TabCardArtRules::removeSelected()
} }
Command_RemoveCardArtRule cmd; Command_RemoveCardArtRule cmd;
cmd.set_card_name(tableModel->cardAt(idx.row()).toStdString()); const auto e = tableModel->entryAt(idx.row());
cmd.set_card_name(e->cardName.toStdString());
cmd.set_card_provider_id(e->cardProviderId.toStdString());
client->sendCommand(client->prepareModeratorCommand(cmd)); client->sendCommand(client->prepareModeratorCommand(cmd));

View file

@ -20,6 +20,7 @@ public:
struct Entry struct Entry
{ {
QString cardName; QString cardName;
QString cardProviderId;
QString mode; QString mode;
QString reason; QString reason;
}; };
@ -35,6 +36,7 @@ public:
void clear(); void clear();
QString cardAt(int row) const; QString cardAt(int row) const;
const Entry *entryAt(int row) const;
private slots: private slots:
void onRefreshFinished(const Response &r); void onRefreshFinished(const Response &r);
@ -70,11 +72,13 @@ private:
QLineEdit *searchEdit; QLineEdit *searchEdit;
void initSearchBar(); void initSearchBar();
void populateProviderCombo(const QString &cardName);
QCompleter *searchCompleter; QCompleter *searchCompleter;
CardDatabaseModel *cardDbModel; CardDatabaseModel *cardDbModel;
CardDatabaseDisplayModel *cardDbDisplayModel; CardDatabaseDisplayModel *cardDbDisplayModel;
CardSearchModel *cardSearchModel; CardSearchModel *cardSearchModel;
CardCompleterProxyModel *cardProxyModel; CardCompleterProxyModel *cardProxyModel;
QComboBox *providerComboBox;
QComboBox *modeBox; QComboBox *modeBox;
QLineEdit *reasonEdit; QLineEdit *reasonEdit;

View file

@ -21,7 +21,6 @@
#include <libcockatrice/models/database/card_database_model.h> #include <libcockatrice/models/database/card_database_model.h>
#include <libcockatrice/network/client/abstract/abstract_client.h> #include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h>
/** /**
* @brief Constructs a new TabDeckEditor object. * @brief Constructs a new TabDeckEditor object.

View file

@ -28,6 +28,7 @@
#include <libcockatrice/protocol/pb/response_deck_download.pb.h> #include <libcockatrice/protocol/pb/response_deck_download.pb.h>
#include <libcockatrice/protocol/pb/response_deck_upload.pb.h> #include <libcockatrice/protocol/pb/response_deck_upload.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/string_limits.h>
TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
AbstractClient *_client, AbstractClient *_client,

View file

@ -44,7 +44,7 @@
#include <libcockatrice/protocol/pb/game_replay.pb.h> #include <libcockatrice/protocol/pb/game_replay.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h> #include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h> #include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
: Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr) : Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr)

View file

@ -17,7 +17,7 @@
#include <libcockatrice/protocol/pb/moderator_commands.pb.h> #include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pb/response_viewlog_history.pb.h> #include <libcockatrice/protocol/pb/response_viewlog_history.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
{ {

View file

@ -17,7 +17,7 @@
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h> #include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h> #include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
TabMessage::TabMessage(TabSupervisor *_tabSupervisor, TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
AbstractClient *_client, AbstractClient *_client,

View file

@ -31,7 +31,7 @@
#include <libcockatrice/protocol/pb/room_commands.pb.h> #include <libcockatrice/protocol/pb/room_commands.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_room.pb.h> #include <libcockatrice/protocol/pb/serverinfo_room.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
TabRoom::TabRoom(TabSupervisor *_tabSupervisor, TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
AbstractClient *_client, AbstractClient *_client,

View file

@ -9,7 +9,7 @@
#include <QLineEdit> #include <QLineEdit>
#include <QWidget> #include <QWidget>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
QString getTextWithMax(QWidget *parent, QString getTextWithMax(QWidget *parent,
const QString &title, const QString &title,

View file

@ -6,6 +6,7 @@
<xs:attribute type="xs:string" name="exclude" use="optional" /> <xs:attribute type="xs:string" name="exclude" use="optional" />
<xs:attribute type="xs:string" name="attach" use="optional" /> <xs:attribute type="xs:string" name="attach" use="optional" />
<xs:attribute type="xs:string" name="persistent" use="optional" /> <xs:attribute type="xs:string" name="persistent" use="optional" />
<xs:attribute type="xs:string" name="facedown" use="optional"/>
</xs:extension> </xs:extension>
</xs:simpleContent> </xs:simpleContent>
</xs:complexType> </xs:complexType>

View file

@ -85,7 +85,8 @@ void CardDatabase::refreshCachedReverseRelatedCards()
for (auto *rel : card->getReverseRelatedCards()) { for (auto *rel : card->getReverseRelatedCards()) {
if (auto target = cards.value(rel->getName())) { if (auto target = cards.value(rel->getName())) {
auto *newRel = new CardRelation(card->getName(), rel->getAttachType(), rel->getIsCreateAllExclusion(), auto *newRel = new CardRelation(card->getName(), rel->getAttachType(), rel->getIsCreateAllExclusion(),
rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent()); rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent(),
rel->getIsFaceDown());
target->addReverseRelatedCards2Me(newRel); target->addReverseRelatedCards2Me(newRel);
} }
} }

View file

@ -329,6 +329,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
bool exclude = false; bool exclude = false;
bool variable = false; bool variable = false;
bool persistent = false; bool persistent = false;
bool facedown = false;
int count = 1; int count = 1;
QXmlStreamAttributes attrs = xml.attributes(); QXmlStreamAttributes attrs = xml.attributes();
QString cardName = xml.readElementText(QXmlStreamReader::IncludeChildElements); QString cardName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
@ -360,7 +361,12 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
persistent = true; persistent = true;
} }
auto *relation = new CardRelation(cardName, attachType, exclude, variable, count, persistent); if (attrs.hasAttribute("facedown")) {
facedown = true;
}
auto *relation =
new CardRelation(cardName, attachType, exclude, variable, count, persistent, facedown);
if (xmlName == "reverse-related") { if (xmlName == "reverse-related") {
reverseRelatedCards << relation; reverseRelatedCards << relation;
} else { } else {
@ -510,6 +516,9 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in
if (i->getIsPersistent()) { if (i->getIsPersistent()) {
xml.writeAttribute("persistent", "persistent"); xml.writeAttribute("persistent", "persistent");
} }
if (i->getIsFaceDown()) {
xml.writeAttribute("facedown", "facedown");
}
if (i->getIsVariable()) { if (i->getIsVariable()) {
if (1 == i->getDefaultCount()) { if (1 == i->getDefaultCount()) {
xml.writeAttribute("count", "x"); xml.writeAttribute("count", "x");

View file

@ -7,8 +7,10 @@ CardRelation::CardRelation(const QString &_name,
bool _isCreateAllExclusion, bool _isCreateAllExclusion,
bool _isVariableCount, bool _isVariableCount,
int _defaultCount, int _defaultCount,
bool _isPersistent) bool _isPersistent,
bool _isFaceDown)
: name(_name), attachType(_attachType), isCreateAllExclusion(_isCreateAllExclusion), : name(_name), attachType(_attachType), isCreateAllExclusion(_isCreateAllExclusion),
isVariableCount(_isVariableCount), defaultCount(_defaultCount), isPersistent(_isPersistent) isVariableCount(_isVariableCount), defaultCount(_defaultCount), isPersistent(_isPersistent),
isFaceDown(_isFaceDown)
{ {
} }

View file

@ -31,6 +31,7 @@ private:
bool isVariableCount; ///< True if the number of creations is variable. bool isVariableCount; ///< True if the number of creations is variable.
int defaultCount; ///< Default number of cards created or involved. int defaultCount; ///< Default number of cards created or involved.
bool isPersistent; ///< True if this relation persists (i.e. is not destroyed) on zone change. bool isPersistent; ///< True if this relation persists (i.e. is not destroyed) on zone change.
bool isFaceDown; ///< True if this relation creates the tokens facedown
public: public:
/** /**
@ -42,13 +43,15 @@ public:
* @param _isVariableCount Whether the count is variable. * @param _isVariableCount Whether the count is variable.
* @param _defaultCount Default number for creations or transformations. * @param _defaultCount Default number for creations or transformations.
* @param _isPersistent Whether the relation persists across zone changes. * @param _isPersistent Whether the relation persists across zone changes.
* @param _isFaceDown Whether the relation creates the token face down
*/ */
explicit CardRelation(const QString &_name = QString(), explicit CardRelation(const QString &_name = QString(),
CardRelationType _attachType = CardRelationType::DoesNotAttach, CardRelationType _attachType = CardRelationType::DoesNotAttach,
bool _isCreateAllExclusion = false, bool _isCreateAllExclusion = false,
bool _isVariableCount = false, bool _isVariableCount = false,
int _defaultCount = 1, int _defaultCount = 1,
bool _isPersistent = false); bool _isPersistent = false,
bool _isFaceDown = false);
/** /**
* @brief Returns the name of the related card. * @brief Returns the name of the related card.
@ -151,6 +154,16 @@ public:
{ {
return isPersistent; return isPersistent;
} }
/**
* @brief Returns whether the relation creates the token facedown.
*
* @return True if facedown, false otherwise.
*/
[[nodiscard]] bool getIsFaceDown() const
{
return isFaceDown;
}
}; };
#endif // COCKATRICE_CARD_RELATION_H #endif // COCKATRICE_CARD_RELATION_H

View file

@ -303,12 +303,14 @@ bool SetsDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex &source
auto typeIndex = sourceModel()->index(sourceRow, SetsModel::SetTypeCol, sourceParent); auto typeIndex = sourceModel()->index(sourceRow, SetsModel::SetTypeCol, sourceParent);
auto nameIndex = sourceModel()->index(sourceRow, SetsModel::LongNameCol, sourceParent); auto nameIndex = sourceModel()->index(sourceRow, SetsModel::LongNameCol, sourceParent);
auto shortNameIndex = sourceModel()->index(sourceRow, SetsModel::ShortNameCol, sourceParent); auto shortNameIndex = sourceModel()->index(sourceRow, SetsModel::ShortNameCol, sourceParent);
auto dateIndex = sourceModel()->index(sourceRow, SetsModel::ReleaseDateCol, sourceParent);
const auto filter = filterRegularExpression(); const auto filter = filterRegularExpression();
return (sourceModel()->data(typeIndex).toString().contains(filter) || return (sourceModel()->data(typeIndex).toString().contains(filter) ||
sourceModel()->data(nameIndex).toString().contains(filter) || sourceModel()->data(nameIndex).toString().contains(filter) ||
sourceModel()->data(shortNameIndex).toString().contains(filter)); sourceModel()->data(shortNameIndex).toString().contains(filter) ||
sourceModel()->data(dateIndex).toString().contains(filter));
} }
bool SetsDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const bool SetsDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const

View file

@ -48,7 +48,7 @@
#include <libcockatrice/protocol/pb/response.pb.h> #include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h> #include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h> #include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game, Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game,
int _playerId, int _playerId,

View file

@ -47,7 +47,8 @@
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h> #include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h> #include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/rng/rng_abstract.h> #include <libcockatrice/rng/rng_abstract.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/dice_limits.h>
#include <libcockatrice/utility/string_limits.h>
#include <libcockatrice/utility/zone_names.h> #include <libcockatrice/utility/zone_names.h>
#include <limits> #include <limits>
#include <ranges> #include <ranges>

View file

@ -26,7 +26,8 @@
#include <libcockatrice/protocol/pb/event_set_card_attr.pb.h> #include <libcockatrice/protocol/pb/event_set_card_attr.pb.h>
#include <libcockatrice/protocol/pb/event_set_card_counter.pb.h> #include <libcockatrice/protocol/pb/event_set_card_counter.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h> #include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/clamped_arithmetic.h>
#include <libcockatrice/utility/counter_limits.h>
#include <limits> #include <limits>
Server_Card::Server_Card(const CardRef &cardRef, int _id, int _coord_x, int _coord_y, Server_CardZone *_zone) Server_Card::Server_Card(const CardRef &cardRef, int _id, int _coord_x, int _coord_y, Server_CardZone *_zone)
@ -114,8 +115,8 @@ QString Server_Card::setAttribute(CardAttribute attribute, const QString &avalue
bool Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event) bool Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event)
{ {
// Clamp to valid card counter range [0, MAX_COUNTERS_ON_CARD] // Clamp to valid card counter range [0, MAX_COUNTER_VALUE]
value = qBound(0, value, MAX_COUNTERS_ON_CARD); value = qBound(0, value, MAX_COUNTER_VALUE);
const int oldValue = counters.value(_id, 0); const int oldValue = counters.value(_id, 0);
if (value == oldValue) { if (value == oldValue) {
@ -139,10 +140,8 @@ bool Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event)
bool Server_Card::incrementCounter(int counterId, int delta, Event_SetCardCounter *event) bool Server_Card::incrementCounter(int counterId, int delta, Event_SetCardCounter *event)
{ {
const int oldValue = counters.value(counterId, 0); const int oldValue = counters.value(counterId, 0);
const auto result = static_cast<int64_t>(oldValue) + static_cast<int64_t>(delta); // Clamp to [0, MAX_COUNTER_VALUE] for card counters
// Clamp to [0, MAX_COUNTERS_ON_CARD] for card counters const int newValue = addClamped(oldValue, delta, 0, MAX_COUNTER_VALUE);
const int newValue =
static_cast<int>(qBound(static_cast<int64_t>(0), result, static_cast<int64_t>(MAX_COUNTERS_ON_CARD)));
if (newValue == oldValue) { if (newValue == oldValue) {
return false; return false;

View file

@ -156,7 +156,7 @@ public:
/** /**
* @brief Sets a card counter to an exact value with clamping. * @brief Sets a card counter to an exact value with clamping.
* @param _id The counter ID. * @param _id The counter ID.
* @param value The desired value (clamped to [0, MAX_COUNTERS_ON_CARD]; 0 removes the counter). * @param value The desired value (clamped to [0, MAX_COUNTER_VALUE]; 0 removes the counter).
* @param event Optional event to populate with counter state. * @param event Optional event to populate with counter state.
* @return true if the value changed, false otherwise. * @return true if the value changed, false otherwise.
*/ */
@ -168,7 +168,7 @@ public:
* @param event Optional event to populate with counter state. * @param event Optional event to populate with counter state.
* @return true if the value changed, false otherwise. * @return true if the value changed, false otherwise.
* @note If counter does not exist, starts from 0. Counter is removed if result is 0. * @note If counter does not exist, starts from 0. Counter is removed if result is 0.
* @note Clamps result to [0, MAX_COUNTERS_ON_CARD]. * @note Clamps result to [0, MAX_COUNTER_VALUE].
*/ */
[[nodiscard]] bool incrementCounter(int counterId, int delta, Event_SetCardCounter *event = nullptr); [[nodiscard]] bool incrementCounter(int counterId, int delta, Event_SetCardCounter *event = nullptr);
void setTapped(bool _tapped) void setTapped(bool _tapped)

View file

@ -1,24 +1,12 @@
#include "server_counter.h" #include "server_counter.h"
#include <libcockatrice/protocol/pb/serverinfo_counter.pb.h> #include <libcockatrice/protocol/pb/serverinfo_counter.pb.h>
#include <limits>
Server_Counter::Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count) Server_Counter::Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count)
: id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count) : id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count)
{ {
} }
//! \todo Extract overflow-safe arithmetic into shared helper.
//! Duplicated in Server_Card::incrementCounter() - keep in sync if modified.
bool Server_Counter::incrementCount(int delta)
{
const int oldCount = count;
const auto result = static_cast<int64_t>(count) + static_cast<int64_t>(delta);
count = static_cast<int>(qBound(static_cast<int64_t>(std::numeric_limits<int>::min()), result,
static_cast<int64_t>(std::numeric_limits<int>::max())));
return count != oldCount;
}
void Server_Counter::getInfo(ServerInfo_Counter *info) void Server_Counter::getInfo(ServerInfo_Counter *info)
{ {
info->set_id(id); info->set_id(id);

View file

@ -22,6 +22,8 @@
#include <QString> #include <QString>
#include <libcockatrice/protocol/pb/color.pb.h> #include <libcockatrice/protocol/pb/color.pb.h>
#include <libcockatrice/utility/clamped_arithmetic.h>
#include <limits>
class ServerInfo_Counter; class ServerInfo_Counter;
@ -92,7 +94,12 @@ public:
* @return true if the value changed, false otherwise. * @return true if the value changed, false otherwise.
* @note Clamps result to [INT_MIN, INT_MAX] to prevent overflow. * @note Clamps result to [INT_MIN, INT_MAX] to prevent overflow.
*/ */
[[nodiscard]] bool incrementCount(int delta); [[nodiscard]] bool incrementCount(int delta)
{
const int oldCount = count;
count = addClamped(count, delta, std::numeric_limits<int>::min(), std::numeric_limits<int>::max());
return count != oldCount;
}
/** /**
* @brief Populates info with this counter's current state for network serialization. * @brief Populates info with this counter's current state for network serialization.

View file

@ -47,7 +47,7 @@
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h> #include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/rng/rng_abstract.h> #include <libcockatrice/rng/rng_abstract.h>
#include <libcockatrice/utility/color.h> #include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
#include <libcockatrice/utility/zone_names.h> #include <libcockatrice/utility/zone_names.h>
Server_Player::Server_Player(Server_Game *_game, Server_Player::Server_Player(Server_Game *_game,

View file

@ -26,7 +26,7 @@
#include <libcockatrice/protocol/pb/response_list_users.pb.h> #include <libcockatrice/protocol/pb/response_list_users.pb.h>
#include <libcockatrice/protocol/pb/response_login.pb.h> #include <libcockatrice/protocol/pb/response_login.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h> #include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
Server_ProtocolHandler::Server_ProtocolHandler(Server *_server, Server_ProtocolHandler::Server_ProtocolHandler(Server *_server,
Server_DatabaseInterface *_databaseInterface, Server_DatabaseInterface *_databaseInterface,

View file

@ -15,7 +15,7 @@
#include <libcockatrice/protocol/pb/room_commands.pb.h> #include <libcockatrice/protocol/pb/room_commands.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_chat_message.pb.h> #include <libcockatrice/protocol/pb/serverinfo_chat_message.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_room.pb.h> #include <libcockatrice/protocol/pb/serverinfo_room.pb.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
Server_Room::Server_Room(int _id, Server_Room::Server_Room(int _id,
int _chatHistorySize, int _chatHistorySize,

View file

@ -5,7 +5,7 @@
#include <google/protobuf/descriptor.h> #include <google/protobuf/descriptor.h>
#include <google/protobuf/message.h> #include <google/protobuf/message.h>
#include <google/protobuf/text_format.h> #include <google/protobuf/text_format.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
// FastFieldValuePrinter is added in protobuf 3.4, going out of our way to add the old FieldValuePrinter is not worth it // FastFieldValuePrinter is added in protobuf 3.4, going out of our way to add the old FieldValuePrinter is not worth it
#if GOOGLE_PROTOBUF_VERSION > 3004000 #if GOOGLE_PROTOBUF_VERSION > 3004000

View file

@ -116,8 +116,9 @@ message Command_AddCardArtRule {
} }
optional string card_name = 1; optional string card_name = 1;
optional string mode = 2; // "ALLOW" or "DENY" optional string card_provider_id = 2;
optional string reason = 3; optional string mode = 3; // "ALLOW" or "DENY"
optional string reason = 4;
} }
message Command_RemoveCardArtRule { message Command_RemoveCardArtRule {
@ -126,6 +127,7 @@ message Command_RemoveCardArtRule {
} }
optional string card_name = 1; optional string card_name = 1;
optional string card_provider_id = 2;
} }
message Command_ListCardArtRules { message Command_ListCardArtRules {

View file

@ -3,8 +3,9 @@ import "response.proto";
message Response_CardArtRuleEntry { message Response_CardArtRuleEntry {
optional string card_name = 1; optional string card_name = 1;
optional string mode = 2; optional string card_provider_id = 2;
optional string reason = 3; optional string mode = 3;
optional string reason = 4;
} }
message Response_ListCardArtRules { message Response_ListCardArtRules {

View file

@ -15,10 +15,11 @@ message ServerInfo_User {
}; };
message CardArtParams { message CardArtParams {
optional string card_name = 1; optional string card_name = 1;
optional double margin_pct_l = 2 [default = 0.33]; optional string card_provider_id = 2;
optional double margin_pct_r = 3 [default = 0.02]; optional double margin_pct_l = 3 [default = 0.33];
optional double vertical_offset = 4 [default = 0.35]; optional double margin_pct_r = 4 [default = 0.02];
optional double zoom = 5 [default = 1.0]; optional double vertical_offset = 5 [default = 0.35];
optional double zoom = 6 [default = 1.0];
}; };
optional string name = 1; optional string name = 1;

View file

@ -212,8 +212,9 @@ message Command_SetCardArtParams {
optional Command_SetCardArtParams ext = 1025; optional Command_SetCardArtParams ext = 1025;
} }
optional string card_name = 1; optional string card_name = 1;
optional double margin_pct_l = 2; optional string card_provider_id = 2;
optional double margin_pct_r = 3; optional double margin_pct_l = 3;
optional double vertical_offset = 4; optional double margin_pct_r = 4;
optional double zoom = 5; optional double vertical_offset = 5;
optional double zoom = 6;
} }

View file

@ -15,7 +15,10 @@ set(UTILITY_HEADERS
libcockatrice/utility/levenshtein.h libcockatrice/utility/levenshtein.h
libcockatrice/utility/macros.h libcockatrice/utility/macros.h
libcockatrice/utility/passwordhasher.h libcockatrice/utility/passwordhasher.h
libcockatrice/utility/trice_limits.h libcockatrice/utility/string_limits.h
libcockatrice/utility/dice_limits.h
libcockatrice/utility/counter_limits.h
libcockatrice/utility/clamped_arithmetic.h
libcockatrice/utility/zone_names.h libcockatrice/utility/zone_names.h
libcockatrice/utility/days_years_between.h libcockatrice/utility/days_years_between.h
) )

View file

@ -0,0 +1,22 @@
#ifndef CLAMPED_ARITHMETIC_H
#define CLAMPED_ARITHMETIC_H
#include <QtGlobal>
#include <cstdint>
/**
* @brief Overflow-safe clamped addition: returns value + delta bounded to [minValue, maxValue].
*
* Uses a 64-bit intermediate so the addition cannot overflow int. Shared by the bounded
* counter arithmetic in both the client and the server.
*
* @note Requires minValue <= maxValue. Bounds come from trusted compile-time call sites;
* qBound() asserts this internally in debug builds.
*/
inline int addClamped(int value, int delta, int minValue, int maxValue)
{
const auto result = static_cast<int64_t>(value) + static_cast<int64_t>(delta);
return static_cast<int>(qBound(static_cast<int64_t>(minValue), result, static_cast<int64_t>(maxValue)));
}
#endif // CLAMPED_ARITHMETIC_H

View file

@ -0,0 +1,17 @@
#ifndef COUNTER_LIMITS_H
#define COUNTER_LIMITS_H
/**
* @brief Upper bound for a bounded counter's value: [0, MAX_COUNTER_VALUE].
*
* Caps an individual counter's VALUE (e.g. a +1/+1 counter at 999), not how many counters
* something holds. Applies to counters that are constrained to a non-negative display range,
* such as card counters and commander tax. Unbounded counters (e.g. a player's life total)
* do not use this limit and may go negative, saturating only at the int range.
*
* The max of 999 is a display constraint (3-digit rendering) and a reasonable gameplay limit.
* The server enforces these bounds; the client may also check them for UX optimization.
*/
constexpr int MAX_COUNTER_VALUE = 999;
#endif // COUNTER_LIMITS_H

View file

@ -0,0 +1,15 @@
#ifndef DICE_LIMITS_H
#define DICE_LIMITS_H
#include <QtGlobal> // for uint
/** @brief Fewest sides a rollable die may have. */
constexpr uint MINIMUM_DIE_SIDES = 2;
/** @brief Most sides a rollable die may have. */
constexpr uint MAXIMUM_DIE_SIDES = 1000000;
/** @brief Fewest dice that may be rolled at once. */
constexpr uint MINIMUM_DICE_TO_ROLL = 1;
/** @brief Most dice that may be rolled at once. */
constexpr uint MAXIMUM_DICE_TO_ROLL = 100;
#endif // DICE_LIMITS_H

View file

@ -1,5 +1,6 @@
#ifndef COCKATRICE_QT_UTILS_H #ifndef COCKATRICE_QT_UTILS_H
#define COCKATRICE_QT_UTILS_H #define COCKATRICE_QT_UTILS_H
#include <QLayout>
#include <QObject> #include <QObject>
namespace QtUtils namespace QtUtils

View file

@ -0,0 +1,31 @@
#ifndef STRING_LIMITS_H
#define STRING_LIMITS_H
#include <QString>
#include <algorithm>
#include <string>
/** @brief Max size for short strings, like names and things that are generally a single phrase. */
constexpr int MAX_NAME_LENGTH = 0xff;
/** @brief Max size for chat messages and text contents. */
constexpr int MAX_TEXT_LENGTH = 0xfff;
/** @brief Max size for deck files and pictures (about 2 megabytes). */
constexpr int MAX_FILE_LENGTH = 0x1fffff;
/** @brief Returns a QString from a std::string, truncated to at most MAX_NAME_LENGTH bytes. */
inline QString nameFromStdString(const std::string &_string)
{
return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_NAME_LENGTH));
}
/** @brief Returns a QString from a std::string, truncated to at most MAX_TEXT_LENGTH bytes. */
inline QString textFromStdString(const std::string &_string)
{
return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_TEXT_LENGTH));
}
/** @brief Returns a QString from a std::string, truncated to at most MAX_FILE_LENGTH bytes. */
inline QString fileFromStdString(const std::string &_string)
{
return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_FILE_LENGTH));
}
#endif // STRING_LIMITS_H

View file

@ -1,38 +0,0 @@
#ifndef TRICE_LIMITS_H
#define TRICE_LIMITS_H
#include <QString>
// max size for short strings, like names and things that are generally a single phrase
constexpr int MAX_NAME_LENGTH = 0xff;
// max size for chat messages and text contents
constexpr int MAX_TEXT_LENGTH = 0xfff;
// max size for deck files and pictures
constexpr int MAX_FILE_LENGTH = 0x1fffff; // about 2 megabytes
constexpr uint MINIMUM_DIE_SIDES = 2;
constexpr uint MAXIMUM_DIE_SIDES = 1000000;
constexpr uint MINIMUM_DICE_TO_ROLL = 1;
constexpr uint MAXIMUM_DICE_TO_ROLL = 100;
// Card counter value bounds [0, MAX_COUNTERS_ON_CARD].
// Counters on cards (e.g., +1/+1 counters, charge counters) are non-negative physical game objects.
// The max of 999 is a display constraint (3-digit rendering) and reasonable gameplay limit.
// Server enforces these bounds; client may also check for UX optimization.
constexpr int MAX_COUNTERS_ON_CARD = 999;
// optimized functions to get qstrings that are at most that long
static inline QString nameFromStdString(const std::string &_string)
{
return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_NAME_LENGTH));
}
static inline QString textFromStdString(const std::string &_string)
{
return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_TEXT_LENGTH));
}
static inline QString fileFromStdString(const std::string &_string)
{
return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_FILE_LENGTH));
}
#endif // TRICE_LIMITS_H

View file

@ -278,7 +278,7 @@
<context> <context>
<name>OracleImporter</name> <name>OracleImporter</name>
<message> <message>
<location filename="src/oracleimporter.cpp" line="540"/> <location filename="src/oracleimporter.cpp" line="542"/>
<source>Dummy set containing tokens</source> <source>Dummy set containing tokens</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -286,7 +286,7 @@
<context> <context>
<name>OracleWizard</name> <name>OracleWizard</name>
<message> <message>
<location filename="src/oraclewizard.cpp" line="97"/> <location filename="src/oraclewizard.cpp" line="101"/>
<source>Oracle Importer</source> <source>Oracle Importer</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>

View file

@ -3,12 +3,13 @@ ALTER TABLE `cockatrice_users` ADD COLUMN `card_art_params` TEXT DEFAULT NULL, A
CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` ( CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT, `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`card_name` varchar(255) NOT NULL, `card_name` varchar(255) NOT NULL,
`card_provider_id` varchar(255) NOT NULL,
`mode` enum('ALLOW','DENY') NOT NULL, `mode` enum('ALLOW','DENY') NOT NULL,
`reason` varchar(255) DEFAULT NULL, `reason` varchar(255) DEFAULT NULL,
`created_by` int(7) unsigned DEFAULT NULL, `created_by` int(7) unsigned DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uniq_card_name` (`card_name`), UNIQUE KEY `uniq_provider_card_name` (`card_provider_id`, `card_name`),
KEY `idx_mode` (`mode`), KEY `idx_mode` (`mode`),
FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`) FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`)
ON DELETE SET NULL ON DELETE SET NULL

View file

@ -305,12 +305,13 @@ CREATE TABLE IF NOT EXISTS `cockatrice_audit` (
CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` ( CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT, `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`card_name` varchar(255) NOT NULL, `card_name` varchar(255) NOT NULL,
`card_provider_id` varchar(255) NOT NULL,
`mode` enum('ALLOW','DENY') NOT NULL, `mode` enum('ALLOW','DENY') NOT NULL,
`reason` varchar(255) DEFAULT NULL, `reason` varchar(255) DEFAULT NULL,
`created_by` int(7) unsigned DEFAULT NULL, `created_by` int(7) unsigned DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uniq_card_name` (`card_name`), UNIQUE KEY `uniq_provider_card_name` (`card_provider_id`, `card_name`),
KEY `idx_mode` (`mode`), KEY `idx_mode` (`mode`),
FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`) FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`)
ON DELETE SET NULL ON DELETE SET NULL

View file

@ -693,6 +693,9 @@ ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuer
if (obj.contains("card_name")) { if (obj.contains("card_name")) {
cap->set_card_name(obj["card_name"].toString().toStdString()); cap->set_card_name(obj["card_name"].toString().toStdString());
} }
if (obj.contains("card_provider_id")) {
cap->set_card_provider_id(obj["card_provider_id"].toString().toStdString());
}
if (obj.contains("marginPctL")) { if (obj.contains("marginPctL")) {
cap->set_margin_pct_l(obj["marginPctL"].toDouble(0.33)); cap->set_margin_pct_l(obj["marginPctL"].toDouble(0.33));
} }

View file

@ -83,7 +83,7 @@
#include <libcockatrice/protocol/pb/serverinfo_deckstorage.pb.h> #include <libcockatrice/protocol/pb/serverinfo_deckstorage.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_replay.pb.h> #include <libcockatrice/protocol/pb/serverinfo_replay.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h> #include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/string_limits.h>
#include <server_response_containers.h> #include <server_response_containers.h>
#include <server_room.h> #include <server_room.h>
#include <string> #include <string>
@ -1577,11 +1577,13 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAccountImage(const Comm
return Response::RespOk; return Response::RespOk;
} }
bool AbstractServerSocketInterface::isCardNameAllowed(const QString &cardName) bool AbstractServerSocketInterface::isCardNameAllowed(const QString &cardName, const QString &cardProviderId)
{ {
QSqlQuery *q = sqlInterface->prepareQuery("SELECT mode FROM {prefix}_card_art_name_rules WHERE card_name = :name"); QSqlQuery *q = sqlInterface->prepareQuery(
"SELECT mode FROM {prefix}_card_art_name_rules WHERE card_name = :name AND card_provider_id = :provider");
q->bindValue(":name", cardName); q->bindValue(":name", cardName);
q->bindValue(":provider", cardProviderId);
if (!sqlInterface->execSqlQuery(q)) { if (!sqlInterface->execSqlQuery(q)) {
qWarning() << "Card art rule lookup failed; failing open for" << cardName; qWarning() << "Card art rule lookup failed; failing open for" << cardName;
@ -1603,8 +1605,9 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const
} }
const QString cardName = QString::fromStdString(cmd.card_name()); const QString cardName = QString::fromStdString(cmd.card_name());
const QString cardProviderId = QString::fromStdString(cmd.card_provider_id());
if (cardName.length() > MAX_NAME_LENGTH) { if (cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) {
return Response::RespInvalidData; return Response::RespInvalidData;
} }
@ -1620,7 +1623,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const
return Response::RespOk; return Response::RespOk;
} }
if (!isCardNameAllowed(cardName)) { if (!isCardNameAllowed(cardName, cardProviderId)) {
return Response::RespFunctionNotAllowed; return Response::RespFunctionNotAllowed;
} }
@ -1633,6 +1636,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const
QJsonObject obj; QJsonObject obj;
obj["card_name"] = cardName; obj["card_name"] = cardName;
obj["card_provider_id"] = cardProviderId;
obj["marginPctL"] = marginPctL; obj["marginPctL"] = marginPctL;
obj["marginPctR"] = marginPctR; obj["marginPctR"] = marginPctR;
obj["verticalOffset"] = verticalOffset; obj["verticalOffset"] = verticalOffset;
@ -1649,6 +1653,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const
// Keep the in-memory userInfo in sync // Keep the in-memory userInfo in sync
auto *cap = userInfo->mutable_card_art_params(); auto *cap = userInfo->mutable_card_art_params();
cap->set_card_name(cmd.card_name()); cap->set_card_name(cmd.card_name());
cap->set_card_provider_id(cmd.card_provider_id());
cap->set_margin_pct_l(marginPctL); cap->set_margin_pct_l(marginPctL);
cap->set_margin_pct_r(marginPctR); cap->set_margin_pct_r(marginPctR);
cap->set_vertical_offset(verticalOffset); cap->set_vertical_offset(verticalOffset);
@ -1664,21 +1669,23 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAddCardArtRule(const Co
ResponseContainer &) ResponseContainer &)
{ {
const QString cardName = QString::fromStdString(cmd.card_name()); const QString cardName = QString::fromStdString(cmd.card_name());
const QString cardProviderId = QString::fromStdString(cmd.card_provider_id());
const QString mode = QString::fromStdString(cmd.mode()); const QString mode = QString::fromStdString(cmd.mode());
if (mode != "ALLOW" && mode != "DENY") { if (mode != "ALLOW" && mode != "DENY") {
return Response::RespInvalidData; return Response::RespInvalidData;
} }
if (cardName.isEmpty() || cardName.length() > MAX_NAME_LENGTH) { if (cardName.isEmpty() || cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) {
return Response::RespInvalidData; return Response::RespInvalidData;
} }
QSqlQuery *q = sqlInterface->prepareQuery("INSERT INTO {prefix}_card_art_name_rules " QSqlQuery *q = sqlInterface->prepareQuery("INSERT INTO {prefix}_card_art_name_rules "
"(card_name, mode, reason, created_by) " "(card_name, card_provider_id, mode, reason, created_by) "
"VALUES (:name, :mode, :reason, :uid) " "VALUES (:name, :provider, :mode, :reason, :uid) "
"ON DUPLICATE KEY UPDATE mode=:mode2, reason=:reason2"); "ON DUPLICATE KEY UPDATE mode=:mode2, reason=:reason2");
q->bindValue(":name", cardName); q->bindValue(":name", cardName);
q->bindValue(":provider", cardProviderId);
q->bindValue(":mode", mode); q->bindValue(":mode", mode);
q->bindValue(":mode2", mode); q->bindValue(":mode2", mode);
q->bindValue(":reason", QString::fromStdString(cmd.reason())); q->bindValue(":reason", QString::fromStdString(cmd.reason()));
@ -1696,12 +1703,15 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRemoveCardArtRule(const
ResponseContainer &) ResponseContainer &)
{ {
auto cardName = QString::fromStdString(cmd.card_name()); auto cardName = QString::fromStdString(cmd.card_name());
if (cardName.length() > MAX_NAME_LENGTH) { auto cardProviderId = QString::fromStdString(cmd.card_provider_id());
if (cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) {
return Response::RespInvalidData; return Response::RespInvalidData;
} }
QSqlQuery *q = sqlInterface->prepareQuery("DELETE FROM {prefix}_card_art_name_rules WHERE card_name=:name"); QSqlQuery *q = sqlInterface->prepareQuery(
"DELETE FROM {prefix}_card_art_name_rules WHERE card_name=:name AND card_provider_id=:provider");
q->bindValue(":name", cardName); q->bindValue(":name", cardName);
q->bindValue(":provider", cardProviderId);
if (!sqlInterface->execSqlQuery(q)) { if (!sqlInterface->execSqlQuery(q)) {
return Response::RespInternalError; return Response::RespInternalError;
@ -1713,7 +1723,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRemoveCardArtRule(const
Response::ResponseCode AbstractServerSocketInterface::cmdListCardArtRules(const Command_ListCardArtRules &, Response::ResponseCode AbstractServerSocketInterface::cmdListCardArtRules(const Command_ListCardArtRules &,
ResponseContainer &rc) ResponseContainer &rc)
{ {
QSqlQuery *q = sqlInterface->prepareQuery("SELECT card_name, mode, reason FROM {prefix}_card_art_name_rules"); QSqlQuery *q = sqlInterface->prepareQuery(
"SELECT card_name, card_provider_id, mode, reason FROM {prefix}_card_art_name_rules");
if (!sqlInterface->execSqlQuery(q)) { if (!sqlInterface->execSqlQuery(q)) {
return Response::RespInternalError; return Response::RespInternalError;
@ -1724,8 +1735,9 @@ Response::ResponseCode AbstractServerSocketInterface::cmdListCardArtRules(const
while (q->next()) { while (q->next()) {
auto *entry = re->add_entries(); auto *entry = re->add_entries();
entry->set_card_name(q->value(0).toString().toStdString()); entry->set_card_name(q->value(0).toString().toStdString());
entry->set_mode(q->value(1).toString().toStdString()); entry->set_card_provider_id(q->value(1).toString().toStdString());
entry->set_reason(q->value(2).toString().toStdString()); entry->set_mode(q->value(2).toString().toStdString());
entry->set_reason(q->value(3).toString().toStdString());
} }
rc.setResponseExtension(re); rc.setResponseExtension(re);

View file

@ -129,7 +129,7 @@ private:
Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc);
Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc);
bool isCardNameAllowed(const QString &cardName); bool isCardNameAllowed(const QString &cardName, const QString &cardProviderId);
Response::ResponseCode cmdSetCardArtParams(const Command_SetCardArtParams &cmd, ResponseContainer &); Response::ResponseCode cmdSetCardArtParams(const Command_SetCardArtParams &cmd, ResponseContainer &);
Response::ResponseCode cmdAddCardArtRule(const Command_AddCardArtRule &cmd, ResponseContainer &); Response::ResponseCode cmdAddCardArtRule(const Command_AddCardArtRule &cmd, ResponseContainer &);
Response::ResponseCode cmdRemoveCardArtRule(const Command_RemoveCardArtRule &cmd, ResponseContainer &); Response::ResponseCode cmdRemoveCardArtRule(const Command_RemoveCardArtRule &cmd, ResponseContainer &);

View file

@ -4,6 +4,7 @@ enable_testing()
add_test(NAME dummy_test COMMAND dummy_test) add_test(NAME dummy_test COMMAND dummy_test)
add_test(NAME expression_test COMMAND expression_test) add_test(NAME expression_test COMMAND expression_test)
add_test(NAME clamped_arithmetic_test COMMAND clamped_arithmetic_test)
add_test(NAME test_age_formatting COMMAND test_age_formatting) add_test(NAME test_age_formatting COMMAND test_age_formatting)
add_test(NAME password_hash_test COMMAND password_hash_test) add_test(NAME password_hash_test COMMAND password_hash_test)
add_test(NAME server_card_counter_test COMMAND server_card_counter_test) add_test(NAME server_card_counter_test COMMAND server_card_counter_test)
@ -16,6 +17,7 @@ set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5)
add_executable(dummy_test dummy_test.cpp) add_executable(dummy_test dummy_test.cpp)
add_executable(expression_test expression_test.cpp) add_executable(expression_test expression_test.cpp)
add_executable(clamped_arithmetic_test clamped_arithmetic_test.cpp)
add_executable(test_age_formatting test_age_formatting.cpp) add_executable(test_age_formatting test_age_formatting.cpp)
add_executable(password_hash_test password_hash_test.cpp) add_executable(password_hash_test password_hash_test.cpp)
add_executable(deck_hash_performance_test deck_hash_performance_test.cpp) add_executable(deck_hash_performance_test deck_hash_performance_test.cpp)
@ -49,6 +51,7 @@ if(NOT GTEST_FOUND)
set(GTEST_BOTH_LIBRARIES gtest) set(GTEST_BOTH_LIBRARIES gtest)
add_dependencies(dummy_test gtest) add_dependencies(dummy_test gtest)
add_dependencies(expression_test gtest) add_dependencies(expression_test gtest)
add_dependencies(clamped_arithmetic_test gtest)
add_dependencies(test_age_formatting gtest) add_dependencies(test_age_formatting gtest)
add_dependencies(password_hash_test gtest) add_dependencies(password_hash_test gtest)
add_dependencies(deck_hash_performance_test gtest) add_dependencies(deck_hash_performance_test gtest)
@ -59,6 +62,9 @@ endif()
include_directories(${GTEST_INCLUDE_DIRS}) include_directories(${GTEST_INCLUDE_DIRS})
target_link_libraries(dummy_test Threads::Threads ${GTEST_BOTH_LIBRARIES}) target_link_libraries(dummy_test Threads::Threads ${GTEST_BOTH_LIBRARIES})
target_link_libraries(expression_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) target_link_libraries(expression_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
target_link_libraries(
clamped_arithmetic_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
)
target_link_libraries( target_link_libraries(
test_age_formatting libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} test_age_formatting libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
) )

View file

@ -0,0 +1,44 @@
/** @file clamped_arithmetic_test.cpp
* @brief Tests for shared helpers in clamped_arithmetic.h.
* @ingroup Tests
*/
#include <gtest/gtest.h>
#include <libcockatrice/utility/clamped_arithmetic.h>
#include <limits>
TEST(AddClamped, AddsWithinBounds)
{
EXPECT_EQ(addClamped(5, 3, 0, 100), 8);
EXPECT_EQ(addClamped(10, -3, 0, 100), 7);
}
TEST(AddClamped, ClampsToUpperAndLowerBound)
{
EXPECT_EQ(addClamped(99, 5, 0, 100), 100); // saturates at max
EXPECT_EQ(addClamped(2, -10, 0, 100), 0); // saturates at min
EXPECT_EQ(addClamped(999, 1, 0, 999), 999); // crossing the counter cap holds at the bound
}
TEST(AddClamped, IntOverflowDoesNotWrap)
{
// The 64-bit intermediate must prevent signed-int overflow UB.
constexpr int intMax = std::numeric_limits<int>::max();
constexpr int intMin = std::numeric_limits<int>::min();
EXPECT_EQ(addClamped(intMax, 1, intMin, intMax), intMax);
EXPECT_EQ(addClamped(intMax, intMax, intMin, intMax), intMax);
}
TEST(AddClamped, IntUnderflowDoesNotWrap)
{
constexpr int intMax = std::numeric_limits<int>::max();
constexpr int intMin = std::numeric_limits<int>::min();
EXPECT_EQ(addClamped(intMin, -1, intMin, intMax), intMin);
EXPECT_EQ(addClamped(intMin, intMin, intMin, intMax), intMin);
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -7,7 +7,7 @@
#include <libcockatrice/network/server/remote/game/server_card.h> #include <libcockatrice/network/server/remote/game/server_card.h>
#include <libcockatrice/protocol/pb/event_set_card_counter.pb.h> #include <libcockatrice/protocol/pb/event_set_card_counter.pb.h>
#include <libcockatrice/utility/card_ref.h> #include <libcockatrice/utility/card_ref.h>
#include <libcockatrice/utility/trice_limits.h> #include <libcockatrice/utility/counter_limits.h>
#include <limits> #include <limits>
TEST(ServerCardCounter, IncrementNewCounter) TEST(ServerCardCounter, IncrementNewCounter)
@ -28,9 +28,9 @@ TEST(ServerCardCounter, IncrementExistingCounter)
TEST(ServerCardCounter, IncrementOverflowProtection) TEST(ServerCardCounter, IncrementOverflowProtection)
{ {
Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0);
ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD)); ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE));
EXPECT_FALSE(card.incrementCounter(1, 1)); EXPECT_FALSE(card.incrementCounter(1, 1));
EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE);
} }
TEST(ServerCardCounter, DecrementUnderflowProtection) TEST(ServerCardCounter, DecrementUnderflowProtection)
@ -113,13 +113,13 @@ TEST(ServerCardCounter, IncrementCounterPopulatesEvent)
TEST(ServerCardCounter, IncrementCounterEventReflectsClampedValue) TEST(ServerCardCounter, IncrementCounterEventReflectsClampedValue)
{ {
Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0);
ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD - 5)); ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE - 5));
Event_SetCardCounter event; Event_SetCardCounter event;
EXPECT_TRUE(card.incrementCounter(1, 10, &event)); EXPECT_TRUE(card.incrementCounter(1, 10, &event));
EXPECT_EQ(event.counter_id(), 1); EXPECT_EQ(event.counter_id(), 1);
EXPECT_EQ(event.counter_value(), MAX_COUNTERS_ON_CARD); EXPECT_EQ(event.counter_value(), MAX_COUNTER_VALUE);
} }
TEST(ServerCardCounter, IncrementCounterNoEventWhenNullptr) TEST(ServerCardCounter, IncrementCounterNoEventWhenNullptr)
@ -133,7 +133,7 @@ TEST(ServerCardCounter, IncrementCounterNoEventWhenNullptr)
TEST(ServerCardCounter, IncrementCounterEventNotPopulatedWhenUnchanged) TEST(ServerCardCounter, IncrementCounterEventNotPopulatedWhenUnchanged)
{ {
Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0);
ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD)); ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE));
Event_SetCardCounter event; Event_SetCardCounter event;
event.set_counter_id(999); event.set_counter_id(999);
@ -156,7 +156,7 @@ TEST(ServerCardCounter, SetCounterClampsAboveMaxToMax)
{ {
Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0);
EXPECT_TRUE(card.setCounter(1, 1500)); EXPECT_TRUE(card.setCounter(1, 1500));
EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE);
} }
TEST(ServerCardCounter, IncrementDoesNotGoBelowZero) TEST(ServerCardCounter, IncrementDoesNotGoBelowZero)
@ -171,9 +171,9 @@ TEST(ServerCardCounter, IncrementDoesNotGoBelowZero)
TEST(ServerCardCounter, IncrementDoesNotExceedMax) TEST(ServerCardCounter, IncrementDoesNotExceedMax)
{ {
Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0);
ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD - 5)); ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE - 5));
EXPECT_TRUE(card.incrementCounter(1, 10)); EXPECT_TRUE(card.incrementCounter(1, 10));
EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE);
} }
int main(int argc, char **argv) int main(int argc, char **argv)