Centralize counter API with server-side bounds and no-op filtering (#6879)

* Refactor server counter API to own overflow protection and filter no-op events

  Counter modifications now clamp to int bounds server-side and return change
  status, allowing command handlers to skip network broadcasts when values
  don't actually change.

* Centralize MAX_COUNTERS_ON_CARD and enforce [0, 999] bounds on server

  - Move MAX_COUNTERS_ON_CARD to trice_limits.h
  - Server clamps values in setCounter() and incrementCounter()
  - Client uses clamped comparison to allow recovery from invalid states
  - Add tests for clamping behavior

* move incrementCount() implementation from header to cpp
This commit is contained in:
DawnFire42 2026-05-21 23:39:35 -04:00 committed by GitHub
parent 74102aa1ec
commit 8dca14933c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 432 additions and 32 deletions

View file

@ -49,6 +49,7 @@
#include <libcockatrice/rng/rng_abstract.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/zone_names.h>
#include <limits>
#include <ranges>
Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game,
@ -1091,8 +1092,9 @@ Server_AbstractPlayer::cmdCreateToken(const Command_CreateToken &cmd, ResponseCo
_event.set_zone_name(card->getZone()->getName().toStdString());
_event.set_card_id(card->getId());
card->setCounter(i.key(), i.value(), &_event);
ges.enqueueGameEvent(_event, playerId);
if (card->setCounter(i.key(), i.value(), &_event)) {
ges.enqueueGameEvent(_event, playerId);
}
}
// Copy parent card
@ -1338,8 +1340,9 @@ Response::ResponseCode Server_AbstractPlayer::cmdSetCardCounter(const Command_Se
Event_SetCardCounter event;
event.set_zone_name(zone->getName().toStdString());
event.set_card_id(card->getId());
card->setCounter(cmd.counter_id(), cmd.counter_value(), &event);
ges.enqueueGameEvent(event, playerId);
if (card->setCounter(cmd.counter_id(), cmd.counter_value(), &event)) {
ges.enqueueGameEvent(event, playerId);
}
return Response::RespOk;
}
@ -1368,14 +1371,13 @@ Response::ResponseCode Server_AbstractPlayer::cmdIncCardCounter(const Command_In
return Response::RespNameNotFound;
}
int newValue = card->getCounter(cmd.counter_id()) + cmd.counter_delta();
card->setCounter(cmd.counter_id(), newValue);
Event_SetCardCounter event;
event.set_zone_name(zone->getName().toStdString());
event.set_card_id(card->getId());
event.set_counter_id(cmd.counter_id());
event.set_counter_value(newValue);
if (!card->incrementCounter(cmd.counter_id(), cmd.counter_delta(), &event)) {
return Response::RespOk;
}
ges.enqueueGameEvent(event, playerId);
return Response::RespOk;

View file

@ -26,6 +26,8 @@
#include <libcockatrice/protocol/pb/event_set_card_attr.pb.h>
#include <libcockatrice/protocol/pb/event_set_card_counter.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
#include <libcockatrice/utility/trice_limits.h>
#include <limits>
Server_Card::Server_Card(const CardRef &cardRef, int _id, int _coord_x, int _coord_y, Server_CardZone *_zone)
: zone(_zone), id(_id), coord_x(_coord_x), coord_y(_coord_y), cardRef(cardRef), tapped(false), attacking(false),
@ -110,8 +112,16 @@ QString Server_Card::setAttribute(CardAttribute attribute, const QString &avalue
return avalue;
}
void 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]
value = qBound(0, value, MAX_COUNTERS_ON_CARD);
const int oldValue = counters.value(_id, 0);
if (value == oldValue) {
return false;
}
if (value) {
counters.insert(_id, value);
} else {
@ -122,6 +132,34 @@ void Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event)
event->set_counter_id(_id);
event->set_counter_value(value);
}
return true;
}
bool Server_Card::incrementCounter(int counterId, int delta, Event_SetCardCounter *event)
{
const int oldValue = counters.value(counterId, 0);
const auto result = static_cast<int64_t>(oldValue) + static_cast<int64_t>(delta);
// Clamp to [0, MAX_COUNTERS_ON_CARD] for card counters
const int newValue =
static_cast<int>(qBound(static_cast<int64_t>(0), result, static_cast<int64_t>(MAX_COUNTERS_ON_CARD)));
if (newValue == oldValue) {
return false;
}
if (newValue) {
counters.insert(counterId, newValue);
} else {
counters.remove(counterId);
}
if (event) {
event->set_counter_id(counterId);
event->set_counter_value(newValue);
}
return true;
}
void Server_Card::setParentCard(Server_Card *_parentCard)

View file

