Compare commits

..

No commits in common. "68e4fa054de040105fb0a6dd7676c30ad66e9935" and "83833f468490935d7196e69f1c4cc772aeeb5a20" have entirely different histories.

19 changed files with 88 additions and 168 deletions

View file

@ -40,7 +40,7 @@ jobs:
steps:
- name: "Checkout repository"
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: "Initialize CodeQL"
uses: github/codeql-action/init@v4

View file

@ -127,7 +127,7 @@ jobs:
steps:
- name: "Download digests"
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
path: ${{ runner.temp }}/digests
pattern: digest-*

View file

@ -8,20 +8,20 @@
# cmake 3.16 is required if using qt6
cmake_minimum_required(VERSION 3.10)
# Use compiler cache (ccache)
option(USE_CCACHE "Cache the build results with ccache" OFF)
# Early detect ccache
option(USE_CCACHE "Cache the build results with ccache" ON)
# Treat warnings as errors (Debug builds only)
option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON)
# Check for translation updates
option(UPDATE_TRANSLATIONS "Update translations on compile" OFF)
# Compile Cockatrice
option(WITH_CLIENT "Build Cockatrice client" ON)
# Compile Oracle
option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
# Compile Servatrice
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
# Compile servatrice
option(WITH_SERVER "build servatrice" OFF)
# Compile cockatrice
option(WITH_CLIENT "build cockatrice" ON)
# Compile oracle
option(WITH_ORACLE "build oracle" ON)
# Compile tests
option(TEST "Build tests" OFF)
option(TEST "build tests" OFF)
# Use vcpkg regardless of OS
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)

View file

@ -1,17 +1,9 @@
#include "dlg_convert_deck_to_cod_format.h"
#include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/deck_loader.h"
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QLabel>
#include <QMessageBox>
#include <QVBoxLayout>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent)
{
@ -46,71 +38,3 @@ bool DialogConvertDeckToCodFormat::dontAskAgain() const
{
return dontAskAgainCheckbox->isChecked();
}
namespace
{
bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
{
QFileInfo fileInfo(filePath);
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
if (QFile::exists(newFileName)) {
QMessageBox::StandardButton reply =
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
QMessageBox::Yes | QMessageBox::No);
return reply == QMessageBox::Yes;
}
return true; // Safe to proceed
}
} // namespace
bool DialogConvertDeckToCodFormat::promptIfRequired(QWidget *parent,
const QString &filePath,
const std::function<bool()> &convert)
{
if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) {
return true;
}
// Retrieve saved preference if the prompt is disabled
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
return false;
}
if (!confirmOverwriteIfExists(parent, filePath)) {
return false;
}
return convert();
}
// Show the dialog to the user
DialogConvertDeckToCodFormat conversionDialog(parent);
if (conversionDialog.exec() != QDialog::Accepted) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
!conversionDialog.dontAskAgain());
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
return false;
}
// Try to convert file
if (!confirmOverwriteIfExists(parent, filePath)) {
return false;
}
if (!convert()) {
return false;
}
if (conversionDialog.dontAskAgain()) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
}
return true;
}

View file

