From bb8213deb58bfce779b6bf6a6bc90de94dfdd759 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 27 Apr 2025 21:30:23 -0700 Subject: [PATCH] Support creating face-down tokens (#5800) * add new fields to proto * update token dlg * send facedown in command * update server to get it to work * disable certain edits when face down * update client event processing * log face-down token creation * Don't support colors on face-down tokens The other client doesn't know about the color, so it causes a desync * Update wording Co-authored-by: Basile Clement * Allow annotations on face-down tokens --------- Co-authored-by: Basile Clement --- cockatrice/src/dialogs/dlg_create_token.cpp | 22 ++++++- cockatrice/src/dialogs/dlg_create_token.h | 3 + cockatrice/src/game/player/player.cpp | 8 ++- cockatrice/src/game/player/player.h | 2 +- cockatrice/src/server/message_log_widget.cpp | 14 +++-- cockatrice/src/server/message_log_widget.h | 2 +- common/pb/command_create_token.proto | 1 + common/pb/event_create_token.proto | 1 + common/server_player.cpp | 62 +++++++++++++++++--- common/server_player.h | 1 + 10 files changed, 97 insertions(+), 19 deletions(-) diff --git a/cockatrice/src/dialogs/dlg_create_token.cpp b/cockatrice/src/dialogs/dlg_create_token.cpp index 7b3db2773..14adaa48a 100644 --- a/cockatrice/src/dialogs/dlg_create_token.cpp +++ b/cockatrice/src/dialogs/dlg_create_token.cpp @@ -63,6 +63,9 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa destroyCheckBox = new QCheckBox(tr("&Destroy token when it leaves the table")); destroyCheckBox->setChecked(true); + faceDownCheckBox = new QCheckBox(tr("Create face-down (Only hides name)")); + connect(faceDownCheckBox, &QCheckBox::toggled, this, &DlgCreateToken::faceDownCheckBoxToggled); + QGridLayout *grid = new QGridLayout; grid->addWidget(nameLabel, 0, 0); grid->addWidget(nameEdit, 0, 1); @@ -73,6 +76,7 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa grid->addWidget(annotationLabel, 3, 0); grid->addWidget(annotationEdit, 3, 1); grid->addWidget(destroyCheckBox, 4, 0, 1, 2); + grid->addWidget(faceDownCheckBox, 5, 0, 1, 2); QGroupBox *tokenDataGroupBox = new QGroupBox(tr("Token data")); tokenDataGroupBox->setLayout(grid); @@ -155,6 +159,21 @@ void DlgCreateToken::closeEvent(QCloseEvent *event) SettingsCache::instance().setTokenDialogGeometry(saveGeometry()); } +void DlgCreateToken::faceDownCheckBoxToggled(bool checked) +{ + if (checked) { + colorEdit->setCurrentIndex(6); + colorEdit->setEnabled(false); + ptEdit->clear(); + ptEdit->clearFocus(); + ptEdit->setEnabled(false); + } else { + colorEdit->setEnabled(true); + ptEdit->setEnabled(true); + annotationEdit->setEnabled(true); + } +} + void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, const QModelIndex & /*previous*/) { const QModelIndex realIndex = cardDatabaseDisplayModel->mapToSource(current); @@ -230,5 +249,6 @@ TokenInfo DlgCreateToken::getTokenInfo() const .color = colorEdit->itemData(colorEdit->currentIndex()).toString(), .pt = ptEdit->text(), .annotation = annotationEdit->text(), - .destroy = destroyCheckBox->isChecked()}; + .destroy = destroyCheckBox->isChecked(), + .faceDown = faceDownCheckBox->isChecked()}; } diff --git a/cockatrice/src/dialogs/dlg_create_token.h b/cockatrice/src/dialogs/dlg_create_token.h index 373e094f1..cd1b48c81 100644 --- a/cockatrice/src/dialogs/dlg_create_token.h +++ b/cockatrice/src/dialogs/dlg_create_token.h @@ -24,6 +24,7 @@ struct TokenInfo QString pt; QString annotation; bool destroy = true; + bool faceDown = false; }; class DlgCreateToken : public QDialog @@ -36,6 +37,7 @@ public: protected: void closeEvent(QCloseEvent *event) override; private slots: + void faceDownCheckBoxToggled(bool checked); void tokenSelectionChanged(const QModelIndex ¤t, const QModelIndex &previous); void updateSearch(const QString &search); void actChooseTokenFromAll(bool checked); @@ -51,6 +53,7 @@ private: QComboBox *colorEdit; QLineEdit *nameEdit, *ptEdit, *annotationEdit; QCheckBox *destroyCheckBox; + QCheckBox *faceDownCheckBox; QRadioButton *chooseTokenFromAllRadioButton, *chooseTokenFromDeckRadioButton; CardInfoPictureWidget *pic; QTreeView *chooseTokenView; diff --git a/cockatrice/src/game/player/player.cpp b/cockatrice/src/game/player/player.cpp index ebbbb5291..160d8d336 100644 --- a/cockatrice/src/game/player/player.cpp +++ b/cockatrice/src/game/player/player.cpp @@ -1855,6 +1855,7 @@ void Player::actCreateAnotherToken() cmd.set_pt(lastTokenInfo.pt.toStdString()); cmd.set_annotation(lastTokenInfo.annotation.toStdString()); cmd.set_destroy_on_zone_change(lastTokenInfo.destroy); + cmd.set_face_down(lastTokenInfo.faceDown); cmd.set_x(-1); cmd.set_y(lastTokenTableRow); @@ -2233,10 +2234,10 @@ void Player::eventCreateToken(const Event_CreateToken &event) CardItem *card = new CardItem(this, nullptr, QString::fromStdString(event.card_name()), QString::fromStdString(event.card_provider_id()), event.card_id()); - // use db PT if not provided in event + // use db PT if not provided in event and not face-down if (!QString::fromStdString(event.pt()).isEmpty()) { card->setPT(QString::fromStdString(event.pt())); - } else { + } else if (!event.face_down()) { CardInfoPtr dbCard = card->getInfo(); if (dbCard) { card->setPT(dbCard->getPowTough()); @@ -2245,8 +2246,9 @@ void Player::eventCreateToken(const Event_CreateToken &event) card->setColor(QString::fromStdString(event.color())); card->setAnnotation(QString::fromStdString(event.annotation())); card->setDestroyOnZoneChange(event.destroy_on_zone_change()); + card->setFaceDown(event.face_down()); - emit logCreateToken(this, card->getName(), card->getPT()); + emit logCreateToken(this, card->getName(), card->getPT(), card->getFaceDown()); zone->addCard(card, true, event.x(), event.y()); } diff --git a/cockatrice/src/game/player/player.h b/cockatrice/src/game/player/player.h index 027a7658b..ea5278572 100644 --- a/cockatrice/src/game/player/player.h +++ b/cockatrice/src/game/player/player.h @@ -128,7 +128,7 @@ signals: Player *targetPlayer, QString targetCard, bool _playerTarget); - void logCreateToken(Player *player, QString cardName, QString pt); + void logCreateToken(Player *player, QString cardName, QString pt, bool faceDown); void logDrawCards(Player *player, int number, bool deckIsEmpty); void logUndoDraw(Player *player, QString cardName); void logMoveCard(Player *player, CardItem *card, CardZone *startZone, int oldX, CardZone *targetZone, int newX); diff --git a/cockatrice/src/server/message_log_widget.cpp b/cockatrice/src/server/message_log_widget.cpp index 7590e044b..0d21be045 100644 --- a/cockatrice/src/server/message_log_widget.cpp +++ b/cockatrice/src/server/message_log_widget.cpp @@ -214,12 +214,16 @@ void MessageLogWidget::logCreateArrow(Player *player, } } -void MessageLogWidget::logCreateToken(Player *player, QString cardName, QString pt) +void MessageLogWidget::logCreateToken(Player *player, QString cardName, QString pt, bool faceDown) { - appendHtmlServerMessage(tr("%1 creates token: %2%3.") - .arg(sanitizeHtml(player->getName())) - .arg(cardLink(std::move(cardName))) - .arg(pt.isEmpty() ? QString() : QString(" (%1)").arg(sanitizeHtml(pt)))); + if (faceDown) { + appendHtmlServerMessage(tr("%1 creates a face down token.").arg(sanitizeHtml(player->getName()))); + } else { + appendHtmlServerMessage(tr("%1 creates token: %2%3.") + .arg(sanitizeHtml(player->getName())) + .arg(cardLink(std::move(cardName))) + .arg(pt.isEmpty() ? QString() : QString(" (%1)").arg(sanitizeHtml(pt)))); + } } void MessageLogWidget::logDeckSelect(Player *player, QString deckHash, int sideboardSize) diff --git a/cockatrice/src/server/message_log_widget.h b/cockatrice/src/server/message_log_widget.h index f6780b729..21b03dee9 100644 --- a/cockatrice/src/server/message_log_widget.h +++ b/cockatrice/src/server/message_log_widget.h @@ -41,7 +41,7 @@ public slots: Player *targetPlayer, QString targetCard, bool playerTarget); - void logCreateToken(Player *player, QString cardName, QString pt); + void logCreateToken(Player *player, QString cardName, QString pt, bool faceDown); void logDeckSelect(Player *player, QString deckHash, int sideboardSize); void logDestroyCard(Player *player, QString cardName); void logDrawCards(Player *player, int number, bool deckIsEmpty); diff --git a/common/pb/command_create_token.proto b/common/pb/command_create_token.proto index 671ce3fe3..4b7d11098 100644 --- a/common/pb/command_create_token.proto +++ b/common/pb/command_create_token.proto @@ -27,4 +27,5 @@ message Command_CreateToken { optional TargetMode target_mode = 11; optional string card_provider_id = 12; + optional bool face_down = 13; } diff --git a/common/pb/event_create_token.proto b/common/pb/event_create_token.proto index bc88a744c..6947b6048 100644 --- a/common/pb/event_create_token.proto +++ b/common/pb/event_create_token.proto @@ -15,4 +15,5 @@ message Event_CreateToken { optional sint32 x = 8; optional sint32 y = 9; optional string card_provider_id = 10; + optional bool face_down = 11; } diff --git a/common/server_player.cpp b/common/server_player.cpp index eb8a9977b..01f6947fa 100644 --- a/common/server_player.cpp +++ b/common/server_player.cpp @@ -386,13 +386,23 @@ void Server_Player::revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorag } } -static Event_CreateToken makeCreateTokenEvent(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord) +/** + * Creates the create token event. + * By default, will set event's name and color fields to empty if the token is face-down + */ +static Event_CreateToken +makeCreateTokenEvent(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord, bool revealFacedownInfo = false) { Event_CreateToken event; event.set_zone_name(zone->getName().toStdString()); event.set_card_id(card->getId()); - event.set_card_name(card->getName().toStdString()); - event.set_card_provider_id(card->getProviderId().toStdString()); + event.set_face_down(card->getFaceDown()); + + if (!card->getFaceDown() || revealFacedownInfo) { + event.set_card_name(card->getName().toStdString()); + event.set_card_provider_id(card->getProviderId().toStdString()); + } + event.set_color(card->getColor().toStdString()); event.set_pt(card->getPT().toStdString()); event.set_annotation(card->getAnnotation().toStdString()); @@ -401,7 +411,6 @@ static Event_CreateToken makeCreateTokenEvent(Server_CardZone *zone, Server_Card event.set_y(yCoord); return event; } - static Event_AttachCard makeAttachCardEvent(Server_Card *attachedCard, Server_Card *parentCard = nullptr) { Event_AttachCard event; @@ -1494,7 +1503,8 @@ Server_Player::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer const QString cardName = nameFromStdString(cmd.card_name()); const QString cardProviderId = nameFromStdString(cmd.card_provider_id()); if (zone->hasCoords()) { - xCoord = zone->getFreeGridColumn(xCoord, yCoord, cardName, false); + bool dontStackSameName = cmd.face_down(); + xCoord = zone->getFreeGridColumn(xCoord, yCoord, cardName, dontStackSameName); } if (xCoord < 0) { xCoord = 0; @@ -1505,13 +1515,17 @@ Server_Player::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer auto *card = new Server_Card(cardName, cardProviderId, newCardId(), xCoord, yCoord); card->moveToThread(thread()); - card->setPT(nameFromStdString(cmd.pt())); - card->setColor(nameFromStdString(cmd.color())); + // Client should already prevent face-down tokens from having attributes; this just an extra server-side check + if (!cmd.face_down()) { + card->setColor(nameFromStdString(cmd.color())); + card->setPT(nameFromStdString(cmd.pt())); + } card->setAnnotation(nameFromStdString(cmd.annotation())); card->setDestroyOnZoneChange(cmd.destroy_on_zone_change()); + card->setFaceDown(cmd.face_down()); zone->insertCard(card, xCoord, yCoord); - ges.enqueueGameEvent(makeCreateTokenEvent(zone, card, xCoord, yCoord), playerId); + sendCreateTokenEvents(zone, card, xCoord, yCoord, ges); // check if the token is a replacement for an existing card if (!targetCard) { @@ -1642,6 +1656,38 @@ Server_Player::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer return Response::RespOk; } +/** + * Creates and sends the events required to properly communicate the given token creation. + * Primarily written to handle creating face-down tokens. + */ +void Server_Player::sendCreateTokenEvents(Server_CardZone *zone, + Server_Card *card, + int xCoord, + int yCoord, + GameEventStorage &ges) +{ + // Token is not face-down; things are easy + if (!card->getFaceDown()) { + ges.enqueueGameEvent(makeCreateTokenEvent(zone, card, xCoord, yCoord), playerId); + return; + } + + // Token is face-down. We have to send different info to each player + auto eventOthers = makeCreateTokenEvent(zone, card, xCoord, yCoord, false); + ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); + + auto eventPrivate = makeCreateTokenEvent(zone, card, xCoord, yCoord, true); + ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId); + + // Event_CreateToken didn't use to have face_down field; send attribute event afterward for backwards compatibility + Event_SetCardAttr event; + event.set_zone_name(zone->getName().toStdString()); + event.set_card_id(card->getId()); + event.set_attribute(AttrFaceDown); + event.set_attr_value("1"); + ges.enqueueGameEvent(event, playerId); +} + Response::ResponseCode Server_Player::cmdCreateArrow(const Command_CreateArrow &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) { diff --git a/common/server_player.h b/common/server_player.h index ffa12f8b0..b9d414482 100644 --- a/common/server_player.h +++ b/common/server_player.h @@ -84,6 +84,7 @@ private: bool conceded; bool sideboardLocked; void revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorage &ges); + void sendCreateTokenEvents(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord, GameEventStorage &ges); public: mutable QMutex playerMutex;