@ -153,7 +153,24 @@ public:
{
cardRef = _cardRef;
}
void setCounter(int _id, int value, Event_SetCardCounter *event = nullptr);
/**
* @brief Sets a card counter to an exact value with clamping.
* @param _id The counter ID.
* @param value The desired value (clamped to [0, MAX_COUNTERS_ON_CARD]; 0 removes the counter).
* @param event Optional event to populate with counter state.
* @return true if the value changed, false otherwise.
*/
[[nodiscard]] bool setCounter(int _id, int value, Event_SetCardCounter *event = nullptr);
/**
* @brief Increments a card counter with overflow-safe arithmetic.
* @param counterId The counter ID to modify.
* @param delta The amount to add (may be negative for decrement).
* @param event Optional event to populate with counter state.
* @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 Clamps result to [0, MAX_COUNTERS_ON_CARD].
*/
[[nodiscard]] bool incrementCounter(int counterId, int delta, Event_SetCardCounter *event = nullptr);
void setTapped(bool _tapped)
{
tapped = _tapped;

View file

@ -1,12 +1,24 @@
#include "server_counter.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)
: 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)
{
info->set_id(id);

View file

@ -25,6 +25,18 @@
class ServerInfo_Counter;
/**
* @class Server_Counter
* @brief Represents a player counter with overflow-safe increment arithmetic.
*
* All value modifications return whether the value actually changed,
* enabling callers to skip unnecessary network events.
*
* @note Direct assignment via setCount() does not clamp; only
* incrementCount() enforces int boundary saturation.
* @note Unlike card counters, player counters are never auto-removed
* when they reach zero - they persist with value 0.
*/
class Server_Counter
{
protected:
@ -59,11 +71,33 @@ public:
{
return count;
}
void setCount(int _count)
/**
* @brief Sets the counter to an exact value.
* @param _count The new value (assigned directly without clamping).
* @return true if the value changed, false otherwise.
* @warning This performs raw assignment. For overflow-safe incrementing,
* use incrementCount().
*/
[[nodiscard]] bool setCount(int _count)
{
const int oldCount = count;
count = _count;
return count != oldCount;
}
/**
* @brief Increments the counter by delta with overflow-safe arithmetic.
* @param delta The amount to add (may be negative for decrement).
* @return true if the value changed, false otherwise.
* @note Clamps result to [INT_MIN, INT_MAX] to prevent overflow.
*/
[[nodiscard]] bool incrementCount(int delta);
/**
* @brief Populates info with this counter's current state for network serialization.
* @param info The protobuf message to populate.
*/
void getInfo(ServerInfo_Counter *info);
};

View file

@ -436,17 +436,19 @@ Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer &
return Response::RespContextError;
}
Server_Counter *c = counters.value(cmd.counter_id(), 0);
const int counterId = cmd.counter_id();
Server_Counter *c = counters.value(counterId, nullptr);
if (!c) {
return Response::RespNameNotFound;
}
c->setCount(c->getCount() + cmd.delta());
Event_SetCounter event;
event.set_counter_id(c->getId());
event.set_value(c->getCount());
ges.enqueueGameEvent(event, playerId);
bool didChange = c->incrementCount(cmd.delta());
if (didChange) {
Event_SetCounter event;
event.set_counter_id(c->getId());
event.set_value(c->getCount());
ges.enqueueGameEvent(event, playerId);
}
return Response::RespOk;
}
@ -487,17 +489,19 @@ Server_Player::cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer &
return Response::RespContextError;
}
Server_Counter *c = counters.value(cmd.counter_id(), 0);
const int counterId = cmd.counter_id();
Server_Counter *c = counters.value(counterId, nullptr);
if (!c) {
return Response::RespNameNotFound;
}
c->setCount(cmd.value());
Event_SetCounter event;
event.set_counter_id(c->getId());
event.set_value(c->getCount());
ges.enqueueGameEvent(event, playerId);
bool didChange = c->setCount(cmd.value());
if (didChange) {
Event_SetCounter event;
event.set_counter_id(c->getId());
event.set_value(c->getCount());
ges.enqueueGameEvent(event, playerId);
}
return Response::RespOk;
}
@ -512,15 +516,16 @@ Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &
return Response::RespContextError;
}
Server_Counter *counter = counters.value(cmd.counter_id(), 0);
const int counterId = cmd.counter_id();
Server_Counter *counter = counters.value(counterId, nullptr);
if (!counter) {
return Response::RespNameNotFound;
}
counters.remove(cmd.counter_id());
counters.remove(counterId);
delete counter;
Event_DelCounter event;
event.set_counter_id(cmd.counter_id());
event.set_counter_id(counterId);
ges.enqueueGameEvent(event, playerId);
return Response::RespOk;