Add lock around deleting arrows for commanding cards Add support for Qt6 w/ Backwards Qt5 Handle Qt5/6 cross compilation better Last cleanups caps matter Fix serv Prevent crash on 6.3.0 Linux & bump to 5.8 min Prevent out of bounds indexing Delete shutdown timer if it exists Fixup ticket comments, remove unneeded guards Try to add support for missing OSes Update .ci/release_template.md Update PR based on comments Update XML name after done and remove Hirsute Address local game crash Address comments from PR (again) Tests don't work on mac, will see if a problem on other OSes make soundengine more consistent across qt versions disable tests on distros that are covered by others Fix Oracle Crash due to bad memory access Update Oracle to use new Qt6 way of adding translations Add support for Qt5/Qt6 compiling of Cockatrice Remove unneeded calls to QtMath/cmath/math.h Update how we handle bitwise comparisons for enums with Tray Icon Change header guards to not duplicate function Leave comment & Fix Path for GHA Qt Update common/server.h Update cockatrice/src/window_main.cpp Rollback change on cmake module path for NSIS check docker image requirements add size limit to ccache put variables in quotes properly set build type on mac avoid names used in cmake fix up cmake module path cmake 3.10 does not recognize prepend Support Tests in FindQtRuntime set ccache size on non debug builds as well immediately return when removing non existing client handle incTxBytes with a signal instead don't set common link libraries in cockatrice/CMakeLists.txt add comments set macos qt version to 6 Try upgrading XCode versions to latest they can be supported on Ensure Qt gets linked add tmate so i can see what's going on Qt6 points two directories further down than Qt5 with regard to the top lib path, so we need to account for this Establish Plugins directory for Qt6 Establish TLS plugins for Qt6 services Minor change for release channel network manager Let windows build in parallel cores Wrong symbols Qt6 patch up for signal add missing qt6 package on deb builds boolean expressions are hard negative indexes should go to the end Intentionally fail cache move size checks to individual zone types Hardcode libs needed for building on Windows, as the regex was annoying Update wording use the --parallel option in all builds clean up the .ci scripts some more tweak fedora build add os parameter to compile.sh I don't really like this but it seems the easiest way I'd prefer if these types of quirks would live in the main configuration file, the yml fixup yml readd appended cache key to vcpkg step fix windows 32 quirk the json hash is already added to the key as well remove os parameter and clean up ci files set name_build.sh to output relative paths set backwards compatible version of xcode and qt on mac set QTDIR for mac builds on qt5 has no effect for qt6 export BUILD_DIR to name_build.sh merge mac build steps merge homebrew steps, set package suffix link qt5 remove brew link set qtdir to qt5 only compile.sh vars need to be empty not 0 fix sets manager search bar on qt 5.12/15 fix oracle subprocess errors being ignored on qt 5 clean up translation loading move en@source translation file so it will not get included in packages NOTE: this needs to be done at transifex as well! Use generator platform over osname Short circuit if not Win defined
833 lines
25 KiB
C++
833 lines
25 KiB
C++
#include "decklist.h"
|
||
|
||
#include <QCryptographicHash>
|
||
#include <QDebug>
|
||
#include <QFile>
|
||
#include <QRegularExpression>
|
||
#include <QTextStream>
|
||
#include <algorithm>
|
||
|
||
#if QT_VERSION < 0x050600
|
||
// qHash on QRegularExpression was added in 5.6, FIX IT
|
||
uint qHash(const QRegularExpression &key, uint seed) noexcept
|
||
{
|
||
return qHash(key.pattern(), seed); // call qHash on pattern QString instead
|
||
}
|
||
#endif
|
||
|
||
SideboardPlan::SideboardPlan(const QString &_name, const QList<MoveCard_ToZone> &_moveList)
|
||
: name(_name), moveList(_moveList)
|
||
{
|
||
}
|
||
|
||
void SideboardPlan::setMoveList(const QList<MoveCard_ToZone> &_moveList)
|
||
{
|
||
moveList = _moveList;
|
||
}
|
||
|
||
bool SideboardPlan::readElement(QXmlStreamReader *xml)
|
||
{
|
||
while (!xml->atEnd()) {
|
||
xml->readNext();
|
||
const QString childName = xml->name().toString();
|
||
if (xml->isStartElement()) {
|
||
if (childName == "name")
|
||
name = xml->readElementText();
|
||
else if (childName == "move_card_to_zone") {
|
||
MoveCard_ToZone m;
|
||
while (!xml->atEnd()) {
|
||
xml->readNext();
|
||
const QString childName2 = xml->name().toString();
|
||
if (xml->isStartElement()) {
|
||
if (childName2 == "card_name")
|
||
m.set_card_name(xml->readElementText().toStdString());
|
||
else if (childName2 == "start_zone")
|
||
m.set_start_zone(xml->readElementText().toStdString());
|
||
else if (childName2 == "target_zone")
|
||
m.set_target_zone(xml->readElementText().toStdString());
|
||
} else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) {
|
||
moveList.append(m);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
} else if (xml->isEndElement() && (childName == "sideboard_plan"))
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void SideboardPlan::write(QXmlStreamWriter *xml)
|
||
{
|
||
xml->writeStartElement("sideboard_plan");
|
||
xml->writeTextElement("name", name);
|
||
for (auto &i : moveList) {
|
||
xml->writeStartElement("move_card_to_zone");
|
||
xml->writeTextElement("card_name", QString::fromStdString(i.card_name()));
|
||
xml->writeTextElement("start_zone", QString::fromStdString(i.start_zone()));
|
||
xml->writeTextElement("target_zone", QString::fromStdString(i.target_zone()));
|
||
xml->writeEndElement();
|
||
}
|
||
xml->writeEndElement();
|
||
}
|
||
|
||
AbstractDecklistNode::AbstractDecklistNode(InnerDecklistNode *_parent) : parent(_parent), sortMethod(Default)
|
||
{
|
||
if (parent) {
|
||
parent->append(this);
|
||
}
|
||
}
|
||
|
||
int AbstractDecklistNode::depth() const
|
||
{
|
||
if (parent) {
|
||
return parent->depth() + 1;
|
||
} else {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
InnerDecklistNode::InnerDecklistNode(InnerDecklistNode *other, InnerDecklistNode *_parent)
|
||
: AbstractDecklistNode(_parent), name(other->getName())
|
||
{
|
||
for (int i = 0; i < other->size(); ++i) {
|
||
auto *inner = dynamic_cast<InnerDecklistNode *>(other->at(i));
|
||
if (inner) {
|
||
new InnerDecklistNode(inner, this);
|
||
} else {
|
||
new DecklistCardNode(dynamic_cast<DecklistCardNode *>(other->at(i)), this);
|
||
}
|
||
}
|
||
}
|
||
|
||
InnerDecklistNode::~InnerDecklistNode()
|
||
{
|
||
clearTree();
|
||
}
|
||
|
||
QString InnerDecklistNode::visibleNameFromName(const QString &_name)
|
||
{
|
||
if (_name == DECK_ZONE_MAIN) {
|
||
return QObject::tr("Maindeck");
|
||
} else if (_name == DECK_ZONE_SIDE) {
|
||
return QObject::tr("Sideboard");
|
||
} else if (_name == DECK_ZONE_TOKENS) {
|
||
return QObject::tr("Tokens");
|
||
} else {
|
||
return _name;
|
||
}
|
||
}
|
||
|
||
void InnerDecklistNode::setSortMethod(DeckSortMethod method)
|
||
{
|
||
sortMethod = method;
|
||
for (int i = 0; i < size(); i++) {
|
||
at(i)->setSortMethod(method);
|
||
}
|
||
}
|
||
|
||
QString InnerDecklistNode::getVisibleName() const
|
||
{
|
||
return visibleNameFromName(name);
|
||
}
|
||
|
||
void InnerDecklistNode::clearTree()
|
||
{
|
||
for (int i = 0; i < size(); i++)
|
||
delete at(i);
|
||
clear();
|
||
}
|
||
|
||
DecklistCardNode::DecklistCardNode(DecklistCardNode *other, InnerDecklistNode *_parent)
|
||
: AbstractDecklistCardNode(_parent), name(other->getName()), number(other->getNumber())
|
||
{
|
||
}
|
||
|
||
AbstractDecklistNode *InnerDecklistNode::findChild(const QString &name)
|
||
{
|
||
for (int i = 0; i < size(); i++) {
|
||
if (at(i)->getName() == name) {
|
||
return at(i);
|
||
}
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
int InnerDecklistNode::height() const
|
||
{
|
||
return at(0)->height() + 1;
|
||
}
|
||
|
||
int InnerDecklistNode::recursiveCount(bool countTotalCards) const
|
||
{
|
||
int result = 0;
|
||
for (int i = 0; i < size(); i++) {
|
||
auto *node = dynamic_cast<InnerDecklistNode *>(at(i));
|
||
|
||
if (node) {
|
||
result += node->recursiveCount(countTotalCards);
|
||
} else if (countTotalCards) {
|
||
result += dynamic_cast<AbstractDecklistCardNode *>(at(i))->getNumber();
|
||
} else {
|
||
result++;
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
bool InnerDecklistNode::compare(AbstractDecklistNode *other) const
|
||
{
|
||
switch (sortMethod) {
|
||
case ByNumber:
|
||
return compareNumber(other);
|
||
case ByName:
|
||
return compareName(other);
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
bool InnerDecklistNode::compareNumber(AbstractDecklistNode *other) const
|
||
{
|
||
auto *other2 = dynamic_cast<InnerDecklistNode *>(other);
|
||
if (other2) {
|
||
int n1 = recursiveCount(true);
|
||
int n2 = other2->recursiveCount(true);
|
||
return (n1 != n2) ? (n1 > n2) : compareName(other);
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
bool InnerDecklistNode::compareName(AbstractDecklistNode *other) const
|
||
{
|
||
auto *other2 = dynamic_cast<InnerDecklistNode *>(other);
|
||
if (other2) {
|
||
return (getName() > other2->getName());
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
bool AbstractDecklistCardNode::compare(AbstractDecklistNode *other) const
|
||
{
|
||
switch (sortMethod) {
|
||
case ByNumber:
|
||
return compareNumber(other);
|
||
case ByName:
|
||
return compareName(other);
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
bool AbstractDecklistCardNode::compareNumber(AbstractDecklistNode *other) const
|
||
{
|
||
auto *other2 = dynamic_cast<AbstractDecklistCardNode *>(other);
|
||
if (other2) {
|
||
int n1 = getNumber();
|
||
int n2 = other2->getNumber();
|
||
return (n1 != n2) ? (n1 > n2) : compareName(other);
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
bool AbstractDecklistCardNode::compareName(AbstractDecklistNode *other) const
|
||
{
|
||
auto *other2 = dynamic_cast<AbstractDecklistCardNode *>(other);
|
||
if (other2) {
|
||
return (getName() > other2->getName());
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
class InnerDecklistNode::compareFunctor
|
||
{
|
||
private:
|
||
Qt::SortOrder order;
|
||
|
||
public:
|
||
explicit compareFunctor(Qt::SortOrder _order) : order(_order)
|
||
{
|
||
}
|
||
inline bool operator()(QPair<int, AbstractDecklistNode *> a, QPair<int, AbstractDecklistNode *> b) const
|
||
{
|
||
return (order == Qt::AscendingOrder) ? (b.second->compare(a.second)) : (a.second->compare(b.second));
|
||
}
|
||
};
|
||
|
||
bool InnerDecklistNode::readElement(QXmlStreamReader *xml)
|
||
{
|
||
while (!xml->atEnd()) {
|
||
xml->readNext();
|
||
const QString childName = xml->name().toString();
|
||
if (xml->isStartElement()) {
|
||
if (childName == "zone") {
|
||
InnerDecklistNode *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), this);
|
||
newZone->readElement(xml);
|
||
} else if (childName == "card") {
|
||
DecklistCardNode *newCard =
|
||
new DecklistCardNode(xml->attributes().value("name").toString(),
|
||
xml->attributes().value("number").toString().toInt(), this);
|
||
newCard->readElement(xml);
|
||
}
|
||
} else if (xml->isEndElement() && (childName == "zone"))
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void InnerDecklistNode::writeElement(QXmlStreamWriter *xml)
|
||
{
|
||
xml->writeStartElement("zone");
|
||
xml->writeAttribute("name", name);
|
||
for (int i = 0; i < size(); i++)
|
||
at(i)->writeElement(xml);
|
||
xml->writeEndElement(); // zone
|
||
}
|
||
|
||
bool AbstractDecklistCardNode::readElement(QXmlStreamReader *xml)
|
||
{
|
||
while (!xml->atEnd()) {
|
||
xml->readNext();
|
||
if (xml->isEndElement() && xml->name().toString() == "card")
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml)
|
||
{
|
||
xml->writeEmptyElement("card");
|
||
xml->writeAttribute("number", QString::number(getNumber()));
|
||
xml->writeAttribute("name", getName());
|
||
}
|
||
|
||
QVector<QPair<int, int>> InnerDecklistNode::sort(Qt::SortOrder order)
|
||
{
|
||
QVector<QPair<int, int>> result(size());
|
||
|
||
// Initialize temporary list with contents of current list
|
||
QVector<QPair<int, AbstractDecklistNode *>> tempList(size());
|
||
for (int i = size() - 1; i >= 0; --i) {
|
||
tempList[i].first = i;
|
||
tempList[i].second = at(i);
|
||
}
|
||
|
||
// Sort temporary list
|
||
compareFunctor cmp(order);
|
||
std::sort(tempList.begin(), tempList.end(), cmp);
|
||
|
||
// Map old indexes to new indexes and
|
||
// copy temporary list to the current one
|
||
for (int i = size() - 1; i >= 0; --i) {
|
||
result[i].first = tempList[i].first;
|
||
result[i].second = i;
|
||
replace(i, tempList[i].second);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
DeckList::DeckList()
|
||
{
|
||
root = new InnerDecklistNode;
|
||
}
|
||
|
||
// TODO: https://qt-project.org/doc/qt-4.8/qobject.html#no-copy-constructor-or-assignment-operator
|
||
DeckList::DeckList(const DeckList &other)
|
||
: QObject(), name(other.name), comments(other.comments), deckHash(other.deckHash)
|
||
{
|
||
root = new InnerDecklistNode(other.getRoot());
|
||
|
||
QMapIterator<QString, SideboardPlan *> spIterator(other.getSideboardPlans());
|
||
while (spIterator.hasNext()) {
|
||
spIterator.next();
|
||
sideboardPlans.insert(spIterator.key(), new SideboardPlan(spIterator.key(), spIterator.value()->getMoveList()));
|
||
}
|
||
updateDeckHash();
|
||
}
|
||
|
||
DeckList::DeckList(const QString &nativeString)
|
||
{
|
||
root = new InnerDecklistNode;
|
||
loadFromString_Native(nativeString);
|
||
}
|
||
|
||
DeckList::~DeckList()
|
||
{
|
||
delete root;
|
||
|
||
QMapIterator<QString, SideboardPlan *> i(sideboardPlans);
|
||
while (i.hasNext())
|
||
delete i.next().value();
|
||
}
|
||
|
||
QList<MoveCard_ToZone> DeckList::getCurrentSideboardPlan()
|
||
{
|
||
SideboardPlan *current = sideboardPlans.value(QString(), 0);
|
||
if (!current)
|
||
return QList<MoveCard_ToZone>();
|
||
else
|
||
return current->getMoveList();
|
||
}
|
||
|
||
void DeckList::setCurrentSideboardPlan(const QList<MoveCard_ToZone> &plan)
|
||
{
|
||
SideboardPlan *current = sideboardPlans.value(QString(), 0);
|
||
if (!current) {
|
||
current = new SideboardPlan;
|
||
sideboardPlans.insert(QString(), current);
|
||
}
|
||
|
||
current->setMoveList(plan);
|
||
}
|
||
|
||
bool DeckList::readElement(QXmlStreamReader *xml)
|
||
{
|
||
const QString childName = xml->name().toString();
|
||
if (xml->isStartElement()) {
|
||
if (childName == "deckname")
|
||
name = xml->readElementText();
|
||
else if (childName == "comments")
|
||
comments = xml->readElementText();
|
||
else if (childName == "zone") {
|
||
InnerDecklistNode *newZone = getZoneObjFromName(xml->attributes().value("name").toString());
|
||
newZone->readElement(xml);
|
||
} else if (childName == "sideboard_plan") {
|
||
SideboardPlan *newSideboardPlan = new SideboardPlan;
|
||
if (newSideboardPlan->readElement(xml))
|
||
sideboardPlans.insert(newSideboardPlan->getName(), newSideboardPlan);
|
||
else
|
||
delete newSideboardPlan;
|
||
}
|
||
} else if (xml->isEndElement() && (childName == "cockatrice_deck"))
|
||
return false;
|
||
return true;
|
||
}
|
||
|
||
void DeckList::write(QXmlStreamWriter *xml)
|
||
{
|
||
xml->writeStartElement("cockatrice_deck");
|
||
xml->writeAttribute("version", "1");
|
||
xml->writeTextElement("deckname", name);
|
||
xml->writeTextElement("comments", comments);
|
||
|
||
for (int i = 0; i < root->size(); i++)
|
||
root->at(i)->writeElement(xml);
|
||
|
||
QMapIterator<QString, SideboardPlan *> i(sideboardPlans);
|
||
while (i.hasNext())
|
||
i.next().value()->write(xml);
|
||
xml->writeEndElement();
|
||
}
|
||
|
||
bool DeckList::loadFromXml(QXmlStreamReader *xml)
|
||
{
|
||
if (xml->error()) {
|
||
qDebug() << "Error loading deck from xml: " << xml->errorString();
|
||
return false;
|
||
}
|
||
|
||
cleanList();
|
||
while (!xml->atEnd()) {
|
||
xml->readNext();
|
||
if (xml->isStartElement()) {
|
||
if (xml->name().toString() != "cockatrice_deck")
|
||
return false;
|
||
while (!xml->atEnd()) {
|
||
xml->readNext();
|
||
if (!readElement(xml))
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
updateDeckHash();
|
||
if (xml->error()) {
|
||
qDebug() << "Error loading deck from xml: " << xml->errorString();
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool DeckList::loadFromString_Native(const QString &nativeString)
|
||
{
|
||
QXmlStreamReader xml(nativeString);
|
||
return loadFromXml(&xml);
|
||
}
|
||
|
||
QString DeckList::writeToString_Native()
|
||
{
|
||
QString result;
|
||
QXmlStreamWriter xml(&result);
|
||
xml.writeStartDocument();
|
||
write(&xml);
|
||
xml.writeEndDocument();
|
||
return result;
|
||
}
|
||
|
||
bool DeckList::loadFromFile_Native(QIODevice *device)
|
||
{
|
||
QXmlStreamReader xml(device);
|
||
return loadFromXml(&xml);
|
||
}
|
||
|
||
bool DeckList::saveToFile_Native(QIODevice *device)
|
||
{
|
||
QXmlStreamWriter xml(device);
|
||
xml.setAutoFormatting(true);
|
||
xml.writeStartDocument();
|
||
|
||
write(&xml);
|
||
|
||
xml.writeEndDocument();
|
||
return true;
|
||
}
|
||
|
||
bool DeckList::loadFromStream_Plain(QTextStream &in)
|
||
{
|
||
const QRegularExpression reCardLine("^\\s*[\\w\\[\\(\\{].*$", QRegularExpression::UseUnicodePropertiesOption);
|
||
const QRegularExpression reEmpty("^\\s*$");
|
||
const QRegularExpression reComment("[\\w\\[\\(\\{].*$", QRegularExpression::UseUnicodePropertiesOption);
|
||
const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption);
|
||
const QRegularExpression reSBComment("^sideboard\\b.*$", QRegularExpression::CaseInsensitiveOption);
|
||
const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b",
|
||
QRegularExpression::CaseInsensitiveOption);
|
||
|
||
// simplified matches
|
||
const QRegularExpression reMultiplier("^[xX\\(\\[]*(\\d+)[xX\\*\\)\\]]* ?(.+)");
|
||
const QRegularExpression reBrace(" ?[\\[\\{][^\\]\\}]*[\\]\\}] ?"); // not nested
|
||
const QRegularExpression reRoundBrace("^\\([^\\)]*\\) ?"); // () are only matched at start of string
|
||
const QRegularExpression reDigitBrace(" ?\\(\\d*\\) ?"); // () are matched if containing digits
|
||
const QRegularExpression reBraceDigit(
|
||
" ?\\([\\dA-Z]+\\) *\\d+$"); // () are matched if containing setcode then a number
|
||
const QHash<QRegularExpression, QString> differences{{QRegularExpression("’"), QString("'")},
|
||
{QRegularExpression("Æ"), QString("Ae")},
|
||
{QRegularExpression("æ"), QString("ae")},
|
||
{QRegularExpression(" ?[|/]+ ?"), QString(" // ")},
|
||
{QRegularExpression("(?<![A-Z]) ?& ?"), QString(" // ")}};
|
||
|
||
cleanList();
|
||
|
||
QStringList inputs = in.readAll().trimmed().split('\n');
|
||
int max_line = inputs.size();
|
||
|
||
// start at the first empty line before the first cardline
|
||
int deckStart = inputs.indexOf(reCardLine);
|
||
if (deckStart == -1) { // there are no cards?
|
||
if (inputs.indexOf(reComment) == -1) {
|
||
return false; // input is empty
|
||
}
|
||
deckStart = max_line;
|
||
} else {
|
||
deckStart = inputs.lastIndexOf(reEmpty, deckStart);
|
||
if (deckStart == -1) {
|
||
deckStart = 0;
|
||
}
|
||
}
|
||
|
||
// find sideboard position, if marks are used this won't be needed
|
||
int sBStart = -1;
|
||
if (inputs.indexOf(reSBMark, deckStart) == -1) {
|
||
sBStart = inputs.indexOf(reSBComment, deckStart);
|
||
if (sBStart == -1) {
|
||
sBStart = inputs.indexOf(reEmpty, deckStart + 1);
|
||
if (sBStart == -1) {
|
||
sBStart = max_line;
|
||
}
|
||
int nextCard = inputs.indexOf(reCardLine, sBStart + 1);
|
||
if (inputs.indexOf(reEmpty, nextCard + 1) != -1) {
|
||
sBStart = max_line; // if there is another empty line all cards are mainboard
|
||
}
|
||
}
|
||
}
|
||
|
||
int index = 0;
|
||
QRegularExpressionMatch match;
|
||
|
||
// parse name and comments
|
||
while (index < deckStart) {
|
||
const QString current = inputs.at(index++);
|
||
if (!current.contains(reEmpty)) {
|
||
match = reComment.match(current);
|
||
name = match.captured();
|
||
break;
|
||
}
|
||
}
|
||
while (index < deckStart) {
|
||
const QString current = inputs.at(index++);
|
||
if (!current.contains(reEmpty)) {
|
||
match = reComment.match(current);
|
||
comments += match.captured() + '\n';
|
||
}
|
||
}
|
||
comments.chop(1); // remove last newline
|
||
|
||
// discard empty lines
|
||
while (index < max_line && inputs.at(index).contains(reEmpty)) {
|
||
++index;
|
||
}
|
||
|
||
// discard line if it starts with deck or mainboard, all cards until the sideboard starts are in the mainboard
|
||
if (inputs.at(index).contains(reDeckComment)) {
|
||
++index;
|
||
}
|
||
|
||
// parse decklist
|
||
for (; index < max_line; ++index) {
|
||
|
||
// check if line is a card
|
||
match = reCardLine.match(inputs.at(index));
|
||
if (!match.hasMatch())
|
||
continue;
|
||
QString cardName = match.captured().simplified();
|
||
|
||
// check if card should be sideboard
|
||
bool sideboard = false;
|
||
if (sBStart < 0) {
|
||
match = reSBMark.match(cardName);
|
||
if (match.hasMatch()) {
|
||
sideboard = true;
|
||
cardName = match.captured(1);
|
||
}
|
||
} else {
|
||
if (index == sBStart) // skip sideboard line itself
|
||
continue;
|
||
sideboard = index > sBStart;
|
||
}
|
||
|
||
// check if a specific amount is mentioned
|
||
int amount = 1;
|
||
match = reMultiplier.match(cardName);
|
||
if (match.hasMatch()) {
|
||
amount = match.captured(1).toInt();
|
||
cardName = match.captured(2);
|
||
}
|
||
|
||
// remove stuff inbetween braces
|
||
cardName.remove(reBrace);
|
||
cardName.remove(reRoundBrace); // I'll be entirely honest here, these are split to accommodate just three cards
|
||
cardName.remove(reDigitBrace); // from un-sets that have a word in between round braces at the end
|
||
cardName.remove(reBraceDigit); // very specific format with the set code in () and collectors number after
|
||
|
||
// replace common differences in cardnames
|
||
for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) {
|
||
cardName.replace(diff.key(), diff.value());
|
||
}
|
||
|
||
// get cardname, this function does nothing if the name is not found
|
||
cardName = getCompleteCardName(cardName);
|
||
|
||
// get zone name based on if it's in sideboard
|
||
QString zoneName = getCardZoneFromName(cardName, sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN);
|
||
|
||
// make new entry in decklist
|
||
new DecklistCardNode(cardName, amount, getZoneObjFromName(zoneName));
|
||
}
|
||
|
||
updateDeckHash();
|
||
return true;
|
||
}
|
||
|
||
InnerDecklistNode *DeckList::getZoneObjFromName(const QString zoneName)
|
||
{
|
||
for (int i = 0; i < root->size(); i++) {
|
||
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||
if (node->getName() == zoneName) {
|
||
return node;
|
||
}
|
||
}
|
||
|
||
return new InnerDecklistNode(zoneName, root);
|
||
}
|
||
|
||
bool DeckList::loadFromFile_Plain(QIODevice *device)
|
||
{
|
||
QTextStream in(device);
|
||
return loadFromStream_Plain(in);
|
||
}
|
||
|
||
struct WriteToStream
|
||
{
|
||
QTextStream &stream;
|
||
bool prefixSideboardCards;
|
||
bool slashTappedOutSplitCards;
|
||
|
||
WriteToStream(QTextStream &_stream, bool _prefixSideboardCards, bool _slashTappedOutSplitCards)
|
||
: stream(_stream), prefixSideboardCards(_prefixSideboardCards),
|
||
slashTappedOutSplitCards(_slashTappedOutSplitCards)
|
||
{
|
||
}
|
||
|
||
void operator()(const InnerDecklistNode *node, const DecklistCardNode *card)
|
||
{
|
||
if (prefixSideboardCards && node->getName() == DECK_ZONE_SIDE) {
|
||
stream << "SB: ";
|
||
}
|
||
if (!slashTappedOutSplitCards) {
|
||
stream << QString("%1 %2\n").arg(card->getNumber()).arg(card->getName());
|
||
} else {
|
||
stream << QString("%1 %2\n").arg(card->getNumber()).arg(card->getName().replace("//", "/"));
|
||
}
|
||
}
|
||
};
|
||
|
||
bool DeckList::saveToStream_Plain(QTextStream &out, bool prefixSideboardCards, bool slashTappedOutSplitCards)
|
||
{
|
||
WriteToStream writeToStream(out, prefixSideboardCards, slashTappedOutSplitCards);
|
||
forEachCard(writeToStream);
|
||
return true;
|
||
}
|
||
|
||
bool DeckList::saveToFile_Plain(QIODevice *device, bool prefixSideboardCards, bool slashTappedOutSplitCards)
|
||
{
|
||
QTextStream out(device);
|
||
return saveToStream_Plain(out, prefixSideboardCards, slashTappedOutSplitCards);
|
||
}
|
||
|
||
QString DeckList::writeToString_Plain(bool prefixSideboardCards, bool slashTappedOutSplitCards)
|
||
{
|
||
QString result;
|
||
QTextStream out(&result);
|
||
saveToStream_Plain(out, prefixSideboardCards, slashTappedOutSplitCards);
|
||
return result;
|
||
}
|
||
|
||
void DeckList::cleanList()
|
||
{
|
||
root->clearTree();
|
||
setName();
|
||
setComments();
|
||
deckHash = QString();
|
||
emit deckHashChanged();
|
||
}
|
||
|
||
void DeckList::getCardListHelper(InnerDecklistNode *item, QSet<QString> &result) const
|
||
{
|
||
for (int i = 0; i < item->size(); ++i) {
|
||
auto *node = dynamic_cast<DecklistCardNode *>(item->at(i));
|
||
|
||
if (node) {
|
||
result.insert(node->getName());
|
||
} else {
|
||
getCardListHelper(dynamic_cast<InnerDecklistNode *>(item->at(i)), result);
|
||
}
|
||
}
|
||
}
|
||
|
||
QStringList DeckList::getCardList() const
|
||
{
|
||
QSet<QString> result;
|
||
getCardListHelper(root, result);
|
||
return result.values();
|
||
}
|
||
|
||
int DeckList::getSideboardSize() const
|
||
{
|
||
int size = 0;
|
||
for (int i = 0; i < root->size(); ++i) {
|
||
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||
if (node->getName() != DECK_ZONE_SIDE) {
|
||
continue;
|
||
}
|
||
|
||
for (int j = 0; j < node->size(); j++) {
|
||
auto *card = dynamic_cast<DecklistCardNode *>(node->at(j));
|
||
size += card->getNumber();
|
||
}
|
||
}
|
||
return size;
|
||
}
|
||
|
||
DecklistCardNode *DeckList::addCard(const QString &cardName, const QString &zoneName)
|
||
{
|
||
auto *zoneNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName));
|
||
if (zoneNode == nullptr) {
|
||
zoneNode = new InnerDecklistNode(zoneName, root);
|
||
}
|
||
|
||
auto *node = new DecklistCardNode(cardName, 1, zoneNode);
|
||
updateDeckHash();
|
||
|
||
return node;
|
||
}
|
||
|
||
bool DeckList::deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode)
|
||
{
|
||
if (node == root) {
|
||
return true;
|
||
}
|
||
|
||
bool updateHash = false;
|
||
if (rootNode == nullptr) {
|
||
rootNode = root;
|
||
updateHash = true;
|
||
}
|
||
|
||
int index = rootNode->indexOf(node);
|
||
if (index != -1) {
|
||
delete rootNode->takeAt(index);
|
||
|
||
if (rootNode->empty()) {
|
||
deleteNode(rootNode, rootNode->getParent());
|
||
}
|
||
|
||
if (updateHash) {
|
||
updateDeckHash();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
for (int i = 0; i < rootNode->size(); i++) {
|
||
auto *inner = dynamic_cast<InnerDecklistNode *>(rootNode->at(i));
|
||
if (inner) {
|
||
if (deleteNode(node, inner)) {
|
||
if (updateHash) {
|
||
updateDeckHash();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
void DeckList::updateDeckHash()
|
||
{
|
||
QStringList cardList;
|
||
bool isValidDeckList = true;
|
||
QSet<QString> hashZones, optionalZones;
|
||
|
||
hashZones << DECK_ZONE_MAIN << DECK_ZONE_SIDE; // Zones in deck to be included in hashing process
|
||
optionalZones << DECK_ZONE_TOKENS; // Optional zones in deck not included in hashing process
|
||
|
||
for (int i = 0; i < root->size(); i++) {
|
||
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||
for (int j = 0; j < node->size(); j++) {
|
||
if (hashZones.contains(node->getName())) // Mainboard or Sideboard
|
||
{
|
||
auto *card = dynamic_cast<DecklistCardNode *>(node->at(j));
|
||
for (int k = 0; k < card->getNumber(); ++k) {
|
||
cardList.append((node->getName() == DECK_ZONE_SIDE ? "SB:" : "") + card->getName().toLower());
|
||
}
|
||
} else if (!optionalZones.contains(node->getName())) // Not a valid zone -> cheater?
|
||
{
|
||
isValidDeckList = false; // Deck is invalid
|
||
}
|
||
}
|
||
}
|
||
cardList.sort();
|
||
QByteArray deckHashArray = QCryptographicHash::hash(cardList.join(";").toUtf8(), QCryptographicHash::Sha1);
|
||
quint64 number = (((quint64)(unsigned char)deckHashArray[0]) << 32) +
|
||
(((quint64)(unsigned char)deckHashArray[1]) << 24) +
|
||
(((quint64)(unsigned char)deckHashArray[2] << 16)) +
|
||
(((quint64)(unsigned char)deckHashArray[3]) << 8) + (quint64)(unsigned char)deckHashArray[4];
|
||
deckHash = (isValidDeckList) ? QString::number(number, 32).rightJustified(8, '0') : "INVALID";
|
||
|
||
emit deckHashChanged();
|
||
}
|