Compare commits

..

7 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
70 changed files with 3467 additions and 2270 deletions

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

@ -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)
{ {

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

@ -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

@ -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

@ -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>

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)