@ -13,9 +13,6 @@
#include <QDialogButtonBox>
#include <QLabel>
#include <QVBoxLayout>
#include <functional>
class QWidget;
class DialogConvertDeckToCodFormat : public QDialog
{
@ -27,21 +24,6 @@ public:
[[nodiscard]] bool dontAskAgain() const;
/**
* @brief Checks whether the deck file at \a filePath can store tags.
*
* If the file is not a .cod deck, prompts the user for conversion to the
* Cockatrice format, honoring the saved "always convert / don't ask again"
* preference. On acceptance \a convert is called to perform the conversion.
*
* @param parent The widget to parent the prompt to.
* @param filePath The path of the deck file to check.
* @param convert Called to convert the deck once the user agrees.
* @return true if tags can be stored (no conversion needed, or the conversion
* was performed), false if the user declined to convert.
*/
static bool promptIfRequired(QWidget *parent, const QString &filePath, const std::function<bool()> &convert);
private:
QVBoxLayout *layout;
QLabel *label;

View file

@ -525,13 +525,6 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
add(games);
// ── Invite (only while the inviter has a joinable game for this user) ────
if (!isSelf && online && gameInviteAvailable && gameInviteAvailable(name)) {
auto *invite = makeBtn(tr("Invite"), tr("Invite to your game"), actionArea, theme);
connect(invite, &QPushButton::clicked, this, [this, name] { emit inviteRequested(name); });
add(invite);
}
// ── Buddy / ignore (registered users only) ────────────────────────────────
if (!isSelf && isReg) {
if (isBuddy) {

View file

@ -9,7 +9,6 @@
#include <QMap>
#include <QPixmap>
#include <QStandardItemModel>
#include <functional>
#include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
@ -150,17 +149,6 @@ public:
/** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */
void refreshHeader();
/**
* Sets a predicate evaluated on every action-button rebuild. It receives
* the name of the user the popup currently shows; when it returns true an
* "Invite" button is shown. The popup itself never resolves the invite
* link, it just forwards the request.
*/
void setGameInviteAvailable(std::function<bool(const QString &userName)> available)
{
gameInviteAvailable = std::move(available);
}
signals:
void mouseEnteredPopup();
void mouseLeftPopup();
@ -171,7 +159,6 @@ signals:
// ── Action signals — connect to UserContextMenu::exec*() ──────────────────
void chatRequested(const QString &userName);
void inviteRequested(const QString &userName);
void detailsRequested(const QString &userName);
void showGamesRequested(const QString &userName);
void addBuddyRequested(const QString &userName);
@ -213,7 +200,6 @@ private:
QString currentUser;
ServerInfo_User currentUserInfo;
bool currentOnline = false;
std::function<bool(const QString &userName)> gameInviteAvailable;
UserInfoHeaderWidget *header;
QWidget *actionArea; ///< rebuilt per user

View file

@ -345,11 +345,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
&cardArtProvider->cache(), &cardArtParamsMap,
window()); // parented to main window so it floats above siblings
// The invite availability is scoped to the room this list belongs to,
// and gated on the room's buddy-only setting for the hovered user.
userInfoPopup->setGameInviteAvailable(
[this](const QString &userName) { return userContextMenu->hasGameInviteLink(userName); });
userInfoPopup->hide();
userInfoPopup->setWindowOpacity(0.0);
userInfoPopup->installEventFilter(this);
@ -667,8 +662,6 @@ void UserListWidget::connectPopupSignals()
// Wire all action signals to UserContextMenu::exec*()
connect(userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat);
connect(userInfoPopup, &UserInfoPopup::inviteRequested, this,
[this](const QString &userName) { userContextMenu->execInvite(userName); });
connect(userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails);
connect(userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames);
connect(userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy);

View file

@ -22,7 +22,6 @@
#include <QTextEdit>
#include <QTreeWidgetItem>
#include <functional>
#include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
class QTreeWidget;

View file

@ -1091,8 +1091,7 @@ QList<GameInviteOption> TabSupervisor::getGameInviteLinksForRoom(int roomId) con
// The inviter may be in several games of the same room (hosting one and
// spectating another, for example). Return every game so the caller can
// let the user choose which one to invite to.
for (auto it = gameTabs.cbegin(); it != gameTabs.cend(); ++it) {
TabGame *tab = it.value();
for (TabGame *tab : gameTabs) {
GameMetaInfo *metaInfo = tab->getGame()->getGameMetaInfo();
if (metaInfo->proto().room_id() != roomId) {
continue;

View file

@ -10,6 +10,8 @@
#include "../visual_deck_storage_widget.h"
#include "deck_preview_deck_tags_display_widget.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QInputDialog>
#include <QLabel>
@ -497,6 +499,21 @@ void DeckPreviewWidget::actDeleteFile()
// The folder widget removes this preview once the row is gone.
}
static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
{
QFileInfo fileInfo(filePath);
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
if (QFile::exists(newFileName)) {
QMessageBox::StandardButton reply =
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
QMessageBox::Yes | QMessageBox::No);
return reply == QMessageBox::Yes;
}
return true; // Safe to proceed
}
/**
* Checks if the deck's file format supports tags.
* If not, then prompt the user for file conversion.
@ -504,8 +521,45 @@ void DeckPreviewWidget::actDeleteFile()
*/
bool DeckPreviewWidget::promptFileConversionIfRequired()
{
return DialogConvertDeckToCodFormat::promptIfRequired(this, filePath, [this] {
if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) {
return true;
}
// Retrieve saved preference if the prompt is disabled
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
return false;
}
if (!confirmOverwriteIfExists(this, filePath)) {
return false;
}
model->convertToCockatriceFormat(row());
return true;
});
}
// Show the dialog to the user
DialogConvertDeckToCodFormat conversionDialog(this);
if (conversionDialog.exec() != QDialog::Accepted) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
!conversionDialog.dontAskAgain());
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
return false;
}
// Try to convert file
if (!confirmOverwriteIfExists(this, filePath)) {
return false;
}
model->convertToCockatriceFormat(row());
if (conversionDialog.dontAskAgain()) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
}
return true;
}

View file

