mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-24 02:13:02 -07:00
Compare commits
15 commits
64e5c0de19
...
34d96d9fe5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34d96d9fe5 | ||
|
|
7d8514ec34 | ||
|
|
61e6a9913e | ||
|
|
4d4ddd4278 | ||
|
|
d6fbfb32a1 | ||
|
|
3ec62df3e7 | ||
|
|
fcfb14cf56 | ||
|
|
35ebae8d7f | ||
|
|
4e9d148163 | ||
|
|
425b16ea0d | ||
|
|
45c7ff6f87 | ||
|
|
d974501277 | ||
|
|
9bf2202739 | ||
|
|
03de1af678 | ||
|
|
3dc9dba67a |
51 changed files with 1686 additions and 1191 deletions
|
|
@ -8,6 +8,7 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
|
|||
gtest \
|
||||
mariadb-libs \
|
||||
ninja \
|
||||
openssl \
|
||||
protobuf \
|
||||
qt6-base \
|
||||
qt6-declarative \
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-image-formats-plugins \
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-image-formats-plugins \
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ RUN dnf install -y \
|
|||
git \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
openssl-devel \
|
||||
protobuf-devel \
|
||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ RUN dnf install -y \
|
|||
git \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
openssl-devel \
|
||||
protobuf-devel \
|
||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ RUN apt-get update && \
|
|||
libmariadb-dev-compat \
|
||||
libprotobuf-dev \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-tools-dev \
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-image-formats-plugins \
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-image-formats-plugins \
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
# This file sets all the variables shared between the projects
|
||||
# like the installation path, compilation flags etc..
|
||||
|
||||
# cmake 3.16 is required if using qt6
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
# 3.16 required for Qt6 and target_precompile_headers()
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# Use compiler cache (ccache)
|
||||
option(USE_CCACHE "Cache the build results with ccache" OFF)
|
||||
|
|
@ -184,6 +184,9 @@ elseif(CMAKE_COMPILER_IS_GNUCXX)
|
|||
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${FLAG}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Reduce compiler I/O by using pipes between stages instead of temp files
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||
else()
|
||||
# other: osx/llvm, bsd/llvm
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
|
||||
|
|
@ -192,6 +195,9 @@ else()
|
|||
else()
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra")
|
||||
endif()
|
||||
|
||||
# Reduce compiler I/O by using pipes between stages instead of temp files
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||
endif()
|
||||
|
||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||
|
|
@ -239,11 +245,6 @@ if(WIN32)
|
|||
find_package(OpenSSL REQUIRED)
|
||||
if(OPENSSL_FOUND)
|
||||
include_directories(${OPENSSL_INCLUDE_DIRS})
|
||||
else()
|
||||
message(
|
||||
WARNING
|
||||
"Could not find OpenSSL runtime libraries. They are not required for compiling, but needs to be available at runtime."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ RUN apt-get update \
|
|||
libmariadb-dev-compat \
|
||||
libprotobuf-dev \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
qt6-websockets-dev \
|
||||
protobuf-compiler \
|
||||
qt6-tools-dev \
|
||||
|
|
@ -42,6 +43,7 @@ RUN apt-get update \
|
|||
libprotobuf32t64 \
|
||||
libqt6sql6-mysql \
|
||||
libqt6websockets6 \
|
||||
libssl3 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
|
|
|||
|
|
@ -44,11 +44,16 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
|||
|
||||
GameScene::~GameScene()
|
||||
{
|
||||
// Sever all incoming connections (animated item destroy-tracking) before the
|
||||
// members below are destroyed: the base QGraphicsScene destructor destroys the
|
||||
// remaining items, and their destroyed() signals must not reach slots that
|
||||
// reference members that no longer exist.
|
||||
QObject::disconnect(nullptr, nullptr, this, nullptr);
|
||||
// Sever all destroyed->removeAnimatedItem connections before the members below
|
||||
// are destroyed: the base QGraphicsScene destructor destroys the remaining items,
|
||||
// and their destroyed() signals must not reach slots that reference members that
|
||||
// no longer exist. The connection handle overload is used because the string-based
|
||||
// disconnect(nullptr, nullptr, this, nullptr) is invalid (the sender must never be
|
||||
// nullptr) and would otherwise fail to sever these pointer-to-member connections.
|
||||
for (auto it = animationItemConnections.constBegin(); it != animationItemConnections.constEnd(); ++it) {
|
||||
QObject::disconnect(*it);
|
||||
}
|
||||
animationItemConnections.clear();
|
||||
|
||||
delete animationTimer;
|
||||
animationTimer = nullptr;
|
||||
|
|
@ -777,8 +782,15 @@ void GameScene::registerAnimationItem(IAnimatedItem *item)
|
|||
if (!object) {
|
||||
return;
|
||||
}
|
||||
if (!animatedItems.contains(object)) {
|
||||
connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem);
|
||||
// Guard against duplicate connections using the connection map, not
|
||||
// animatedItems: the animation timer removes entries from animatedItems when an
|
||||
// animation completes, but the destroyed->removeAnimatedItem connection must
|
||||
// persist until the object is destroyed. Relying on animatedItems here would let
|
||||
// a re-registered item (e.g. a life counter that flashes repeatedly) accumulate
|
||||
// duplicate destroyed connections, the older ones of which would survive teardown.
|
||||
if (!animationItemConnections.contains(object)) {
|
||||
animationItemConnections.insert(object,
|
||||
connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem));
|
||||
}
|
||||
animatedItems.insert(object, item);
|
||||
if (animationTimer && !animationTimer->isActive()) {
|
||||
|
|
@ -797,6 +809,7 @@ void GameScene::unregisterAnimationItem(IAnimatedItem *item)
|
|||
void GameScene::removeAnimatedItem(QObject *item)
|
||||
{
|
||||
animatedItems.remove(item);
|
||||
animationItemConnections.remove(item);
|
||||
if (animationTimer && animatedItems.isEmpty()) {
|
||||
animationTimer->stop();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,9 +54,11 @@ private:
|
|||
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
||||
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||
int playerRotation; ///< Rotation offset for player layout
|
||||
bool rearranging = false; ///< Guard against re-entrant rearrange
|
||||
bool needsReArrange = false; ///< Pending rearrange requested during a pass
|
||||
QHash<QObject *, QMetaObject::Connection>
|
||||
animationItemConnections; ///< destroyed->removeAnimatedItem handles per animated item
|
||||
int playerRotation; ///< Rotation offset for player layout
|
||||
bool rearranging = false; ///< Guard against re-entrant rearrange
|
||||
bool needsReArrange = false; ///< Pending rearrange requested during a pass
|
||||
|
||||
/**
|
||||
* @brief Updates which card is currently hovered based on scene coordinates.
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
}
|
||||
}
|
||||
} else {
|
||||
x = calcDropIndexFromY(dropPoint.y());
|
||||
bool sameZone = startZone == getLogic();
|
||||
x = calcDropIndexFromY(dropPoint.y(), !sameZone);
|
||||
}
|
||||
|
||||
Command_MoveCard cmd;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) cons
|
|||
return {cardCount, boundingRect().height(), cardHeight, offset, minOffset};
|
||||
}
|
||||
|
||||
int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const
|
||||
int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset) const
|
||||
{
|
||||
const auto &cards = getLogic()->getCards();
|
||||
if (cards.isEmpty()) {
|
||||
|
|
@ -94,7 +94,8 @@ int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const
|
|||
if (effectiveOffset <= 0.0) {
|
||||
return 0;
|
||||
}
|
||||
return qBound(0, qRound((dropY - start) / effectiveOffset), params.cardCount - 1);
|
||||
int max = allowCountExpand ? params.cardCount : params.cardCount - 1;
|
||||
return qBound(0, qRound((dropY - start) / effectiveOffset), max);
|
||||
}
|
||||
|
||||
void SelectZone::restoreStaleEscapedCards()
|
||||
|
|
|
|||
|
|
@ -104,8 +104,12 @@ protected:
|
|||
/**
|
||||
* @brief Computes the card index at a given y-coordinate within the zone's vertical layout.
|
||||
* Returns 0 if the zone has no cards or the offset is zero.
|
||||
*
|
||||
* @param dropY The y-coordinate that the card was dropped at
|
||||
* @param allowCountExpand If false, clamps the index at the number of cards minus 1
|
||||
* @param minOffset Minimum offset to preserve
|
||||
*/
|
||||
int calcDropIndexFromY(qreal dropY, qreal minOffset = 0.0) const;
|
||||
int calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset = 0.0) const;
|
||||
|
||||
/**
|
||||
* @brief Positions cards vertically with alternating left/right x-offsets.
|
||||
|
|
|
|||
|
|
@ -57,18 +57,14 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
return;
|
||||
}
|
||||
|
||||
const auto &cards = getLogic()->getCards();
|
||||
int index;
|
||||
if (startZone == getLogic()) {
|
||||
// Reordering within the zone: use drop position
|
||||
index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
|
||||
bool sameZone = startZone == getLogic();
|
||||
int index = calcDropIndexFromY(dropPoint.y(), !sameZone, MIN_CARD_VISIBLE);
|
||||
if (sameZone) {
|
||||
// Same-zone no-op: don't move a card onto itself
|
||||
const auto &cards = getLogic()->getCards();
|
||||
if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Coming from another zone: append at end (top of stack, rendered on top)
|
||||
index = static_cast<int>(cards.size());
|
||||
}
|
||||
|
||||
Command_MoveCard cmd;
|
||||
|
|
|
|||
|
|
@ -154,6 +154,48 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
homeTabGroupBox = new QGroupBox;
|
||||
homeTabGroupBox->setLayout(homeTabGrid);
|
||||
|
||||
// Playmat settings
|
||||
playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll);
|
||||
playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly);
|
||||
playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone);
|
||||
int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility());
|
||||
if (visIdx >= 0) {
|
||||
playmatVisibilityCombo.setCurrentIndex(visIdx);
|
||||
}
|
||||
connect(&playmatVisibilityCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
|
||||
|
||||
// Playmat mode: Override / Fallback / Deck-only
|
||||
playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck);
|
||||
playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback);
|
||||
playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly);
|
||||
int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
|
||||
if (modeIdx >= 0) {
|
||||
playmatModeCombo.setCurrentIndex(modeIdx);
|
||||
}
|
||||
connect(&playmatModeCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatModeLabel.setBuddy(&playmatModeCombo);
|
||||
|
||||
// User-level playmat settings: fallback collection.
|
||||
connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
|
||||
&AppearanceSettingsPage::openPlaymatCollectionDialog);
|
||||
|
||||
auto *playmatGrid = new QGridLayout;
|
||||
playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
|
||||
|
||||
playmatGroupBox = new QGroupBox;
|
||||
playmatGroupBox->setLayout(playmatGrid);
|
||||
|
||||
// Styling settings
|
||||
styleUserListCheckBox.setChecked(settings.appearance().getStyleUserList());
|
||||
connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
|
||||
&AppearanceSettings::setStyleUserList);
|
||||
|
|
@ -259,7 +301,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
cardLayoutGroupBox->setLayout(cardLayoutGrid);
|
||||
|
||||
// Card counter colors
|
||||
|
||||
auto *cardCounterColorsLayout = new QGridLayout;
|
||||
cardCounterColorsLayout->setColumnStretch(1, 1);
|
||||
cardCounterColorsLayout->setColumnStretch(3, 1);
|
||||
|
|
@ -339,47 +380,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
tableGroupBox = new QGroupBox;
|
||||
tableGroupBox->setLayout(tableGrid);
|
||||
|
||||
// Playmat settings
|
||||
playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll);
|
||||
playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly);
|
||||
playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone);
|
||||
int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility());
|
||||
if (visIdx >= 0) {
|
||||
playmatVisibilityCombo.setCurrentIndex(visIdx);
|
||||
}
|
||||
connect(&playmatVisibilityCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
|
||||
|
||||
// Playmat mode: Override / Fallback / Deck-only
|
||||
playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck);
|
||||
playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback);
|
||||
playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly);
|
||||
int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
|
||||
if (modeIdx >= 0) {
|
||||
playmatModeCombo.setCurrentIndex(modeIdx);
|
||||
}
|
||||
connect(&playmatModeCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatModeLabel.setBuddy(&playmatModeCombo);
|
||||
|
||||
// User-level playmat settings: fallback collection.
|
||||
connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
|
||||
&AppearanceSettingsPage::openPlaymatCollectionDialog);
|
||||
|
||||
auto *playmatGrid = new QGridLayout;
|
||||
playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
|
||||
|
||||
playmatGroupBox = new QGroupBox;
|
||||
playmatGroupBox->setLayout(playmatGrid);
|
||||
|
||||
// putting it all together
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(themeGroupBox);
|
||||
|
|
@ -512,6 +512,12 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
homeTabButtonColorSourceBox.setToolTip(
|
||||
tr("Automatic: extract from background if present, otherwise use theme default"));
|
||||
|
||||
playmatGroupBox->setTitle(tr("Playmat settings"));
|
||||
playmatVisibilityLabel.setText(tr("Playmat visibility:"));
|
||||
playmatModeLabel.setText(tr("Default collection behavior:"));
|
||||
playmatDefaultLabel.setText(tr("Default playmat collection:"));
|
||||
playmatDefaultEditButton.setText(tr("Edit..."));
|
||||
|
||||
stylingGroupBox->setTitle(tr("Styling settings"));
|
||||
styleUserListCheckBox.setText(tr("Style user list"));
|
||||
|
||||
|
|
@ -554,9 +560,4 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
tableGroupBox->setTitle(tr("Table grid layout"));
|
||||
invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate"));
|
||||
minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:"));
|
||||
playmatGroupBox->setTitle(tr("Playmat settings"));
|
||||
playmatVisibilityLabel.setText(tr("Playmat visibility:"));
|
||||
playmatModeLabel.setText(tr("Default collection behavior:"));
|
||||
playmatDefaultLabel.setText(tr("Default playmat collection:"));
|
||||
playmatDefaultEditButton.setText(tr("Edit..."));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,46 +44,55 @@ private:
|
|||
QLabel homeTabButtonColorSourceLabel;
|
||||
QComboBox homeTabButtonColorSourceBox;
|
||||
|
||||
QCheckBox styleUserListCheckBox;
|
||||
QCheckBox showShortcutsCheckBox;
|
||||
QCheckBox showGameSelectorFilterToolbarCheckBox;
|
||||
QLabel minPlayersForMultiColumnLayoutLabel;
|
||||
QLabel maxFontSizeForCardsLabel;
|
||||
QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox;
|
||||
QCheckBox bumpSetsWithCardsInDeckToTopCheckBox;
|
||||
QCheckBox displayCardNamesCheckBox;
|
||||
QCheckBox autoRotateSidewaysLayoutCardsCheckBox;
|
||||
QCheckBox cardScalingCheckBox;
|
||||
QCheckBox roundCardCornersCheckBox;
|
||||
QLabel verticalCardOverlapPercentLabel;
|
||||
QSpinBox verticalCardOverlapPercentBox;
|
||||
QLabel cardViewInitialRowsMaxLabel;
|
||||
QSpinBox cardViewInitialRowsMaxBox;
|
||||
QLabel cardViewExpandedRowsMaxLabel;
|
||||
QSpinBox cardViewExpandedRowsMaxBox;
|
||||
QCheckBox horizontalHandCheckBox;
|
||||
QCheckBox leftJustifiedHandCheckBox;
|
||||
QCheckBox invertVerticalCoordinateCheckBox;
|
||||
QLabel playmatVisibilityLabel;
|
||||
QComboBox playmatVisibilityCombo;
|
||||
QLabel playmatModeLabel;
|
||||
QComboBox playmatModeCombo;
|
||||
QLabel playmatDefaultLabel;
|
||||
QPushButton playmatDefaultEditButton;
|
||||
|
||||
QCheckBox styleUserListCheckBox;
|
||||
|
||||
QCheckBox showShortcutsCheckBox;
|
||||
QCheckBox showGameSelectorFilterToolbarCheckBox;
|
||||
|
||||
QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox;
|
||||
QCheckBox bumpSetsWithCardsInDeckToTopCheckBox;
|
||||
|
||||
QCheckBox displayCardNamesCheckBox;
|
||||
QCheckBox autoRotateSidewaysLayoutCardsCheckBox;
|
||||
QCheckBox cardScalingCheckBox;
|
||||
QCheckBox roundCardCornersCheckBox;
|
||||
QLabel maxFontSizeForCardsLabel;
|
||||
QSpinBox maxFontSizeForCardsEdit;
|
||||
|
||||
QLabel verticalCardOverlapPercentLabel;
|
||||
QSpinBox verticalCardOverlapPercentBox;
|
||||
QLabel cardViewInitialRowsMaxLabel;
|
||||
QSpinBox cardViewInitialRowsMaxBox;
|
||||
QLabel cardViewExpandedRowsMaxLabel;
|
||||
QSpinBox cardViewExpandedRowsMaxBox;
|
||||
|
||||
QList<QLabel *> cardCounterNames;
|
||||
|
||||
QCheckBox horizontalHandCheckBox;
|
||||
QCheckBox leftJustifiedHandCheckBox;
|
||||
|
||||
QCheckBox invertVerticalCoordinateCheckBox;
|
||||
QLabel minPlayersForMultiColumnLayoutLabel;
|
||||
QSpinBox minPlayersForMultiColumnLayoutEdit;
|
||||
|
||||
QGroupBox *themeGroupBox;
|
||||
QGroupBox *homeTabGroupBox;
|
||||
QGroupBox *playmatGroupBox;
|
||||
QGroupBox *stylingGroupBox;
|
||||
QGroupBox *menuGroupBox;
|
||||
QGroupBox *printingsGroupBox;
|
||||
QGroupBox *cardsGroupBox;
|
||||
QGroupBox *cardLayoutGroupBox;
|
||||
QGroupBox *handGroupBox;
|
||||
QGroupBox *playmatGroupBox;
|
||||
QGroupBox *tableGroupBox;
|
||||
QGroupBox *cardCountersGroupBox;
|
||||
QList<QLabel *> cardCounterNames;
|
||||
QSpinBox minPlayersForMultiColumnLayoutEdit;
|
||||
QSpinBox maxFontSizeForCardsEdit;
|
||||
QGroupBox *handGroupBox;
|
||||
QGroupBox *tableGroupBox;
|
||||
|
||||
public:
|
||||
AppearanceSettingsPage();
|
||||
|
|
|
|||
|
|
@ -425,29 +425,28 @@ void GeneralSettingsPage::updateStartupServerControlsVisibility()
|
|||
|
||||
void GeneralSettingsPage::retranslateUi()
|
||||
{
|
||||
const auto &settings = SettingsCache::instance();
|
||||
|
||||
languageGroupBox->setTitle(tr("Language settings"));
|
||||
languageLabel.setText(tr("Language:"));
|
||||
|
||||
versionGroupBox->setTitle(tr("Version settings"));
|
||||
cardDatabaseGroupBox->setTitle(tr("Card database"));
|
||||
startupGroupBox->setTitle(tr("Startup settings"));
|
||||
|
||||
if (SettingsCache::instance().getIsPortableBuild()) {
|
||||
pathsGroupBox->setTitle(tr("Paths (editing disabled in portable mode)"));
|
||||
} else {
|
||||
pathsGroupBox->setTitle(tr("Paths"));
|
||||
}
|
||||
advertiseTranslationPageLabel.setText(
|
||||
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
|
||||
deckPathLabel.setText(tr("Decks directory:"));
|
||||
filtersPathLabel.setText(tr("Filters directory:"));
|
||||
replaysPathLabel.setText(tr("Replays directory:"));
|
||||
picsPathLabel.setText(tr("Pictures directory:"));
|
||||
cardDatabasePathLabel.setText(tr("Card database:"));
|
||||
customCardDatabasePathLabel.setText(tr("Custom database directory:"));
|
||||
tokenDatabasePathLabel.setText(tr("Token database:"));
|
||||
|
||||
versionGroupBox->setTitle(tr("Version settings"));
|
||||
updateReleaseChannelLabel.setText(tr("Update channel"));
|
||||
startupUpdateCheckCheckBox.setText(tr("Check for client updates on startup"));
|
||||
updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client"));
|
||||
newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice"));
|
||||
|
||||
// We can't change the strings after they're put into the QComboBox, so this is our workaround
|
||||
int oldIndex = updateReleaseChannelBox.currentIndex();
|
||||
updateReleaseChannelBox.clear();
|
||||
for (ReleaseChannel *chan : settings.getUpdateReleaseChannels()) {
|
||||
updateReleaseChannelBox.addItem(tr(chan->getName().toUtf8()));
|
||||
}
|
||||
updateReleaseChannelBox.setCurrentIndex(oldIndex);
|
||||
|
||||
cardDatabaseGroupBox->setTitle(tr("Card database"));
|
||||
startupCardUpdateCheckBehaviorLabel.setText(tr("Check for card database updates on startup"));
|
||||
startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexNone, tr("Don't check"));
|
||||
startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexPrompt,
|
||||
|
|
@ -456,8 +455,13 @@ void GeneralSettingsPage::retranslateUi()
|
|||
tr("Always update in the background"));
|
||||
cardUpdateCheckIntervalLabel.setText(tr("Check for card database updates every"));
|
||||
cardUpdateCheckIntervalSpinBox.setSuffix(tr(" days"));
|
||||
updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client"));
|
||||
newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice"));
|
||||
|
||||
QDate lastCheckDate = settings.updates().getLastCardUpdateCheck();
|
||||
int daysAgo = lastCheckDate.daysTo(QDate::currentDate());
|
||||
lastCardUpdateCheckDateLabel.setText(
|
||||
tr("Last update check on %1 (%2 days ago)").arg(lastCheckDate.toString()).arg(daysAgo));
|
||||
|
||||
startupGroupBox->setTitle(tr("Startup settings"));
|
||||
showTipsOnStartup.setText(tr("Show tips on startup"));
|
||||
startupTabLabel.setText(tr("Startup tab:"));
|
||||
startupTabSelector.setItemText(StartupTab::StartupTabHome, tr("Home"));
|
||||
|
|
@ -473,21 +477,18 @@ void GeneralSettingsPage::retranslateUi()
|
|||
startupServerLabel.setText(tr("Server:"));
|
||||
startupRoomLabel.setText(tr("Room:"));
|
||||
startupRoomNameEdit->setPlaceholderText(tr("Room name"));
|
||||
resetAllPathsButton->setText(tr("Reset all paths"));
|
||||
|
||||
const auto &settings = SettingsCache::instance();
|
||||
|
||||
QDate lastCheckDate = settings.updates().getLastCardUpdateCheck();
|
||||
int daysAgo = lastCheckDate.daysTo(QDate::currentDate());
|
||||
|
||||
lastCardUpdateCheckDateLabel.setText(
|
||||
tr("Last update check on %1 (%2 days ago)").arg(lastCheckDate.toString()).arg(daysAgo));
|
||||
|
||||
// We can't change the strings after they're put into the QComboBox, so this is our workaround
|
||||
int oldIndex = updateReleaseChannelBox.currentIndex();
|
||||
updateReleaseChannelBox.clear();
|
||||
for (ReleaseChannel *chan : settings.getUpdateReleaseChannels()) {
|
||||
updateReleaseChannelBox.addItem(tr(chan->getName().toUtf8()));
|
||||
if (settings.getIsPortableBuild()) {
|
||||
pathsGroupBox->setTitle(tr("Paths (editing disabled in portable mode)"));
|
||||
} else {
|
||||
pathsGroupBox->setTitle(tr("Paths"));
|
||||
}
|
||||
updateReleaseChannelBox.setCurrentIndex(oldIndex);
|
||||
}
|
||||
deckPathLabel.setText(tr("Decks directory:"));
|
||||
filtersPathLabel.setText(tr("Filters directory:"));
|
||||
replaysPathLabel.setText(tr("Replays directory:"));
|
||||
picsPathLabel.setText(tr("Pictures directory:"));
|
||||
cardDatabasePathLabel.setText(tr("Card database:"));
|
||||
customCardDatabasePathLabel.setText(tr("Custom database directory:"));
|
||||
tokenDatabasePathLabel.setText(tr("Token database:"));
|
||||
resetAllPathsButton->setText(tr("Reset all paths"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,37 @@ private:
|
|||
QGroupBox *startupGroupBox;
|
||||
QGroupBox *pathsGroupBox;
|
||||
|
||||
QLabel languageLabel;
|
||||
QComboBox languageBox;
|
||||
QLabel advertiseTranslationPageLabel;
|
||||
|
||||
QLabel updateReleaseChannelLabel;
|
||||
QComboBox updateReleaseChannelBox;
|
||||
QCheckBox startupUpdateCheckCheckBox;
|
||||
QCheckBox updateNotificationCheckBox;
|
||||
QCheckBox newVersionOracleCheckBox;
|
||||
|
||||
QLabel startupCardUpdateCheckBehaviorLabel;
|
||||
QComboBox startupCardUpdateCheckBehaviorSelector;
|
||||
QLabel cardUpdateCheckIntervalLabel;
|
||||
QSpinBox cardUpdateCheckIntervalSpinBox;
|
||||
QLabel lastCardUpdateCheckDateLabel;
|
||||
|
||||
QCheckBox showTipsOnStartup;
|
||||
QLabel startupTabLabel;
|
||||
QComboBox startupTabSelector;
|
||||
QLabel startupServerLabel;
|
||||
QComboBox startupServerSelector;
|
||||
QLabel startupRoomLabel;
|
||||
QLineEdit *startupRoomNameEdit;
|
||||
|
||||
QLabel deckPathLabel;
|
||||
QLabel filtersPathLabel;
|
||||
QLabel replaysPathLabel;
|
||||
QLabel picsPathLabel;
|
||||
QLabel cardDatabasePathLabel;
|
||||
QLabel customCardDatabasePathLabel;
|
||||
QLabel tokenDatabasePathLabel;
|
||||
QLineEdit *deckPathEdit;
|
||||
QLineEdit *filtersPathEdit;
|
||||
QLineEdit *replaysPathEdit;
|
||||
|
|
@ -51,33 +82,6 @@ private:
|
|||
QLineEdit *tokenDatabasePathEdit;
|
||||
QPushButton *resetAllPathsButton;
|
||||
QLabel *allPathsResetLabel;
|
||||
QComboBox languageBox;
|
||||
QCheckBox startupUpdateCheckCheckBox;
|
||||
QLabel startupCardUpdateCheckBehaviorLabel;
|
||||
QComboBox startupCardUpdateCheckBehaviorSelector;
|
||||
QLabel cardUpdateCheckIntervalLabel;
|
||||
QSpinBox cardUpdateCheckIntervalSpinBox;
|
||||
QLabel lastCardUpdateCheckDateLabel;
|
||||
QCheckBox updateNotificationCheckBox;
|
||||
QCheckBox newVersionOracleCheckBox;
|
||||
QComboBox updateReleaseChannelBox;
|
||||
QLabel languageLabel;
|
||||
QLabel deckPathLabel;
|
||||
QLabel filtersPathLabel;
|
||||
QLabel replaysPathLabel;
|
||||
QLabel picsPathLabel;
|
||||
QLabel cardDatabasePathLabel;
|
||||
QLabel customCardDatabasePathLabel;
|
||||
QLabel tokenDatabasePathLabel;
|
||||
QLabel updateReleaseChannelLabel;
|
||||
QLabel advertiseTranslationPageLabel;
|
||||
QCheckBox showTipsOnStartup;
|
||||
QLabel startupTabLabel;
|
||||
QComboBox startupTabSelector;
|
||||
QLabel startupServerLabel;
|
||||
QComboBox startupServerSelector;
|
||||
QLabel startupRoomLabel;
|
||||
QLineEdit *startupRoomNameEdit;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H
|
||||
|
|
|
|||
|
|
@ -20,26 +20,7 @@ enum visualDeckStoragePromptForConversionIndex
|
|||
|
||||
UserInterfaceSettingsPage::UserInterfaceSettingsPage()
|
||||
{
|
||||
// general settings and notification settings
|
||||
notificationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getNotificationsEnabled());
|
||||
connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
|
||||
&InterfaceSettings::setNotificationsEnabled);
|
||||
connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&UserInterfaceSettingsPage::setNotificationEnabled);
|
||||
|
||||
specNotificationsEnabledCheckBox.setChecked(
|
||||
SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled());
|
||||
specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().userInterface().getNotificationsEnabled());
|
||||
connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
|
||||
&InterfaceSettings::setSpectatorNotificationsEnabled);
|
||||
|
||||
buddyConnectNotificationsEnabledCheckBox.setChecked(
|
||||
SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled());
|
||||
buddyConnectNotificationsEnabledCheckBox.setEnabled(
|
||||
SettingsCache::instance().userInterface().getNotificationsEnabled());
|
||||
connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED,
|
||||
&SettingsCache::instance().userInterface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled);
|
||||
|
||||
// general settings
|
||||
doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().userInterface().getDoubleClickToPlay());
|
||||
connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
|
||||
&InterfaceSettings::setDoubleClickToPlay);
|
||||
|
|
@ -103,6 +84,26 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
|
|||
generalGroupBox = new QGroupBox;
|
||||
generalGroupBox->setLayout(generalGrid);
|
||||
|
||||
// notification settings
|
||||
notificationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getNotificationsEnabled());
|
||||
connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
|
||||
&InterfaceSettings::setNotificationsEnabled);
|
||||
connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&UserInterfaceSettingsPage::setNotificationEnabled);
|
||||
|
||||
specNotificationsEnabledCheckBox.setChecked(
|
||||
SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled());
|
||||
specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().userInterface().getNotificationsEnabled());
|
||||
connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
|
||||
&InterfaceSettings::setSpectatorNotificationsEnabled);
|
||||
|
||||
buddyConnectNotificationsEnabledCheckBox.setChecked(
|
||||
SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled());
|
||||
buddyConnectNotificationsEnabledCheckBox.setEnabled(
|
||||
SettingsCache::instance().userInterface().getNotificationsEnabled());
|
||||
connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED,
|
||||
&SettingsCache::instance().userInterface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled);
|
||||
|
||||
auto *notificationsGrid = new QGridLayout;
|
||||
notificationsGrid->addWidget(¬ificationsEnabledCheckBox, 0, 0);
|
||||
notificationsGrid->addWidget(&specNotificationsEnabledCheckBox, 1, 0);
|
||||
|
|
@ -355,6 +356,7 @@ void UserInterfaceSettingsPage::retranslateUi()
|
|||
notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar"));
|
||||
specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating"));
|
||||
buddyConnectNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar when users in your buddy list connect"));
|
||||
|
||||
animationGroupBox->setTitle(tr("Animation settings"));
|
||||
enableAllAnimationsButton.setText(tr("&Enable all animations"));
|
||||
disableAllAnimationsButton.setText(tr("&Disable all animations"));
|
||||
|
|
@ -362,6 +364,7 @@ void UserInterfaceSettingsPage::retranslateUi()
|
|||
arrowDrawAnimationCheckBox.setText(tr("&Arrow draw animation"));
|
||||
lifeCounterAnimationsCheckBox.setText(tr("Life counter flash"));
|
||||
battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage"));
|
||||
|
||||
deckEditorGroupBox->setTitle(tr("Deck editor/storage settings"));
|
||||
openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default"));
|
||||
visualDeckStorageInGameCheckBox.setText(tr("Use visual deck storage in game lobby"));
|
||||
|
|
@ -397,8 +400,8 @@ void UserInterfaceSettingsPage::retranslateUi()
|
|||
0, CommanderBracketNames::CommanderSpellbookBracketNames);
|
||||
commanderSpellbookIntegrationBracketNamingSelector.setItemText(
|
||||
1, CommanderBracketNames::OfficialCommanderBracketNames);
|
||||
|
||||
commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setToolTip(CommanderBracketNames::Explainer);
|
||||
|
||||
replayGroupBox->setTitle(tr("Replay settings"));
|
||||
rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:"));
|
||||
rewindBufferingMsBox.setSuffix(" ms");
|
||||
|
|
|
|||
|
|
@ -23,9 +23,6 @@ private slots:
|
|||
void updateCommanderSpellbookUiState();
|
||||
|
||||
private:
|
||||
QCheckBox notificationsEnabledCheckBox;
|
||||
QCheckBox specNotificationsEnabledCheckBox;
|
||||
QCheckBox buddyConnectNotificationsEnabledCheckBox;
|
||||
QCheckBox doubleClickToPlayCheckBox;
|
||||
QCheckBox clickPlaysAllSelectedCheckBox;
|
||||
QCheckBox playToStackCheckBox;
|
||||
|
|
@ -37,12 +34,18 @@ private:
|
|||
QCheckBox showTotalSelectionCountCheckBox;
|
||||
QCheckBox useTearOffMenusCheckBox;
|
||||
QCheckBox keepGameChatFocusCheckBox;
|
||||
|
||||
QCheckBox notificationsEnabledCheckBox;
|
||||
QCheckBox specNotificationsEnabledCheckBox;
|
||||
QCheckBox buddyConnectNotificationsEnabledCheckBox;
|
||||
|
||||
QPushButton enableAllAnimationsButton;
|
||||
QPushButton disableAllAnimationsButton;
|
||||
QCheckBox tapAnimationCheckBox;
|
||||
QCheckBox arrowDrawAnimationCheckBox;
|
||||
QCheckBox lifeCounterAnimationsCheckBox;
|
||||
QCheckBox battlefieldFlashCheckBox;
|
||||
|
||||
QCheckBox openDeckInNewTabCheckBox;
|
||||
QLabel visualDeckStoragePromptForConversionLabel;
|
||||
QComboBox visualDeckStoragePromptForConversionSelector;
|
||||
|
|
@ -57,8 +60,10 @@ private:
|
|||
QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel;
|
||||
QToolButton commanderSpellbookIntegrationUseOfficialBracketNamesExplainer;
|
||||
QComboBox commanderSpellbookIntegrationBracketNamingSelector;
|
||||
|
||||
QLabel rewindBufferingMsLabel;
|
||||
QSpinBox rewindBufferingMsBox;
|
||||
|
||||
QGroupBox *generalGroupBox;
|
||||
QGroupBox *notificationsGroupBox;
|
||||
QGroupBox *animationGroupBox;
|
||||
|
|
|
|||
|
|
@ -116,9 +116,10 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
|
|||
}
|
||||
|
||||
TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent)
|
||||
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr),
|
||||
tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr),
|
||||
tabLog(nullptr), tabReport(nullptr), tabModeration(nullptr), isLocalGame(false)
|
||||
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr),
|
||||
tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr),
|
||||
tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr),
|
||||
tabModeration(nullptr), isLocalGame(false)
|
||||
{
|
||||
setElideMode(Qt::ElideRight);
|
||||
setMovable(true);
|
||||
|
|
@ -245,6 +246,7 @@ void TabSupervisor::retranslateUi()
|
|||
aTabLog->setText(tr("Logs"));
|
||||
aTabReport->setText(tr("Report Queue"));
|
||||
aTabModeration->setText(tr("Moderation"));
|
||||
aTabCardArtRules->setText(tr("Card Art Rules"));
|
||||
|
||||
// tabs
|
||||
QList<Tab *> tabs;
|
||||
|
|
@ -256,6 +258,7 @@ void TabSupervisor::retranslateUi()
|
|||
tabs.append(tabLog);
|
||||
tabs.append(tabReport);
|
||||
tabs.append(tabModeration);
|
||||
tabs.append(tabCardArtRules);
|
||||
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
|
||||
while (roomIterator.hasNext()) {
|
||||
tabs.append(roomIterator.next().value());
|
||||
|
|
@ -520,7 +523,9 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
|||
if (SettingsCache::instance().tabs().getTabModerationOpen()) {
|
||||
openTabModeration();
|
||||
}
|
||||
openTabCardArtRules();
|
||||
if (SettingsCache::instance().tabs().getTabCardArtRulesOpen()) {
|
||||
openTabCardArtRules();
|
||||
}
|
||||
}
|
||||
|
||||
retranslateUi();
|
||||
|
|
@ -582,6 +587,9 @@ void TabSupervisor::stop()
|
|||
if (tabModeration) {
|
||||
tabModeration->close();
|
||||
}
|
||||
if (tabCardArtRules) {
|
||||
tabCardArtRules->close();
|
||||
}
|
||||
}
|
||||
|
||||
QList<Tab *> tabsToDelete;
|
||||
|
|
@ -775,6 +783,7 @@ void TabSupervisor::openTabAdmin()
|
|||
|
||||
void TabSupervisor::actTabCardArtRules(bool checked)
|
||||
{
|
||||
SettingsCache::instance().tabs().setTabCardArtRulesOpen(checked);
|
||||
if (checked && !tabCardArtRules) {
|
||||
openTabCardArtRules();
|
||||
setCurrentWidget(tabCardArtRules);
|
||||
|
|
|
|||
|
|
@ -125,9 +125,7 @@ void VisualDeckStorageFolderDisplayWidget::continueDeckPass()
|
|||
}
|
||||
|
||||
const bool matches = index.data(VisualDeckStorageRoles::FilterMatchRole).toBool();
|
||||
if (matches == deckPreviewWidget->isHidden()) {
|
||||
deckPreviewWidget->setVisible(matches);
|
||||
}
|
||||
deckPreviewWidget->setVisible(matches);
|
||||
if (matches) {
|
||||
++visibleDeckCount;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/settings/network_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/utility/cryptoutil.h>
|
||||
|
||||
QTranslator *translator, *qtTranslator;
|
||||
RNG_Abstract *rng;
|
||||
|
|
@ -292,7 +293,7 @@ int main(int argc, char *argv[])
|
|||
}
|
||||
}
|
||||
|
||||
rng = new RNG_SFMT;
|
||||
rng = new RNG_SFMT(CryptoUtil::randomUInt64());
|
||||
themeManager = new ThemeManager;
|
||||
soundEngine = new SoundEngine;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ libcockatrice_* \
|
|||
exclude=("libcockatrice_rng/libcockatrice/rng/sfmt/" \
|
||||
"libcockatrice_utility/libcockatrice/utility/peglib.h" \
|
||||
"oracle/src/lzma/" \
|
||||
"oracle/src/qt-json/" \
|
||||
"oracle/src/zip/" \
|
||||
"servatrice/src/smtp/")
|
||||
exts=("cpp" "h" "proto")
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public:
|
|||
[[nodiscard]] virtual bool getTabLogOpen() const = 0;
|
||||
[[nodiscard]] virtual bool getTabReportOpen() const = 0;
|
||||
[[nodiscard]] virtual bool getTabModerationOpen() const = 0;
|
||||
[[nodiscard]] virtual bool getTabCardArtRulesOpen() const = 0;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ set(PROTO_FILES
|
|||
event_game_log_notice.proto
|
||||
event_game_say.proto
|
||||
event_game_state_changed.proto
|
||||
event_game_state_changed.proto
|
||||
event_join.proto
|
||||
event_join_room.proto
|
||||
event_kicked.proto
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#include "rng_sfmt.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <stdexcept>
|
||||
|
|
@ -11,10 +10,11 @@
|
|||
#define UINT64_MAX (~(uint64_t)0)
|
||||
#endif
|
||||
|
||||
RNG_SFMT::RNG_SFMT(QObject *parent) : RNG_Abstract(parent)
|
||||
RNG_SFMT::RNG_SFMT(uint64_t seed, QObject *parent) : RNG_Abstract(parent)
|
||||
{
|
||||
// initialize the random number generator with a 32bit integer seed (timestamp)
|
||||
sfmt_init_gen_rand(&sfmt, QDateTime::currentDateTime().toSecsSinceEpoch());
|
||||
// initialize the random number generator with a 64bit seed, e.g. from a CSPRNG
|
||||
uint32_t seedArray[2] = {static_cast<uint32_t>(seed), static_cast<uint32_t>(seed >> 32)};
|
||||
sfmt_init_by_array(&sfmt, seedArray, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ private:
|
|||
unsigned int cdf(unsigned int min, unsigned int max);
|
||||
|
||||
public:
|
||||
explicit RNG_SFMT(QObject *parent = nullptr);
|
||||
explicit RNG_SFMT(uint64_t seed, QObject *parent = nullptr);
|
||||
unsigned int rand(int min, int max) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,11 @@ bool TabsSettings::getTabModerationOpen() const
|
|||
return getValue("moderation", QString(), QString(), false).toBool();
|
||||
}
|
||||
|
||||
bool TabsSettings::getTabCardArtRulesOpen() const
|
||||
{
|
||||
return getValue("cardArtRules", QString(), QString(), false).toBool();
|
||||
}
|
||||
|
||||
void TabsSettings::setTabVisualDeckStorageOpen(bool value)
|
||||
{
|
||||
setValue(value, "visualDeckStorage");
|
||||
|
|
@ -150,3 +155,8 @@ void TabsSettings::setTabModerationOpen(bool value)
|
|||
{
|
||||
setValue(value, "moderation");
|
||||
}
|
||||
|
||||
void TabsSettings::setTabCardArtRulesOpen(bool value)
|
||||
{
|
||||
setValue(value, "cardArtRules");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ public:
|
|||
[[nodiscard]] bool getTabLogOpen() const override;
|
||||
[[nodiscard]] bool getTabReportOpen() const override;
|
||||
[[nodiscard]] bool getTabModerationOpen() const override;
|
||||
[[nodiscard]] bool getTabCardArtRulesOpen() const override;
|
||||
|
||||
void setStartupTabIndex(int value);
|
||||
void setStartupServerHost(const QString &host);
|
||||
|
|
@ -57,6 +58,7 @@ public:
|
|||
void setTabLogOpen(bool value);
|
||||
void setTabReportOpen(bool value);
|
||||
void setTabModerationOpen(bool value);
|
||||
void setTabCardArtRulesOpen(bool value);
|
||||
|
||||
signals:
|
||||
void startupTabIndexChanged(int index);
|
||||
|
|
|
|||
|
|
@ -6,13 +6,15 @@ set(CMAKE_AUTOUIC ON)
|
|||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(UTILITY_SOURCES
|
||||
libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp libcockatrice/utility/passwordhasher.cpp
|
||||
libcockatrice/utility/server_rate_limiter.cpp libcockatrice/utility/warning_categories.cpp
|
||||
libcockatrice/utility/cryptoutil.cpp libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp
|
||||
libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/server_rate_limiter.cpp
|
||||
libcockatrice/utility/warning_categories.cpp
|
||||
)
|
||||
|
||||
set(UTILITY_HEADERS
|
||||
libcockatrice/utility/card_ref.h
|
||||
libcockatrice/utility/color.h
|
||||
libcockatrice/utility/cryptoutil.h
|
||||
libcockatrice/utility/expression.h
|
||||
libcockatrice/utility/levenshtein.h
|
||||
libcockatrice/utility/macros.h
|
||||
|
|
@ -32,7 +34,9 @@ add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS})
|
|||
|
||||
target_include_directories(libcockatrice_utility PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng ${QT_CORE_MODULE})
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng OpenSSL::Crypto ${QT_CORE_MODULE})
|
||||
|
||||
set(ORACLE_LIBS)
|
||||
|
||||
|
|
|
|||
25
libcockatrice_utility/libcockatrice/utility/cryptoutil.cpp
Normal file
25
libcockatrice_utility/libcockatrice/utility/cryptoutil.cpp
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#include "cryptoutil.h"
|
||||
|
||||
#include <openssl/rand.h>
|
||||
|
||||
namespace CryptoUtil
|
||||
{
|
||||
QByteArray randomBytes(int count)
|
||||
{
|
||||
QByteArray bytes(count, '\0');
|
||||
if (RAND_bytes(reinterpret_cast<unsigned char *>(bytes.data()), count) != 1) {
|
||||
// Randomness failure is fatal: never fall back to a predictable source.
|
||||
qFatal("CryptoUtil::randomBytes: RAND_bytes failed");
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
quint64 randomUInt64()
|
||||
{
|
||||
quint64 value;
|
||||
if (RAND_bytes(reinterpret_cast<unsigned char *>(&value), sizeof(value)) != 1) {
|
||||
qFatal("CryptoUtil::randomUInt64: RAND_bytes failed");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
} // namespace CryptoUtil
|
||||
13
libcockatrice_utility/libcockatrice/utility/cryptoutil.h
Normal file
13
libcockatrice_utility/libcockatrice/utility/cryptoutil.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#ifndef CRYPTOUTIL_H
|
||||
#define CRYPTOUTIL_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace CryptoUtil
|
||||
{
|
||||
QByteArray randomBytes(int count);
|
||||
quint64 randomUInt64();
|
||||
} // namespace CryptoUtil
|
||||
|
||||
#endif
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#include "passwordhasher.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <libcockatrice/rng/rng_sfmt.h>
|
||||
#include <libcockatrice/utility/cryptoutil.h>
|
||||
|
||||
QString PasswordHasher::computeHash(const QString &password, const QString &salt)
|
||||
{
|
||||
|
|
@ -21,12 +21,28 @@ QString PasswordHasher::generateRandomSalt(const int len)
|
|||
static const char alphanum[] = "0123456789"
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz";
|
||||
const int size = sizeof(alphanum) - 1;
|
||||
|
||||
// Two bytes per character, corrected for modulo bias via rejection sampling.
|
||||
const int bucketSize = 65536 / size;
|
||||
const int limit = bucketSize * size;
|
||||
|
||||
QString ret;
|
||||
int size = sizeof(alphanum) - 1;
|
||||
|
||||
ret.reserve(len);
|
||||
QByteArray random = CryptoUtil::randomBytes(len * 2);
|
||||
int bytesUsed = 0;
|
||||
for (int i = 0; i < len; ++i) {
|
||||
ret.append(alphanum[rng->rand(0, size)]);
|
||||
unsigned int value;
|
||||
do {
|
||||
if (bytesUsed >= random.size()) {
|
||||
random = CryptoUtil::randomBytes(len * 2);
|
||||
bytesUsed = 0;
|
||||
}
|
||||
value = static_cast<unsigned int>(static_cast<unsigned char>(random.at(bytesUsed))) << 8 |
|
||||
static_cast<unsigned int>(static_cast<unsigned char>(random.at(bytesUsed + 1)));
|
||||
bytesUsed += 2;
|
||||
} while (value >= limit);
|
||||
ret.append(alphanum[value / bucketSize]);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
|
@ -34,5 +50,5 @@ QString PasswordHasher::generateRandomSalt(const int len)
|
|||
|
||||
QString PasswordHasher::generateActivationToken()
|
||||
{
|
||||
return QCryptographicHash::hash(generateRandomSalt().toUtf8(), QCryptographicHash::Md5).toBase64().left(16);
|
||||
return QString(CryptoUtil::randomBytes(16).toBase64().left(16));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ set(oracle_SOURCES
|
|||
src/pages.cpp
|
||||
src/pagetemplates.cpp
|
||||
src/parsehelpers.cpp
|
||||
src/qt-json/json.cpp
|
||||
../cockatrice/src/client/settings/cache_settings.cpp
|
||||
../cockatrice/src/client/settings/card_counter_settings.cpp
|
||||
../cockatrice/src/client/settings/shortcuts_settings.cpp
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
#include "libcockatrice/interfaces/noop_card_preference_provider.h"
|
||||
#include "libcockatrice/interfaces/noop_card_set_priority_controller.h"
|
||||
#include "parsehelpers.h"
|
||||
#include "qt-json/json.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
|
||||
|
|
@ -44,29 +46,30 @@ static CardSet::Priority getSetPriority(const QString &setType, const QString &s
|
|||
|
||||
bool OracleImporter::readSetsFromByteArray(const QByteArray &data)
|
||||
{
|
||||
bool ok;
|
||||
auto setsMap = QtJson::Json::parse(QString(data), ok).toMap().value("data").toMap();
|
||||
if (!ok) {
|
||||
qDebug() << "error: QtJson::Json::parse()";
|
||||
QJsonParseError error;
|
||||
auto doc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qDebug() << "error: QJsonDocument::fromJson():" << error.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto setsObj = doc.object().value("data").toObject();
|
||||
|
||||
QList<SetToDownload> newSetList;
|
||||
|
||||
QListIterator it(setsMap.values());
|
||||
|
||||
while (it.hasNext()) {
|
||||
QVariantMap map = it.next().toMap();
|
||||
QString shortName = map.value("code").toString().toUpper();
|
||||
QString longName = map.value("name").toString();
|
||||
QList<QVariant> setCards = map.value("cards").toList();
|
||||
QString setType = map.value("type").toString();
|
||||
QDate releaseDate = map.value("releaseDate").toDate();
|
||||
for (auto it = setsObj.constBegin(); it != setsObj.constEnd(); ++it) {
|
||||
QJsonObject setObj = it.value().toObject();
|
||||
QString shortName = setObj.value("code").toString().toUpper();
|
||||
QString longName = setObj.value("name").toString();
|
||||
QJsonArray setCards = setObj.value("cards").toArray();
|
||||
QString setType = setObj.value("type").toString();
|
||||
QDate releaseDate = QDate::fromString(setObj.value("releaseDate").toString(), Qt::ISODate);
|
||||
CardSet::Priority priority = getSetPriority(setType, shortName);
|
||||
// capitalize set type
|
||||
if (setType.length() > 0) {
|
||||
// basic grammar for words that aren't capitalized, like in "From the Vault"
|
||||
const QStringList noCapitalize = {"the", "a", "an", "on", "to", "for", "of", "in", "and", "with", "or"};
|
||||
static const QStringList noCapitalize = {"the", "a", "an", "on", "to", "for",
|
||||
"of", "in", "and", "with", "or"};
|
||||
QStringList words = setType.split("_");
|
||||
setType.clear();
|
||||
bool first = false;
|
||||
|
|
@ -74,7 +77,7 @@ bool OracleImporter::readSetsFromByteArray(const QByteArray &data)
|
|||
if (first && noCapitalize.contains(item)) {
|
||||
setType += item + QString(" ");
|
||||
} else {
|
||||
setType += item[0].toUpper() + item.mid(1, -1) + QString(" ");
|
||||
setType += item[0].toUpper() + item.mid(1) + QString(" ");
|
||||
first = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -122,14 +125,8 @@ static void sortAndReduceColors(QString &colors)
|
|||
std::sort(colors.begin(), colors.end(),
|
||||
[](const QChar a, const QChar b) { return colorOrder.value(a, INT_MAX) < colorOrder.value(b, INT_MAX); });
|
||||
// reduce
|
||||
QChar lastChar = '\0';
|
||||
for (int i = 0; i < colors.size(); ++i) {
|
||||
if (colors.at(i) == lastChar) {
|
||||
colors.remove(i, 1);
|
||||
} else {
|
||||
lastChar = colors.at(i);
|
||||
}
|
||||
}
|
||||
auto last = std::unique(colors.begin(), colors.end());
|
||||
colors.erase(last, colors.end());
|
||||
}
|
||||
|
||||
CardInfoPtr OracleImporter::addCard(QString name,
|
||||
|
|
@ -142,9 +139,12 @@ CardInfoPtr OracleImporter::addCard(QString name,
|
|||
// Workaround for card name weirdness
|
||||
name = name.replace("Æ", "AE");
|
||||
name = name.replace("’", "'");
|
||||
if (cards.contains(name)) {
|
||||
CardInfoPtr card = cards.value(name);
|
||||
auto existingIt = cards.constFind(name);
|
||||
if (existingIt != cards.constEnd()) {
|
||||
CardInfoPtr card = existingIt.value();
|
||||
card->addToSet(printingInfo.getSet(), printingInfo);
|
||||
// Only merge legalities when the card has none yet, so multi-format
|
||||
// printings don't overwrite each other's legality lists.
|
||||
if (card->getProperties().filter(formatRegex).empty()) {
|
||||
card->combineLegalities(properties);
|
||||
}
|
||||
|
|
@ -182,8 +182,9 @@ CardInfoPtr OracleImporter::addCard(QString name,
|
|||
|
||||
// DETECT CARD POSITIONING INFO
|
||||
|
||||
bool landscapeOrientation = properties.value("maintype") == "Battle" || properties.value("layout") == "split" ||
|
||||
properties.value("layout") == "planar";
|
||||
QString layoutVal = properties.value("layout");
|
||||
bool landscapeOrientation =
|
||||
properties.value("maintype") == "Battle" || layoutVal == "split" || layoutVal == "planar";
|
||||
|
||||
// cards that enter the field tapped
|
||||
bool cipt = parseCipt(name, text) || landscapeOrientation;
|
||||
|
|
@ -222,12 +223,15 @@ CardInfoPtr OracleImporter::addCard(QString name,
|
|||
return newCard;
|
||||
}
|
||||
|
||||
static QString getStringPropertyFromMap(const QVariantMap &card, const QString &propertyName)
|
||||
static QString getJsonString(const QJsonObject &obj, const QString &key)
|
||||
{
|
||||
return card.contains(propertyName) ? card.value(propertyName).toString() : QString("");
|
||||
// QVariant coerces numbers and booleans to text, while QJsonValue::toString()
|
||||
// returns a null string for them — some MTGJSON fields (manaValue,
|
||||
// convertedManaCost, isOnlineOnly, isRebalanced) carry those types.
|
||||
return obj.value(key).toVariant().toString();
|
||||
}
|
||||
|
||||
int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList<QVariant> &cardsList)
|
||||
int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList)
|
||||
{
|
||||
// mtgjson name => xml name
|
||||
static const QMap<QString, QString> cardProperties{
|
||||
|
|
@ -248,7 +252,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
|
||||
static const QString ptSeparator = "/";
|
||||
static constexpr bool isToken = false;
|
||||
static const QList<QString> setsWithCardsWithSameNameButDifferentText = {"UST"};
|
||||
static const QSet<QString> setsWithCardsWithSameNameButDifferentText = {"UST"};
|
||||
|
||||
int numCards = 0;
|
||||
|
||||
|
|
@ -256,16 +260,16 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
QMap<QString, QPair<QList<SplitCardPart>, QString>> splitCards;
|
||||
|
||||
// Keeps track of all names encountered so far
|
||||
QList<QString> allNameProps;
|
||||
QSet<QString> allNameProps;
|
||||
|
||||
for (const QVariant &cardVar : cardsList) {
|
||||
QVariantMap card = cardVar.toMap();
|
||||
for (const QJsonValue &cardVal : cardsList) {
|
||||
QJsonObject card = cardVal.toObject();
|
||||
|
||||
/* Currently used layouts are:
|
||||
* augment, double_faced_token, flip, host, leveler, meld, normal, planar,
|
||||
* saga, scheme, split, token, transform, vanguard
|
||||
*/
|
||||
QString layout = getStringPropertyFromMap(card, "layout");
|
||||
QString layout = getJsonString(card, "layout");
|
||||
|
||||
// don't import tokens from the json file
|
||||
if (layout == "token") {
|
||||
|
|
@ -273,9 +277,9 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
}
|
||||
|
||||
// normal cards handling
|
||||
QString name = getStringPropertyFromMap(card, "name");
|
||||
QString text = getStringPropertyFromMap(card, "text");
|
||||
QString faceName = getStringPropertyFromMap(card, "faceName");
|
||||
QString name = getJsonString(card, "name");
|
||||
QString text = getJsonString(card, "text");
|
||||
QString faceName = getJsonString(card, "faceName");
|
||||
if (faceName.isEmpty()) {
|
||||
faceName = name;
|
||||
}
|
||||
|
|
@ -283,39 +287,34 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
// card properties
|
||||
QHash<QString, QString> properties;
|
||||
for (auto i = cardProperties.cbegin(), end = cardProperties.cend(); i != end; ++i) {
|
||||
QString mtgjsonProperty = i.key();
|
||||
QString xmlPropertyName = i.value();
|
||||
QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
|
||||
QString propertyValue = getJsonString(card, i.key());
|
||||
if (!propertyValue.isEmpty()) {
|
||||
properties.insert(xmlPropertyName, propertyValue);
|
||||
properties.insert(i.value(), propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
// per-set properties
|
||||
QHash<QString, QString> printingProps;
|
||||
for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) {
|
||||
QString mtgjsonProperty = i.key();
|
||||
QString xmlPropertyName = i.value();
|
||||
QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
|
||||
QString propertyValue = getJsonString(card, i.key());
|
||||
if (!propertyValue.isEmpty()) {
|
||||
printingProps.insert(xmlPropertyName, propertyValue);
|
||||
printingProps.insert(i.value(), propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
// handle flavorNames specially due to double-faced cards
|
||||
QString faceFlavorName = getStringPropertyFromMap(card, "faceFlavorName");
|
||||
QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getStringPropertyFromMap(card, "flavorName");
|
||||
QString faceFlavorName = getJsonString(card, "faceFlavorName");
|
||||
QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getJsonString(card, "flavorName");
|
||||
if (!flavorName.isEmpty()) {
|
||||
printingProps.insert("flavorName", flavorName);
|
||||
}
|
||||
|
||||
// Identifiers
|
||||
QJsonObject identifiers = card.value("identifiers").toObject();
|
||||
for (auto i = identifierProperties.cbegin(), end = identifierProperties.cend(); i != end; ++i) {
|
||||
QString mtgjsonProperty = i.key();
|
||||
QString xmlPropertyName = i.value();
|
||||
QString propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty);
|
||||
QString propertyValue = getJsonString(identifiers, i.key());
|
||||
if (!propertyValue.isEmpty()) {
|
||||
printingProps.insert(xmlPropertyName, propertyValue);
|
||||
printingProps.insert(i.value(), propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -331,21 +330,26 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
allNameProps.contains(faceName) && layout == "normal" && lastChar.isLetter()) {
|
||||
numComponent = " (" + QString(lastChar).toLower() + ")";
|
||||
}
|
||||
allNameProps.append(faceName);
|
||||
allNameProps.insert(faceName);
|
||||
|
||||
// special handling properties
|
||||
QString colors = card.value("colors").toStringList().join("");
|
||||
QString colors;
|
||||
for (const QJsonValue &color : card.value("colors").toArray()) {
|
||||
colors += color.toString();
|
||||
}
|
||||
if (!colors.isEmpty()) {
|
||||
properties.insert("colors", colors);
|
||||
}
|
||||
|
||||
// special handling properties
|
||||
QString colorIdentity = card.value("colorIdentity").toStringList().join("");
|
||||
QString colorIdentity;
|
||||
for (const QJsonValue &color : card.value("colorIdentity").toArray()) {
|
||||
colorIdentity += color.toString();
|
||||
}
|
||||
if (!colorIdentity.isEmpty()) {
|
||||
properties.insert("coloridentity", colorIdentity);
|
||||
}
|
||||
|
||||
const auto &mainCardType = getMainCardType(card.value("types").toStringList());
|
||||
const auto &mainCardType = getMainCardType(card.value("types").toVariant().toStringList());
|
||||
if (mainCardType.isEmpty()) {
|
||||
qDebug() << "warning: no mainCardType for card:" << name;
|
||||
} else {
|
||||
|
|
@ -354,22 +358,22 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
|
||||
// Depending on whether power and/or toughness are present, the format
|
||||
// is either P/T (most common), P (no toughness), or /T (no power).
|
||||
QString power = getStringPropertyFromMap(card, "power");
|
||||
QString toughness = getStringPropertyFromMap(card, "toughness");
|
||||
QString power = getJsonString(card, "power");
|
||||
QString toughness = getJsonString(card, "toughness");
|
||||
if (toughness.isEmpty() && !power.isEmpty()) {
|
||||
properties.insert("pt", power);
|
||||
} else if (!toughness.isEmpty()) {
|
||||
properties.insert("pt", power + ptSeparator + toughness);
|
||||
}
|
||||
|
||||
auto legalities = card.value("legalities").toMap();
|
||||
for (auto i = legalities.cbegin(), end = legalities.cend(); i != end; ++i) {
|
||||
auto legalities = card.value("legalities").toObject();
|
||||
for (auto i = legalities.constBegin(), end = legalities.constEnd(); i != end; ++i) {
|
||||
properties.insert(QString("format-%1").arg(i.key()), i.value().toString().toLower());
|
||||
}
|
||||
|
||||
// split cards are considered a single card, enqueue for later merging
|
||||
if (layout == "split" || layout == "aftermath" || layout == "adventure" || layout == "prepare") {
|
||||
auto _faceName = getStringPropertyFromMap(card, "faceName");
|
||||
auto _faceName = getJsonString(card, "faceName");
|
||||
SplitCardPart split(_faceName, text, properties, printingInfo);
|
||||
auto found_iter = splitCards.find(name + numProperty);
|
||||
if (found_iter == splitCards.end()) {
|
||||
|
|
@ -382,11 +386,11 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
QList<CardRelation *> relatedCards;
|
||||
|
||||
// add other face for split cards as card relation
|
||||
if (!getStringPropertyFromMap(card, "side").isEmpty()) {
|
||||
auto faceManaValue = getStringPropertyFromMap(card, "faceManaValue");
|
||||
if (!getJsonString(card, "side").isEmpty()) {
|
||||
auto faceManaValue = getJsonString(card, "faceManaValue");
|
||||
if (faceManaValue.isEmpty()) {
|
||||
// check the old name for the property, for backwards compatibility purposes
|
||||
faceManaValue = getStringPropertyFromMap(card, "faceConvertedManaCost");
|
||||
faceManaValue = getJsonString(card, "faceConvertedManaCost");
|
||||
}
|
||||
properties["cmc"] = faceManaValue;
|
||||
|
||||
|
|
@ -406,20 +410,21 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
name = faceName;
|
||||
}
|
||||
|
||||
// mtgjon related cards
|
||||
if (card.contains("relatedCards")) {
|
||||
QVariantMap givenRelated = card.value("relatedCards").toMap();
|
||||
// mtgjson related cards
|
||||
QJsonObject givenRelated = card.value("relatedCards").toObject();
|
||||
if (!givenRelated.isEmpty()) {
|
||||
// conjured cards from a spellbook
|
||||
if (givenRelated.contains("spellbook")) {
|
||||
auto spbk = givenRelated.value("spellbook").toStringList();
|
||||
for (const QString &spbkName : spbk) {
|
||||
relatedCards.append(
|
||||
new CardRelation(spbkName, CardRelationType::DoesNotAttach, false, false, 1, true));
|
||||
QJsonArray spellbook = givenRelated.value("spellbook").toArray();
|
||||
if (!spellbook.isEmpty()) {
|
||||
for (const QJsonValue &spbkVal : spellbook) {
|
||||
relatedCards.append(new CardRelation(spbkVal.toString(), CardRelationType::DoesNotAttach, false,
|
||||
false, 1, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CardInfoPtr newCard = addCard(name + numComponent, text, isToken, properties, relatedCards, printingInfo);
|
||||
CardInfoPtr newCard =
|
||||
addCard(name + numComponent, text, isToken, std::move(properties), relatedCards, printingInfo);
|
||||
numCards++;
|
||||
}
|
||||
}
|
||||
|
|
@ -427,7 +432,6 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
// split cards handling
|
||||
static const QString splitCardPropSeparator = QString(" // ");
|
||||
static const QString splitCardTextSeparator = QString("\n\n---\n\n");
|
||||
static const QList<CardRelation *> noRelatedCards = {};
|
||||
|
||||
QList<QPair<QList<SplitCardPart>, QString>> partsAndNames = splitCards.values();
|
||||
for (auto [splitCardParts, name] : partsAndNames) {
|
||||
|
|
@ -453,7 +457,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) {
|
||||
if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty
|
||||
properties.insert(prop, thisCardPropertyValue);
|
||||
} else if (prop == "colors") { // the card is both colors
|
||||
} else if (prop == "colors" || prop == "coloridentity") { // the card is both colors
|
||||
properties.insert(prop, originalPropertyValue + thisCardPropertyValue);
|
||||
} else if (prop == "maintype") { // don't create maintypes with //es in them
|
||||
continue;
|
||||
|
|
@ -465,20 +469,20 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
}
|
||||
}
|
||||
}
|
||||
CardInfoPtr newCard = addCard(name, text, isToken, properties, noRelatedCards, printingInfo);
|
||||
CardInfoPtr newCard = addCard(name, text, isToken, std::move(properties), {}, printingInfo);
|
||||
numCards++;
|
||||
}
|
||||
|
||||
return numCards;
|
||||
}
|
||||
|
||||
FormatRulesNameMap OracleImporter::createDefaultMagicFormats()
|
||||
static FormatRulesNameMap buildDefaultMagicFormats()
|
||||
{
|
||||
// Predefined common exceptions
|
||||
CardCondition superTypeIsBasic;
|
||||
superTypeIsBasic.field = "type";
|
||||
superTypeIsBasic.matchType = "regex";
|
||||
superTypeIsBasic.value = "\bBasic\b[^—]+\bLand\b";
|
||||
superTypeIsBasic.value = R"(\bBasic\b[^—]+\bLand\b)";
|
||||
|
||||
ExceptionRule basicLands;
|
||||
basicLands.conditions.append(superTypeIsBasic);
|
||||
|
|
@ -491,7 +495,6 @@ FormatRulesNameMap OracleImporter::createDefaultMagicFormats()
|
|||
ExceptionRule mayContainAnyNumber;
|
||||
mayContainAnyNumber.conditions.append(anyNumberAllowed);
|
||||
|
||||
// Map to store default rules
|
||||
FormatRulesNameMap defaultFormatRulesNameMap;
|
||||
|
||||
// ----------------- Helper lambda to create format -----------------
|
||||
|
|
@ -537,10 +540,33 @@ FormatRulesNameMap OracleImporter::createDefaultMagicFormats()
|
|||
return defaultFormatRulesNameMap;
|
||||
}
|
||||
|
||||
const FormatRulesNameMap &OracleImporter::createDefaultMagicFormats()
|
||||
{
|
||||
static const FormatRulesNameMap cached = buildDefaultMagicFormats();
|
||||
return cached;
|
||||
}
|
||||
|
||||
int OracleImporter::startImport()
|
||||
{
|
||||
static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController();
|
||||
|
||||
// Pre-allocate the cards hash to avoid rehashing during import. The hash
|
||||
// is keyed by distinct card name rather than by printings: AllPrintings
|
||||
// ships ~100k printings for ~35k names, so reserving the printing count
|
||||
// would overallocate ~3x (against this stack's RAM goal). Collecting
|
||||
// distinct names is cheap — one pass over the already-parsed name fields.
|
||||
{
|
||||
QSet<QString> distinctNames;
|
||||
for (const SetToDownload &curSetToParse : allSets) {
|
||||
for (const QJsonValue &cardValue : curSetToParse.getCards()) {
|
||||
distinctNames.insert(cardValue.toObject().value("name").toString());
|
||||
}
|
||||
}
|
||||
cards.reserve(distinctNames.size());
|
||||
// The set goes out of scope here, handing the ~35k name QStrings back
|
||||
// to the allocator before the (memory-heavy) import loop starts.
|
||||
}
|
||||
|
||||
// add an empty set for tokens
|
||||
CardSetPtr tokenSet =
|
||||
CardSet::newInstance(noOpController, CardSet::TOKENS_SETNAME, tr("Dummy set containing tokens"), "Tokens");
|
||||
|
|
@ -576,6 +602,11 @@ bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUr
|
|||
return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName, sourceUrl, sourceVersion);
|
||||
}
|
||||
|
||||
void OracleImporter::releaseSetData()
|
||||
{
|
||||
allSets.clear();
|
||||
}
|
||||
|
||||
void OracleImporter::clear()
|
||||
{
|
||||
sets.clear();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#ifndef ORACLEIMPORTER_H
|
||||
#define ORACLEIMPORTER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QRegularExpression>
|
||||
#include <QVariant>
|
||||
|
|
@ -44,7 +46,7 @@ class SetToDownload
|
|||
{
|
||||
private:
|
||||
QString shortName, longName;
|
||||
QList<QVariant> cards;
|
||||
QJsonArray cards;
|
||||
QDate releaseDate;
|
||||
QString setType;
|
||||
CardSet::Priority priority;
|
||||
|
|
@ -58,7 +60,7 @@ public:
|
|||
{
|
||||
return longName;
|
||||
}
|
||||
const QList<QVariant> &getCards() const
|
||||
const QJsonArray &getCards() const
|
||||
{
|
||||
return cards;
|
||||
}
|
||||
|
|
@ -76,7 +78,7 @@ public:
|
|||
}
|
||||
SetToDownload(QString _shortName,
|
||||
QString _longName,
|
||||
QList<QVariant> _cards,
|
||||
QJsonArray _cards,
|
||||
CardSet::Priority _priority,
|
||||
QString _setType = QString(),
|
||||
const QDate &_releaseDate = QDate())
|
||||
|
|
@ -154,8 +156,11 @@ public:
|
|||
bool readSetsFromByteArray(const QByteArray &data);
|
||||
int startImport();
|
||||
bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion);
|
||||
int importCardsFromSet(const CardSetPtr ¤tSet, const QList<QVariant> &cardsList);
|
||||
FormatRulesNameMap createDefaultMagicFormats();
|
||||
int importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList);
|
||||
/**
|
||||
* @brief Returns the default format rules. The result is memoized on first use and must be treated as immutable.
|
||||
*/
|
||||
const FormatRulesNameMap &createDefaultMagicFormats();
|
||||
const CardNameMap &getCardList() const
|
||||
{
|
||||
return cards;
|
||||
|
|
@ -164,6 +169,7 @@ public:
|
|||
{
|
||||
return allSets;
|
||||
}
|
||||
void releaseSetData();
|
||||
void clear();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -560,6 +560,9 @@ void SaveSetsPage::initializePage()
|
|||
|
||||
int setsImported = wizard()->importer->startImport();
|
||||
|
||||
// JSON data no longer needed after CardInfo objects are built
|
||||
wizard()->importer->releaseSetData();
|
||||
|
||||
if (setsImported == 0) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("No set has been imported."));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
Eeli Reilin <eeli@emicode.fi>
|
||||
Luis Gustavo S. Barreto <gustavosbarreto@gmail.com>
|
||||
Stephen Kockentiedt <Stephen@Kockentiedt.name>
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
Copyright 2011 Eeli Reilin. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The views and conclusions contained in the software and documentation
|
||||
are those of the authors and should not be interpreted as representing
|
||||
official policies, either expressed or implied, of Eeli Reilin.
|
||||
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
########################################################################
|
||||
1. INTRODUCTION
|
||||
|
||||
The Json class is a simple class for parsing JSON data into a QVariant
|
||||
hierarchies. Now, we can also reverse the process and serialize
|
||||
QVariant hierarchies into valid JSON data.
|
||||
|
||||
|
||||
########################################################################
|
||||
2. HOW TO USE
|
||||
|
||||
The parser is really easy to use. Let's say we have the following
|
||||
QString of JSON data:
|
||||
|
||||
------------------------------------------------------------------------
|
||||
{
|
||||
"encoding" : "UTF-8",
|
||||
"plug-ins" : [
|
||||
"python",
|
||||
"c++",
|
||||
"ruby"
|
||||
],
|
||||
"indent" : {
|
||||
"length" : 3,
|
||||
"use_space" : true
|
||||
}
|
||||
}
|
||||
------------------------------------------------------------------------
|
||||
|
||||
We would first call the parse-method:
|
||||
|
||||
------------------------------------------------------------------------
|
||||
//Say that we're using the QtJson namespace
|
||||
using namespace QtJson;
|
||||
bool ok;
|
||||
//json is a QString containing the JSON data
|
||||
QVariantMap result = Json::parse(json, ok).toMap();
|
||||
|
||||
if(!ok) {
|
||||
qFatal("An error occurred during parsing");
|
||||
exit(1);
|
||||
}
|
||||
------------------------------------------------------------------------
|
||||
|
||||
Assuming the parsing process completed without errors, we would then
|
||||
go through the hierarchy:
|
||||
|
||||
------------------------------------------------------------------------
|
||||
qDebug() << "encoding:" << result["encoding"].toString();
|
||||
qDebug() << "plugins:";
|
||||
|
||||
foreach(QVariant plugin, result["plug-ins"].toList()) {
|
||||
qDebug() << "\t-" << plugin.toString();
|
||||
}
|
||||
|
||||
QVariantMap nestedMap = result["indent"].toMap();
|
||||
qDebug() << "length:" << nestedMap["length"].toInt();
|
||||
qDebug() << "use_space:" << nestedMap["use_space"].toBool();
|
||||
------------------------------------------------------------------------
|
||||
|
||||
The previous code would print out the following:
|
||||
|
||||
------------------------------------------------------------------------
|
||||
encoding: "UTF-8"
|
||||
plugins:
|
||||
- "python"
|
||||
- "c++"
|
||||
- "ruby"
|
||||
length: 3
|
||||
use_space: true
|
||||
------------------------------------------------------------------------
|
||||
|
||||
To write JSON data from Qt object is as simple as parsing:
|
||||
|
||||
------------------------------------------------------------------------
|
||||
QVariantMap map;
|
||||
map["name"] = "Name";
|
||||
map["age"] = 22;
|
||||
|
||||
QByteArray data = Json::serialize(map);
|
||||
------------------------------------------------------------------------
|
||||
|
||||
The byte array 'data' contains valid JSON data:
|
||||
|
||||
------------------------------------------------------------------------
|
||||
{
|
||||
name: "Luis Gustavo",
|
||||
age: 22,
|
||||
}
|
||||
------------------------------------------------------------------------
|
||||
|
||||
|
||||
########################################################################
|
||||
4. CONTRIBUTING
|
||||
|
||||
The code is available to download at GitHub. Contribute if you dare!
|
||||
|
|
@ -1,545 +0,0 @@
|
|||
/* Copyright 2011 Eeli Reilin. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY EXPRESS OR
|
||||
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
* EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
||||
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation
|
||||
* are those of the authors and should not be interpreted as representing
|
||||
* official policies, either expressed or implied, of Eeli Reilin.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file json.cpp
|
||||
*/
|
||||
|
||||
#include "json.h"
|
||||
|
||||
#include <QMetaType>
|
||||
#include <iostream>
|
||||
|
||||
namespace QtJson
|
||||
{
|
||||
|
||||
static QString sanitizeString(QString str)
|
||||
{
|
||||
str.replace(QLatin1String("\\"), QLatin1String("\\\\"));
|
||||
str.replace(QLatin1String("\""), QLatin1String("\\\""));
|
||||
str.replace(QLatin1String("\b"), QLatin1String("\\b"));
|
||||
str.replace(QLatin1String("\f"), QLatin1String("\\f"));
|
||||
str.replace(QLatin1String("\n"), QLatin1String("\\n"));
|
||||
str.replace(QLatin1String("\r"), QLatin1String("\\r"));
|
||||
str.replace(QLatin1String("\t"), QLatin1String("\\t"));
|
||||
return QString(QLatin1String("\"%1\"")).arg(str);
|
||||
}
|
||||
|
||||
static QByteArray join(const QList<QByteArray> &list, const QByteArray &sep)
|
||||
{
|
||||
QByteArray res;
|
||||
for (const QByteArray &i : list) {
|
||||
if (!res.isEmpty()) {
|
||||
res += sep;
|
||||
}
|
||||
res += i;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse
|
||||
*/
|
||||
QVariant Json::parse(const QString &json)
|
||||
{
|
||||
bool success = true;
|
||||
return Json::parse(json, success);
|
||||
}
|
||||
|
||||
/**
|
||||
* parse
|
||||
*/
|
||||
QVariant Json::parse(const QString &json, bool &success)
|
||||
{
|
||||
success = true;
|
||||
|
||||
// Return an empty QVariant if the JSON data is either null or empty
|
||||
if (!json.isNull() || !json.isEmpty()) {
|
||||
// We'll start from index 0
|
||||
int index = 0;
|
||||
|
||||
// Parse the first value
|
||||
QVariant value = Json::parseValue(json, index, success);
|
||||
|
||||
// Return the parsed value
|
||||
return value;
|
||||
} else {
|
||||
// Return the empty QVariant
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray Json::serialize(const QVariant &data)
|
||||
{
|
||||
bool success = true;
|
||||
return Json::serialize(data, success);
|
||||
}
|
||||
|
||||
QByteArray Json::serialize(const QVariant &data, bool &success)
|
||||
{
|
||||
QByteArray str;
|
||||
success = true;
|
||||
|
||||
if (!data.isValid()) // invalid or null?
|
||||
{
|
||||
str = "null";
|
||||
}
|
||||
else if ((data.typeId() == QMetaType::Type::QVariantList) ||
|
||||
(data.typeId() == QMetaType::Type::QStringList)) // variant is a list?
|
||||
{
|
||||
QList<QByteArray> values;
|
||||
const QVariantList list = data.toList();
|
||||
for (const QVariant &v : list) {
|
||||
QByteArray serializedValue = serialize(v);
|
||||
if (serializedValue.isNull()) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
values << serializedValue;
|
||||
}
|
||||
|
||||
str = "[ " + join(values, ", ") + " ]";
|
||||
}
|
||||
else if ((data.typeId() == QMetaType::Type::QVariantHash)) // variant is a hash?
|
||||
{
|
||||
const QVariantHash vhash = data.toHash();
|
||||
QHashIterator<QString, QVariant> it(vhash);
|
||||
str = "{ ";
|
||||
QList<QByteArray> pairs;
|
||||
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
QByteArray serializedValue = serialize(it.value());
|
||||
|
||||
if (serializedValue.isNull()) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue;
|
||||
}
|
||||
|
||||
str += join(pairs, ", ");
|
||||
str += " }";
|
||||
}
|
||||
else if ((data.typeId() == QMetaType::Type::QVariantMap)) // variant is a map?
|
||||
{
|
||||
const QVariantMap vmap = data.toMap();
|
||||
QMapIterator<QString, QVariant> it(vmap);
|
||||
str = "{ ";
|
||||
QList<QByteArray> pairs;
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
QByteArray serializedValue = serialize(it.value());
|
||||
if (serializedValue.isNull()) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue;
|
||||
}
|
||||
str += join(pairs, ", ");
|
||||
str += " }";
|
||||
}
|
||||
else if ((data.typeId() == QMetaType::Type::QString) ||
|
||||
(data.typeId() == QMetaType::Type::QByteArray)) // a string or a byte array?
|
||||
{
|
||||
str = sanitizeString(data.toString()).toUtf8();
|
||||
}
|
||||
else if (data.typeId() == QMetaType::Type::Double) // double?
|
||||
{
|
||||
str = QByteArray::number(data.toDouble(), 'g', 20);
|
||||
if (!str.contains(".") && !str.contains("e")) {
|
||||
str += ".0";
|
||||
}
|
||||
}
|
||||
else if (data.typeId() == QMetaType::Type::Bool) // boolean value?
|
||||
{
|
||||
str = data.toBool() ? "true" : "false";
|
||||
}
|
||||
else if (data.typeId() == QMetaType::Type::ULongLong) // large unsigned number?
|
||||
{
|
||||
str = QByteArray::number(data.value<qulonglong>());
|
||||
} else if (data.canConvert<qlonglong>()) // any signed number?
|
||||
{
|
||||
str = QByteArray::number(data.value<qlonglong>());
|
||||
} else if (data.canConvert<long>()) {
|
||||
str = QString::number(data.value<long>()).toUtf8();
|
||||
} else if (data.canConvert<QString>()) // can value be converted to string?
|
||||
{
|
||||
// this will catch QDate, QDateTime, QUrl, ...
|
||||
str = sanitizeString(data.toString()).toUtf8();
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
if (success) {
|
||||
return str;
|
||||
} else {
|
||||
return QByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* parseValue
|
||||
*/
|
||||
QVariant Json::parseValue(const QString &json, int &index, bool &success)
|
||||
{
|
||||
// Determine what kind of data we should parse by
|
||||
// checking out the upcoming token
|
||||
switch (Json::lookAhead(json, index)) {
|
||||
case JsonTokenString:
|
||||
return Json::parseString(json, index, success);
|
||||
case JsonTokenNumber:
|
||||
return Json::parseNumber(json, index);
|
||||
case JsonTokenCurlyOpen:
|
||||
return Json::parseObject(json, index, success);
|
||||
case JsonTokenSquaredOpen:
|
||||
return Json::parseArray(json, index, success);
|
||||
case JsonTokenTrue:
|
||||
Json::nextToken(json, index);
|
||||
return QVariant(true);
|
||||
case JsonTokenFalse:
|
||||
Json::nextToken(json, index);
|
||||
return QVariant(false);
|
||||
case JsonTokenNull:
|
||||
Json::nextToken(json, index);
|
||||
return QVariant();
|
||||
case JsonTokenNone:
|
||||
break;
|
||||
}
|
||||
|
||||
// If there were no tokens, flag the failure and return an empty QVariant
|
||||
success = false;
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
/**
|
||||
* parseObject
|
||||
*/
|
||||
QVariant Json::parseObject(const QString &json, int &index, bool &success)
|
||||
{
|
||||
QVariantMap map;
|
||||
int token;
|
||||
|
||||
// Get rid of the whitespace and increment index
|
||||
Json::nextToken(json, index);
|
||||
|
||||
// Loop through all of the key/value pairs of the object
|
||||
bool done = false;
|
||||
while (!done) {
|
||||
// Get the upcoming token
|
||||
token = Json::lookAhead(json, index);
|
||||
|
||||
if (token == JsonTokenNone) {
|
||||
success = false;
|
||||
return QVariantMap();
|
||||
} else if (token == JsonTokenComma) {
|
||||
Json::nextToken(json, index);
|
||||
} else if (token == JsonTokenCurlyClose) {
|
||||
Json::nextToken(json, index);
|
||||
return map;
|
||||
} else {
|
||||
// Parse the key/value pair's name
|
||||
QString name = Json::parseString(json, index, success).toString();
|
||||
|
||||
if (!success) {
|
||||
return QVariantMap();
|
||||
}
|
||||
|
||||
// Get the next token
|
||||
token = Json::nextToken(json, index);
|
||||
|
||||
// If the next token is not a colon, flag the failure
|
||||
// return an empty QVariant
|
||||
if (token != JsonTokenColon) {
|
||||
success = false;
|
||||
return QVariant(QVariantMap());
|
||||
}
|
||||
|
||||
// Parse the key/value pair's value
|
||||
QVariant value = Json::parseValue(json, index, success);
|
||||
|
||||
if (!success) {
|
||||
return QVariantMap();
|
||||
}
|
||||
|
||||
// Assign the value to the key in the map
|
||||
map[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the map successfully
|
||||
return QVariant(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* parseArray
|
||||
*/
|
||||
QVariant Json::parseArray(const QString &json, int &index, bool &success)
|
||||
{
|
||||
QVariantList list;
|
||||
|
||||
Json::nextToken(json, index);
|
||||
|
||||
bool done = false;
|
||||
while (!done) {
|
||||
int token = Json::lookAhead(json, index);
|
||||
|
||||
if (token == JsonTokenNone) {
|
||||
success = false;
|
||||
return QVariantList();
|
||||
} else if (token == JsonTokenComma) {
|
||||
Json::nextToken(json, index);
|
||||
} else if (token == JsonTokenSquaredClose) {
|
||||
Json::nextToken(json, index);
|
||||
break;
|
||||
} else {
|
||||
QVariant value = Json::parseValue(json, index, success);
|
||||
|
||||
if (!success) {
|
||||
return QVariantList();
|
||||
}
|
||||
|
||||
list.push_back(value);
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* parseString
|
||||
*/
|
||||
QVariant Json::parseString(const QString &json, int &index, bool &success)
|
||||
{
|
||||
QString s;
|
||||
QChar c;
|
||||
|
||||
Json::eatWhitespace(json, index);
|
||||
|
||||
c = json[index++];
|
||||
|
||||
bool complete = false;
|
||||
while (!complete) {
|
||||
if (index == json.size()) {
|
||||
break;
|
||||
}
|
||||
|
||||
c = json[index++];
|
||||
|
||||
if (c == '\"') {
|
||||
complete = true;
|
||||
break;
|
||||
} else if (c == '\\') {
|
||||
if (index == json.size()) {
|
||||
break;
|
||||
}
|
||||
|
||||
c = json[index++];
|
||||
|
||||
if (c == '\"') {
|
||||
s.append('\"');
|
||||
} else if (c == '\\') {
|
||||
s.append('\\');
|
||||
} else if (c == '/') {
|
||||
s.append('/');
|
||||
} else if (c == 'b') {
|
||||
s.append('\b');
|
||||
} else if (c == 'f') {
|
||||
s.append('\f');
|
||||
} else if (c == 'n') {
|
||||
s.append('\n');
|
||||
} else if (c == 'r') {
|
||||
s.append('\r');
|
||||
} else if (c == 't') {
|
||||
s.append('\t');
|
||||
} else if (c == 'u') {
|
||||
int remainingLength = json.size() - index;
|
||||
|
||||
if (remainingLength >= 4) {
|
||||
QString unicodeStr = json.mid(index, 4);
|
||||
|
||||
int symbol = unicodeStr.toInt(0, 16);
|
||||
|
||||
s.append(QChar(symbol));
|
||||
|
||||
index += 4;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (!complete) {
|
||||
success = false;
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
return QVariant(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* parseNumber
|
||||
*/
|
||||
QVariant Json::parseNumber(const QString &json, int &index)
|
||||
{
|
||||
Json::eatWhitespace(json, index);
|
||||
|
||||
int lastIndex = Json::lastIndexOfNumber(json, index);
|
||||
int charLength = (lastIndex - index) + 1;
|
||||
QString numberStr;
|
||||
|
||||
numberStr = json.mid(index, charLength);
|
||||
|
||||
index = lastIndex + 1;
|
||||
|
||||
if (numberStr.contains('.')) {
|
||||
return QVariant(numberStr.toDouble(NULL));
|
||||
} else if (numberStr.startsWith('-')) {
|
||||
return QVariant(numberStr.toLongLong(NULL));
|
||||
} else {
|
||||
return QVariant(numberStr.toULongLong(NULL));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* lastIndexOfNumber
|
||||
*/
|
||||
int Json::lastIndexOfNumber(const QString &json, int index)
|
||||
{
|
||||
static const QString numericCharacters("0123456789+-.eE");
|
||||
int lastIndex;
|
||||
|
||||
for (lastIndex = index; lastIndex < json.size(); lastIndex++) {
|
||||
if (numericCharacters.indexOf(json[lastIndex]) == -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lastIndex - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* eatWhitespace
|
||||
*/
|
||||
void Json::eatWhitespace(const QString &json, int &index)
|
||||
{
|
||||
static const QString whitespaceChars(" \t\n\r");
|
||||
for (; index < json.size(); index++) {
|
||||
if (whitespaceChars.indexOf(json[index]) == -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* lookAhead
|
||||
*/
|
||||
int Json::lookAhead(const QString &json, int index)
|
||||
{
|
||||
int saveIndex = index;
|
||||
return Json::nextToken(json, saveIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* nextToken
|
||||
*/
|
||||
int Json::nextToken(const QString &json, int &index)
|
||||
{
|
||||
Json::eatWhitespace(json, index);
|
||||
|
||||
if (index == json.size()) {
|
||||
return JsonTokenNone;
|
||||
}
|
||||
|
||||
QChar c = json[index];
|
||||
index++;
|
||||
switch (c.toLatin1()) {
|
||||
case '{':
|
||||
return JsonTokenCurlyOpen;
|
||||
case '}':
|
||||
return JsonTokenCurlyClose;
|
||||
case '[':
|
||||
return JsonTokenSquaredOpen;
|
||||
case ']':
|
||||
return JsonTokenSquaredClose;
|
||||
case ',':
|
||||
return JsonTokenComma;
|
||||
case '"':
|
||||
return JsonTokenString;
|
||||
case '0':
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
case '-':
|
||||
return JsonTokenNumber;
|
||||
case ':':
|
||||
return JsonTokenColon;
|
||||
}
|
||||
|
||||
index--;
|
||||
|
||||
int remainingLength = json.size() - index;
|
||||
|
||||
// True
|
||||
if (remainingLength >= 4) {
|
||||
if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && json[index + 3] == 'e') {
|
||||
index += 4;
|
||||
return JsonTokenTrue;
|
||||
}
|
||||
}
|
||||
|
||||
// False
|
||||
if (remainingLength >= 5) {
|
||||
if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && json[index + 3] == 's' &&
|
||||
json[index + 4] == 'e') {
|
||||
index += 5;
|
||||
return JsonTokenFalse;
|
||||
}
|
||||
}
|
||||
|
||||
// Null
|
||||
if (remainingLength >= 4) {
|
||||
if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && json[index + 3] == 'l') {
|
||||
index += 4;
|
||||
return JsonTokenNull;
|
||||
}
|
||||
}
|
||||
|
||||
return JsonTokenNone;
|
||||
}
|
||||
|
||||
} // namespace QtJson
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
/* Copyright 2011 Eeli Reilin. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY EXPRESS OR
|
||||
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
* EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
||||
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation
|
||||
* are those of the authors and should not be interpreted as representing
|
||||
* official policies, either expressed or implied, of Eeli Reilin.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file json.h
|
||||
*/
|
||||
|
||||
#ifndef JSON_H
|
||||
#define JSON_H
|
||||
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
namespace QtJson
|
||||
{
|
||||
|
||||
/**
|
||||
* \enum JsonToken
|
||||
*/
|
||||
enum JsonToken
|
||||
{
|
||||
JsonTokenNone = 0,
|
||||
JsonTokenCurlyOpen = 1,
|
||||
JsonTokenCurlyClose = 2,
|
||||
JsonTokenSquaredOpen = 3,
|
||||
JsonTokenSquaredClose = 4,
|
||||
JsonTokenColon = 5,
|
||||
JsonTokenComma = 6,
|
||||
JsonTokenString = 7,
|
||||
JsonTokenNumber = 8,
|
||||
JsonTokenTrue = 9,
|
||||
JsonTokenFalse = 10,
|
||||
JsonTokenNull = 11
|
||||
};
|
||||
|
||||
/**
|
||||
* \class Json
|
||||
* \brief A JSON data parser
|
||||
*
|
||||
* Json parses a JSON data into a QVariant hierarchy.
|
||||
*/
|
||||
class Json
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Parse a JSON string
|
||||
*
|
||||
* \param json The JSON data
|
||||
*/
|
||||
static QVariant parse(const QString &json);
|
||||
|
||||
/**
|
||||
* Parse a JSON string
|
||||
*
|
||||
* \param json The JSON data
|
||||
* \param success The success of the parsing
|
||||
*/
|
||||
static QVariant parse(const QString &json, bool &success);
|
||||
|
||||
/**
|
||||
* This method generates a textual JSON representation
|
||||
*
|
||||
* \param data The JSON data generated by the parser.
|
||||
* \param success The success of the serialization
|
||||
*/
|
||||
static QByteArray serialize(const QVariant &data);
|
||||
|
||||
/**
|
||||
* This method generates a textual JSON representation
|
||||
*
|
||||
* \param data The JSON data generated by the parser.
|
||||
* \param success The success of the serialization
|
||||
*
|
||||
* \return QByteArray Textual JSON representation
|
||||
*/
|
||||
static QByteArray serialize(const QVariant &data, bool &success);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Parses a value starting from index
|
||||
*
|
||||
* \param json The JSON data
|
||||
* \param index The start index
|
||||
* \param success The success of the parse process
|
||||
*
|
||||
* \return QVariant The parsed value
|
||||
*/
|
||||
static QVariant parseValue(const QString &json, int &index,
|
||||
bool &success);
|
||||
|
||||
/**
|
||||
* Parses an object starting from index
|
||||
*
|
||||
* \param json The JSON data
|
||||
* \param index The start index
|
||||
* \param success The success of the object parse
|
||||
*
|
||||
* \return QVariant The parsed object map
|
||||
*/
|
||||
static QVariant parseObject(const QString &json, int &index,
|
||||
bool &success);
|
||||
|
||||
/**
|
||||
* Parses an array starting from index
|
||||
*
|
||||
* \param json The JSON data
|
||||
* \param index The starting index
|
||||
* \param success The success of the array parse
|
||||
*
|
||||
* \return QVariant The parsed variant array
|
||||
*/
|
||||
static QVariant parseArray(const QString &json, int &index,
|
||||
bool &success);
|
||||
|
||||
/**
|
||||
* Parses a string starting from index
|
||||
*
|
||||
* \param json The JSON data
|
||||
* \param index The starting index
|
||||
* \param success The success of the string parse
|
||||
*
|
||||
* \return QVariant The parsed string
|
||||
*/
|
||||
static QVariant parseString(const QString &json, int &index,
|
||||
bool &success);
|
||||
|
||||
/**
|
||||
* Parses a number starting from index
|
||||
*
|
||||
* \param json The JSON data
|
||||
* \param index The starting index
|
||||
*
|
||||
* \return QVariant The parsed number
|
||||
*/
|
||||
static QVariant parseNumber(const QString &json, int &index);
|
||||
|
||||
/**
|
||||
* Get the last index of a number starting from index
|
||||
*
|
||||
* \param json The JSON data
|
||||
* \param index The starting index
|
||||
*
|
||||
* \return The last index of the number
|
||||
*/
|
||||
static int lastIndexOfNumber(const QString &json, int index);
|
||||
|
||||
/**
|
||||
* Skip unwanted whitespace symbols starting from index
|
||||
*
|
||||
* \param json The JSON data
|
||||
* \param index The start index
|
||||
*/
|
||||
static void eatWhitespace(const QString &json, int &index);
|
||||
|
||||
/**
|
||||
* Check what token lies ahead
|
||||
*
|
||||
* \param json The JSON data
|
||||
* \param index The starting index
|
||||
*
|
||||
* \return int The upcoming token
|
||||
*/
|
||||
static int lookAhead(const QString &json, int index);
|
||||
|
||||
/**
|
||||
* Get the next JSON token
|
||||
*
|
||||
* \param json The JSON data
|
||||
* \param index The starting index
|
||||
*
|
||||
* \return int The next JSON token
|
||||
*/
|
||||
static int nextToken(const QString &json, int &index);
|
||||
};
|
||||
|
||||
|
||||
} //end namespace
|
||||
|
||||
#endif //JSON_H
|
||||
|
|
@ -33,6 +33,7 @@
|
|||
#include <QtGlobal>
|
||||
#include <iostream>
|
||||
#include <libcockatrice/rng/rng_sfmt.h>
|
||||
#include <libcockatrice/utility/cryptoutil.h>
|
||||
#include <libcockatrice/utility/passwordhasher.h>
|
||||
|
||||
RNG_Abstract *rng;
|
||||
|
|
@ -169,7 +170,7 @@ int main(int argc, char *argv[])
|
|||
|
||||
signalhandler = new SignalHandler();
|
||||
|
||||
rng = new RNG_SFMT;
|
||||
rng = new RNG_SFMT(CryptoUtil::randomUInt64());
|
||||
|
||||
std::cerr << "Servatrice " << VERSION_STRING << " starting." << std::endl;
|
||||
std::cerr << "-------------------------" << std::endl;
|
||||
|
|
|
|||
|
|
@ -7,3 +7,63 @@ endif()
|
|||
target_link_libraries(parse_cipt_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
||||
|
||||
add_test(NAME parse_cipt_test COMMAND parse_cipt_test)
|
||||
|
||||
# Oracle importer unit tests
|
||||
add_executable(
|
||||
oracle_importer_test ${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp
|
||||
oracle_importer_test.cpp
|
||||
)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(oracle_importer_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
oracle_importer_test libcockatrice_card libcockatrice_interfaces Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
|
||||
add_test(NAME oracle_importer_test COMMAND oracle_importer_test)
|
||||
|
||||
# Oracle importer benchmark tests (manual, not run in CI, incl. RAM benchmark)
|
||||
# Optional compression libs, mirrored from oracle/CMakeLists.txt, so the benchmark
|
||||
# can download and decompress whatever AllPrintings format the default URL selects.
|
||||
find_package(ZLIB)
|
||||
if(ZLIB_FOUND)
|
||||
add_definitions("-DHAS_ZLIB")
|
||||
set(_ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/zip/unzip.cpp ../../oracle/src/zip/zipglobal.cpp)
|
||||
set(_ORACLE_BENCH_EXTRA_LIBRARIES ${ZLIB_LIBRARIES})
|
||||
include_directories(${ZLIB_INCLUDE_DIRS})
|
||||
else()
|
||||
message(STATUS "Oracle tests: zlib not found; zip download benchmark disabled")
|
||||
endif()
|
||||
|
||||
find_package(LibLZMA)
|
||||
if(LIBLZMA_FOUND)
|
||||
add_definitions("-DHAS_LZMA")
|
||||
list(APPEND _ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/lzma/decompress.cpp)
|
||||
list(APPEND _ORACLE_BENCH_EXTRA_LIBRARIES ${LIBLZMA_LIBRARIES})
|
||||
include_directories(${LIBLZMA_INCLUDE_DIRS})
|
||||
else()
|
||||
message(STATUS "Oracle tests: LibLZMA not found; xz download benchmark disabled")
|
||||
endif()
|
||||
|
||||
add_executable(
|
||||
oracle_importer_benchmark_test
|
||||
${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp
|
||||
oracle_importer_benchmark_test.cpp ${_ORACLE_BENCH_EXTRA_SOURCES}
|
||||
)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(oracle_importer_benchmark_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
oracle_importer_benchmark_test
|
||||
libcockatrice_card
|
||||
libcockatrice_interfaces
|
||||
Threads::Threads
|
||||
${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
${_ORACLE_BENCH_EXTRA_LIBRARIES}
|
||||
)
|
||||
|
|
|
|||
577
tests/oracle/oracle_importer_benchmark_test.cpp
Normal file
577
tests/oracle/oracle_importer_benchmark_test.cpp
Normal file
|
|
@ -0,0 +1,577 @@
|
|||
#include "../../oracle/src/oracleimporter.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QBuffer>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QElapsedTimer>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||
|
||||
#if defined(HAS_LZMA)
|
||||
#include "../../oracle/src/lzma/decompress.h"
|
||||
#endif
|
||||
#if defined(HAS_ZLIB)
|
||||
#include "../../oracle/src/zip/unzip.h"
|
||||
#endif
|
||||
#if defined(Q_OS_MACOS)
|
||||
#include <mach/mach.h>
|
||||
#include <sys/resource.h>
|
||||
#endif
|
||||
|
||||
// Helper: build a synthetic MTGJSON-style JSON with the given number of sets and cards per set
|
||||
static QByteArray buildSyntheticData(int numSets, int cardsPerSet)
|
||||
{
|
||||
QJsonObject dataObj;
|
||||
for (int s = 0; s < numSets; ++s) {
|
||||
QJsonArray cardsArray;
|
||||
for (int c = 0; c < cardsPerSet; ++c) {
|
||||
QJsonObject card;
|
||||
card["name"] = QString("Card %1").arg(s * cardsPerSet + c);
|
||||
card["text"] = "This is a test card with some rules text.";
|
||||
card["layout"] = "normal";
|
||||
card["manaCost"] = "{W}";
|
||||
card["type"] = "Creature — Human";
|
||||
card["power"] = "2";
|
||||
card["toughness"] = "2";
|
||||
card["colors"] = QJsonArray{"W"};
|
||||
card["colorIdentity"] = QJsonArray{"W"};
|
||||
card["types"] = QJsonArray{"Creature"};
|
||||
// Real MTGJSON types: floats and booleans, not strings. This
|
||||
// exercises the QVariant coercion in the property reader.
|
||||
card["convertedManaCost"] = 1.0;
|
||||
card["manaValue"] = 1.0;
|
||||
card["isOnlineOnly"] = false;
|
||||
card["isRebalanced"] = false;
|
||||
|
||||
QJsonObject legalities;
|
||||
legalities["standard"] = "legal";
|
||||
legalities["modern"] = "legal";
|
||||
legalities["legacy"] = "legal";
|
||||
legalities["vintage"] = "legal";
|
||||
legalities["commander"] = "legal";
|
||||
card["legalities"] = legalities;
|
||||
|
||||
QJsonObject identifiers;
|
||||
identifiers["scryfallId"] = QString("id-%1-%2").arg(s).arg(c);
|
||||
card["identifiers"] = identifiers;
|
||||
|
||||
// In AllPrintings, number and rarity are flat fields on the card
|
||||
// object, exactly as set below.
|
||||
card["number"] = QString::number(c + 1);
|
||||
card["rarity"] = "common";
|
||||
|
||||
cardsArray.append(card);
|
||||
}
|
||||
|
||||
QJsonObject setObj;
|
||||
setObj["code"] = QString("T%1").arg(s, 2, 10, QChar('0'));
|
||||
setObj["name"] = QString("Test Set %1").arg(s);
|
||||
setObj["type"] = "expansion";
|
||||
setObj["releaseDate"] = "2024-01-01";
|
||||
setObj["cards"] = cardsArray;
|
||||
|
||||
dataObj[QString("T%1").arg(s, 2, 10, QChar('0'))] = setObj;
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
root["data"] = dataObj;
|
||||
return QJsonDocument(root).toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Import throughput benchmark
|
||||
// ============================================================================
|
||||
|
||||
TEST(OracleBenchmark, ImportThroughput)
|
||||
{
|
||||
static constexpr int numSets = 10;
|
||||
static constexpr int cardsPerSet = 500;
|
||||
|
||||
QByteArray data = buildSyntheticData(numSets, cardsPerSet);
|
||||
|
||||
OracleImporter importer;
|
||||
|
||||
// Phase 1: Parse JSON
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
bool ok = importer.readSetsFromByteArray(data);
|
||||
ASSERT_TRUE(ok);
|
||||
qint64 parseMs = timer.elapsed();
|
||||
|
||||
// Phase 2: Import cards
|
||||
timer.restart();
|
||||
int importedSets = importer.startImport();
|
||||
qint64 importMs = timer.elapsed();
|
||||
|
||||
int totalImported = 0;
|
||||
for (const auto &card : importer.getCardList()) {
|
||||
Q_UNUSED(card);
|
||||
totalImported++;
|
||||
}
|
||||
|
||||
// The fixture generates globally unique card names, so the expected
|
||||
// counts are exact: a regression here means cards were dropped.
|
||||
ASSERT_EQ(importedSets, numSets);
|
||||
ASSERT_EQ(totalImported, numSets * cardsPerSet);
|
||||
// Real-data probe: numeric convertedManaCost must be coerced to text
|
||||
// (regression for the QJsonValue::toString() reader in #7214).
|
||||
auto probeCard = importer.getCardList().value("Card 0");
|
||||
ASSERT_FALSE(probeCard.isNull());
|
||||
ASSERT_EQ(probeCard->getProperty("cmc"), "1");
|
||||
|
||||
qDebug().noquote()
|
||||
<< QString("Oracle Import Benchmark: %1 sets, %2 unique cards").arg(importedSets).arg(totalImported);
|
||||
qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs);
|
||||
qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs);
|
||||
qDebug().noquote() << QString(" Total: %1 ms").arg(parseMs + importMs);
|
||||
if (importMs > 0) {
|
||||
qDebug().noquote() << QString(" Throughput: %1 cards/sec")
|
||||
.arg(static_cast<double>(totalImported) / importMs * 1000.0, 0, 'f', 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// readSetsFromByteArray benchmark
|
||||
// ============================================================================
|
||||
|
||||
TEST(OracleBenchmark, ParseJsonThroughput)
|
||||
{
|
||||
static constexpr int numSets = 20;
|
||||
static constexpr int cardsPerSet = 1000;
|
||||
|
||||
QByteArray data = buildSyntheticData(numSets, cardsPerSet);
|
||||
|
||||
// Run 5 iterations and report average
|
||||
static constexpr int iterations = 5;
|
||||
qint64 totalMs = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
OracleImporter importer;
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
bool ok = importer.readSetsFromByteArray(data);
|
||||
ASSERT_TRUE(ok);
|
||||
totalMs += timer.elapsed();
|
||||
}
|
||||
|
||||
qint64 avgMs = totalMs / iterations;
|
||||
qDebug().noquote() << QString("Parse Benchmark (%1 iterations): avg %2 ms for %3 sets x %4 cards")
|
||||
.arg(iterations)
|
||||
.arg(avgMs)
|
||||
.arg(numSets)
|
||||
.arg(cardsPerSet);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Split card merging benchmark
|
||||
// ============================================================================
|
||||
|
||||
TEST(OracleBenchmark, SplitCardMerging)
|
||||
{
|
||||
static constexpr int numSplitCards = 1000;
|
||||
|
||||
QJsonArray cardsList;
|
||||
for (int i = 0; i < numSplitCards; ++i) {
|
||||
QJsonObject face1;
|
||||
face1["name"] = QString("Fire %1 // Ice %1").arg(i);
|
||||
face1["text"] = "Fire side text.";
|
||||
face1["layout"] = "split";
|
||||
face1["side"] = "a";
|
||||
face1["faceName"] = QString("Fire %1").arg(i);
|
||||
face1["colors"] = QJsonArray{"R"};
|
||||
face1["colorIdentity"] = QJsonArray{"R"};
|
||||
face1["types"] = QJsonArray{"Instant"};
|
||||
face1["manaCost"] = "{R}";
|
||||
face1["legalities"] = QJsonObject{{"standard", "not_legal"}};
|
||||
face1["identifiers"] = QJsonObject{{"scryfallId", QString("f-%1").arg(i)}};
|
||||
face1["number"] = QString::number(i + 1);
|
||||
face1["rarity"] = "uncommon";
|
||||
|
||||
QJsonObject face2;
|
||||
face2["name"] = QString("Fire %1 // Ice %1").arg(i);
|
||||
face2["text"] = "Ice side text.";
|
||||
face2["layout"] = "split";
|
||||
face2["side"] = "b";
|
||||
face2["faceName"] = QString("Ice %1").arg(i);
|
||||
face2["colors"] = QJsonArray{"U"};
|
||||
face2["colorIdentity"] = QJsonArray{"U"};
|
||||
face2["types"] = QJsonArray{"Instant"};
|
||||
face2["manaCost"] = "{U}";
|
||||
face2["legalities"] = QJsonObject{{"standard", "not_legal"}};
|
||||
face2["identifiers"] = QJsonObject{{"scryfallId", QString("i-%1").arg(i)}};
|
||||
face2["number"] = QString::number(i + 1);
|
||||
face2["rarity"] = "uncommon";
|
||||
|
||||
cardsList.append(face1);
|
||||
cardsList.append(face2);
|
||||
}
|
||||
|
||||
NoopCardSetPriorityController controller;
|
||||
OracleImporter importer;
|
||||
CardSetPtr set = CardSet::newInstance(&controller, "TST", "Split Test");
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
int count = importer.importCardsFromSet(set, cardsList);
|
||||
qint64 ms = timer.elapsed();
|
||||
|
||||
ASSERT_EQ(count, numSplitCards);
|
||||
qDebug().noquote() << QString("Split Card Merge Benchmark: %1 cards in %2 ms (%3 cards/sec)")
|
||||
.arg(count)
|
||||
.arg(ms)
|
||||
.arg(ms > 0 ? static_cast<double>(count) / ms * 1000.0 : 0.0, 0, 'f', 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// sortAndReduceColors microbenchmark
|
||||
// ============================================================================
|
||||
|
||||
// We can't call sortAndReduceColors directly (it's static), so we benchmark
|
||||
// through importCardsFromSet with color properties.
|
||||
|
||||
TEST(OracleBenchmark, ImportCardsWithColors)
|
||||
{
|
||||
static constexpr int numCards = 10000;
|
||||
|
||||
NoopCardSetPriorityController controller;
|
||||
OracleImporter importer;
|
||||
CardSetPtr set = CardSet::newInstance(&controller, "TST", "Color Test");
|
||||
|
||||
QJsonArray cardsList;
|
||||
for (int i = 0; i < numCards; ++i) {
|
||||
QJsonObject card;
|
||||
card["name"] = QString("Color Card %1").arg(i);
|
||||
card["text"] = "Rules text.";
|
||||
card["layout"] = "normal";
|
||||
card["manaCost"] = "{W}";
|
||||
card["type"] = "Creature — Human";
|
||||
card["types"] = QJsonArray{"Creature"};
|
||||
card["colors"] = QJsonArray{"B", "R", "G", "W", "U"};
|
||||
card["colorIdentity"] = QJsonArray{"B", "R", "G", "W", "U"};
|
||||
card["number"] = QString::number(i + 1);
|
||||
card["rarity"] = "common";
|
||||
card["legalities"] = QJsonObject{{"standard", "legal"}};
|
||||
card["identifiers"] = QJsonObject{{"scryfallId", QString("c-%1").arg(i)}};
|
||||
cardsList.append(card);
|
||||
}
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
int count = importer.importCardsFromSet(set, cardsList);
|
||||
qint64 ms = timer.elapsed();
|
||||
|
||||
ASSERT_EQ(count, numCards);
|
||||
qDebug().noquote() << QString("Import with Colors Benchmark: %1 cards in %2 ms (%3 cards/sec)")
|
||||
.arg(count)
|
||||
.arg(ms)
|
||||
.arg(ms > 0 ? static_cast<double>(count) / ms * 1000.0 : 0.0, 0, 'f', 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RAM usage measurement
|
||||
// ============================================================================
|
||||
|
||||
// Mirrors the default AllPrintings URL selection in oracle/src/pages.cpp.
|
||||
#if defined(HAS_LZMA)
|
||||
static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json.xz");
|
||||
#elif defined(HAS_ZLIB)
|
||||
static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json.zip");
|
||||
#else
|
||||
static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json");
|
||||
#endif
|
||||
|
||||
// Magic bytes also from oracle/src/pages.cpp
|
||||
static const QByteArray kXzSignature("\xFD\x37\x7A\x58\x5A", 6);
|
||||
static const QByteArray kZipSignature("PK");
|
||||
|
||||
struct MemorySnapshot
|
||||
{
|
||||
qint64 peakRssKb = -1; // process high-water mark (VmHWM on Linux, ru_maxrss on macOS)
|
||||
qint64 rssKb = -1; // current resident set size
|
||||
bool available = false;
|
||||
|
||||
static MemorySnapshot current()
|
||||
{
|
||||
MemorySnapshot snap;
|
||||
#if defined(Q_OS_LINUX)
|
||||
QFile statusFile("/proc/self/status");
|
||||
if (statusFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
// /proc files report size() == 0, so atEnd() is immediately true: read everything first.
|
||||
const QList<QByteArray> lines = statusFile.readAll().split('\n');
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.startsWith("VmHWM:")) {
|
||||
snap.peakRssKb = line.mid(6).trimmed().split(' ').value(0).toLongLong();
|
||||
} else if (line.startsWith("VmRSS:")) {
|
||||
snap.rssKb = line.mid(6).trimmed().split(' ').value(0).toLongLong();
|
||||
}
|
||||
}
|
||||
snap.available = snap.peakRssKb >= 0;
|
||||
}
|
||||
#elif defined(Q_OS_MACOS)
|
||||
struct rusage usage;
|
||||
if (getrusage(RUSAGE_SELF, &usage) == 0) {
|
||||
snap.peakRssKb = usage.ru_maxrss / 1024; // bytes -> kB
|
||||
snap.available = snap.peakRssKb >= 0;
|
||||
}
|
||||
// getrusage has no current-RSS equivalent; task_info's resident_size
|
||||
// is the closest macOS analog to Linux VmRSS.
|
||||
mach_task_basic_info info = {};
|
||||
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
|
||||
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast<task_info_t>(&info), &count) ==
|
||||
KERN_SUCCESS) {
|
||||
snap.rssKb = info.resident_size / 1024;
|
||||
}
|
||||
#endif
|
||||
return snap;
|
||||
}
|
||||
};
|
||||
|
||||
static QString formatKb(qint64 kb)
|
||||
{
|
||||
if (kb < 0) {
|
||||
return "N/A";
|
||||
}
|
||||
return QString("%1 MB").arg(kb / 1024.0, 0, 'f', 1);
|
||||
}
|
||||
|
||||
static void logRamPhase(const QString &phase, const MemorySnapshot &baseline, const MemorySnapshot ¤t)
|
||||
{
|
||||
if (!baseline.available || !current.available) {
|
||||
qDebug().noquote() << QString(" %1: memory stats unavailable on this platform").arg(phase);
|
||||
return;
|
||||
}
|
||||
// VmHWM / ru_maxrss are monotonically non-decreasing high-water marks, so a
|
||||
// peak-based delta between phases is ~0.0 MB by construction once the
|
||||
// fixture build has set the process peak. The live signals are current RSS
|
||||
// and the process peak; the delta is meaningful only where the baseline was
|
||||
// taken immediately before the phase it measures (e.g. the import phase,
|
||||
// which compares afterParse against afterImport).
|
||||
QString rssDelta = "N/A";
|
||||
if (current.rssKb >= 0 && baseline.rssKb >= 0) {
|
||||
rssDelta = formatKb(current.rssKb - baseline.rssKb);
|
||||
}
|
||||
qDebug().noquote() << QString(" %1: current RSS %2 | delta vs baseline %3 | process peak %4")
|
||||
.arg(phase)
|
||||
.arg(formatKb(current.rssKb))
|
||||
.arg(rssDelta)
|
||||
.arg(formatKb(current.peakRssKb));
|
||||
}
|
||||
|
||||
// Decompresses the download payload when the default URL is a compressed build,
|
||||
// mirroring the wizard's magic-byte handling in oracle/src/pages.cpp.
|
||||
static QByteArray decompressSetsData(const QByteArray &payload)
|
||||
{
|
||||
if (payload.startsWith(kXzSignature)) {
|
||||
#if defined(HAS_LZMA)
|
||||
QBuffer inBuffer(const_cast<QByteArray *>(&payload));
|
||||
QByteArray out;
|
||||
QBuffer outBuffer(&out);
|
||||
inBuffer.open(QIODevice::ReadOnly);
|
||||
outBuffer.open(QIODevice::WriteOnly);
|
||||
XzDecompressor xz;
|
||||
if (!xz.decompress(&inBuffer, &outBuffer)) {
|
||||
qDebug() << "RAM benchmark: xz decompression failed";
|
||||
return {};
|
||||
}
|
||||
return out;
|
||||
#else
|
||||
qDebug() << "RAM benchmark: download is xz-compressed but this build has no LZMA support";
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
if (payload.startsWith(kZipSignature)) {
|
||||
#if defined(HAS_ZLIB)
|
||||
QBuffer inBuffer(const_cast<QByteArray *>(&payload));
|
||||
inBuffer.open(QIODevice::ReadOnly);
|
||||
UnZip unzip;
|
||||
if (unzip.openArchive(&inBuffer) != UnZip::Ok) {
|
||||
qDebug() << "RAM benchmark: zip archive open failed";
|
||||
return {};
|
||||
}
|
||||
if (unzip.fileList().size() != 1) {
|
||||
qDebug() << "RAM benchmark: zip archive doesn't contain exactly one file";
|
||||
return {};
|
||||
}
|
||||
QByteArray out;
|
||||
QBuffer outBuffer(&out);
|
||||
outBuffer.open(QIODevice::WriteOnly);
|
||||
const auto errorCode = unzip.extractFile(unzip.fileList().value(0), &outBuffer);
|
||||
unzip.closeArchive();
|
||||
if (errorCode != UnZip::Ok) {
|
||||
qDebug() << "RAM benchmark: zip extraction failed";
|
||||
return {};
|
||||
}
|
||||
return out;
|
||||
#else
|
||||
qDebug() << "RAM benchmark: download is zip-compressed but this build has no zlib support";
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
TEST(OracleBenchmark, ImportRamUsage)
|
||||
{
|
||||
static constexpr int numSets = 30;
|
||||
static constexpr int cardsPerSet = 2000; // ~60k cards, roughly AllPrintings scale
|
||||
|
||||
// Baseline must precede the fixture build: a high-water mark set while
|
||||
// generating the synthetic JSON would otherwise mask the importer phases.
|
||||
// Where memory stats are unavailable (Windows), skip before doing the
|
||||
// 60k-card fixture build, which would otherwise be pure wasted work.
|
||||
const MemorySnapshot baseline = MemorySnapshot::current();
|
||||
if (!baseline.available) {
|
||||
GTEST_SKIP() << "Memory stats unavailable on this platform";
|
||||
}
|
||||
|
||||
const QByteArray data = buildSyntheticData(numSets, cardsPerSet);
|
||||
|
||||
// The fixture build leaves freed-but-unreturned arenas behind (current RSS
|
||||
// rarely falls once glibc allocates). Baseline immediately after it so the
|
||||
// parse phase measures only the importer's own growth (~40 MB) rather than
|
||||
// swallowing the fixture builder's spike.
|
||||
const MemorySnapshot afterFixture = MemorySnapshot::current();
|
||||
logRamPhase("fixture build", baseline, afterFixture);
|
||||
|
||||
NoopCardSetPriorityController controller;
|
||||
OracleImporter importer;
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
ASSERT_TRUE(importer.readSetsFromByteArray(data));
|
||||
const qint64 parseMs = timer.elapsed();
|
||||
const MemorySnapshot afterParse = MemorySnapshot::current();
|
||||
|
||||
timer.restart();
|
||||
const int importedSets = importer.startImport();
|
||||
const qint64 importMs = timer.elapsed();
|
||||
const MemorySnapshot afterImport = MemorySnapshot::current();
|
||||
|
||||
importer.releaseSetData();
|
||||
const MemorySnapshot afterRelease = MemorySnapshot::current();
|
||||
|
||||
const int totalCards = importer.getCardList().size();
|
||||
qDebug().noquote() << QString("Oracle RAM Benchmark (synthetic): %1 sets, %2 cards, %3 MB JSON")
|
||||
.arg(importedSets)
|
||||
.arg(totalCards)
|
||||
.arg(data.size() / (1024.0 * 1024.0), 0, 'f', 1);
|
||||
qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs);
|
||||
qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs);
|
||||
logRamPhase("parse", afterFixture, afterParse);
|
||||
logRamPhase("import", afterParse, afterImport);
|
||||
logRamPhase("after releaseSetData()", afterImport, afterRelease);
|
||||
|
||||
// Freeing the parsed tree rarely moves current RSS (allocator reuse), so the
|
||||
// meaningful signal that release actually dropped the buffers is emptiness,
|
||||
// not an RSS delta.
|
||||
ASSERT_TRUE(importer.getSets().isEmpty());
|
||||
}
|
||||
|
||||
TEST(OracleBenchmark, ImportRamUsageAllPrintings)
|
||||
{
|
||||
// Only "1" enables the download: unset (the default and the CI setup) and
|
||||
// an explicit "0" both disable it.
|
||||
bool envOk = false;
|
||||
const int enabled = qEnvironmentVariableIntValue("COCKATRICE_ORACLE_RAM_BENCHMARK", &envOk);
|
||||
if (!envOk || enabled == 0) {
|
||||
GTEST_SKIP() << "Set COCKATRICE_ORACLE_RAM_BENCHMARK=1 to download the real AllPrintings dataset for this "
|
||||
"RAM benchmark. Default URL: "
|
||||
<< kDefaultAllPrintingsUrl.toDisplayString().toStdString();
|
||||
}
|
||||
|
||||
// Baseline must precede the request so the phase covers the download +
|
||||
// decompress step, including the payload materialized by readAll().
|
||||
const MemorySnapshot baseline = MemorySnapshot::current();
|
||||
if (!baseline.available) {
|
||||
GTEST_SKIP() << "Memory stats unavailable on this platform";
|
||||
}
|
||||
|
||||
QNetworkAccessManager nam;
|
||||
QNetworkRequest request(kDefaultAllPrintingsUrl);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, "Cockatrice Oracle RAM benchmark");
|
||||
QNetworkReply *reply = nam.get(request);
|
||||
|
||||
QEventLoop loop;
|
||||
QTimer timeoutTimer;
|
||||
timeoutTimer.setSingleShot(true);
|
||||
bool timedOut = false;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&timeoutTimer, &QTimer::timeout, &loop, [&] {
|
||||
timedOut = true;
|
||||
reply->abort();
|
||||
});
|
||||
timeoutTimer.start(10 * 60 * 1000);
|
||||
loop.exec();
|
||||
timeoutTimer.stop();
|
||||
|
||||
// abort() leaves reply->error() as OperationCanceledError, so a timed-out
|
||||
// download takes the same GTEST_SKIP path as any other network error
|
||||
// instead of reading a truncated body and failing the parse below.
|
||||
if (timedOut || reply->error() != QNetworkReply::NoError) {
|
||||
GTEST_SKIP() << "Download failed: " << reply->errorString().toStdString();
|
||||
}
|
||||
const QByteArray payload = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
// mtgjson can answer 200 with an HTML page (mirrors the wizard's '<' check
|
||||
// in pages.cpp); reject it before trying to decompress/parse.
|
||||
if (payload.startsWith("<")) {
|
||||
GTEST_SKIP() << "Download returned a non-JSON body (HTML page instead of data), skipping";
|
||||
}
|
||||
|
||||
const QByteArray setsData = decompressSetsData(payload);
|
||||
const MemorySnapshot afterDownload = MemorySnapshot::current();
|
||||
if (setsData.isEmpty()) {
|
||||
GTEST_SKIP() << "No data to import (download or decompression failed)";
|
||||
}
|
||||
|
||||
NoopCardSetPriorityController controller;
|
||||
OracleImporter importer;
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
ASSERT_TRUE(importer.readSetsFromByteArray(setsData));
|
||||
const qint64 parseMs = timer.elapsed();
|
||||
const MemorySnapshot afterParse = MemorySnapshot::current();
|
||||
|
||||
timer.restart();
|
||||
const int importedSets = importer.startImport();
|
||||
const qint64 importMs = timer.elapsed();
|
||||
const MemorySnapshot afterImport = MemorySnapshot::current();
|
||||
|
||||
importer.releaseSetData();
|
||||
const MemorySnapshot afterRelease = MemorySnapshot::current();
|
||||
|
||||
const int totalCards = importer.getCardList().size();
|
||||
qDebug().noquote() << QString("Oracle RAM Benchmark (real AllPrintings): %1 sets, %2 unique cards")
|
||||
.arg(importedSets)
|
||||
.arg(totalCards);
|
||||
qDebug().noquote() << QString(" URL: %1").arg(kDefaultAllPrintingsUrl.toDisplayString());
|
||||
qDebug().noquote() << QString(" Downloaded: %1 MB, decompressed: %2 MB")
|
||||
.arg(payload.size() / (1024.0 * 1024.0), 0, 'f', 1)
|
||||
.arg(setsData.size() / (1024.0 * 1024.0), 0, 'f', 1);
|
||||
qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs);
|
||||
qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs);
|
||||
logRamPhase("download+decompress", baseline, afterDownload);
|
||||
logRamPhase("parse", afterDownload, afterParse);
|
||||
logRamPhase("import", afterParse, afterImport);
|
||||
logRamPhase("after releaseSetData()", afterImport, afterRelease);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Required for the event loop used by the real-AllPrintings download benchmark
|
||||
QCoreApplication app(argc, argv);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
552
tests/oracle/oracle_importer_test.cpp
Normal file
552
tests/oracle/oracle_importer_test.cpp
Normal file
|
|
@ -0,0 +1,552 @@
|
|||
#include "../../oracle/src/oracleimporter.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSet>
|
||||
#include <libcockatrice/card/format/format_legality_rules.h>
|
||||
#include <libcockatrice/card/set/card_set.h>
|
||||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||
|
||||
class OracleImporterTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
controller = new NoopCardSetPriorityController();
|
||||
importer = new OracleImporter();
|
||||
set = CardSet::newInstance(controller, "TST", "Test Set");
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
delete importer;
|
||||
delete controller;
|
||||
}
|
||||
|
||||
// Helper: build a minimal card JSON object
|
||||
QJsonObject makeCard(const QString &name,
|
||||
const QString &colors = "",
|
||||
const QString &colorIdentity = "",
|
||||
const QVariantMap &legalities = {})
|
||||
{
|
||||
QJsonObject card;
|
||||
card["name"] = name;
|
||||
card["text"] = "Rules text.";
|
||||
card["layout"] = "normal";
|
||||
card["manaCost"] = "{W}";
|
||||
card["type"] = "Creature — Human";
|
||||
card["types"] = QJsonArray{"Creature"};
|
||||
card["number"] = "1";
|
||||
card["rarity"] = "common";
|
||||
|
||||
if (!colors.isEmpty()) {
|
||||
QJsonArray arr;
|
||||
for (const QChar &c : colors) {
|
||||
arr.append(QString(c));
|
||||
}
|
||||
card["colors"] = arr;
|
||||
}
|
||||
if (!colorIdentity.isEmpty()) {
|
||||
QJsonArray arr;
|
||||
for (const QChar &c : colorIdentity) {
|
||||
arr.append(QString(c));
|
||||
}
|
||||
card["colorIdentity"] = arr;
|
||||
}
|
||||
if (!legalities.isEmpty()) {
|
||||
QJsonObject legalObj;
|
||||
for (auto it = legalities.constBegin(); it != legalities.constEnd(); ++it) {
|
||||
legalObj[it.key()] = it.value().toString();
|
||||
}
|
||||
card["legalities"] = legalObj;
|
||||
}
|
||||
|
||||
QJsonObject identifiers;
|
||||
identifiers["scryfallId"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
card["identifiers"] = identifiers;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
NoopCardSetPriorityController *controller;
|
||||
OracleImporter *importer;
|
||||
CardSetPtr set;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// sortAndReduceColors tests (tested via importCardsFromSet)
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(OracleImporterTest, SortAndReduceColorsSingleColor)
|
||||
{
|
||||
QJsonArray cards{makeCard("Red Card", "R", "R")};
|
||||
importer->importCardsFromSet(set, cards);
|
||||
|
||||
auto card = importer->getCardList().value("Red Card");
|
||||
ASSERT_FALSE(card.isNull());
|
||||
ASSERT_EQ(card->getProperty("colors"), "R");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, SortAndReduceColorsDeduplicates)
|
||||
{
|
||||
QJsonArray cards{makeCard("Dedup Card", "WWUUB", "WU")};
|
||||
importer->importCardsFromSet(set, cards);
|
||||
|
||||
auto card = importer->getCardList().value("Dedup Card");
|
||||
ASSERT_FALSE(card.isNull());
|
||||
ASSERT_EQ(card->getProperty("colors"), "WUB");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, SortAndReduceColorsSortsWUBRG)
|
||||
{
|
||||
QJsonArray cards{makeCard("Sort Card", "RGW", "RGW")};
|
||||
importer->importCardsFromSet(set, cards);
|
||||
|
||||
auto card = importer->getCardList().value("Sort Card");
|
||||
ASSERT_FALSE(card.isNull());
|
||||
ASSERT_EQ(card->getProperty("colors"), "WRG");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, SortAndReduceColorsAllFive)
|
||||
{
|
||||
QJsonArray cards{makeCard("Five Color", "BRGWU", "BRGWU")};
|
||||
importer->importCardsFromSet(set, cards);
|
||||
|
||||
auto card = importer->getCardList().value("Five Color");
|
||||
ASSERT_FALSE(card.isNull());
|
||||
ASSERT_EQ(card->getProperty("colors"), "WUBRG");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, SortAndReduceColorIdentity)
|
||||
{
|
||||
QJsonArray cards{makeCard("Color Id Card", "W", "GWR")};
|
||||
importer->importCardsFromSet(set, cards);
|
||||
|
||||
auto card = importer->getCardList().value("Color Id Card");
|
||||
ASSERT_FALSE(card.isNull());
|
||||
ASSERT_EQ(card->getProperty("coloridentity"), "WRG");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, SingleColorNotSorted)
|
||||
{
|
||||
QJsonArray cards{makeCard("Single Card", "B", "B")};
|
||||
importer->importCardsFromSet(set, cards);
|
||||
|
||||
auto card = importer->getCardList().value("Single Card");
|
||||
ASSERT_FALSE(card.isNull());
|
||||
ASSERT_EQ(card->getProperty("colors"), "B");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Legality guard tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(OracleImporterTest, NewCardKeepsLegalityProperties)
|
||||
{
|
||||
// Verifies that format-* properties survive addCard on a fresh card
|
||||
// (not the combineLegalities guard, which only runs on existing printings).
|
||||
QVariantMap leg;
|
||||
leg["standard"] = "legal";
|
||||
leg["modern"] = "legal";
|
||||
QJsonArray cards{makeCard("Legal Card", "", "", leg)};
|
||||
|
||||
importer->importCardsFromSet(set, cards);
|
||||
auto card = importer->getCardList().value("Legal Card");
|
||||
ASSERT_FALSE(card.isNull());
|
||||
ASSERT_EQ(card->getProperty("format-standard"), "legal");
|
||||
ASSERT_EQ(card->getProperty("format-modern"), "legal");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, LegalityMergeAllowedWhenCardHasNoLegalities)
|
||||
{
|
||||
// First printing carries no legalities at all, so the guard's
|
||||
// `properties.filter(formatRegex).empty()` predicate is true and the
|
||||
// second printing's legalities must be merged in.
|
||||
QJsonArray cards1{makeCard("Unmerged Card")};
|
||||
importer->importCardsFromSet(set, cards1);
|
||||
|
||||
CardSetPtr set2 = CardSet::newInstance(controller, "TS2", "Second Set");
|
||||
QVariantMap leg;
|
||||
leg["standard"] = "legal";
|
||||
QJsonArray cards2{makeCard("Unmerged Card", "", "", leg)};
|
||||
importer->importCardsFromSet(set2, cards2);
|
||||
|
||||
auto card = importer->getCardList().value("Unmerged Card");
|
||||
ASSERT_FALSE(card.isNull());
|
||||
ASSERT_EQ(card->getProperty("format-standard"), "legal");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, LegalityGuardPreservesFirstPrinting)
|
||||
{
|
||||
// First printing: standard=legal, modern=legal
|
||||
QVariantMap leg1;
|
||||
leg1["standard"] = "legal";
|
||||
leg1["modern"] = "legal";
|
||||
QJsonArray cards1{makeCard("Guarded Card", "", "", leg1)};
|
||||
importer->importCardsFromSet(set, cards1);
|
||||
|
||||
// Second printing: standard=banned, modern=not_legal
|
||||
CardSetPtr set2 = CardSet::newInstance(controller, "TS2", "Second Set");
|
||||
QVariantMap leg2;
|
||||
leg2["standard"] = "banned";
|
||||
leg2["modern"] = "not_legal";
|
||||
QJsonArray cards2{makeCard("Guarded Card", "", "", leg2)};
|
||||
importer->importCardsFromSet(set2, cards2);
|
||||
|
||||
auto card = importer->getCardList().value("Guarded Card");
|
||||
ASSERT_FALSE(card.isNull());
|
||||
// Guard should preserve first printing's legalities
|
||||
ASSERT_EQ(card->getProperty("format-standard"), "legal");
|
||||
ASSERT_EQ(card->getProperty("format-modern"), "legal");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// createDefaultMagicFormats tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(OracleImporterTest, CreateDefaultMagicFormatsContainsExpectedFormats)
|
||||
{
|
||||
auto formats = importer->createDefaultMagicFormats();
|
||||
ASSERT_TRUE(formats.contains("standard"));
|
||||
ASSERT_TRUE(formats.contains("modern"));
|
||||
ASSERT_TRUE(formats.contains("legacy"));
|
||||
ASSERT_TRUE(formats.contains("vintage"));
|
||||
ASSERT_TRUE(formats.contains("commander"));
|
||||
ASSERT_TRUE(formats.contains("pauper"));
|
||||
ASSERT_TRUE(formats.contains("pioneer"));
|
||||
ASSERT_TRUE(formats.contains("brawl"));
|
||||
ASSERT_TRUE(formats.contains("historic"));
|
||||
ASSERT_TRUE(formats.contains("timeless"));
|
||||
ASSERT_TRUE(formats.contains("duel"));
|
||||
ASSERT_TRUE(formats.contains("oathbreaker"));
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, CreateDefaultMagicFormatsSingletonDeckSizes)
|
||||
{
|
||||
auto formats = importer->createDefaultMagicFormats();
|
||||
auto commander = formats.value("commander");
|
||||
ASSERT_FALSE(commander.isNull());
|
||||
ASSERT_EQ(commander->minDeckSize, 100);
|
||||
ASSERT_EQ(commander->maxDeckSize, 100);
|
||||
ASSERT_EQ(commander->maxSideboardSize, 15);
|
||||
|
||||
auto brawl = formats.value("brawl");
|
||||
ASSERT_FALSE(brawl.isNull());
|
||||
ASSERT_EQ(brawl->minDeckSize, 60);
|
||||
ASSERT_EQ(brawl->maxDeckSize, 60);
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, CreateDefaultMagicFormatsVintageHasRestricted)
|
||||
{
|
||||
auto formats = importer->createDefaultMagicFormats();
|
||||
auto vintage = formats.value("vintage");
|
||||
ASSERT_FALSE(vintage.isNull());
|
||||
bool hasRestricted = false;
|
||||
for (const auto &ac : vintage->allowedCounts) {
|
||||
if (ac.label == "restricted") {
|
||||
hasRestricted = true;
|
||||
ASSERT_EQ(ac.max, 1);
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(hasRestricted);
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, CreateDefaultMagicFormatsRegexMatchesBasicLands)
|
||||
{
|
||||
auto formats = importer->createDefaultMagicFormats();
|
||||
auto standard = formats.value("standard");
|
||||
ASSERT_FALSE(standard.isNull());
|
||||
ASSERT_FALSE(standard->exceptions.isEmpty());
|
||||
|
||||
auto &basicLandsException = standard->exceptions.first();
|
||||
ASSERT_FALSE(basicLandsException.conditions.isEmpty());
|
||||
|
||||
auto &condition = basicLandsException.conditions.first();
|
||||
ASSERT_EQ(condition.field, "type");
|
||||
ASSERT_EQ(condition.matchType, "regex");
|
||||
|
||||
// Verify the regex actually works (was broken before: \b = backspace, not word boundary)
|
||||
QRegularExpression regex(condition.value);
|
||||
ASSERT_TRUE(regex.isValid());
|
||||
ASSERT_TRUE(regex.match("Basic Land — Forest").hasMatch());
|
||||
ASSERT_TRUE(regex.match("Basic Snow Land — Mountain").hasMatch());
|
||||
ASSERT_FALSE(regex.match("Creature — Elf Warrior").hasMatch());
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, CreateDefaultMagicFormatsCaching)
|
||||
{
|
||||
// The memoized map returns the same FormatRulesPtr instances, so the
|
||||
// shared pointers must be identical across calls. This is the only
|
||||
// observable effect of the cache: contents would match either way.
|
||||
auto first = importer->createDefaultMagicFormats();
|
||||
auto second = importer->createDefaultMagicFormats();
|
||||
ASSERT_EQ(first.value("standard").data(), second.value("standard").data());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// readSetsFromByteArray tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(OracleImporterTest, ReadSetsFromByteArrayValidJson)
|
||||
{
|
||||
QJsonObject setObj;
|
||||
setObj["code"] = "tst";
|
||||
setObj["name"] = "Test Set";
|
||||
setObj["type"] = "expansion";
|
||||
setObj["releaseDate"] = "2024-01-01";
|
||||
setObj["cards"] = QJsonArray();
|
||||
|
||||
QJsonObject root;
|
||||
root["data"] = QJsonObject{{"TST", setObj}};
|
||||
|
||||
QByteArray data = QJsonDocument(root).toJson();
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(data));
|
||||
ASSERT_EQ(importer->getSets().size(), 1);
|
||||
ASSERT_EQ(importer->getSets().first().getShortName(), "TST");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, ReadSetsFromByteArrayInvalidJson)
|
||||
{
|
||||
QByteArray data = "not valid json";
|
||||
ASSERT_FALSE(importer->readSetsFromByteArray(data));
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, ReadSetsFromByteArrayEmptyData)
|
||||
{
|
||||
QJsonObject root;
|
||||
root["data"] = QJsonObject();
|
||||
|
||||
QByteArray data = QJsonDocument(root).toJson();
|
||||
ASSERT_FALSE(importer->readSetsFromByteArray(data));
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, ReadSetsFromByteArrayCapitalizesSetType)
|
||||
{
|
||||
QJsonObject setObj;
|
||||
setObj["code"] = "ftv";
|
||||
setObj["name"] = "From The Vault";
|
||||
setObj["type"] = "from_the_vault";
|
||||
setObj["releaseDate"] = "2024-01-01";
|
||||
setObj["cards"] = QJsonArray();
|
||||
|
||||
QJsonObject root;
|
||||
root["data"] = QJsonObject{{"FTV", setObj}};
|
||||
|
||||
QByteArray data = QJsonDocument(root).toJson();
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(data));
|
||||
ASSERT_EQ(importer->getSets().first().getSetType(), "From the Vault");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, ReadSetsFromByteArraySortsSetsByName)
|
||||
{
|
||||
// QJsonObject iterates keys in lexicographic order ("AAA" before "ZZZ"),
|
||||
// so leaving the natural order matching the alphabetical sort makes the
|
||||
// assertion pass trivially. Inverting it keeps the sort meaningful:
|
||||
// iteration yields "AAA" (Zeta Set) first, then the sort by name must
|
||||
// promote "ZZZ" (Alpha Set) to the front.
|
||||
QJsonObject setA;
|
||||
setA["code"] = "aaa";
|
||||
setA["name"] = "Zeta Set";
|
||||
setA["type"] = "expansion";
|
||||
setA["releaseDate"] = "2024-01-01";
|
||||
setA["cards"] = QJsonArray();
|
||||
|
||||
QJsonObject setB;
|
||||
setB["code"] = "zzz";
|
||||
setB["name"] = "Alpha Set";
|
||||
setB["type"] = "expansion";
|
||||
setB["releaseDate"] = "2024-01-01";
|
||||
setB["cards"] = QJsonArray();
|
||||
|
||||
QJsonObject root;
|
||||
root["data"] = QJsonObject{{"AAA", setA}, {"ZZZ", setB}};
|
||||
|
||||
QByteArray data = QJsonDocument(root).toJson();
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(data));
|
||||
auto sets = importer->getSets();
|
||||
ASSERT_GE(sets.size(), 2);
|
||||
ASSERT_EQ(sets.first().getShortName(), "ZZZ");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Split card coloridentity tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(OracleImporterTest, SplitCardColorIdentityConcatenated)
|
||||
{
|
||||
QJsonObject leg{{"standard", "not_legal"}};
|
||||
|
||||
QJsonObject face1;
|
||||
face1["name"] = "Fire // Ice";
|
||||
face1["text"] = "Fire deals 2 damage.";
|
||||
face1["layout"] = "split";
|
||||
face1["side"] = "a";
|
||||
face1["faceName"] = "Fire";
|
||||
face1["colors"] = QJsonArray{"R"};
|
||||
face1["colorIdentity"] = QJsonArray{"R"};
|
||||
face1["types"] = QJsonArray{"Instant"};
|
||||
face1["manaCost"] = "{R}";
|
||||
face1["legalities"] = leg;
|
||||
face1["identifiers"] = QJsonObject{{"scryfallId", "aaa"}};
|
||||
face1["number"] = "1";
|
||||
face1["rarity"] = "uncommon";
|
||||
|
||||
QJsonObject face2;
|
||||
face2["name"] = "Fire // Ice";
|
||||
face2["text"] = "Ice taps target artifact.";
|
||||
face2["layout"] = "split";
|
||||
face2["side"] = "b";
|
||||
face2["faceName"] = "Ice";
|
||||
face2["colors"] = QJsonArray{"U"};
|
||||
face2["colorIdentity"] = QJsonArray{"U"};
|
||||
face2["types"] = QJsonArray{"Instant"};
|
||||
face2["manaCost"] = "{U}";
|
||||
face2["legalities"] = leg;
|
||||
face2["identifiers"] = QJsonObject{{"scryfallId", "bbb"}};
|
||||
face2["number"] = "1";
|
||||
face2["rarity"] = "uncommon";
|
||||
|
||||
QJsonArray cardsList{face1, face2};
|
||||
int count = importer->importCardsFromSet(set, cardsList);
|
||||
ASSERT_EQ(count, 1);
|
||||
|
||||
auto card = importer->getCardList().value("Fire // Ice");
|
||||
ASSERT_FALSE(card.isNull());
|
||||
|
||||
// coloridentity should be "RU" (concatenated), then sorted to "UR"
|
||||
// by sortAndReduceColors when it reaches addCard
|
||||
ASSERT_EQ(card->getProperty("coloridentity"), "UR");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, SplitCardColorsConcatenated)
|
||||
{
|
||||
QJsonObject leg{{"standard", "not_legal"}};
|
||||
|
||||
QJsonObject face1;
|
||||
face1["name"] = "Fire // Ice";
|
||||
face1["text"] = "Fire deals 2 damage.";
|
||||
face1["layout"] = "split";
|
||||
face1["side"] = "a";
|
||||
face1["faceName"] = "Fire";
|
||||
face1["colors"] = QJsonArray{"R"};
|
||||
face1["colorIdentity"] = QJsonArray{"R"};
|
||||
face1["types"] = QJsonArray{"Instant"};
|
||||
face1["manaCost"] = "{R}";
|
||||
face1["legalities"] = leg;
|
||||
face1["identifiers"] = QJsonObject{{"scryfallId", "aaa"}};
|
||||
face1["number"] = "1";
|
||||
face1["rarity"] = "uncommon";
|
||||
|
||||
QJsonObject face2;
|
||||
face2["name"] = "Fire // Ice";
|
||||
face2["text"] = "Ice taps target artifact.";
|
||||
face2["layout"] = "split";
|
||||
face2["side"] = "b";
|
||||
face2["faceName"] = "Ice";
|
||||
face2["colors"] = QJsonArray{"U"};
|
||||
face2["colorIdentity"] = QJsonArray{"U"};
|
||||
face2["types"] = QJsonArray{"Instant"};
|
||||
face2["manaCost"] = "{U}";
|
||||
face2["legalities"] = leg;
|
||||
face2["identifiers"] = QJsonObject{{"scryfallId", "bbb"}};
|
||||
face2["number"] = "1";
|
||||
face2["rarity"] = "uncommon";
|
||||
|
||||
QJsonArray cardsList{face1, face2};
|
||||
importer->importCardsFromSet(set, cardsList);
|
||||
|
||||
auto card = importer->getCardList().value("Fire // Ice");
|
||||
ASSERT_FALSE(card.isNull());
|
||||
|
||||
QString colors = card->getProperty("colors");
|
||||
ASSERT_FALSE(colors.contains("//")) << "colors should not contain '//', got: " << colors.toStdString();
|
||||
ASSERT_TRUE(colors.contains("R"));
|
||||
ASSERT_TRUE(colors.contains("U"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mana cost formatting tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(OracleImporterTest, ManaCostStripsBraces)
|
||||
{
|
||||
QJsonObject card = makeCard("Mana Card");
|
||||
card["manaCost"] = "{2}{W}{B}";
|
||||
QJsonArray cards{card};
|
||||
|
||||
importer->importCardsFromSet(set, cards);
|
||||
auto result = importer->getCardList().value("Mana Card");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
ASSERT_EQ(result->getProperty("manacost"), "2WB");
|
||||
}
|
||||
|
||||
// cmc comes through as a JSON number ("convertedManaCost"/"manaValue" are
|
||||
// floats in AllPrintings), so this pins the number-to-text coercion that
|
||||
// QJsonValue::toString() dropped in #7214.
|
||||
TEST_F(OracleImporterTest, NumericManaValueCoercedToCmc)
|
||||
{
|
||||
QJsonObject card = makeCard("Cmc Card");
|
||||
card["manaValue"] = 3;
|
||||
QJsonArray cards{card};
|
||||
|
||||
importer->importCardsFromSet(set, cards);
|
||||
auto result = importer->getCardList().value("Cmc Card");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
ASSERT_EQ(result->getProperty("cmc"), "3");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, LegacyConvertedManaCostCoercedToCmc)
|
||||
{
|
||||
QJsonObject card = makeCard("Legacy Cmc Card");
|
||||
card["convertedManaCost"] = 3.0;
|
||||
QJsonArray cards{card};
|
||||
|
||||
importer->importCardsFromSet(set, cards);
|
||||
auto result = importer->getCardList().value("Legacy Cmc Card");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
ASSERT_EQ(result->getProperty("cmc"), "3");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Card deduplication tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(OracleImporterTest, DuplicateCardNameReturnsExisting)
|
||||
{
|
||||
QJsonArray cards{makeCard("Dupe Card")};
|
||||
importer->importCardsFromSet(set, cards);
|
||||
|
||||
CardSetPtr set2 = CardSet::newInstance(controller, "TS2", "Second Set");
|
||||
QJsonArray cards2{makeCard("Dupe Card")};
|
||||
importer->importCardsFromSet(set2, cards2);
|
||||
|
||||
ASSERT_EQ(importer->getCardList().size(), 1);
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, AELigatureReplaced)
|
||||
{
|
||||
QJsonObject card = makeCard(QString::fromUtf8("\xC3\x86ther Vial")); // Æther Vial
|
||||
QJsonArray cards{card};
|
||||
|
||||
importer->importCardsFromSet(set, cards);
|
||||
// Æ is replaced with AE, resulting in "AEther Vial"
|
||||
ASSERT_FALSE(importer->getCardList().contains(QString::fromUtf8("\xC3\x86ther Vial")));
|
||||
ASSERT_TRUE(importer->getCardList().contains("AEther Vial"));
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, ApostropheNormalized)
|
||||
{
|
||||
QJsonObject card = makeCard(QString::fromUtf8("Jace\u2019s Ingenuity"));
|
||||
QJsonArray cards{card};
|
||||
|
||||
importer->importCardsFromSet(set, cards);
|
||||
ASSERT_TRUE(importer->getCardList().contains("Jace's Ingenuity"));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
@ -1,25 +1,9 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
#include <libcockatrice/rng/rng_sfmt.h>
|
||||
#include <cstring>
|
||||
#include <libcockatrice/utility/passwordhasher.h>
|
||||
|
||||
RNG_Abstract *rng;
|
||||
|
||||
namespace
|
||||
{
|
||||
class PasswordHashTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
rng = new RNG_SFMT;
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
delete rng;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(PasswordHashTest, RegressionTest)
|
||||
{
|
||||
|
|
@ -29,6 +13,29 @@ TEST(PasswordHashTest, RegressionTest)
|
|||
QString hash = PasswordHasher::computeHash(password, salt);
|
||||
ASSERT_EQ(hash, salt + expected) << "The computed hash value remains the same";
|
||||
}
|
||||
|
||||
TEST(PasswordHashTest, SaltUsesAlphanumericCharset)
|
||||
{
|
||||
static const char alphanum[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
const QString salt = PasswordHasher::generateRandomSalt();
|
||||
ASSERT_EQ(salt.size(), 16);
|
||||
for (const QChar &c : salt) {
|
||||
ASSERT_NE(strchr(alphanum, c.toLatin1()), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PasswordHashTest, SaltsAreUnique)
|
||||
{
|
||||
const QString salt1 = PasswordHasher::generateRandomSalt();
|
||||
const QString salt2 = PasswordHasher::generateRandomSalt();
|
||||
ASSERT_NE(salt1, salt2);
|
||||
}
|
||||
|
||||
TEST(PasswordHashTest, TokenHasExpectedLength)
|
||||
{
|
||||
const QString token = PasswordHasher::generateActivationToken();
|
||||
ASSERT_EQ(token.size(), 16);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
|
|
|
|||
|
|
@ -238,6 +238,12 @@ TEST_F(SettingsDefaultsTest, Tabs_ModerationOpen_Default)
|
|||
ASSERT_EQ(s.getTabModerationOpen(), false);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Tabs_CardArtRulesOpen_Default)
|
||||
{
|
||||
TabsSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getTabCardArtRulesOpen(), false);
|
||||
}
|
||||
|
||||
// --- ChatSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_Mention_Default)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue