mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-07-08 17:13:57 -07:00
feat(command-zone): add server-side support with tests
Implement server-side protocol and game logic for networked command zone play. Protocol changes: - room_commands.proto: command zone messages and responses Server components: - ServerCounter: counter synchronization for commander tax - ServerGame: command zone game state management - ServerPlayer: player-level command zone handling - Server/ServerProtocolHandler: command zone message routing - Servatrice: command zone support in the main server Design decisions: - setCount() returns bool for event suppression (avoid duplicate events) - Zone state synchronized across all connected clients - Commander tax persists across zone transitions Test coverage verifies setCount() return value behavior for proper event suppression in networked scenarios.
This commit is contained in:
parent
47fade7bcb
commit
c2a10466db
13 changed files with 457 additions and 22 deletions
|
|
@ -54,7 +54,7 @@ add_library(
|
|||
|
||||
add_dependencies(libcockatrice_network_server_remote libcockatrice_protocol)
|
||||
|
||||
target_include_directories(libcockatrice_network_server_remote PUBLIC .)
|
||||
target_include_directories(libcockatrice_network_server_remote PUBLIC . ${CMAKE_CURRENT_SOURCE_DIR}/../../../../)
|
||||
|
||||
# Make cockatrice_server depend on cockatrice_protocol
|
||||
target_link_libraries(
|
||||
|
|
|
|||
|
|
@ -2,8 +2,15 @@
|
|||
|
||||
#include <libcockatrice/protocol/pb/serverinfo_counter.pb.h>
|
||||
|
||||
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)
|
||||
Server_Counter::Server_Counter(int _id,
|
||||
const QString &_name,
|
||||
const color &_counterColor,
|
||||
int _radius,
|
||||
int _count,
|
||||
int _minValue,
|
||||
int _maxValue)
|
||||
: id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(qBound(_minValue, _count, _maxValue)),
|
||||
minValue(_minValue), maxValue(_maxValue)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
#include <QString>
|
||||
#include <libcockatrice/protocol/pb/color.pb.h>
|
||||
#include <limits>
|
||||
|
||||
class ServerInfo_Counter;
|
||||
|
||||
|
|
@ -33,9 +34,20 @@ protected:
|
|||
color counterColor;
|
||||
int radius;
|
||||
int count;
|
||||
int minValue;
|
||||
int maxValue;
|
||||
|
||||
// Maximum reasonable counter value to prevent rendering/memory issues
|
||||
static constexpr int DEFAULT_MAX_VALUE = 9999;
|
||||
|
||||
public:
|
||||
Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count = 0);
|
||||
Server_Counter(int _id,
|
||||
const QString &_name,
|
||||
const color &_counterColor,
|
||||
int _radius,
|
||||
int _count = 0,
|
||||
int _minValue = std::numeric_limits<int>::min(),
|
||||
int _maxValue = DEFAULT_MAX_VALUE);
|
||||
~Server_Counter()
|
||||
{
|
||||
}
|
||||
|
|
@ -59,9 +71,11 @@ public:
|
|||
{
|
||||
return count;
|
||||
}
|
||||
void setCount(int _count)
|
||||
bool setCount(int _count)
|
||||
{
|
||||
count = _count;
|
||||
int oldCount = count;
|
||||
count = qBound(minValue, _count, maxValue);
|
||||
return count != oldCount;
|
||||
}
|
||||
|
||||
void getInfo(ServerInfo_Counter *info);
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo,
|
|||
bool _spectatorsSeeEverything,
|
||||
int _startingLifeTotal,
|
||||
bool _shareDecklistsOnLoad,
|
||||
bool _enableCommandZone,
|
||||
bool _enableCompanionZone,
|
||||
bool _enableBackgroundZone,
|
||||
Server_Room *_room)
|
||||
: QObject(), room(_room), nextPlayerId(0), hostId(0), creatorInfo(new ServerInfo_User(_creatorInfo)),
|
||||
gameStarted(false), gameClosed(false), gameId(_gameId), password(_password), maxPlayers(_maxPlayers),
|
||||
|
|
@ -73,9 +76,10 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo,
|
|||
onlyRegistered(_onlyRegistered), spectatorsAllowed(_spectatorsAllowed),
|
||||
spectatorsNeedPassword(_spectatorsNeedPassword), spectatorsCanTalk(_spectatorsCanTalk),
|
||||
spectatorsSeeEverything(_spectatorsSeeEverything), startingLifeTotal(_startingLifeTotal),
|
||||
shareDecklistsOnLoad(_shareDecklistsOnLoad), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0),
|
||||
firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr),
|
||||
gameMutex()
|
||||
shareDecklistsOnLoad(_shareDecklistsOnLoad), enableCommandZone(_enableCommandZone),
|
||||
enableCompanionZone(_enableCompanionZone), enableBackgroundZone(_enableBackgroundZone), inactivityCounter(0),
|
||||
startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), turnOrderReversed(false),
|
||||
startTime(QDateTime::currentDateTime()), pingClock(nullptr), gameMutex()
|
||||
{
|
||||
currentReplay = new GameReplay;
|
||||
currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId());
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ private:
|
|||
bool spectatorsSeeEverything;
|
||||
int startingLifeTotal;
|
||||
bool shareDecklistsOnLoad;
|
||||
bool enableCommandZone;
|
||||
bool enableCompanionZone;
|
||||
bool enableBackgroundZone;
|
||||
int inactivityCounter;
|
||||
int startTimeOfThisGame, secondsElapsed;
|
||||
bool firstGameStarted;
|
||||
|
|
@ -105,6 +108,9 @@ public:
|
|||
bool _spectatorsSeeEverything,
|
||||
int _startingLifeTotal,
|
||||
bool _shareDecklistsOnLoad,
|
||||
bool _enableCommandZone,
|
||||
bool _enableCompanionZone,
|
||||
bool _enableBackgroundZone,
|
||||
Server_Room *parent);
|
||||
~Server_Game() override;
|
||||
Server_Room *getRoom() const
|
||||
|
|
@ -172,6 +178,18 @@ public:
|
|||
{
|
||||
return shareDecklistsOnLoad;
|
||||
}
|
||||
bool getEnableCommandZone() const
|
||||
{
|
||||
return enableCommandZone;
|
||||
}
|
||||
bool getEnableCompanionZone() const
|
||||
{
|
||||
return enableCompanionZone;
|
||||
}
|
||||
bool getEnableBackgroundZone() const
|
||||
{
|
||||
return enableBackgroundZone;
|
||||
}
|
||||
Response::ResponseCode
|
||||
checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions, bool asJudge);
|
||||
bool containsUser(const QString &userName) const;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <QDebug>
|
||||
#include <QRegularExpression>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/common/counter_ids.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
|
||||
|
|
@ -99,6 +100,24 @@ void Server_Player::setupZones()
|
|||
addCounter(new Server_Counter(6, "x", makeColor(255, 255, 255), 20, 0));
|
||||
addCounter(new Server_Counter(7, "storm", makeColor(255, 150, 30), 20, 0));
|
||||
|
||||
// Command zones for Commander format
|
||||
if (game->getEnableCommandZone()) {
|
||||
addZone(new Server_CardZone(this, "command", false, ServerInfo_Zone::PublicZone));
|
||||
addCounter(new Server_Counter(CounterIds::CommanderTax, CounterNames::CommanderTax, makeColor(128, 128, 128),
|
||||
20, 0, 0));
|
||||
addZone(new Server_CardZone(this, "partner", false, ServerInfo_Zone::PublicZone));
|
||||
addCounter(
|
||||
new Server_Counter(CounterIds::PartnerTax, CounterNames::PartnerTax, makeColor(128, 128, 128), 20, 0, 0));
|
||||
}
|
||||
|
||||
// Companion and Background zones are independent mechanics
|
||||
if (game->getEnableCompanionZone()) {
|
||||
addZone(new Server_CardZone(this, "companion", false, ServerInfo_Zone::PublicZone));
|
||||
}
|
||||
if (game->getEnableBackgroundZone()) {
|
||||
addZone(new Server_CardZone(this, "background", false, ServerInfo_Zone::PublicZone));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// Assign card ids and create deck from deck list
|
||||
|
|
@ -424,6 +443,10 @@ Server_Player::cmdUndoDraw(const Command_UndoDraw & /*cmd*/, ResponseContainer &
|
|||
Response::ResponseCode
|
||||
Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
|
||||
{
|
||||
// Security note: Counter ownership is implicitly validated through command routing.
|
||||
// Each participant's processGameCommand operates on their own counters map,
|
||||
// so players can only modify their own counters.
|
||||
|
||||
if (!game->getGameStarted()) {
|
||||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
|
@ -431,17 +454,27 @@ 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();
|
||||
|
||||
// Defensive validation: Commander Tax counters require command zone to be enabled
|
||||
if ((counterId == CounterIds::CommanderTax || counterId == CounterIds::PartnerTax) &&
|
||||
!game->getEnableCommandZone()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
Server_Counter *c = counters.value(counterId, nullptr);
|
||||
if (!c) {
|
||||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
c->setCount(c->getCount() + cmd.delta());
|
||||
bool didChange = c->setCount(c->getCount() + cmd.delta());
|
||||
|
||||
Event_SetCounter event;
|
||||
event.set_counter_id(c->getId());
|
||||
event.set_value(c->getCount());
|
||||
ges.enqueueGameEvent(event, playerId);
|
||||
if (didChange) {
|
||||
Event_SetCounter event;
|
||||
event.set_counter_id(c->getId());
|
||||
event.set_value(c->getCount());
|
||||
ges.enqueueGameEvent(event, playerId);
|
||||
}
|
||||
|
||||
return Response::RespOk;
|
||||
}
|
||||
|
|
@ -475,6 +508,8 @@ Server_Player::cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContai
|
|||
Response::ResponseCode
|
||||
Server_Player::cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
|
||||
{
|
||||
// Security note: Counter ownership is implicitly validated through command routing.
|
||||
|
||||
if (!game->getGameStarted()) {
|
||||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
|
@ -482,17 +517,27 @@ 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();
|
||||
|
||||
// Defensive validation: Commander Tax counters require command zone to be enabled
|
||||
if ((counterId == CounterIds::CommanderTax || counterId == CounterIds::PartnerTax) &&
|
||||
!game->getEnableCommandZone()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
Server_Counter *c = counters.value(counterId, nullptr);
|
||||
if (!c) {
|
||||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
c->setCount(cmd.value());
|
||||
bool didChange = c->setCount(cmd.value());
|
||||
|
||||
Event_SetCounter event;
|
||||
event.set_counter_id(c->getId());
|
||||
event.set_value(c->getCount());
|
||||
ges.enqueueGameEvent(event, playerId);
|
||||
if (didChange) {
|
||||
Event_SetCounter event;
|
||||
event.set_counter_id(c->getId());
|
||||
event.set_value(c->getCount());
|
||||
ges.enqueueGameEvent(event, playerId);
|
||||
}
|
||||
|
||||
return Response::RespOk;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,6 +174,10 @@ public:
|
|||
{
|
||||
return false;
|
||||
}
|
||||
virtual bool permitCommandZone() const
|
||||
{
|
||||
return true; // Default: allow command zones
|
||||
}
|
||||
|
||||
Server_DatabaseInterface *getDatabaseInterface() const;
|
||||
int getNextLocalGameId()
|
||||
|
|
|
|||
|
|
@ -830,6 +830,12 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room
|
|||
int startingLifeTotal = cmd.has_starting_life_total() ? cmd.starting_life_total() : 20;
|
||||
|
||||
bool shareDecklistsOnLoad = cmd.has_share_decklists_on_load() ? cmd.share_decklists_on_load() : false;
|
||||
// Only enable command zone if server permits it
|
||||
bool enableCommandZone = cmd.has_enable_command_zone() && cmd.enable_command_zone() && server->permitCommandZone();
|
||||
|
||||
// Companion and Background zones are independent of command zone (explicit opt-in)
|
||||
bool enableCompanionZone = cmd.has_enable_companion_zone() && cmd.enable_companion_zone();
|
||||
bool enableBackgroundZone = cmd.has_enable_background_zone() && cmd.enable_background_zone();
|
||||
|
||||
const int gameId = databaseInterface->getNextGameId();
|
||||
if (gameId == -1) {
|
||||
|
|
@ -841,7 +847,8 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room
|
|||
auto *game = new Server_Game(copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()),
|
||||
cmd.max_players(), gameTypes, cmd.only_buddies(), onlyRegisteredUsers,
|
||||
cmd.spectators_allowed(), cmd.spectators_need_password(), cmd.spectators_can_talk(),
|
||||
cmd.spectators_see_everything(), startingLifeTotal, shareDecklistsOnLoad, room);
|
||||
cmd.spectators_see_everything(), startingLifeTotal, shareDecklistsOnLoad,
|
||||
enableCommandZone, enableCompanionZone, enableBackgroundZone, room);
|
||||
|
||||
game->addPlayer(this, rc, asSpectator, asJudge, false);
|
||||
room->addGame(game);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue