Initial commit of mtgonline project

This commit is contained in:
2026-07-18 04:57:40 +00:00
commit 86c12376f8
1870 changed files with 547994 additions and 0 deletions
@@ -0,0 +1,45 @@
#ifndef COCKATRICE_CARD_ZONE_ALGORITHMS_H
#define COCKATRICE_CARD_ZONE_ALGORITHMS_H
namespace CardZoneAlgorithms
{
/**
* Shared insertion logic for zones where cards become visible on add and follow
* the standard pattern: clamp index, insert, clear identity if contents unknown,
* reset state, show card.
*
* Zones with different post-add behavior (signal connections, positional resets,
* hidden cards, or coordinate-based placement) should NOT use this — implement
* addCardImpl directly instead.
*
* Template parameters allow testing with lightweight mocks that avoid Qt graphics
* dependencies.
*
* @tparam CardList Must provide: size() -> int, insert(int, CardType*),
* getContentsKnown() -> bool
* @tparam CardType Must provide: setId(int), setCardRef(CardRefType),
* resetState(bool), setVisible(bool)
* @param keepAnnotations Forwarded to card->resetState(). Stack-like zones preserve
* annotations across zone transitions; hand-like zones clear them.
*/
template <typename CardList, typename CardType>
void addCardToList(CardList &cards, CardType *card, int x, bool keepAnnotations)
{
if (x < 0 || x >= cards.size()) {
x = static_cast<int>(cards.size());
}
cards.insert(x, card);
if (!cards.getContentsKnown()) {
card->setId(-1);
card->setCardRef({});
}
card->resetState(keepAnnotations);
card->setVisible(true);
}
} // namespace CardZoneAlgorithms
#endif // COCKATRICE_CARD_ZONE_ALGORITHMS_H
@@ -0,0 +1,221 @@
#include "card_zone_logic.h"
#include "../../game_graphics/board/card_item.h"
#include "../../game_graphics/zones/view_zone.h"
#include "../player/player_actions.h"
#include "../player/player_logic.h"
#include "view_zone_logic.h"
#include <QAction>
#include <QDebug>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/utility/zone_names.h>
/**
* @param _player the player that the zone belongs to
* @param _name internal name of the zone
* @param _hasCardAttr whether cards in the zone can have attributes set on them
* @param _isShufflable whether it makes sense to shuffle this zone by default after viewing it
* @param _contentsKnown whether the cards in the zone are known to the client
* @param parent the parent QObject.
*/
CardZoneLogic::CardZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent)
: QObject(parent), player(_player), name(_name), cards(_contentsKnown), views{}, hasCardAttr(_hasCardAttr),
isShufflable(_isShufflable)
{
// If we join a game before the card db finishes loading, the cards might have the wrong printings.
// Force refresh all cards in the zone when db finishes loading to fix that.
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&CardZoneLogic::refreshCardInfos);
}
void CardZoneLogic::addCard(CardItem *card, const bool reorganize, const int x, const int y)
{
if (!card) {
qCWarning(CardZoneLog) << "CardZoneLogic::addCard() card is null; this shouldn't normally happen";
return;
}
for (auto *view : views) {
if (qobject_cast<ZoneViewZoneLogic *>(view->getLogic())->prepareAddCard(x)) {
auto copy = new CardItem(player, nullptr, card->getCardRef(), card->getId());
copy->setFaceDown(card->getFaceDown());
view->getLogic()->addCard(copy, reorganize, x, y);
}
}
card->setZone(this);
emit cardAdded(card);
addCardImpl(card, x, y);
if (reorganize) {
emit reorganizeCards();
}
emit cardCountChanged();
}
CardItem *CardZoneLogic::takeCard(int position, int cardId, bool toNewZone)
{
if (position == -1) {
// position == -1 means either that the zone is indexed by card id
// or that it doesn't matter which card you take.
for (int i = 0; i < cards.size(); ++i) {
if (cards[i]->getId() == cardId) {
position = i;
break;
}
}
if (position == -1) {
position = 0;
}
}
if (position >= cards.size()) {
return nullptr;
}
for (auto *view : views) {
qobject_cast<ZoneViewZoneLogic *>(view->getLogic())->removeCard(position, toNewZone);
}
CardItem *c = cards.takeAt(position);
c->setId(cardId);
emit reorganizeCards();
emit cardCountChanged();
return c;
}
CardItem *CardZoneLogic::getCard(int cardId)
{
CardItem *c = cards.findCard(cardId);
if (!c) {
qCWarning(CardZoneLog) << "CardZoneLogic::getCard: card id=" << cardId << "not found";
return nullptr;
}
// If the card's id is -1, this zone is invisible,
// so we need to give the card an id as it comes out.
// It can be assumed that in an invisible zone, all cards are equal.
if (c->getId() == -1) {
c->setId(cardId);
}
return c;
}
void CardZoneLogic::removeCard(CardItem *card)
{
if (!card) {
qCWarning(CardZoneLog) << "CardZoneLogic::removeCard: card is null, this shouldn't normally happen";
return;
}
cards.removeOne(card);
emit reorganizeCards();
emit cardCountChanged();
player->deleteCard(card);
}
void CardZoneLogic::refreshCardInfos()
{
for (const auto &cardItem : cards) {
cardItem->refreshCardInfo();
}
}
void CardZoneLogic::moveAllToZone()
{
QList<QVariant> data = static_cast<QAction *>(sender())->data().toList();
if (data.length() < 2) {
return;
}
QString targetZone = data[0].toString();
int targetX = data[1].toInt();
Command_MoveCard cmd;
cmd.set_start_zone(getName().toStdString());
cmd.set_target_player_id(player->getPlayerInfo()->getId());
cmd.set_target_zone(targetZone.toStdString());
cmd.set_x(targetX);
for (int i = 0; i < cards.size(); ++i) {
cmd.mutable_cards_to_move()->add_card()->set_card_id(cards[i]->getId());
}
player->getPlayerActions()->sendGameCommand(cmd);
}
void CardZoneLogic::clearContents()
{
// First gather the cards into a safe temporary list.
const CardList toClear = cards;
// Detach and notify attached cards and zones *before* deleting anything.
for (CardItem *card : toClear) {
// If an incorrectly implemented server doesn't return attached cards to whom they belong before dropping a
// player, we have to return them to avoid a crash.
const QList<CardItem *> &attachedCards = card->getAttachedCards();
for (CardItem *attachedCard : attachedCards) {
emit attachedCard->getZone()->cardAdded(attachedCard);
}
}
// Now request deletions after all manipulations are done.
for (CardItem *card : toClear) {
player->deleteCard(card);
}
cards.clear();
emit cardCountChanged();
}
QString CardZoneLogic::getTranslatedName(bool theirOwn, GrammaticalCase gc) const
{
QString ownerName = player->getPlayerInfo()->getName();
if (name == ZoneNames::HAND) {
return (theirOwn ? tr("their hand", "nominative") : tr("%1's hand", "nominative").arg(ownerName));
} else if (name == ZoneNames::DECK) {
switch (gc) {
case CaseLookAtZone:
return (theirOwn ? tr("their library", "look at zone")
: tr("%1's library", "look at zone").arg(ownerName));
case CaseTopCardsOfZone:
return (theirOwn ? tr("of their library", "top cards of zone,")
: tr("of %1's library", "top cards of zone").arg(ownerName));
case CaseRevealZone:
return (theirOwn ? tr("their library", "reveal zone")
: tr("%1's library", "reveal zone").arg(ownerName));
case CaseShuffleZone:
return (theirOwn ? tr("their library", "shuffle") : tr("%1's library", "shuffle").arg(ownerName));
default:
return (theirOwn ? tr("their library", "nominative") : tr("%1's library", "nominative").arg(ownerName));
}
} else if (name == ZoneNames::GRAVE) {
return (theirOwn ? tr("their graveyard", "nominative") : tr("%1's graveyard", "nominative").arg(ownerName));
} else if (name == ZoneNames::EXILE) {
return (theirOwn ? tr("their exile", "nominative") : tr("%1's exile", "nominative").arg(ownerName));
} else if (name == ZoneNames::SIDEBOARD) {
switch (gc) {
case CaseLookAtZone:
return (theirOwn ? tr("their sideboard", "look at zone")
: tr("%1's sideboard", "look at zone").arg(ownerName));
case CaseNominative:
return (theirOwn ? tr("their sideboard", "nominative")
: tr("%1's sideboard", "nominative").arg(ownerName));
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();
}
@@ -0,0 +1,119 @@
/**
* @file card_zone_logic.h
* @ingroup GameLogicZones
*/
//! \todo Document this file.
#ifndef COCKATRICE_CARD_ZONE_LOGIC_H
#define COCKATRICE_CARD_ZONE_LOGIC_H
#include "../../client/translation.h"
#include "../board/card_list.h"
#include <QLoggingCategory>
#include <QObject>
inline Q_LOGGING_CATEGORY(CardZoneLogicLog, "card_zone_logic");
class PlayerLogic;
class ZoneViewZone;
class QMenu;
class QAction;
class QPainter;
class CardDragItem;
class CardZoneLogic : public QObject
{
Q_OBJECT
signals:
void cardAdded(CardItem *addedCard);
void cardCountChanged();
void reorganizeCards();
void updateGraphics();
void setGraphicsVisibility(bool visible);
void retranslateUi();
public:
explicit CardZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent = nullptr);
void addCard(CardItem *card, bool reorganize, int x, int y = -1);
// getCard() finds a card by id.
CardItem *getCard(int cardId);
void removeCard(CardItem *card);
// takeCard() finds a card by position and removes it from the zone and from all of its views.
virtual CardItem *takeCard(int position, int cardId, bool canResize = true);
void rawInsertCard(CardItem *card, int index)
{
cards.insert(index, card);
}
[[nodiscard]] const CardList &getCards() const
{
return cards;
}
void sortCards(const QList<CardList::SortOption> &options)
{
cards.sortBy(options);
}
[[nodiscard]] QString getName() const
{
return name;
}
[[nodiscard]] QString getTranslatedName(bool theirOwn, GrammaticalCase gc) const;
[[nodiscard]] PlayerLogic *getPlayer() const
{
return player;
}
[[nodiscard]] bool contentsKnown() const
{
return cards.getContentsKnown();
}
QList<ZoneViewZone *> &getViews()
{
return views;
}
void setAlwaysRevealTopCard(bool _alwaysRevealTopCard)
{
alwaysRevealTopCard = _alwaysRevealTopCard;
}
[[nodiscard]] bool getAlwaysRevealTopCard() const
{
return alwaysRevealTopCard;
}
[[nodiscard]] bool getHasCardAttr() const
{
return hasCardAttr;
}
[[nodiscard]] bool getIsShufflable() const
{
return isShufflable;
}
void clearContents();
public slots:
void moveAllToZone();
private slots:
void refreshCardInfos();
protected:
PlayerLogic *player;
QString name;
CardList cards;
QList<ZoneViewZone *> views;
bool hasCardAttr;
bool isShufflable;
bool alwaysRevealTopCard;
virtual void addCardImpl(CardItem *card, int x, int y) = 0;
};
#endif // COCKATRICE_CARD_ZONE_LOGIC_H
@@ -0,0 +1,19 @@
#include "hand_zone_logic.h"
#include "../../game_graphics/board/card_item.h"
#include "card_zone_algorithms.h"
HandZoneLogic::HandZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent)
: CardZoneLogic(_player, _name, _hasCardAttr, _isShufflable, _contentsKnown, parent)
{
}
void HandZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/)
{
CardZoneAlgorithms::addCardToList(cards, card, x, false);
}
@@ -0,0 +1,26 @@
/**
* @file hand_zone_logic.h
* @ingroup GameLogicZones
*/
//! \todo Document this file.
#ifndef COCKATRICE_HAND_ZONE_LOGIC_H
#define COCKATRICE_HAND_ZONE_LOGIC_H
#include "card_zone_logic.h"
class HandZoneLogic : public CardZoneLogic
{
Q_OBJECT
public:
HandZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent = nullptr);
protected:
void addCardImpl(CardItem *card, int x, int y) override;
};
#endif // COCKATRICE_HAND_ZONE_LOGIC_H
@@ -0,0 +1,34 @@
#include "pile_zone_logic.h"
#include "../../game_graphics/board/card_item.h"
PileZoneLogic::PileZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent)
: CardZoneLogic(_player, _name, _hasCardAttr, _isShufflable, _contentsKnown, parent)
{
}
void PileZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/)
{
connect(card, &CardItem::sigPixmapUpdated, this, &PileZoneLogic::callUpdate);
// if x is negative set it to add at end
if (x < 0 || x >= cards.size()) {
x = cards.size();
}
cards.insert(x, card);
card->setPos(0, 0);
if (!contentsKnown()) {
card->setCardRef({});
card->setId(-1);
// If we obscure a previously revealed card, its name has to be forgotten
if (cards.size() > x + 1) {
cards.at(x + 1)->setCardRef({});
}
}
card->setVisible(false);
card->resetState();
}
@@ -0,0 +1,31 @@
/**
* @file pile_zone_logic.h
* @ingroup GameLogicZones
*/
//! \todo Document this file.
#ifndef COCKATRICE_PILE_ZONE_LOGIC_H
#define COCKATRICE_PILE_ZONE_LOGIC_H
#include "card_zone_logic.h"
class PileZoneLogic : public CardZoneLogic
{
Q_OBJECT
signals:
void callUpdate();
public:
PileZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent = nullptr);
protected:
void addCardImpl(CardItem *card, int x, int y) override;
};
#endif // COCKATRICE_PILE_ZONE_LOGIC_H
@@ -0,0 +1,19 @@
#include "stack_zone_logic.h"
#include "../../game_graphics/board/card_item.h"
#include "card_zone_algorithms.h"
StackZoneLogic::StackZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent)
: CardZoneLogic(_player, _name, _hasCardAttr, _isShufflable, _contentsKnown, parent)
{
}
void StackZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/)
{
CardZoneAlgorithms::addCardToList(cards, card, x, true);
}
@@ -0,0 +1,26 @@
/**
* @file stack_zone_logic.h
* @ingroup GameLogicZones
*/
//! \todo Document this file.
#ifndef COCKATRICE_STACK_ZONE_LOGIC_H
#define COCKATRICE_STACK_ZONE_LOGIC_H
#include "card_zone_logic.h"
class StackZoneLogic : public CardZoneLogic
{
Q_OBJECT
public:
StackZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent = nullptr);
protected:
void addCardImpl(CardItem *card, int x, int y) override;
};
#endif // COCKATRICE_STACK_ZONE_LOGIC_H
@@ -0,0 +1,36 @@
#include "table_zone_logic.h"
#include "../../game_graphics/board/card_item.h"
TableZoneLogic::TableZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent)
: CardZoneLogic(_player, _name, _hasCardAttr, _isShufflable, _contentsKnown, parent)
{
}
void TableZoneLogic::addCardImpl(CardItem *card, int _x, int _y)
{
cards.append(card);
if (!card->getFaceDown() && card->getPT().isEmpty()) {
card->setPT(card->getCardInfo().getPowTough());
}
if (card->getCardInfo().getUiAttributes().cipt && card->getCardInfo().getUiAttributes().landscapeOrientation) {
card->setDoesntUntap(true);
}
card->setGridPoint(QPoint(_x, _y));
card->setVisible(true);
}
CardItem *TableZoneLogic::takeCard(int position, int cardId, bool toNewZone)
{
CardItem *result = CardZoneLogic::takeCard(position, cardId);
if (toNewZone) {
emit contentSizeChanged();
}
return result;
}
@@ -0,0 +1,40 @@
/**
* @file table_zone_logic.h
* @ingroup GameLogicZones
*/
//! \todo Document this file.
#ifndef COCKATRICE_TABLE_ZONE_LOGIC_H
#define COCKATRICE_TABLE_ZONE_LOGIC_H
#include "card_zone_logic.h"
class TableZoneLogic : public CardZoneLogic
{
Q_OBJECT
signals:
void contentSizeChanged();
void toggleTapped();
public:
TableZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent = nullptr);
protected:
void addCardImpl(CardItem *card, int x, int y) override;
/**
* @brief Removes a card from view.
*
* @param position card position
* @param cardId id of card to take
* @param toNewZone Whether the destination of the card is not the same as the starting zone. Defaults to true
* @return CardItem that has been removed
*/
CardItem *takeCard(int position, int cardId, bool toNewZone = true) override;
};
#endif // COCKATRICE_TABLE_ZONE_LOGIC_H
@@ -0,0 +1,165 @@
#include "view_zone_logic.h"
#include "../../client/settings/cache_settings.h"
#include "../../game_graphics/board/card_item.h"
/**
* @param _player the player that the cards are revealed to.
* @param _origZone the zone the cards were revealed from.
* @param _revealZone if false, the cards will be face down.
* @param _writeableRevealZone whether the player can interact with the revealed cards.
*/
ZoneViewZoneLogic::ZoneViewZoneLogic(PlayerLogic *_player,
CardZoneLogic *_origZone,
int _numberCards,
bool _revealZone,
bool _writeableRevealZone,
bool _isReversed,
QObject *parent)
: CardZoneLogic(_player, _origZone->getName(), false, false, true, parent), origZone(_origZone),
numberCards(_numberCards), revealZone(_revealZone), writeableRevealZone(_writeableRevealZone),
isReversed(_isReversed)
{
}
/**
* Checks if inserting a card at the given position requires an actual new card to be created and added to the view.
* Also does any cardId updates that would be required if a card is inserted in that position.
*
* Note that this method can end up modifying the cardIds despite returning false.
* (for example, if the card is inserted into a hidden portion of the deck while the view is reversed)
*
* Make sure to call this method once before calling addCard(), so that you skip creating a new CardItem and calling
* addCard() if it's not required.
*
* @param x The position to insert the card at.
* @return Whether to proceed with calling addCard.
*/
bool ZoneViewZoneLogic::prepareAddCard(int x)
{
bool doInsert = false;
if (!isReversed) {
if (x <= cards.size() || cards.size() == -1) {
doInsert = true;
}
} else {
// map x (which is in origZone indexes) to this viewZone's cardList index
int firstId = cards.isEmpty() ? origZone->getCards().size() : cards.front()->getId();
int insertionIndex = x - firstId;
if (insertionIndex >= 0) {
// card was put into a portion of the deck that's in the view
doInsert = true;
} else {
// card was put into a portion of the deck that's not in the view; update ids but don't insert card
updateCardIds(ADD_CARD);
}
}
// autoclose check is done both here and in removeCard
if (cards.isEmpty() && !doInsert && SettingsCache::instance().getCloseEmptyCardView()) {
emit closeView();
}
return doInsert;
}
/**
* Make sure prepareAddCard() was called before calling addCard().
* This method assumes we already checked that the card is being inserted into the visible portion
*/
void ZoneViewZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/)
{
if (!isReversed) {
// if x is negative set it to add at end
// if x is out-of-bounds then also set it to add at the end
if (x < 0 || x >= cards.size()) {
x = cards.size();
}
cards.insert(x, card);
} else {
// map x (which is in origZone indexes) to this viewZone's cardList index
int firstId = cards.isEmpty() ? origZone->getCards().size() : cards.front()->getId();
int insertionIndex = x - firstId;
// qMin to prevent out-of-bounds error when bottoming a card that is already in the view
cards.insert(qMin(insertionIndex, cards.size()), card);
}
updateCardIds(ADD_CARD);
reorganizeCards();
}
void ZoneViewZoneLogic::updateCardIds(CardAction action)
{
if (origZone->contentsKnown()) {
return;
}
if (cards.isEmpty()) {
return;
}
int cardCount = cards.size();
auto startId = 0;
if (isReversed) {
// the card has not been added to origZone's cardList at this point
startId = origZone->getCards().size() - cardCount;
switch (action) {
case INITIALIZE:
break;
case ADD_CARD:
startId += 1;
break;
case REMOVE_CARD:
startId -= 1;
break;
}
}
for (int i = 0; i < cardCount; ++i) {
cards[i]->setId(i + startId);
}
}
void ZoneViewZoneLogic::removeCard(int position, bool toNewZone)
{
if (isReversed) {
position -= cards.first()->getId();
if (position < 0 || position >= cards.size()) {
updateCardIds(REMOVE_CARD);
return;
}
}
if (position >= cards.size()) {
return;
}
CardItem *card = cards.takeAt(position);
card->deleteLater();
// The toNewZone check is to prevent the view from auto-closing if the view contains only a single card and that
// card gets dragged within the view.
// Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an
// unrevealed portion of the same zone.
if (cards.isEmpty() && SettingsCache::instance().getCloseEmptyCardView() && toNewZone) {
emit closeView();
return;
}
updateCardIds(REMOVE_CARD);
reorganizeCards();
}
void ZoneViewZoneLogic::setWriteableRevealZone(bool _writeableRevealZone)
{
if (writeableRevealZone && !_writeableRevealZone) {
emit addToViews();
} else if (!writeableRevealZone && _writeableRevealZone) {
emit removeFromViews();
}
writeableRevealZone = _writeableRevealZone;
}
@@ -0,0 +1,71 @@
/**
* @file view_zone_logic.h
* @ingroup GameLogicZones
*/
//! \todo Document this file.
#ifndef COCKATRICE_VIEW_ZONE_LOGIC_H
#define COCKATRICE_VIEW_ZONE_LOGIC_H
#include "card_zone_logic.h"
class ZoneViewZoneLogic : public CardZoneLogic
{
Q_OBJECT
signals:
void addToViews();
void removeFromViews();
void closeView();
private:
CardZoneLogic *origZone;
int numberCards;
bool revealZone, writeableRevealZone;
bool isReversed;
public:
enum CardAction
{
INITIALIZE,
ADD_CARD,
REMOVE_CARD
};
ZoneViewZoneLogic(PlayerLogic *_player,
CardZoneLogic *_origZone,
int _numberCards,
bool _revealZone,
bool _writeableRevealZone,
bool _isReversed,
QObject *parent = nullptr);
bool prepareAddCard(int x);
void removeCard(int position, bool toNewZone);
void updateCardIds(CardAction action);
int getNumberCards() const
{
return numberCards;
}
bool getRevealZone() const
{
return revealZone;
}
bool getWriteableRevealZone() const
{
return writeableRevealZone;
}
void setWriteableRevealZone(bool _writeableRevealZone);
bool getIsReversed() const
{
return isReversed;
}
CardZoneLogic *getOriginalZone() const
{
return origZone;
}
protected:
void addCardImpl(CardItem *card, int x, int y) override;
};
#endif // COCKATRICE_VIEW_ZONE_LOGIC_H