diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 0094b808c..e0dacd773 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -51,10 +52,10 @@ PlayerActions::PlayerActions(PlayerLogic *_player) connect(moveTopCardTimer, &QTimer::timeout, [this]() { actMoveTopCardToPlay(); }); } -void PlayerActions::playCard(CardItem *card, bool faceDown) +PendingCommand *PlayerActions::prepareCardMove(CardItem *card, bool faceDown) { if (card == nullptr) { - return; + return nullptr; } Command_MoveCard cmd; @@ -66,7 +67,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown) ExactCard exactCard = card->getCard(); if (!exactCard) { - return; + return nullptr; } const CardInfo &info = exactCard.getInfo(); @@ -97,7 +98,14 @@ void PlayerActions::playCard(CardItem *card, bool faceDown) cmd.set_x(gridPoint.x()); cmd.set_y(gridPoint.y()); } - sendGameCommand(cmd); + return prepareGameCommand(cmd); +} + +void PlayerActions::playCard(CardItem *card, bool faceDown) +{ + if (PendingCommand *pend = prepareCardMove(card, faceDown)) { + sendGameCommand(pend); + } } /** @@ -1638,9 +1646,10 @@ void PlayerActions::playSelectedCards(QList selectedCards, const boo playSelectedCardsImpl(selectedCards, faceDown, nullptr); } -void PlayerActions::playSelectedCardsImpl(QList selectedCards, - bool faceDown, - const std::function &postPlayCallback) +void PlayerActions::playSelectedCardsImpl( + QList selectedCards, + bool faceDown, + const std::function &postPlayCallback) { // CardIds will get shuffled downwards when cards leave the deck. // We need to iterate through the cards in reverse order so cardIds don't get changed out from under us as we play @@ -1651,10 +1660,16 @@ void PlayerActions::playSelectedCardsImpl(QList selectedCards, for (auto &card : selectedCards) { if (card && !isUnwritableRevealZone(card->getZone()) && card->getZone()->getName() != ZoneNames::TABLE) { const QString originalZone = card->getZone()->getName(); - playCard(card, faceDown); - if (postPlayCallback) { - postPlayCallback(card, originalZone); + PendingCommand *pend = prepareCardMove(card, faceDown); + if (pend == nullptr) { + continue; } + // Connect before send: a local game processes the command synchronously inside + // sendGameCommand, firing the pend's finished signal before this call returns. + if (postPlayCallback) { + postPlayCallback(pend, originalZone); + } + sendGameCommand(pend); } } } @@ -1671,13 +1686,22 @@ void PlayerActions::actPlayAndIncreasePartnerTax(QList selectedCards void PlayerActions::playAndIncreaseTax(QList selectedCards, int counterId) { - playSelectedCardsImpl(selectedCards, false, [this, counterId](CardItem * /*card*/, const QString &originalZone) { - if (originalZone == ZoneNames::COMMAND) { - CounterState *state = player->getCounters().value(counterId, nullptr); - if (state && state->isActive()) { - sendIncCounter(counterId, 1); - } + playSelectedCardsImpl(selectedCards, false, [this, counterId](PendingCommand *pend, const QString &originalZone) { + if (originalZone != ZoneNames::COMMAND || pend == nullptr) { + return; } + // Gate the tax increment on the server accepting the move, so a rejected move + // (the card couldn't legally leave the command zone) never inflates the tax. + connect(pend, &PendingCommand::finished, this, + [this, counterId](const Response &response, const CommandContainer &, const QVariant &) { + if (response.response_code() != Response::RespOk) { + return; + } + CounterState *state = player->getCounters().value(counterId, nullptr); + if (state && state->isActive()) { + sendIncCounter(counterId, 1); + } + }); }); } diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index 84a065479..0f295a396 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -257,23 +257,29 @@ private: void playSelectedCards(QList selectedCards, bool faceDown = false); /** - * @brief Shared implementation for playing selected cards with an optional post-play callback. - * @param selectedCards Cards to play - * @param faceDown Whether to play cards face-down - * @param postPlayCallback Called after each card is played, receiving the card and its *original* zone name - * (captured before playCard, since playCard sends a move command that may change the card's zone). + * @brief Builds the move command for playing a card, returning the prepared (unsent) PendingCommand. + * @return The prepared command, or nullptr if the card cannot be played. */ - void playSelectedCardsImpl(QList selectedCards, - bool faceDown, - const std::function &postPlayCallback = nullptr); + PendingCommand *prepareCardMove(CardItem *card, bool faceDown); /** - * @brief Plays the selected cards and, for each that came from the command zone, - * increments the given (active) tax counter by one. + * @brief Shared implementation for playing selected cards with an optional per-card callback. + * @param selectedCards Cards to play + * @param faceDown Whether to play cards face-down + * @param postPlayCallback Called for each card once its move is prepared but before it is sent, + * receiving the move's PendingCommand and the card's *original* zone name (the move may + * change the card's zone). Connect response handling here so it is attached before send. + */ + void + playSelectedCardsImpl(QList selectedCards, + bool faceDown, + const std::function &postPlayCallback = nullptr); + + /** + * @brief Plays the selected cards and, for each that came from the command zone and whose move + * the server accepts, increments the given (active) tax counter by one. * @param selectedCards Cards to play * @param counterId The tax counter to increment (CounterIds::CommanderTax or PartnerTax) - * @note The increment is sent optimistically, not gated on the move succeeding; a rejected - * move can leave the tax incremented without a card being cast. */ void playAndIncreaseTax(QList selectedCards, int counterId);