(.*?)",
- QRegularExpression::DotMatchesEverythingOption);
- const auto match = errorsRx.match(data);
- if (match.hasMatch()) {
- static const QRegularExpression itemRx("
", QRegularExpression::DotMatchesEverythingOption);
- auto items = itemRx.globalMatch(match.captured(1));
- while (items.hasNext()) {
- const QString error = QTextDocumentFragment::fromHtml(items.next().captured(1)).toPlainText().simplified();
- if (!error.isEmpty()) {
- errorMessageList.append(error);
+ int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (reply->hasRawHeader("Location")) {
+ /*
+ * If the reply contains a "Location" header, a relative URL to the deck on TappedOut
+ * can be extracted from the header. The http status is a 302 "redirect".
+ */
+ QString deckUrl = reply->rawHeader("Location");
+ qCInfo(TappedOutInterfaceLog) << "Tappedout: good reply, http status" << httpStatus << "location" << deckUrl;
+ QDesktopServices::openUrl("https://tappedout.net" + deckUrl);
+ } else {
+ /*
+ * Otherwise, the deck has not been parsed correctly. Error messages can be extracted
+ * from the html. Css pseudo selector for errors: $("div.alert-danger > ul > li")
+ */
+ QString data(reply->readAll());
+ QStringList errorMessageList = {tr("Unable to analyze the deck.")};
+
+ static const QRegularExpression rx("
(.*?)");
+ auto match = rx.match(data);
+ if (match.hasMatch()) {
+ QString errors = match.captured(1);
+ static const QRegularExpression rx2("
(.*?)");
+ static const QRegularExpression rxremove("<[^>]*>");
+ auto matchIterator = rx2.globalMatch(errors);
+ while (matchIterator.hasNext()) {
+ auto match2 = matchIterator.next();
+ errorMessageList.append(match2.captured(1).remove(rxremove).simplified());
}
}
+
+ QString errorMessage = errorMessageList.join("\n");
+ qCWarning(TappedOutInterfaceLog) << "Tappedout: bad reply, http status" << httpStatus << "size" << data.size()
+ << "message" << errorMessage;
+
+ QMessageBox::critical(nullptr, tr("Error"), errorMessage);
}
- qCWarning(TappedOutInterfaceLog) << "TappedOut analysis failed, HTTP status" << httpStatus;
- showError(errorMessageList.join('\n'));
+
+ reply->deleteLater();
+ deleteLater();
}
void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data)
@@ -106,9 +90,13 @@ void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray
DeckList mainboard, sideboard;
copyDeckSplitMainAndSide(deck, mainboard, sideboard);
- data.append("name=" + QUrl::toPercentEncoding(deck.getName()));
- data.append("&mainboard=" + QUrl::toPercentEncoding(mainboard.writeToString_Plain(false, true)));
- data.append("&sideboard=" + QUrl::toPercentEncoding(sideboard.writeToString_Plain(false, true)));
+ QUrl params;
+ QUrlQuery urlQuery;
+ urlQuery.addQueryItem("name", deck.getName());
+ urlQuery.addQueryItem("mainboard", mainboard.writeToString_Plain(false, true));
+ urlQuery.addQueryItem("sideboard", sideboard.writeToString_Plain(false, true));
+ params.setQuery(urlQuery);
+ data.append(params.query(QUrl::EncodeReserved).toUtf8());
}
void TappedOutInterface::analyzeDeck(const DeckList &deck)
@@ -129,9 +117,7 @@ void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckLi
{
auto copyMainOrSide = [&mainboard, &sideboard](const auto node, const auto card) {
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
- // Exclude confirmed tokens, but preserve cards missing from the local
- // database, including newly released cards in an imported deck.
- if ((dbCard && dbCard->getIsToken()) || node->getName() == DECK_ZONE_MAYBEBOARD) {
+ if (!dbCard || dbCard->getIsToken() || node->getName() == DECK_ZONE_MAYBEBOARD) {
return;
}
diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.h b/cockatrice/src/client/network/interfaces/tapped_out_interface.h
index 27e8c2273..615991b07 100644
--- a/cockatrice/src/client/network/interfaces/tapped_out_interface.h
+++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.h
@@ -14,7 +14,6 @@
inline Q_LOGGING_CATEGORY(TappedOutInterfaceLog, "tapped_out_interface");
class QByteArray;
-class QString;
class QNetworkAccessManager;
class QNetworkReply;
class DeckList;
@@ -30,8 +29,6 @@ class TappedOutInterface : public QObject
Q_OBJECT
private:
QNetworkAccessManager *manager;
- bool replyHandled = false;
- void showError(const QString &message);
void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard);
private slots:
diff --git a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp
index 2b307735d..7127b0a29 100644
--- a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp
+++ b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp
@@ -15,18 +15,14 @@ static const QString ARCHIDEKT_SUFFIX = "/?format=json";
static const QString MOXFIELD_BASE = "https://api.moxfield.com/v2/decks/all/";
static const QString MOXFIELD_SUFFIX = "/";
-static const QString DECKSTATS_BASE = "https://deckstats.net/decks/";
static const QString DECKSTATS_SUFFIX = "?include_comments=1&export_mtgarena=1";
-static const QString MANABOX_BASE = "https://cloud.manabox.app/decks/";
-
bool parseDeckUrl(const QString &url, ParsedDeckInfo &outInfo)
{
static QRegularExpression rxTappedOut("tappedout\\.net/(?:mtg-decks/)?([^/?#]+)");
static QRegularExpression rxArchidekt("archidekt\\.com/decks/(\\d+)");
static QRegularExpression rxMoxfield("moxfield\\.com/decks/([a-zA-Z0-9_-]+)");
static QRegularExpression rxDeckstats("deckstats\\.net/decks/(\\d+/[a-zA-Z0-9_-]+)");
- static QRegularExpression rxManabox("manabox\\.app/decks/([a-zA-Z0-9_-]+)");
QRegularExpressionMatch match;
@@ -53,18 +49,11 @@ bool parseDeckUrl(const QString &url, ParsedDeckInfo &outInfo)
return true;
} else if ((match = rxDeckstats.match(url)).hasMatch()) {
QString deckPath = match.captured(1);
- outInfo = ParsedDeckInfo{.baseUrl = DECKSTATS_BASE,
+ outInfo = ParsedDeckInfo{.baseUrl = "https://deckstats.net/decks/",
.deckID = deckPath,
- .fullUrl = DECKSTATS_BASE + deckPath + DECKSTATS_SUFFIX,
+ .fullUrl = "https://deckstats.net/decks/" + deckPath + DECKSTATS_SUFFIX,
.provider = DeckProvider::Deckstats};
return true;
- } else if ((match = rxManabox.match(url)).hasMatch()) {
- QString deckPath = match.captured(1);
- outInfo = ParsedDeckInfo{.baseUrl = MANABOX_BASE,
- .deckID = deckPath,
- .fullUrl = MANABOX_BASE + deckPath,
- .provider = DeckProvider::Manabox};
- return true;
}
return false;
diff --git a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h
index d3bff976f..bbdd70c97 100644
--- a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h
+++ b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h
@@ -15,7 +15,6 @@ enum class DeckProvider
Archidekt,
Moxfield,
Deckstats,
- Manabox,
Unknown
};
diff --git a/cockatrice/src/client/network/parsers/interface_json_deck_parser.h b/cockatrice/src/client/network/parsers/interface_json_deck_parser.h
index 7e15d472f..87fde4d54 100644
--- a/cockatrice/src/client/network/parsers/interface_json_deck_parser.h
+++ b/cockatrice/src/client/network/parsers/interface_json_deck_parser.h
@@ -8,19 +8,12 @@
#define INTERFACE_JSON_DECK_PARSER_H
#include "../../../interface/deck_loader/card_node_function.h"
-#include "libcockatrice/deck_list/tree/inner_deck_list_node.h"
#include
#include
-#include
-#include
-#include
-#include
#include
#include
-inline Q_LOGGING_CATEGORY(InterfaceJsonDeckParserLog, "interface_json_deck_parser");
-
class IJsonDeckParser
{
public:
@@ -39,6 +32,9 @@ public:
QString deckName = obj.value("name").toString();
QString deckDescription = obj.value("description").toString();
+ deckList.setName(deckName);
+ deckList.setComments(deckDescription);
+
QString outputText;
QTextStream outStream(&outputText);
@@ -54,14 +50,8 @@ public:
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
- outStream.flush();
- outStream.seek(0);
deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer());
- deckList.setName(deckName);
- deckList.setComments(deckDescription);
deckList.forEachCard(CardNodeFunction::ResolveProviderId());
- deckList.setName(deckName);
- deckList.setComments(deckDescription);
return deckList;
}
@@ -77,6 +67,9 @@ public:
QString deckName = obj.value("name").toString();
QString deckDescription = obj.value("description").toString();
+ deckList.setName(deckName);
+ deckList.setComments(deckDescription);
+
QString outputText;
QTextStream outStream(&outputText);
@@ -104,14 +97,8 @@ public:
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
- outStream.flush();
- outStream.seek(0);
deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer());
- deckList.setName(deckName);
- deckList.setComments(deckDescription);
deckList.forEachCard(CardNodeFunction::ResolveProviderId());
- deckList.setName(deckName);
- deckList.setComments(deckDescription);
QJsonObject commandersObj = obj.value("commanders").toObject();
if (!commandersObj.isEmpty()) {
@@ -127,113 +114,6 @@ public:
}
}
- // Moxfield stores Oathbreaker signature spells in their own board,
- // separate from both mainboard and commanders. They are playable deck
- // cards, but must not replace the Oathbreaker as the banner card.
- const QJsonObject signatureSpells = obj.value("signatureSpells").toObject();
- for (auto it = signatureSpells.begin(); it != signatureSpells.end(); ++it) {
- const QJsonObject entry = it.value().toObject();
- const QJsonObject card = entry.value("card").toObject();
- const QString name = card.value("name").toString();
- const int quantity = entry.value("quantity").toInt();
- if (name.isEmpty() || quantity <= 0) {
- continue;
- }
- const QString set = card.value("set").toString().toUpper();
- const QString collectorNumber = card.value("cn").toString();
- const QString providerId = card.value("scryfall_id").toString();
- deckList.addCards(name, quantity, DECK_ZONE_MAIN, -1, set, collectorNumber, providerId);
- }
-
- return deckList;
- }
-};
-
-class ManaboxJsonParser : public IJsonDeckParser
-{
- enum BoardType : quint32
- {
- MainBoard = 0,
- SideBoard = 1,
- Commander = 2,
- MaybeBoard = 3,
- };
-
-public:
- DeckList parse(const QJsonObject &obj) override
- {
- DeckList deckList;
-
- QString deckName = obj.value("name").toString();
-
- QString outputText;
- QString mainboardText;
- QString sideboardText;
- QTextStream mainboardStream(&mainboardText);
- QTextStream sideboardStream(&sideboardText);
- QTextStream outStream(&outputText);
-
- auto commanderList = QList();
- auto maybeboardList = QList();
- for (auto entry : obj.value("cards").toArray()) {
- auto card = entry.toObject();
-
- auto collectorNumber = card.value("collectorNumber").toString();
- auto cardName = card.value("name").toString();
- auto boardType = static_cast(card.value("boardCategory").toInt());
- auto setName = card.value("setId").toString();
- auto quantity = card.value("quantity").toInt();
- auto providerId = card.value("scryfallId").toString();
-
- switch (boardType) {
- case MainBoard:
- mainboardStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber
- << '\n';
- break;
- case SideBoard:
- sideboardStream << "SB: " << quantity << ' ' << cardName << " (" << setName << ") "
- << collectorNumber << '\n';
- break;
- case MaybeBoard:
- maybeboardList.append(card);
- break;
- case Commander:
- commanderList.append(card);
- break;
- default:
- qCWarning(InterfaceJsonDeckParserLog)
- << "Unknown ManaBox Zone" << card.value("boardCategory").toInt() << "for" << cardName;
- break;
- }
- }
-
- outStream << mainboardText << sideboardText;
-
- deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer());
- deckList.setName(deckName);
- deckList.forEachCard(CardNodeFunction::ResolveProviderId());
-
- for (const auto &maybeboardCard : maybeboardList) {
- QString cardName = maybeboardCard.value("name").toString();
- QString setName = maybeboardCard.value("setId").toString().toUpper();
- QString collectorNumber = maybeboardCard.value("collectorNumber").toString();
- QString providerId = maybeboardCard.value("scryfallId").toString();
-
- for (int x = 0; x < maybeboardCard.value("quantity").toInt(); x++) {
- deckList.addCard(cardName, DECK_ZONE_MAYBEBOARD, -1, setName, collectorNumber, providerId);
- }
- }
- for (const auto &commanderCard : commanderList) {
-
- QString commanderName = commanderCard.value("name").toString();
- QString setName = commanderCard.value("setId").toString().toUpper();
- QString collectorNumber = commanderCard.value("collectorNumber").toString();
- QString providerId = commanderCard.value("scryfallId").toString();
-
- deckList.setBannerCard({commanderName, providerId});
- deckList.addCard(commanderName, DECK_ZONE_MAIN, -1, setName, collectorNumber, providerId);
- }
-
return deckList;
}
};
diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp
index d74414d4a..62668c4d9 100644
--- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp
+++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp
@@ -2,305 +2,253 @@
#include "../../../../main.h"
#include "../../../settings/cache_settings.h"
-#include "spoiler_file.h"
-#include
+#include
+#include
+#include
#include
+#include
#include
#include
#include
+#include
+#include
+#include
+#include
#include
#include
#include
+#include
#include
-#include
+#include
#include
#include
-#include
+#include
+#include
#include
#include
#include
#include
#include
+#include
+#include
#include
-namespace
-{
-const QUrl SPOILERS_STATUS_URL(
- QStringLiteral("https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"));
-const QUrl SPOILERS_URL(QStringLiteral("https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"));
-// Request coordination stays on the GUI thread; reload workers never access it.
-QPointer currentUpdater;
-QPointer> activeReload;
-quint64 newestOperation = 0;
+#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
+#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"
-bool removeSpoilerFile(const QString &path)
+SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr)
{
- QFile file(path);
- if (!file.exists()) {
- return false;
- }
- if (!file.remove()) {
- qCWarning(SpoilerBackgroundUpdaterLog) << "Could not remove spoiler database" << path << file.errorString();
- return false;
- }
- return true;
-}
-} // namespace
-
-SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *parent, bool forceReload)
- : QObject(parent), enabledAtStart(SettingsCache::instance().downloads().getDownloadSpoilersStatus()),
- forceReload(forceReload), targetPath(SettingsCache::instance().paths().getSpoilerCardDatabasePath()),
- operationId(++newestOperation), manager(NetworkManagerFactory::create(this)), notificationIcon(trayIcon)
-{
- connect(&SettingsCache::instance().downloads(), &DownloadSettings::downloadSpoilerStatusChanged, this,
- &SpoilerBackgroundUpdater::cancelIfSettingsChanged);
- connect(&SettingsCache::instance().paths(), &PathsSettings::cardDatabasePathChanged, this,
- &SpoilerBackgroundUpdater::cancelIfSettingsChanged);
- // Even a disabled/no-op operation completes after its caller can connect signals.
- QTimer::singleShot(0, this, &SpoilerBackgroundUpdater::begin);
-}
-
-void SpoilerBackgroundUpdater::begin()
-{
- if (completed) {
- return;
- }
- if (cancelled || operationId != newestOperation ||
- (reloadOnly ? SettingsCache::instance().downloads().getDownloadSpoilersStatus()
- : (!enabledAtStart || !SettingsCache::instance().downloads().getDownloadSpoilersStatus())) ||
- targetPath != SettingsCache::instance().paths().getSpoilerCardDatabasePath()) {
- finish();
- return;
- }
- const auto previous = currentUpdater;
- if (activeReload) {
- // The existing database API stores its final load status after its parser
- // mutex is released. Do not overlap updater-owned calls to that API.
- connect(activeReload, &QFutureWatcher::finished, this, &SpoilerBackgroundUpdater::begin,
- static_cast(Qt::QueuedConnection | Qt::SingleShotConnection));
- if (previous && previous != this) {
- previous->cancel();
- }
- return;
- }
- currentUpdater = this;
- QPointer guard(this);
- if (previous && previous != this) {
- previous->cancel();
- }
- if (!guard) {
- return;
- }
- if (!isCurrent()) {
- finish();
- } else if (reloadOnly) {
- if (QFileInfo::exists(targetPath) && !removeSpoilerFile(targetPath)) {
- finish(tr("The spoiler database could not be removed. Check its file permissions and use Reload Card "
- "Database to retry."));
- return;
- }
- reloadDatabase(false);
+ isSpoilerDownloadEnabled = SettingsCache::instance().downloads().getDownloadSpoilersStatus();
+ if (isSpoilerDownloadEnabled) {
+ // Start the process of checking if we're in spoiler season
+ // File exists means we're in spoiler season
+ startSpoilerDownloadProcess(SPOILERS_STATUS_URL, false);
} else {
- downloadFromURL(SPOILERS_STATUS_URL, false);
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Disabled";
}
}
-bool SpoilerBackgroundUpdater::isCurrent() const
+void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool saveResults)
{
- return !completed && !cancelled && currentUpdater == this && operationId == newestOperation &&
- (reloadOnly != SettingsCache::instance().downloads().getDownloadSpoilersStatus()) &&
- targetPath == SettingsCache::instance().paths().getSpoilerCardDatabasePath();
+ auto spoilerURL = QUrl(url);
+ downloadFromURL(spoilerURL, saveResults);
}
-void SpoilerBackgroundUpdater::cancelIfSettingsChanged()
+void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
{
- if (reloadOnly == SettingsCache::instance().downloads().getDownloadSpoilersStatus() ||
- targetPath != SettingsCache::instance().paths().getSpoilerCardDatabasePath()) {
- cancel();
- }
-}
-
-void SpoilerBackgroundUpdater::cancel()
-{
- if (completed) {
- return;
- }
- cancelled = true;
- const auto reply = activeReply;
- activeReply.clear();
- QPointer guard(this);
- if (reply) {
- reply->abort();
- if (reply) {
- reply->deleteLater();
- }
- }
- // A running reload owns no updater state. Allow its completion to retire its
- // watcher before a heap-owned updater is released by the caller.
- if (guard && !reloading) {
- finish();
- }
-}
-
-void SpoilerBackgroundUpdater::downloadFromURL(const QUrl &url, bool saveResults)
-{
- QNetworkRequest request(url);
+ auto *nam = NetworkManagerFactory::create(this);
+ auto request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
- request.setTransferTimeout(60000);
- activeReply = manager->get(request);
- if (saveResults) {
- connect(activeReply, &QNetworkReply::finished, this,
- &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile);
- } else {
- connect(activeReply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled);
- }
-}
+ QNetworkReply *reply = nam->get(request);
-void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
-{
- auto *reply = qobject_cast(sender());
- if (!reply || reply != activeReply) {
- return;
- }
- activeReply.clear();
- reply->deleteLater();
- if (!isCurrent()) {
- finish();
- return;
- }
- if (reply->error() == QNetworkReply::NoError) {
- downloadFromURL(SPOILERS_URL, true);
- } else if (reply->error() == QNetworkReply::ContentNotFoundError) {
- const bool existed = QFileInfo::exists(targetPath);
- seasonEnded = existed;
- if (existed && !removeSpoilerFile(targetPath)) {
- finish(tr("The spoiler season has ended, but the old spoiler database could not be removed."));
- return;
- }
- if (existed || forceReload) {
- reloadDatabase(false);
- } else {
- finish();
- }
+ if (saveResults) {
+ // This will write out to the file (used for spoiler.xml)
+ connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile);
} else {
- finish(tr("Could not check spoiler availability: %1").arg(reply->errorString()));
+ // This will check the status (used to see if we're in spoiler season or not)
+ connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled);
}
}
void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile()
{
- auto *reply = qobject_cast(sender());
- if (!reply || reply != activeReply) {
- return;
- }
- activeReply.clear();
- reply->deleteLater();
- if (!isCurrent()) {
- finish();
- return;
- }
- if (reply->error() != QNetworkReply::NoError) {
- finish(tr("Could not download spoilers: %1").arg(reply->errorString()));
- return;
- }
- spoilerData = reply->readAll();
- QString error;
- const auto result = SpoilerFile::save(targetPath, spoilerData, [this] { return isCurrent(); }, error);
- if (result == SpoilerFile::SaveResult::Failed) {
- finish(isCurrent() ? error : QString());
- } else if (result == SpoilerFile::SaveResult::Written || forceReload) {
- reloadDatabase(result == SpoilerFile::SaveResult::Written);
+ // Check for server reply
+ auto *reply = dynamic_cast(sender());
+ QNetworkReply::NetworkError errorCode = reply->error();
+
+ if (errorCode == QNetworkReply::NoError) {
+ spoilerData = reply->readAll();
+
+ // Save the spoiler.xml file to the disk
+ saveDownloadedFile(spoilerData);
+
+ reply->deleteLater();
+ emit spoilerCheckerDone();
} else {
- finish();
- }
-}
-
-void SpoilerBackgroundUpdater::reloadDatabase(bool updated)
-{
- reloading = true;
- auto *database = CardDatabaseManager::getInstance();
- // The job outlives an individual request, so destroying that request cannot
- // let a newer updater overlap the worker that is still completing.
- auto *watcher = new QFutureWatcher(QCoreApplication::instance());
- activeReload = watcher;
- connect(watcher, &QFutureWatcher::finished, watcher, [watcher] {
- if (activeReload == watcher) {
- activeReload.clear();
- }
- watcher->deleteLater();
- });
- connect(watcher, &QFutureWatcher::finished, this, [this, watcher, database, updated] {
- bool loaded = false;
- try {
- loaded = watcher->result() && database->getLoadStatus() == Ok;
- } catch (const std::exception &error) {
- qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler database reload failed:" << error.what();
- }
- reloading = false;
- if (!isCurrent()) {
- finish();
- } else if (!loaded) {
- finish(reloadOnly
- ? tr("Could not reload card data after disabling spoilers. Use Reload Card Database to retry.")
- : tr("Could not reload card data after updating spoilers. Use Update Spoilers to retry the "
- "reload."));
- } else {
- finish({}, updated);
- }
- });
- watcher->setFuture(QtConcurrent::run([database] {
- database->reloadCardDatabasesAndNotify();
- return true;
- }));
-}
-
-void SpoilerBackgroundUpdater::finish(const QString &error, bool updated)
-{
- if (completed) {
- return;
- }
- completed = true;
- spoilerData = QByteArray();
- if (currentUpdater == this) {
- currentUpdater.clear();
- }
- QPointer guard(this);
- if (!error.isEmpty() && !cancelled) {
- qCWarning(SpoilerBackgroundUpdaterLog) << error;
- if (notificationIcon) {
- notificationIcon->showMessage(tr("Spoiler update failed"), error);
- }
- if (!guard) {
- return;
- }
- emit spoilerUpdateFailed(error);
- } else if (updated && !cancelled) {
- if (notificationIcon) {
- notificationIcon->showMessage(tr("Spoilers have been updated!"),
- tr("The card database now includes the downloaded spoilers."));
- }
- } else if (seasonEnded && !cancelled && notificationIcon) {
- notificationIcon->showMessage(tr("Spoiler season has ended"),
- tr("The spoiler database was removed. Please run Oracle to get released cards."));
- }
- if (guard) {
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Error downloading spoilers file" << errorCode;
emit spoilerCheckerDone();
}
}
-SpoilerBackgroundUpdater *SpoilerBackgroundUpdater::current()
+bool SpoilerBackgroundUpdater::deleteSpoilerFile()
{
- return currentUpdater;
+ QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
+ QFileInfo fi(fileName);
+ QDir fileDir(fi.path());
+ QFile file(fileName);
+
+ // Delete the spoiler.xml file
+ if (file.exists() && file.remove()) {
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Deleting spoiler.xml";
+ return true;
+ }
+
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Error: Spoiler.xml not found or not deleted";
+ return false;
}
-SpoilerBackgroundUpdater *SpoilerBackgroundUpdater::deleteSpoilerFile()
+void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
{
- // Deletion and the following reload share the operation lifetime. Callers can
- // observe either failure before the queued operation starts, and an explicit
- // retry still reloads when the file has already been removed.
- auto *updater = new SpoilerBackgroundUpdater(QCoreApplication::instance(), true);
- updater->reloadOnly = true;
- connect(updater, &SpoilerBackgroundUpdater::spoilerCheckerDone, updater, &QObject::deleteLater);
- return updater;
+ auto *response = dynamic_cast(sender());
+ QNetworkReply::NetworkError errorCode = response->error();
+
+ if (errorCode == QNetworkReply::ContentNotFoundError) {
+ // Spoiler season is offline at this point, so the spoiler.xml file can be safely deleted
+ // The user should run Oracle to get the latest card information
+ if (deleteSpoilerFile() && trayIcon) {
+ trayIcon->showMessage(tr("Spoilers season has ended"), tr("Deleting spoiler.xml. Please run Oracle"));
+ }
+
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Season Offline";
+ emit spoilerCheckerDone();
+ } else if (errorCode == QNetworkReply::NoError) {
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Online";
+ startSpoilerDownloadProcess(SPOILERS_URL, true);
+ } else if (errorCode == QNetworkReply::HostNotFoundError) {
+ if (trayIcon) {
+ trayIcon->showMessage(tr("Spoilers download failed"), tr("No internet connection"));
+ }
+
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed due to no internet connection";
+ emit spoilerCheckerDone();
+ } else {
+ if (trayIcon) {
+ trayIcon->showMessage(tr("Spoilers download failed"), tr("Error") + " " + (short)errorCode);
+ }
+
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed with reason" << errorCode;
+ emit spoilerCheckerDone();
+ }
+}
+
+bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
+{
+ QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
+ QFileInfo fi(fileName);
+ QDir fileDir(fi.path());
+
+ if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) {
+ return false;
+ }
+
+ // Check if the data matches. If it does, then spoilers are up to date.
+ if (getHash(fileName) == getHash(data)) {
+ if (trayIcon) {
+ trayIcon->showMessage(tr("Spoilers already up to date"), tr("No new spoilers added"));
+ }
+
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Up to Date";
+ return false;
+ }
+
+ QFile file(fileName);
+ if (!file.open(QIODevice::WriteOnly)) {
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File open (w) failed for" << fileName;
+ file.close();
+ return false;
+ }
+
+ if (file.write(data) == -1) {
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File write (w) failed for" << fileName;
+ file.close();
+ return false;
+ }
+
+ file.close();
+
+ // Data written, so reload the card database
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written";
+ // Spoilers can introduce sets the config has never seen; the loader never
+ // runs checkUnknownSets() itself, so run it once the finished snapshot has
+ // been swapped in.
+ auto *watcher = new QFutureWatcher(this);
+ connect(watcher, &QFutureWatcher::finished, this, [watcher] {
+ watcher->deleteLater();
+ CardDatabaseManager::getInstance()->checkUnknownSets();
+ });
+ watcher->setFuture(QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); }));
+
+ // If the user has notifications enabled, let them know
+ // when the database was last updated
+ if (trayIcon) {
+ QList lines = data.split('\n');
+
+ for (const QByteArray &line : lines) {
+ if (line.contains("Created At:")) {
+ QString timeStamp = QString(line).replace("Created At:", "").trimmed();
+ timeStamp.chop(6); // Remove " (UTC)"
+
+ auto utcTime = QLocale().toDateTime(timeStamp, "ddd, MMM dd yyyy, hh:mm:ss");
+#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
+ utcTime.setTimeZone(QTimeZone::UTC);
+#else
+ utcTime.setTimeSpec(Qt::UTC);
+#endif
+
+ QString localTime = utcTime.toLocalTime().toString("MMM d, hh:mm");
+
+ trayIcon->showMessage(tr("Spoilers have been updated!"), tr("Last change:") + " " + localTime);
+ emit spoilersUpdatedSuccessfully();
+ return true;
+ }
+ }
+ }
+
+ return true;
+}
+
+QByteArray SpoilerBackgroundUpdater::getHash(const QString fileName)
+{
+ QFile file(fileName);
+
+ if (file.open(QFile::ReadOnly)) {
+ // Only read the first 512 bytes (enough to get the "created" tag)
+ const QByteArray bytes = file.read(512);
+
+ QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
+ hash.addData(bytes);
+
+ qCInfo(SpoilerBackgroundUpdaterLog) << "File Hash =" << hash.result();
+
+ file.close();
+ return hash.result();
+ } else {
+ qCWarning(SpoilerBackgroundUpdaterLog) << "getHash ReadOnly failed!";
+ file.close();
+ return QByteArray();
+ }
+}
+
+QByteArray SpoilerBackgroundUpdater::getHash(QByteArray data)
+{
+ // Only read the first 512 bytes (enough to get the "created" tag)
+ const QByteArray bytes = data.left(512);
+
+ QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
+ hash.addData(bytes);
+
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Data Hash =" << hash.result();
+
+ return hash.result();
}
diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h
index fd90eb688..a6c4ed00f 100644
--- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h
+++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h
@@ -2,6 +2,7 @@
* @file spoiler_background_updater.h
* @ingroup Client
*/
+//! \todo Document this file.
#ifndef COCKATRICE_SPOILER_DOWNLOADER_H
#define COCKATRICE_SPOILER_DOWNLOADER_H
@@ -9,13 +10,10 @@
#include
#include
#include
-#include
#include
#include
-class QNetworkAccessManager;
-class QNetworkReply;
-class QSystemTrayIcon;
+class QProcess;
class QUrl;
inline Q_LOGGING_CATEGORY(SpoilerBackgroundUpdaterLog, "spoiler_background_updater");
@@ -24,48 +22,30 @@ class SpoilerBackgroundUpdater : public QObject
{
Q_OBJECT
public:
- explicit SpoilerBackgroundUpdater(QObject *parent = nullptr, bool forceReload = false);
+ explicit SpoilerBackgroundUpdater(QObject *apParent = nullptr);
inline QString getCardUpdaterBinaryName()
{
return "oracle";
}
- // Queues removal and reload; the application owns the returned updater and
- // deletes it on completion. Callers can connect its failure/completion signals.
- static SpoilerBackgroundUpdater *deleteSpoilerFile();
- // The operation currently running, if any. The updater outlives the page that
- // started it, so a reopened page uses this to observe the in-flight update.
- static SpoilerBackgroundUpdater *current();
+ QByteArray getHash(const QString fileName);
+ QByteArray getHash(QByteArray data);
+ static bool deleteSpoilerFile();
private slots:
- void begin();
void actDownloadFinishedSpoilersFile();
void actCheckIfSpoilerSeasonEnabled();
- void cancelIfSettingsChanged();
private:
- const bool enabledAtStart;
- const bool forceReload;
- const QString targetPath;
- const quint64 operationId;
- QNetworkAccessManager *manager;
- QPointer activeReply;
- QPointer notificationIcon;
+ bool isSpoilerDownloadEnabled;
+ QProcess *cardUpdateProcess;
QByteArray spoilerData;
- bool completed = false;
- bool cancelled = false;
- bool reloading = false;
- bool seasonEnded = false;
- bool reloadOnly = false;
-
- bool isCurrent() const;
- void cancel();
- void finish(const QString &error = {}, bool updated = false);
- void downloadFromURL(const QUrl &url, bool saveResults);
- void reloadDatabase(bool updated);
+ void startSpoilerDownloadProcess(QString url, bool saveResults);
+ void downloadFromURL(QUrl url, bool saveResults);
+ bool saveDownloadedFile(QByteArray data);
signals:
+ void spoilersUpdatedSuccessfully();
void spoilerCheckerDone();
- void spoilerUpdateFailed(const QString &message);
};
-#endif // COCKATRICE_SPOILER_DOWNLOADER_H
+#endif // COCKATRICE_SPOILER_DOWNLOADER_H
\ No newline at end of file
diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp
deleted file mode 100644
index 554ea1108..000000000
--- a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp
+++ /dev/null
@@ -1,109 +0,0 @@
-#include "spoiler_file.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-namespace
-{
-QByteArray hashFile(const QString &path)
-{
- QFile file(path);
- if (!file.open(QIODevice::ReadOnly)) {
- return {};
- }
- QCryptographicHash hash(QCryptographicHash::Sha256);
- return hash.addData(&file) ? hash.result() : QByteArray();
-}
-} // namespace
-
-namespace SpoilerFile
-{
-QString validate(const QByteArray &data)
-{
- QXmlStreamReader xml(data);
- if (!xml.readNextStartElement() || xml.name() != QStringLiteral("cockatrice_carddatabase")) {
- return QCoreApplication::translate(
- "SpoilerBackgroundUpdater",
- "The download is not a Cockatrice card database. The previous spoiler file was preserved.");
- }
- bool versionOk = false;
- const int version = xml.attributes().value(QStringLiteral("version")).toInt(&versionOk);
- if (!versionOk || (version != 3 && version != 4)) {
- return QCoreApplication::translate(
- "SpoilerBackgroundUpdater",
- "The spoiler database uses an unsupported format. The previous spoiler file was preserved.");
- }
- bool hasCards = false;
- int depth = 1;
- while (!xml.atEnd()) {
- const auto token = xml.readNext();
- if (token == QXmlStreamReader::StartElement) {
- ++depth;
- if (depth == 2 && xml.name() == QStringLiteral("cards")) {
- hasCards = true;
- }
- } else if (token == QXmlStreamReader::EndElement) {
- --depth;
- }
- }
- if (xml.hasError() || !hasCards) {
- return QCoreApplication::translate(
- "SpoilerBackgroundUpdater",
- "The spoiler database is incomplete or invalid. The previous spoiler file was preserved.");
- }
- return {};
-}
-
-SaveResult
-save(const QString &targetPath, const QByteArray &data, const std::function &isCurrent, QString &error)
-{
- error = validate(data);
- if (!error.isEmpty()) {
- return SaveResult::Failed;
- }
- if (!isCurrent()) {
- error = QCoreApplication::translate("SpoilerBackgroundUpdater",
- "The spoiler update was cancelled because its settings changed.");
- return SaveResult::Failed;
- }
- if (hashFile(targetPath) == QCryptographicHash::hash(data, QCryptographicHash::Sha256)) {
- return SaveResult::Unchanged;
- }
- const QDir directory = QFileInfo(targetPath).dir();
- if (!directory.exists() && !directory.mkpath(QStringLiteral("."))) {
- error = QCoreApplication::translate("SpoilerBackgroundUpdater",
- "Could not create the spoiler database directory: %1")
- .arg(directory.absolutePath());
- return SaveResult::Failed;
- }
- QSaveFile file(targetPath);
- file.setDirectWriteFallback(false);
- if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.flush()) {
- error = QCoreApplication::translate(
- "SpoilerBackgroundUpdater",
- "Could not save the spoiler database to %1: %2. The previous file was preserved.")
- .arg(targetPath, file.errorString());
- return SaveResult::Failed;
- }
- if (!isCurrent()) {
- file.cancelWriting();
- error = QCoreApplication::translate("SpoilerBackgroundUpdater",
- "The spoiler update was cancelled because its settings changed.");
- return SaveResult::Failed;
- }
- if (!file.commit()) {
- error = QCoreApplication::translate(
- "SpoilerBackgroundUpdater",
- "Could not replace the spoiler database at %1: %2. The previous file was preserved.")
- .arg(targetPath, file.errorString());
- return SaveResult::Failed;
- }
- return SaveResult::Written;
-}
-
-} // namespace SpoilerFile
diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h b/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h
deleted file mode 100644
index 3a138cdea..000000000
--- a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h
+++ /dev/null
@@ -1,24 +0,0 @@
-#ifndef COCKATRICE_SPOILER_FILE_H
-#define COCKATRICE_SPOILER_FILE_H
-
-#include
-#include
-#include
-
-namespace SpoilerFile
-{
-enum class SaveResult
-{
- Failed,
- Unchanged,
- Written
-};
-
-QString validate(const QByteArray &data);
-// Validate before opening the destination, compare complete files, and replace
-// atomically. Check operation ownership both before writing and before committing.
-SaveResult
-save(const QString &targetPath, const QByteArray &data, const std::function &isCurrent, QString &error);
-} // namespace SpoilerFile
-
-#endif // COCKATRICE_SPOILER_FILE_H
diff --git a/cockatrice/src/client/network/update/client/update_downloader.cpp b/cockatrice/src/client/network/update/client/update_downloader.cpp
index 8b5ef3d6c..8868876fa 100644
--- a/cockatrice/src/client/network/update/client/update_downloader.cpp
+++ b/cockatrice/src/client/network/update/client/update_downloader.cpp
@@ -1,16 +1,15 @@
#include "update_downloader.h"
+#include
+#include
#include
-#include
+#include
#include
#include
#include
-#include
-#include
#include
#include
#include
-#include
UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent), response(nullptr)
{
@@ -24,116 +23,48 @@ void UpdateDownloader::beginDownload(QUrl downloadUrl)
originalUrl = downloadUrl;
}
- if (auto *old = std::exchange(response, nullptr)) {
- old->abort();
- old->deleteLater();
- }
-
- errorReported = false;
- cancelRequested = false;
-
- // The transfer timeout comes from the manager (NetworkManagerFactory).
response = netMan->get(QNetworkRequest(downloadUrl));
connect(response, &QNetworkReply::finished, this, &UpdateDownloader::fileFinished);
- connect(response, &QNetworkReply::errorOccurred, this, &UpdateDownloader::downloadError);
connect(response, &QNetworkReply::downloadProgress, this, &UpdateDownloader::downloadProgress);
- connect(this, &UpdateDownloader::stopDownload, this, &UpdateDownloader::cancelDownload);
+ connect(this, &UpdateDownloader::stopDownload, response, &QNetworkReply::abort);
}
-void UpdateDownloader::downloadError(QNetworkReply::NetworkError networkError)
+void UpdateDownloader::downloadError(QNetworkReply::NetworkError)
{
- // A reply that was superseded by beginDownload() can still deliver a
- // queued error; it must not be reported against the current reply.
- if (response == nullptr || sender() != response || errorReported) {
+ if (response == nullptr) {
return;
}
- // A transfer timeout aborts the reply, so it arrives as OperationCanceledError.
- // Only an abort the user asked for is suppressed; every other cancel is a
- // stalled transfer and has to be reported.
- if (networkError == QNetworkReply::OperationCanceledError) {
- if (cancelRequested) {
- return;
- }
-
- errorReported = true;
- emit error(tr("The download timed out. Check your connection and try again."));
- return;
- }
-
- errorReported = true;
- emit error(response->errorString());
-}
-
-void UpdateDownloader::cancelDownload()
-{
- cancelRequested = true;
- if (response != nullptr) {
- response->abort();
- }
+ emit error(response->errorString().toUtf8());
}
void UpdateDownloader::fileFinished()
{
- // An aborted, superseded reply also emits finished(). Only the reply
- // currently stored in response may be consumed.
- if (response == nullptr || sender() != response) {
+ // If we finished but there's a redirect, follow it
+ QVariant redirect = response->attribute(QNetworkRequest::RedirectionTargetAttribute);
+ if (!redirect.isNull()) {
+ beginDownload(redirect.toUrl());
return;
}
- // errorOccurred normally reports this first; keep the check for errors that
- // only surface when the reply finishes.
- if (response->error() != QNetworkReply::NoError) {
- downloadError(response->error());
- response->deleteLater();
- response = nullptr;
+ // Handle any errors we had
+ if (response->error()) {
+ emit error(response->errorString());
return;
}
- // Give the update its own private directory. The name is unpredictable, so a
- // local user cannot pre-create or symlink the path, and the original file
- // name (including the installer suffix) is preserved inside it.
- QTemporaryDir downloadDir(QDir::temp().filePath(QStringLiteral("cockatrice-update-XXXXXX")));
- if (!downloadDir.isValid()) {
- emit error(tr("Could not create a temporary directory for the download."));
- response->deleteLater();
- response = nullptr;
- return;
- }
- downloadDir.setAutoRemove(false);
+ // Work out the file name of the download
+ QString fileName = QDir::temp().path() + QDir::separator() + originalUrl.toString().section('/', -1);
- const QString remoteName = QFileInfo(originalUrl.path()).fileName();
- const QString fileName = downloadDir.filePath(remoteName.isEmpty() ? QStringLiteral("update") : remoteName);
-
- QSaveFile file(fileName);
+ // Save the build in a temporary directory
+ QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
- downloadDir.remove();
- emit error(tr("Could not open the file for writing: %1").arg(file.errorString()));
- response->deleteLater();
- response = nullptr;
+ emit error(tr("Could not open the file for reading."));
return;
}
- const QByteArray data = response->readAll();
- if (file.write(data) != data.size()) {
- file.cancelWriting();
- downloadDir.remove();
- emit error(tr("Could not write the downloaded file to disk."));
- response->deleteLater();
- response = nullptr;
- return;
- }
-
- if (!file.commit()) {
- downloadDir.remove();
- emit error(tr("Could not save the downloaded file: %1").arg(file.errorString()));
- response->deleteLater();
- response = nullptr;
- return;
- }
-
- response->deleteLater();
- response = nullptr;
+ file.write(response->readAll());
+ file.close();
// Emit the success signal with a URL to the download file
emit downloadSuccessful(QUrl::fromLocalFile(fileName));
diff --git a/cockatrice/src/client/network/update/client/update_downloader.h b/cockatrice/src/client/network/update/client/update_downloader.h
index 7704eca38..8aeefb22a 100644
--- a/cockatrice/src/client/network/update/client/update_downloader.h
+++ b/cockatrice/src/client/network/update/client/update_downloader.h
@@ -32,13 +32,10 @@ private:
QUrl originalUrl;
QNetworkAccessManager *netMan;
QNetworkReply *response;
- bool errorReported = false;
- bool cancelRequested = false;
private slots:
void fileFinished();
void downloadProgress(qint64 bytesRead, qint64 totalBytes);
void downloadError(QNetworkReply::NetworkError);
- void cancelDownload();
};
#endif // COCKATRICE_UPDATEDOWNLOADER_H
diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp
index b40e26377..08eccf1e4 100644
--- a/cockatrice/src/client/settings/cache_settings.cpp
+++ b/cockatrice/src/client/settings/cache_settings.cpp
@@ -6,7 +6,6 @@
#include "shortcuts_settings.h"
#include
-#include
#include
#include
#include
@@ -48,32 +47,7 @@ Q_GLOBAL_STATIC(SettingsCache, settingsCache)
SettingsCache &SettingsCache::instance()
{
- SettingsCache &cache = *settingsCache; // returns a QT managed singleton reference
- // The release channels own QNetworkAccessManagers, which must be destroyed
- // while the application object is alive. ~SettingsCache() runs during
- // static teardown, after QApplication is gone, so hook the application's
- // shutdown instead. Retried here because the first access may predate the
- // application object.
- cache.installShutdownHook();
- return cache;
-}
-
-void SettingsCache::installShutdownHook()
-{
- if (shutdownHookInstalled) {
- return;
- }
- QCoreApplication *application = QCoreApplication::instance();
- if (application == nullptr) {
- return;
- }
- connect(application, &QCoreApplication::aboutToQuit, this, [this] {
- for (ReleaseChannel *releaseChannel : std::as_const(releaseChannels)) {
- delete releaseChannel;
- }
- releaseChannels.clear();
- });
- shutdownHookInstalled = true;
+ return *settingsCache; // returns a QT managed singleton reference
}
QString SettingsCache::getDataPath()
diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h
index 8ccf6f76b..9aeb88a18 100644
--- a/cockatrice/src/client/settings/cache_settings.h
+++ b/cockatrice/src/client/settings/cache_settings.h
@@ -90,13 +90,8 @@ private:
QString themeName;
QList releaseChannels;
- bool shutdownHookInstalled = false;
bool isPortableBuild;
- // Connects the aboutToQuit handler that deletes the release channels while
- // the application object is still alive; a no-op before it exists.
- void installShutdownHook();
-
[[nodiscard]] QString getSafeConfigPath(QString configEntry, QString defaultPath) const;
[[nodiscard]] QString getSafeConfigFilePath(QString configEntry, QString defaultPath) const;
void loadPaths();
diff --git a/cockatrice/src/client/settings/shortcut_treeview.cpp b/cockatrice/src/client/settings/shortcut_treeview.cpp
index 2501798cb..4a5bafb01 100644
--- a/cockatrice/src/client/settings/shortcut_treeview.cpp
+++ b/cockatrice/src/client/settings/shortcut_treeview.cpp
@@ -1,6 +1,5 @@
#include "shortcut_treeview.h"
-#include "../../interface/theme_manager.h"
#include "cache_settings.h"
#include "shortcuts_settings.h"
@@ -79,18 +78,6 @@ ShortcutTreeView::ShortcutTreeView(QWidget *parent) : QTreeView(parent)
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&ShortcutTreeView::refreshShortcuts);
-
- // Group headers store their font on the item; re-apply the role family on
- // font changes (setFont emits itemChanged, which repaints the rows).
- connect(themeManager, &ThemeManager::fontsChanged, this, [this] {
- QFont groupFont = themeManager->appFont(AppFont::InterfaceDefault);
- groupFont.setBold(true);
- for (int row = 0; row < shortcutsModel->rowCount(); ++row) {
- if (auto *groupItem = shortcutsModel->item(row)) {
- groupItem->setFont(groupFont);
- }
- }
- });
}
void ShortcutTreeView::populateShortcutsModel()
@@ -106,8 +93,7 @@ void ShortcutTreeView::populateShortcutsModel()
curParent = parentItems.value(group);
} else {
curParent = new QStandardItem(group);
- // Family comes from the InterfaceDefault theme role; bold stays caller-side.
- QFont font = themeManager->appFont(AppFont::InterfaceDefault);
+ static QFont font = curParent->font();
font.setBold(true);
curParent->setFont(font);
parentItems.insert(group, curParent);
diff --git a/libcockatrice_game/libcockatrice/game/translation.h b/cockatrice/src/client/translation.h
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/translation.h
rename to cockatrice/src/client/translation.h
diff --git a/cockatrice/src/database/interface/settings_card_preference_provider.h b/cockatrice/src/database/interface/settings_card_preference_provider.h
index f4bf23141..1feb2006b 100644
--- a/cockatrice/src/database/interface/settings_card_preference_provider.h
+++ b/cockatrice/src/database/interface/settings_card_preference_provider.h
@@ -2,18 +2,13 @@
#define COCKATRICE_SETTINGS_CARD_PREFERENCE_PROVIDER_H
#include "../../client/settings/cache_settings.h"
-#include
#include
#include
#include
-class SettingsCardPreferenceProvider : public QObject, public ICardPreferenceProvider
+class SettingsCardPreferenceProvider : public ICardPreferenceProvider
{
public:
- explicit SettingsCardPreferenceProvider(QObject *parent = nullptr) : QObject(parent)
- {
- }
-
[[nodiscard]] QString getCardPreferenceOverride(const QString &cardName) const override
{
return SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardName);
diff --git a/cockatrice/src/filters/deck_filter_string.cpp b/cockatrice/src/filters/deck_filter_string.cpp
index f0df897fc..616311750 100644
--- a/cockatrice/src/filters/deck_filter_string.cpp
+++ b/cockatrice/src/filters/deck_filter_string.cpp
@@ -1,5 +1,6 @@
#include "deck_filter_string.h"
+#include "../interface/deck_loader/loaded_deck.h"
#include "libcockatrice/card/card_localization.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/deck_list/deck_list.h"
@@ -14,7 +15,6 @@
#include
#include
#include
-#include
#include
#include
#include
diff --git a/cockatrice/src/filters/deck_filter_string.h b/cockatrice/src/filters/deck_filter_string.h
index b27cf87ca..76b891b59 100644
--- a/cockatrice/src/filters/deck_filter_string.h
+++ b/cockatrice/src/filters/deck_filter_string.h
@@ -11,7 +11,6 @@
#include
#include
#include
-#include
struct CardSearchLanguage;
struct LoadedDeck;
diff --git a/cockatrice/src/filters/filter_builder.cpp b/cockatrice/src/filters/filter_builder.cpp
index 9c2a91a5b..22679abb1 100644
--- a/cockatrice/src/filters/filter_builder.cpp
+++ b/cockatrice/src/filters/filter_builder.cpp
@@ -1,6 +1,6 @@
#include "filter_builder.h"
-#include "../interface/theme_icon.h"
+#include "../interface/pixel_map_generator.h"
#include "../interface/widgets/utility/custom_line_edit.h"
#include
@@ -30,7 +30,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
typeCombo->addItem(CardFilter::typeName(static_cast(i)), QVariant(i));
}
- QPushButton *ok = new QPushButton(loadThemedIcon(QStringLiteral("icons/increment")), QString());
+ QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString());
ok->setObjectName("ok");
ok->setMaximumSize(20, 20);
diff --git a/cockatrice/src/filters/filter_tree_model.cpp b/cockatrice/src/filters/filter_tree_model.cpp
index 66fe39821..b3bf307f1 100644
--- a/cockatrice/src/filters/filter_tree_model.cpp
+++ b/cockatrice/src/filters/filter_tree_model.cpp
@@ -1,7 +1,5 @@
#include "filter_tree_model.h"
-#include "../interface/theme_manager.h"
-
#include
#include
#include
@@ -13,27 +11,6 @@
class QObject;
-namespace
-{
-/**
- * @brief Visits every filter item below @p node, depth first.
- */
-template void forEachFilter(const FilterTreeNode *node, const Callback &callback)
-{
- if (node == nullptr) {
- return;
- }
-
- if (const auto *filterItem = dynamic_cast(node)) {
- callback(filterItem);
- }
-
- for (int i = 0; i < node->childCount(); ++i) {
- forEachFilter(node->nodeAt(i), callback);
- }
-}
-} // namespace
-
FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent)
{
fTree = new FilterTree;
@@ -41,12 +18,6 @@ FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent)
connect(fTree, &FilterTree::postInsertRow, this, &FilterTreeModel::proxyEndInsertRow);
connect(fTree, &FilterTree::preRemoveRow, this, &FilterTreeModel::proxyBeginRemoveRow);
connect(fTree, &FilterTree::postRemoveRow, this, &FilterTreeModel::proxyEndRemoveRow);
- // FontRole data reads the DeckList role live; rebroadcast so attached
- // views re-query instead of waiting for an incidental repaint.
- connect(themeManager, &ThemeManager::fontsChanged, this, [this] {
- emit layoutAboutToBeChanged();
- emit layoutChanged();
- });
}
FilterTreeModel::~FilterTreeModel()
@@ -139,31 +110,46 @@ void FilterTreeModel::clearFiltersOfType(CardFilter::Attr filterType)
QList FilterTreeModel::getFiltersOfType(CardFilter::Attr filterType) const
{
QList filters;
- forEachFilter(fTree, [&](const FilterItem *filterItem) {
- if (filterItem->attr() == filterType) {
- filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
- }
- });
- return filters;
-}
+ if (!fTree) {
+ return filters;
+ }
-QStringList FilterTreeModel::getFilterTermsOfType(CardFilter::Attr filterType) const
-{
- QStringList terms;
- forEachFilter(fTree, [&](const FilterItem *filterItem) {
- if (filterItem->attr() == filterType) {
- terms.append(filterItem->text());
+ std::function traverse;
+ traverse = [&](const FilterTreeNode *node) {
+ if (const auto *filterItem = dynamic_cast(node)) {
+ if (filterItem->attr() == filterType) {
+ QString text = filterItem->text();
+ filters.append(new CardFilter(text, filterItem->type(), filterItem->attr()));
+ }
}
- });
- return terms;
+ for (int i = 0; i < node->childCount(); ++i) {
+ traverse(node->nodeAt(i));
+ }
+ };
+
+ traverse(fTree);
+ return filters;
}
QList FilterTreeModel::allFilters() const
{
QList filters;
- forEachFilter(fTree, [&](const FilterItem *filterItem) {
- filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
- });
+ if (!fTree) {
+ return filters;
+ }
+
+ std::function traverse;
+ traverse = [&](const FilterTreeNode *node) {
+ if (const auto *filterItem = dynamic_cast(node)) {
+ QString text = filterItem->text();
+ filters.append(new CardFilter(text, filterItem->type(), filterItem->attr()));
+ }
+ for (int i = 0; i < node->childCount(); ++i) {
+ traverse(node->nodeAt(i));
+ }
+ };
+
+ traverse(fTree);
return filters;
}
@@ -210,8 +196,7 @@ QVariant FilterTreeModel::data(const QModelIndex &index, int role) const
switch (role) {
case Qt::FontRole:
if (!node->isLeaf()) {
- // Family comes from the DeckList theme role; bold stays caller-side.
- QFont f = themeManager->appFont(AppFont::DeckList);
+ QFont f;
f.setBold(true);
return f;
}
diff --git a/cockatrice/src/filters/filter_tree_model.h b/cockatrice/src/filters/filter_tree_model.h
index 05f34f508..826fcc941 100644
--- a/cockatrice/src/filters/filter_tree_model.h
+++ b/cockatrice/src/filters/filter_tree_model.h
@@ -28,14 +28,7 @@ public slots:
void addFilter(const CardFilter *f);
void removeFilter(const CardFilter *f);
void clearFiltersOfType(CardFilter::Attr filterType);
- /**
- * @brief Copies every active filter of one attribute into new CardFilter objects.
- *
- * The caller owns the returned filters. Prefer getFilterTermsOfType() when only
- * the terms are needed.
- */
[[nodiscard]] QList getFiltersOfType(CardFilter::Attr filterType) const;
- /// @brief Copies every active filter into new CardFilter objects. The caller owns them.
[[nodiscard]] QList allFilters() const;
private slots:
@@ -55,13 +48,6 @@ public:
{
return fTree;
}
-
- /**
- * @brief The texts of every active filter of one attribute, without allocating filters.
- *
- * Prefer this over getFiltersOfType() when only the filter terms are needed.
- */
- [[nodiscard]] QStringList getFilterTermsOfType(CardFilter::Attr filterType) const;
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
[[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
diff --git a/libcockatrice_game/libcockatrice/game/abstract_game.cpp b/cockatrice/src/game/abstract_game.cpp
similarity index 88%
rename from libcockatrice_game/libcockatrice/game/abstract_game.cpp
rename to cockatrice/src/game/abstract_game.cpp
index a93f44018..efc575eff 100644
--- a/libcockatrice_game/libcockatrice/game/abstract_game.cpp
+++ b/cockatrice/src/game/abstract_game.cpp
@@ -18,6 +18,8 @@ AbstractGame::AbstractGame(QObject *_parent) : QObject(_parent)
{
gameMetaInfo = new GameMetaInfo(this);
gameEventHandler = new GameEventHandler(this);
+
+ activeCard = nullptr;
}
bool AbstractGame::isHost() const
@@ -46,7 +48,12 @@ void AbstractGame::loadReplay(const GameReplay *replay)
gameMetaInfo->setSpectatorsOmniscient(true);
}
-CardState *AbstractGame::getCard(int playerId, const QString &zoneName, int cardId) const
+void AbstractGame::setActiveCard(CardItem *card)
+{
+ activeCard = card;
+}
+
+CardItem *AbstractGame::getCard(int playerId, const QString &zoneName, int cardId) const
{
PlayerLogic *player = playerManager->getPlayer(playerId);
if (!player) {
diff --git a/libcockatrice_game/libcockatrice/game/abstract_game.h b/cockatrice/src/game/abstract_game.h
similarity index 82%
rename from libcockatrice_game/libcockatrice/game/abstract_game.h
rename to cockatrice/src/game/abstract_game.h
index 00a95a3b8..326af8ac9 100644
--- a/libcockatrice_game/libcockatrice/game/abstract_game.h
+++ b/cockatrice/src/game/abstract_game.h
@@ -11,14 +11,14 @@
#include
#include
+class CardItem;
class AbstractClient;
-class CardState;
class GameEventHandler;
class GameMetaInfo;
class GameReplay;
class GameState;
class PlayerManager;
-class CardState;
+
class AbstractGame : public QObject
{
Q_OBJECT
@@ -30,6 +30,7 @@ public:
GameState *gameState;
GameEventHandler *gameEventHandler;
PlayerManager *playerManager;
+ CardItem *activeCard;
GameMetaInfo *getGameMetaInfo()
{
@@ -57,7 +58,13 @@ public:
void loadReplay(const GameReplay *replay);
- CardState *getCard(int playerId, const QString &zoneName, int cardId) const;
+ CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;
+
+ void setActiveCard(CardItem *card);
+ CardItem *getActiveCard() const
+ {
+ return activeCard;
+ }
};
#endif // COCKATRICE_ABSTRACT_GAME_H
diff --git a/cockatrice/src/game_graphics/board/arrow_registry.cpp b/cockatrice/src/game/arrow_registry.cpp
similarity index 93%
rename from cockatrice/src/game_graphics/board/arrow_registry.cpp
rename to cockatrice/src/game/arrow_registry.cpp
index 10b3cde2f..c05134a75 100644
--- a/cockatrice/src/game_graphics/board/arrow_registry.cpp
+++ b/cockatrice/src/game/arrow_registry.cpp
@@ -1,9 +1,9 @@
#include "arrow_registry.h"
-#include "arrow_item.h"
+#include "../game_graphics/board/arrow_item.h"
+#include "board/arrow_data.h"
#include
-#include
void ArrowRegistry::insert(QSharedPointer data, ArrowItem *arrow)
{
diff --git a/cockatrice/src/game_graphics/board/arrow_registry.h b/cockatrice/src/game/arrow_registry.h
similarity index 94%
rename from cockatrice/src/game_graphics/board/arrow_registry.h
rename to cockatrice/src/game/arrow_registry.h
index 7103747e8..ca3b04090 100644
--- a/cockatrice/src/game_graphics/board/arrow_registry.h
+++ b/cockatrice/src/game/arrow_registry.h
@@ -4,7 +4,6 @@
#include
#include
#include
-#include
class ArrowItem;
struct ArrowData;
diff --git a/libcockatrice_game/libcockatrice/game/board/arrow_data.cpp b/cockatrice/src/game/board/arrow_data.cpp
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/board/arrow_data.cpp
rename to cockatrice/src/game/board/arrow_data.cpp
diff --git a/libcockatrice_game/libcockatrice/game/board/arrow_data.h b/cockatrice/src/game/board/arrow_data.h
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/board/arrow_data.h
rename to cockatrice/src/game/board/arrow_data.h
diff --git a/libcockatrice_game/libcockatrice/game/board/card_list.cpp b/cockatrice/src/game/board/card_list.cpp
similarity index 70%
rename from libcockatrice_game/libcockatrice/game/board/card_list.cpp
rename to cockatrice/src/game/board/card_list.cpp
index 361c51502..94bae67cd 100644
--- a/libcockatrice_game/libcockatrice/game/board/card_list.cpp
+++ b/cockatrice/src/game/board/card_list.cpp
@@ -1,6 +1,7 @@
#include "card_list.h"
-#include "card_state.h"
+#include "../../game_graphics/board/card_item.h"
+#include "libcockatrice/card/printing/exact_card.h"
#include
#include
@@ -10,26 +11,26 @@
#include
#include
-CardList::CardList(bool _contentsKnown) : QList(), contentsKnown(_contentsKnown)
+CardList::CardList(bool _contentsKnown) : QList(), contentsKnown(_contentsKnown)
{
}
/**
- * @brief Finds the CardState with the given id in the list.
+ * @brief Finds the CardItem with the given id in the list.
* If contentsKnown is false, then this just returns the first element of the list.
*
* @param cardId The id of the card to find.
*
- * @returns A pointer to the CardState, or a nullptr if not found.
+ * @returns A pointer to the CardItem, or a nullptr if not found.
*/
-CardState *CardList::findCard(const int cardId) const
+CardItem *CardList::findCard(const int cardId) const
{
if (!contentsKnown && !empty()) {
return at(0);
} else {
- for (auto *cardState : *this) {
- if (cardState->getId() == cardId) {
- return cardState;
+ for (auto *cardItem : *this) {
+ if (cardItem->getId() == cardId) {
+ return cardItem;
}
}
}
@@ -37,7 +38,7 @@ CardState *CardList::findCard(const int cardId) const
}
/**
- * @brief sorts the list by using string comparison on properties extracted from the CardState
+ * @brief sorts the list by using string comparison on properties extracted from the CardItem
* The cards are compared using each property in order.
* If two cards have the same value for a property, then the next property in the list is used.
*
@@ -50,7 +51,7 @@ void CardList::sortBy(const QList &option)
return;
}
- auto comparator = [&option](CardState *a, CardState *b) {
+ auto comparator = [&option](CardItem *a, CardItem *b) {
for (auto prop : option) {
auto extractor = getExtractorFor(prop);
QString t1 = extractor(a);
@@ -115,26 +116,26 @@ static QString getColorSortString(const CardInfo &c, bool appendAtEnd)
}
/**
- * @brief returns the function that extracts the given property from the CardState.
+ * @brief returns the function that extracts the given property from the CardItem.
*/
-std::function CardList::getExtractorFor(SortOption option)
+std::function CardList::getExtractorFor(SortOption option)
{
switch (option) {
case NoSort:
- return [](CardState *) { return ""; };
+ return [](CardItem *) { return ""; };
case SortByMainType:
- return [](CardState *c) { return c->getCardInfo().getMainCardType(); };
+ return [](CardItem *c) { return c->getCardInfo().getMainCardType(); };
case SortByManaValue:
// getCmc returns the int as a string. We pad with 0's so that string comp also works on it
- return [](CardState *c) { return c->getCard() ? c->getCardInfo().getCmc().rightJustified(4, '0') : ""; };
+ return [](CardItem *c) { return c->getCard() ? c->getCardInfo().getCmc().rightJustified(4, '0') : ""; };
case SortByColorGrouping:
- return [](CardState *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), false) : ""; };
+ return [](CardItem *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), false) : ""; };
case SortByName:
- return [](CardState *c) { return c->getName(); };
+ return [](CardItem *c) { return c->getName(); };
case SortByType:
- return [](CardState *c) { return c->getCardInfo().getCardType(); };
+ return [](CardItem *c) { return c->getCardInfo().getCardType(); };
case SortByManaCost:
- return [](CardState *c) {
+ return [](CardItem *c) {
if (!c->getCard()) {
return QString();
}
@@ -146,18 +147,18 @@ std::function CardList::getExtractorFor(SortOption option)
return QString("%1%2").arg(info.getCmc(), 4, QChar('0')).arg(info.getManaCost());
};
case SortByColors:
- return [](CardState *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), true) : ""; };
+ return [](CardItem *c) { return c->getCard() ? getColorSortString(c->getCardInfo(), true) : ""; };
case SortByPt:
// do the same padding trick as above
return
- [](CardState *c) { return c->getCard() ? c->getCardInfo().getPowTough().rightJustified(10, '0') : ""; };
+ [](CardItem *c) { return c->getCard() ? c->getCardInfo().getPowTough().rightJustified(10, '0') : ""; };
case SortBySet:
- return [](CardState *c) { return c->getCardInfo().getSetsNames(); };
+ return [](CardItem *c) { return c->getCardInfo().getSetsNames(); };
case SortByPrinting:
- return [](CardState *c) { return c->getProviderId(); };
+ return [](CardItem *c) { return c->getProviderId(); };
}
// this line should never be reached
qCWarning(CardListLog) << "cardlist.cpp: Could not find extractor for SortOption" << option;
- return [](CardState *) { return ""; };
+ return [](CardItem *) { return ""; };
}
\ No newline at end of file
diff --git a/libcockatrice_game/libcockatrice/game/board/card_list.h b/cockatrice/src/game/board/card_list.h
similarity index 84%
rename from libcockatrice_game/libcockatrice/game/board/card_list.h
rename to cockatrice/src/game/board/card_list.h
index b3037d0f6..b5427df0d 100644
--- a/libcockatrice_game/libcockatrice/game/board/card_list.h
+++ b/cockatrice/src/game/board/card_list.h
@@ -14,9 +14,9 @@
inline Q_LOGGING_CATEGORY(CardListLog, "card_list");
-class CardState;
+class CardItem;
-class CardList : public QList
+class CardList : public QList
{
protected:
bool contentsKnown;
@@ -43,7 +43,7 @@ public:
SortByPrinting
};
explicit CardList(bool _contentsKnown);
- CardState *findCard(const int cardId) const;
+ CardItem *findCard(const int cardId) const;
bool getContentsKnown() const
{
return contentsKnown;
@@ -51,7 +51,7 @@ public:
void sortBy(const QList &options);
- static std::function getExtractorFor(SortOption option);
+ static std::function getExtractorFor(SortOption option);
};
#endif
diff --git a/cockatrice/src/game/board/card_state.cpp b/cockatrice/src/game/board/card_state.cpp
new file mode 100644
index 000000000..3f879a849
--- /dev/null
+++ b/cockatrice/src/game/board/card_state.cpp
@@ -0,0 +1,113 @@
+#include "card_state.h"
+
+#include
+
+void CardState::resetState(bool keepAnnotations)
+{
+ attacking = false;
+ counters.clear();
+ pt.clear();
+ if (!keepAnnotations) {
+ annotation.clear();
+ }
+ attachedTo = nullptr;
+}
+
+void CardState::setZone(CardZoneLogic *_zone)
+{
+ if (zone == _zone) {
+ return;
+ }
+
+ zone = _zone;
+ emit zoneChanged(this, zone);
+ emit stateChanged();
+}
+
+void CardState::setAttacking(bool _attacking)
+{
+ if (attacking == _attacking) {
+ return;
+ }
+ attacking = _attacking;
+ emit attackingChanged(_attacking);
+ emit stateChanged();
+}
+
+void CardState::insertCounter(int id, int value)
+{
+ counters.insert(id, value);
+
+ emit countersChanged(counters);
+ emit stateChanged();
+}
+
+void CardState::setCounter(int id, int value)
+{
+ if (value) {
+ counters[id] = value;
+ } else {
+ counters.remove(id);
+ }
+
+ emit countersChanged(counters);
+ emit stateChanged();
+}
+
+void CardState::clearCounters()
+{
+ counters.clear();
+ emit countersChanged(counters);
+ emit stateChanged();
+}
+
+void CardState::setAnnotation(const QString &_annotation)
+{
+ if (annotation == _annotation) {
+ return;
+ }
+ annotation = _annotation;
+ emit annotationChanged(annotation);
+ emit stateChanged();
+}
+
+void CardState::setPT(const QString &_pt)
+{
+ if (pt == _pt) {
+ return;
+ }
+ pt = _pt;
+ emit ptChanged(pt);
+ emit stateChanged();
+}
+
+void CardState::setDoesntUntap(bool _doesntUntap)
+{
+ if (doesntUntap == _doesntUntap) {
+ return;
+ }
+ doesntUntap = _doesntUntap;
+ emit doesntUntapChanged(_doesntUntap);
+ emit stateChanged();
+}
+
+void CardState::setDestroyOnZoneChange(bool _destroyOnZoneChange)
+{
+ if (destroyOnZoneChange == _destroyOnZoneChange) {
+ return;
+ }
+
+ destroyOnZoneChange = _destroyOnZoneChange;
+ emit destroyOnZoneChangeChanged(_destroyOnZoneChange);
+ emit stateChanged();
+}
+
+void CardState::setAttachedTo(CardItem *_attachedTo)
+{
+ if (attachedTo == _attachedTo) {
+ return;
+ }
+ attachedTo = _attachedTo;
+ emit attachedToChanged(_attachedTo);
+ emit stateChanged();
+}
\ No newline at end of file
diff --git a/cockatrice/src/game/board/card_state.h b/cockatrice/src/game/board/card_state.h
new file mode 100644
index 000000000..ca7aaf927
--- /dev/null
+++ b/cockatrice/src/game/board/card_state.h
@@ -0,0 +1,106 @@
+#ifndef COCKATRICE_CARD_STATE_H
+#define COCKATRICE_CARD_STATE_H
+
+#include
+#include
+#include
+#include
+
+class CardZoneLogic;
+class CardItem;
+
+class CardState : public QObject
+{
+ Q_OBJECT
+
+private:
+ bool attacking = false;
+ QMap counters;
+ QString annotation;
+ QString pt;
+ bool doesntUntap = false;
+ bool destroyOnZoneChange = false;
+
+ CardItem *attachedTo = nullptr;
+ CardZoneLogic *zone = nullptr;
+
+signals:
+ void stateChanged();
+
+ void attackingChanged(bool newValue);
+ void countersChanged(const QMap &newCounters);
+ void annotationChanged(const QString &newAnnotation);
+ void ptChanged(const QString &newPt);
+ void doesntUntapChanged(bool newValue);
+ void destroyOnZoneChangeChanged(bool newValue);
+ void attachedToChanged(CardItem *newAttachedTo);
+ void zoneChanged(CardState *changedCard, CardZoneLogic *newZone);
+
+public:
+ explicit CardState(QObject *parent, CardZoneLogic *_zone) : QObject(parent), zone(_zone)
+ {
+ }
+
+ void resetState(bool keepAnnotations);
+
+ CardZoneLogic *getZone() const
+ {
+ return zone;
+ }
+
+ void setZone(CardZoneLogic *_zone);
+
+ bool getAttacking() const
+ {
+ return attacking;
+ }
+ void setAttacking(bool _attacking);
+
+ const QMap &getCounters() const
+ {
+ return counters;
+ }
+
+ void insertCounter(int id, int value);
+
+ void setCounter(int id, int value);
+
+ void clearCounters();
+
+ QString getAnnotation() const
+ {
+ return annotation;
+ }
+
+ void setAnnotation(const QString &_annotation);
+
+ QString getPT() const
+ {
+ return pt;
+ }
+
+ void setPT(const QString &_pt);
+
+ bool getDoesntUntap() const
+ {
+ return doesntUntap;
+ }
+
+ void setDoesntUntap(bool _doesntUntap);
+
+ bool getDestroyOnZoneChange() const
+ {
+ return destroyOnZoneChange;
+ }
+
+ void setDestroyOnZoneChange(bool _destroyOnZoneChange);
+
+ CardItem *getAttachedTo() const
+ {
+ return attachedTo;
+ }
+
+ void setAttachedTo(CardItem *_attachedTo);
+};
+
+#endif // COCKATRICE_CARD_STATE_H
diff --git a/libcockatrice_game/libcockatrice/game/board/counter_state.cpp b/cockatrice/src/game/board/counter_state.cpp
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/board/counter_state.cpp
rename to cockatrice/src/game/board/counter_state.cpp
diff --git a/libcockatrice_game/libcockatrice/game/board/counter_state.h b/cockatrice/src/game/board/counter_state.h
similarity index 100%
rename from libcockatrice_game/libcockatrice/game/board/counter_state.h
rename to cockatrice/src/game/board/counter_state.h
diff --git a/libcockatrice_game/libcockatrice/game/game.cpp b/cockatrice/src/game/game.cpp
similarity index 75%
rename from libcockatrice_game/libcockatrice/game/game.cpp
rename to cockatrice/src/game/game.cpp
index a1a4a31a8..7343d052a 100644
--- a/libcockatrice_game/libcockatrice/game/game.cpp
+++ b/cockatrice/src/game/game.cpp
@@ -16,16 +16,13 @@ Game::Game(QObject *_parent,
bool isLocalGame,
QList &_clients,
const Event_GameJoined &event,
- const QMap &_roomGameTypes,
- IInterfaceSettingsProvider *interfaceSettings,
- ICardPreferenceProvider *cardPreferences)
+ const QMap &_roomGameTypes)
: AbstractGame(_parent)
{
gameMetaInfo->setFromProto(event.game_info());
gameMetaInfo->setRoomGameTypes(_roomGameTypes);
gameState = new GameState(this, 0, event.host_id(), isLocalGame, _clients, false, event.resuming(), -1, false);
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
- playerManager = new PlayerManager(this, event.player_id(), event.judge(), event.spectator(), interfaceSettings,
- cardPreferences);
+ playerManager = new PlayerManager(this, event.player_id(), event.judge(), event.spectator());
gameMetaInfo->setStarted(false);
}
diff --git a/libcockatrice_game/libcockatrice/game/game.h b/cockatrice/src/game/game.h
similarity index 69%
rename from libcockatrice_game/libcockatrice/game/game.h
rename to cockatrice/src/game/game.h
index f80b75ea5..6a309cfb2 100644
--- a/libcockatrice_game/libcockatrice/game/game.h
+++ b/cockatrice/src/game/game.h
@@ -18,9 +18,6 @@ class QString;
template class QMap;
template class QList;
-class ICardPreferenceProvider;
-class IInterfaceSettingsProvider;
-
class Game : public AbstractGame
{
Q_OBJECT
@@ -30,9 +27,7 @@ public:
bool isLocalGame,
QList &_clients,
const Event_GameJoined &event,
- const QMap &_roomGameTypes,
- IInterfaceSettingsProvider *interfaceSettings = nullptr,
- ICardPreferenceProvider *cardPreferences = nullptr);
+ const QMap &_roomGameTypes);
};
#endif // COCKATRICE_GAME_H
diff --git a/libcockatrice_game/libcockatrice/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp
similarity index 80%
rename from libcockatrice_game/libcockatrice/game/game_event_handler.cpp
rename to cockatrice/src/game/game_event_handler.cpp
index 7ef502400..7b08af47c 100644
--- a/libcockatrice_game/libcockatrice/game/game_event_handler.cpp
+++ b/cockatrice/src/game/game_event_handler.cpp
@@ -21,7 +21,6 @@
#include
#include
#include
-#include
#include
#include
#include