From 3cdc44322d69c0396548ac1a98ea7a26e064e142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Basile=20Cl=C3=A9ment?= Date: Wed, 23 Apr 2025 21:00:22 +0200 Subject: [PATCH] client: Support arbitrary game zones Currently, the client ignores cards in unknown zones, as there is an implicit assumption that the set of zones known by the server and the client are the same. This patch makes it so that the client accept "custom zones" from the server (zones outside the builtin deck, graveyard, exile, sideboard, table, stack and hand zones) using the information from the ServerInfo_CardZone. Moving cards from/into these zones happens through a "View custom zone" action in the Game > Player menu and properly appears in the chat. Note that this patch intentionally does not introduce any support for having the server actually create such zones. Instead, this patch aims to improve backwards compatibility for when we do get to adding this capability in the future, by making sure that current clients will be able to interact with future new zones (even if suboptimally). --- cockatrice/src/game/player/player.cpp | 75 +++++++++++++++++++- cockatrice/src/game/player/player.h | 2 +- cockatrice/src/game/zones/card_zone.cpp | 4 ++ cockatrice/src/game/zones/table_zone.cpp | 4 +- cockatrice/src/game/zones/table_zone.h | 2 +- cockatrice/src/server/message_log_widget.cpp | 7 +- common/pb/serverinfo_zone.proto | 4 ++ 7 files changed, 90 insertions(+), 8 deletions(-) diff --git a/cockatrice/src/game/player/player.cpp b/cockatrice/src/game/player/player.cpp index c893e27af..235c4a734 100644 --- a/cockatrice/src/game/player/player.cpp +++ b/cockatrice/src/game/player/player.cpp @@ -144,7 +144,7 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, T PileZone *sb = addZone(new PileZone(this, "sb", false, false, playerArea)); sb->setVisible(false); - table = addZone(new TableZone(this, this)); + table = addZone(new TableZone(this, "table", this)); connect(table, &TableZone::sizeChanged, this, &Player::updateBoundingRect); stack = addZone(new StackZone(this, (int)table->boundingRect().height(), this)); @@ -400,6 +400,9 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, T sbMenu->addAction(aViewSideboard); sb->setMenu(sbMenu, aViewSideboard); + mCustomZones = playerMenu->addMenu(QString()); + mCustomZones->menuAction()->setVisible(false); + aUntapAll = new QAction(this); connect(aUntapAll, &QAction::triggered, this, &Player::actUntapAll); @@ -455,6 +458,7 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, T if (!local && !judge) { countersMenu = nullptr; sbMenu = nullptr; + mCustomZones = nullptr; aCreateAnotherToken = nullptr; createPredefinedTokenMenu = nullptr; } @@ -829,6 +833,11 @@ void Player::retranslateUi() sbMenu->setTitle(tr("&Sideboard")); libraryMenu->setTitle(tr("&Library")); countersMenu->setTitle(tr("&Counters")); + mCustomZones->setTitle(tr("C&ustom Zones")); + + for (auto aViewZone : mCustomZones->actions()) { + aViewZone->setText(tr("View custom zone '%1'").arg(aViewZone->data().toString())); + } aUntapAll->setText(tr("&Untap all permanents")); aRollDie->setText(tr("R&oll die...")); @@ -2711,19 +2720,79 @@ void Player::paint(QPainter * /*painter*/, const QStyleOptionGraphicsItem * /*op void Player::processPlayerInfo(const ServerInfo_Player &info) { + static QSet builtinZones{/* PileZones */ + "deck", "grave", "rfg", "sb", + /* TableZone */ + "table", + /* StackZone */ + "stack", + /* HandZone */ + "hand"}; clearCounters(); clearArrows(); - QMapIterator zoneIt(zones); + QMutableMapIterator zoneIt(zones); while (zoneIt.hasNext()) { zoneIt.next().value()->clearContents(); + + if (!builtinZones.contains(zoneIt.key())) { + zoneIt.remove(); + } + } + + // Can be null if we are not the local player! + if (mCustomZones) { + mCustomZones->clear(); + mCustomZones->menuAction()->setVisible(false); } const int zoneListSize = info.zone_list_size(); for (int i = 0; i < zoneListSize; ++i) { const ServerInfo_Zone &zoneInfo = info.zone_list(i); - CardZone *zone = zones.value(QString::fromStdString(zoneInfo.name()), 0); + + QString zoneName = QString::fromStdString(zoneInfo.name()); + CardZone *zone = zones.value(zoneName, 0); if (!zone) { + // Create a new CardZone if it doesn't exist + + if (zoneInfo.with_coords()) { + // Visibility not currently supported for TableZone + zone = addZone(new TableZone(this, zoneName, this)); + } else { + // Zones without coordinats are always treated as non-shufflable + // PileZones, although supporting alternate hand or stack zones + // might make sense in some scenarios. + bool contentsKnown; + + switch (zoneInfo.type()) { + case ServerInfo_Zone::PrivateZone: + contentsKnown = local || judge || (game->getSpectator() && game->getSpectatorsSeeEverything()); + break; + + case ServerInfo_Zone::PublicZone: + contentsKnown = true; + break; + + case ServerInfo_Zone::HiddenZone: + contentsKnown = false; + break; + } + + zone = addZone(new PileZone(this, zoneName, /* isShufflable */ false, contentsKnown, this)); + } + + // Non-builtin zones are hidden by default and can't be interacted + // with, except through menus. + zone->setVisible(false); + + if (mCustomZones) { + mCustomZones->menuAction()->setVisible(true); + QAction *aViewZone = mCustomZones->addAction(tr("View custom zone '%1'").arg(zoneName)); + aViewZone->setData(zoneName); + connect(aViewZone, &QAction::triggered, this, + [zoneName, this]() { static_cast(scene())->toggleZoneView(this, zoneName, -1); }); + } + continue; } diff --git a/cockatrice/src/game/player/player.h b/cockatrice/src/game/player/player.h index 027a7658b..9bd59fca0 100644 --- a/cockatrice/src/game/player/player.h +++ b/cockatrice/src/game/player/player.h @@ -253,7 +253,7 @@ public: private: TabGame *game; QMenu *sbMenu, *countersMenu, *sayMenu, *createPredefinedTokenMenu, *mRevealLibrary, *mLendLibrary, *mRevealTopCard, - *mRevealHand, *mRevealRandomHandCard, *mRevealRandomGraveyardCard; + *mRevealHand, *mRevealRandomHandCard, *mRevealRandomGraveyardCard, *mCustomZones; TearOffMenu *moveGraveMenu, *moveRfgMenu, *graveMenu, *moveHandMenu, *handMenu, *libraryMenu, *topLibraryMenu, *bottomLibraryMenu, *rfgMenu, *playerMenu; QList playerLists; diff --git a/cockatrice/src/game/zones/card_zone.cpp b/cockatrice/src/game/zones/card_zone.cpp index 05b4c35b9..089ed0a2d 100644 --- a/cockatrice/src/game/zones/card_zone.cpp +++ b/cockatrice/src/game/zones/card_zone.cpp @@ -92,6 +92,10 @@ QString CardZone::getTranslatedName(bool theirOwn, GrammaticalCase gc) const default: break; } + else { + return (theirOwn ? tr("their custom zone '%1'", "nominative").arg(name) + : tr("%1's custom zone '%2'", "nominative").arg(ownerName).arg(name)); + } return QString(); } diff --git a/cockatrice/src/game/zones/table_zone.cpp b/cockatrice/src/game/zones/table_zone.cpp index e214b2e09..cee782560 100644 --- a/cockatrice/src/game/zones/table_zone.cpp +++ b/cockatrice/src/game/zones/table_zone.cpp @@ -19,8 +19,8 @@ const QColor TableZone::FADE_MASK = QColor(0, 0, 0, 80); const QColor TableZone::GRADIENT_COLOR = QColor(255, 255, 255, 150); const QColor TableZone::GRADIENT_COLORLESS = QColor(255, 255, 255, 0); -TableZone::TableZone(Player *_p, QGraphicsItem *parent) - : SelectZone(_p, "table", true, false, true, parent), active(false) +TableZone::TableZone(Player *_p, const QString &name, QGraphicsItem *parent) + : SelectZone(_p, name, true, false, true, parent), active(false) { connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg); connect(&SettingsCache::instance(), &SettingsCache::invertVerticalCoordinateChanged, this, diff --git a/cockatrice/src/game/zones/table_zone.h b/cockatrice/src/game/zones/table_zone.h index f3fbdccb6..3d464e6f3 100644 --- a/cockatrice/src/game/zones/table_zone.h +++ b/cockatrice/src/game/zones/table_zone.h @@ -98,7 +98,7 @@ public: @param _p the Player @param parent defaults to null */ - explicit TableZone(Player *_p, QGraphicsItem *parent = nullptr); + explicit TableZone(Player *_p, const QString &name, QGraphicsItem *parent = nullptr); /** @return a QRectF of the TableZone bounding box. diff --git a/cockatrice/src/server/message_log_widget.cpp b/cockatrice/src/server/message_log_widget.cpp index 7590e044b..cabc86733 100644 --- a/cockatrice/src/server/message_log_widget.cpp +++ b/cockatrice/src/server/message_log_widget.cpp @@ -86,6 +86,8 @@ QPair MessageLogWidget::getFromStr(CardZone *zone, QString car fromStr = tr(" from sideboard"); } else if (zoneName == STACK_ZONE_NAME) { fromStr = tr(" from the stack"); + } else { + fromStr = tr(" from custom zone '%1'").arg(zoneName); } if (!cardNameContainsStartZone) { @@ -317,13 +319,16 @@ void MessageLogWidget::logMoveCard(Player *player, } else if (targetZoneName == STACK_ZONE_NAME) { soundEngine->playSound("play_card"); finalStr = tr("%1 plays %2%3."); + } else { + finalStr = tr("%1 moves %2%3 to custom zone '%4'."); } if (usesNewX) { appendHtmlServerMessage( finalStr.arg(sanitizeHtml(player->getName())).arg(cardStr).arg(nameFrom.second).arg(newX)); } else { - appendHtmlServerMessage(finalStr.arg(sanitizeHtml(player->getName())).arg(cardStr).arg(nameFrom.second)); + appendHtmlServerMessage( + finalStr.arg(sanitizeHtml(player->getName())).arg(cardStr).arg(nameFrom.second).arg(targetZoneName)); } } diff --git a/common/pb/serverinfo_zone.proto b/common/pb/serverinfo_zone.proto index 0efa2d9be..f0ad5d709 100644 --- a/common/pb/serverinfo_zone.proto +++ b/common/pb/serverinfo_zone.proto @@ -11,6 +11,10 @@ message ServerInfo_Zone { // setting beingLookedAt to true. // Cards in a zone with the type HiddenZone are referenced by their // list index, whereas cards in any other zone are referenced by their ids. + // + // WARNING: Adding new zone types will break compatibility with older + // clients. Older clients will read new zone types as PrivateZone, which + // is likely *NOT* what you want. PrivateZone = 0; PublicZone = 1;