@ -5,8 +5,6 @@
#include <QCryptographicHash>
#include <QSet>
static constexpr int MAX_DECK_SIZE = 1e5;
DecklistNodeTree::DecklistNodeTree() : root(new InnerDecklistNode())
{
}
@ -115,7 +113,7 @@ void DecklistNodeTree::readZoneElement(QXmlStreamReader *xml)
{
QString zoneName = xml->attributes().value("name").toString();
InnerDecklistNode *newZone = getZoneObjFromName(zoneName);
totalCards += newZone->readElement(xml, MAX_DECK_SIZE - totalCards);
newZone->readElement(xml);
}
DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName,
@ -127,8 +125,6 @@ DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName,
const QString &cardProviderId,
const bool formatLegal)
{
amount = qMin(amount, MAX_DECK_SIZE - totalCards);
totalCards += amount;
auto *zoneNode = getZoneObjFromName(zoneName);
auto *node = new DecklistCardNode(cardName, amount, zoneNode, position, cardSetName, cardSetCollectorNumber,
cardProviderId, formatLegal);

View file

@ -10,7 +10,6 @@
class DecklistNodeTree
{
InnerDecklistNode *root; ///< Root of the deck tree (zones + cards).
int totalCards = 0;
public:
/** @brief Constructs an empty DecklistNodeTree. */

View file

@ -34,15 +34,15 @@ bool AbstractDecklistCardNode::compareName(AbstractDecklistNode *other) const
}
}
int AbstractDecklistCardNode::readElement(QXmlStreamReader *xml, int /* limit */)
bool AbstractDecklistCardNode::readElement(QXmlStreamReader *xml)
{
while (!xml->atEnd()) {
xml->readNext();
if (xml->isEndElement() && xml->name().toString() == "card") {
return 0;
return false;
}
}
return 0;
return true;
}
void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml)
@ -60,4 +60,4 @@ void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml)
if (!getCardProviderId().isEmpty()) {
xml->writeAttribute("uuid", getCardProviderId());
}
}
}

View file

@ -141,7 +141,7 @@ public:
*
* This supports loading deck files from Cockatrice’s XML format.
*/
int readElement(QXmlStreamReader *xml, int limit) override;
bool readElement(QXmlStreamReader *xml) override;
/**
* @brief Serialize this node’s properties to XML.

View file

@ -183,7 +183,7 @@ public:
* Cockatrice deck XML format.
* @{
*/
virtual int readElement(QXmlStreamReader *xml, int limit) = 0;
virtual bool readElement(QXmlStreamReader *xml) = 0;
virtual void writeElement(QXmlStreamWriter *xml) = 0;
/// @}
};

View file

@ -141,31 +141,27 @@ bool InnerDecklistNode::compareName(AbstractDecklistNode *other) const
}
}
int InnerDecklistNode::readElement(QXmlStreamReader *xml, int limit)
bool InnerDecklistNode::readElement(QXmlStreamReader *xml)
{
int totalCards = 0;
while (!xml->atEnd()) {
xml->readNext();
const QString childName = xml->name().toString();
if (xml->isStartElement()) {
if (childName == "zone") {
auto *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), this);
totalCards += newZone->readElement(xml, limit - totalCards);
newZone->readElement(xml);
} else if (childName == "card") {
int amount = xml->attributes().value("number").toString().toInt();
amount = qMin(amount, limit - totalCards);
auto *newCard = new DecklistCardNode(xml->attributes().value("name").toString(), amount, this, -1,
xml->attributes().value("setShortName").toString(),
xml->attributes().value("collectorNumber").toString(),
xml->attributes().value("uuid").toString());
totalCards += amount;
totalCards += newCard->readElement(xml, limit - totalCards);
auto *newCard = new DecklistCardNode(
xml->attributes().value("name").toString(), xml->attributes().value("number").toString().toInt(),
this, -1, xml->attributes().value("setShortName").toString(),
xml->attributes().value("collectorNumber").toString(), xml->attributes().value("uuid").toString());
newCard->readElement(xml);
}
} else if (xml->isEndElement() && (childName == "zone")) {
return totalCards;
return false;
}
}
return totalCards;
return true;
}
void InnerDecklistNode::writeElement(QXmlStreamWriter *xml)
@ -205,4 +201,4 @@ QVector<QPair<int, int>> InnerDecklistNode::sort(Qt::SortOrder order)
}
return result;
}
}

View file

@ -216,10 +216,9 @@ public:
/**
* @brief Deserialize this node and its children from XML.
* @param xml Reader positioned at this element.
* @param limit The maximum amount of cards to read
* @return the amount of cards found
* @return true if parsing succeeded.
*/
int readElement(QXmlStreamReader *xml, int limit) override;
bool readElement(QXmlStreamReader *xml) override;
/**
* @brief Serialize this node and its children to XML.

View file

@ -16,7 +16,7 @@ add_test(NAME lag_monitor_test COMMAND lag_monitor_test)
add_test(NAME latency_tracker_test COMMAND latency_tracker_test)
add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test)
set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 15)
set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5)
# Find GTest