Merge branch 'master' into 2474-server-status

This commit is contained in:
tooomm 2019-02-04 18:54:50 +01:00
commit 894828962b
146 changed files with 18357 additions and 12225 deletions

View file

@ -26,6 +26,7 @@ SET(common_SOURCES
server_room.cpp
serverinfo_user_container.cpp
sfmt/SFMT.c
expression.cpp
)
set(ORACLE_LIBS)

View file

@ -2,8 +2,17 @@
#include <QCryptographicHash>
#include <QDebug>
#include <QFile>
#include <QRegularExpression>
#include <QTextStream>
#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)
{
@ -477,161 +486,131 @@ bool DeckList::saveToFile_Native(QIODevice *device)
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", 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 QHash<QRegularExpression, QString> differences{{QRegularExpression(""), QString("'")},
{QRegularExpression("Æ"), QString("Ae")},
{QRegularExpression("æ"), QString("ae")},
{QRegularExpression(" ?[|/]+ ?"), QString(" // ")},
{QRegularExpression("(?<![A-Z]) ?& ?"), QString(" // ")}};
cleanList();
QVector<QString> inputs; // QTextStream -> QVector
bool priorEntryIsBlank = true, isAtBeginning = true;
int blankLines = 0;
while (!in.atEnd()) {
QString line = in.readLine().simplified().toLower();
QStringList inputs = in.readAll().trimmed().split('\n');
int max_line = inputs.size();
/*
* Removes all blank lines at start of inputs
* Ex: ("", "", "", "Card1", "Card2") => ("Card1", "Card2")
*
* This will also concise multiple blank lines in a row to just one blank
* Ex: ("Card1", "Card2", "", "", "", "Card3") => ("Card1", "Card2", "", "Card3")
*/
if (line.isEmpty()) {
if (priorEntryIsBlank || isAtBeginning) {
continue;
}
priorEntryIsBlank = true;
blankLines++;
} else {
isAtBeginning = false;
priorEntryIsBlank = false;
// 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;
}
inputs.push_back(line);
}
/*
* Removes blank line at end of inputs (if applicable)
* Ex: ("Card1", "Card2", "") => ("Card1", "Card2")
* NOTE: Any duplicates were taken care of above, so there can be
* at most one blank line at the very end
*/
if (!inputs.empty() && inputs.last().isEmpty()) {
blankLines--;
inputs.erase(inputs.end() - 1);
}
// If "Sideboard" line appears in inputs, then blank lines mean nothing
if (inputs.contains("sideboard")) {
blankLines = 2;
}
bool inSideboard = false, titleFound = false, isSideboard;
int okRows = 0;
foreach (QString line, inputs) {
// This is a comment line, ignore it
if (line.startsWith("//")) {
if (!titleFound) // Set the title to the first comment
{
name = line.mid(2).trimmed();
titleFound = true;
} else if (okRows == 0) // We haven't processed any cards yet
{
comments += line.mid(2).trimmed() + "\n";
// 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
// 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();
// If we have a blank line and it's the _ONLY_ blank line in the paste
// and it follows at least one valid card
// Then we assume it means to start the sideboard section of the paste.
// If we have the word "Sideboard" appear on any line, then that will
// also indicate the start of the sideboard.
if ((line.isEmpty() && blankLines == 1 && okRows > 0) || line.startsWith("sideboard")) {
inSideboard = true;
continue; // The line isn't actually a card
}
isSideboard = inSideboard;
if (line.startsWith("sb:")) {
line = line.mid(3).trimmed();
isSideboard = true;
}
if (line.trimmed().isEmpty()) {
continue; // The line was " " instead of "\n"
}
// Filter out MWS edition symbols and basic land extras
QRegExp rx("\\[.*\\]\\s?");
line.remove(rx);
rx.setPattern("\\s?\\(.*\\)");
line.remove(rx);
// Filter out post card name editions
rx.setPattern("\\|.*$");
line.remove(rx);
// If the user inputs "Quicksilver Elemental" then it will cut it off
// 1x Squishy Treaker
int i = line.indexOf(' ');
int cardNameStart = i + 1;
if (i > 0) {
// If the count ends with an 'x', ignore it. For example,
// "4x Storm Crow" will count 4 correctly.
if (line.at(i - 1) == 'x') {
i--;
} else if (!line.at(i - 1).isDigit()) {
// If the user inputs "Quicksilver Elemental" then it will work as 1x of that card
cardNameStart = 0;
// 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;
}
bool ok;
int number = line.left(i).toInt(&ok);
if (!ok) {
number = 1; // If input is "cardName" assume it's "1x cardName"
// check if a specific amount is mentioned
int amount = 1;
match = reMultiplier.match(cardName);
if (match.hasMatch()) {
amount = match.capturedRef(1).toInt();
cardName = match.captured(2);
}
QString cardName = line.mid(cardNameStart);
// 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); // all cards from un-sets that have a word in between round braces at the end
// Common differences between Cockatrice's card names
// and what's commonly used in decklists
rx.setPattern("");
cardName.replace(rx, "'");
rx.setPattern("Æ");
cardName.replace(rx, "Ae");
rx.setPattern("\\s*[|/]{1,2}\\s*");
cardName.replace(rx, " // ");
// Replace only if the ampersand is preceded by a non-capital letter,
// as would happen with acronyms. So 'Fire & Ice' is replaced but not
// 'R&D' or 'R & D'.
// Qt regexes don't support lookbehind so we capture and replace instead.
rx.setPattern("([^A-Z])\\s*&\\s*");
if (rx.indexIn(cardName) != -1) {
cardName.replace(rx, QString("%1 // ").arg(rx.cap(1)));
// replace common differences in cardnames
for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) {
cardName.replace(diff.key(), diff.value());
}
// We need to get the name of the card from the database,
// but we can't do that until we get the "real" name
// (name stored in database for the card)
// and establish a card info that is of the card, then it's
// a simple getting the _real_ name of the card
// (i.e. "STOrm, CrOW" => "Storm Crow")
// get cardname, this function does nothing if the name is not found
cardName = getCompleteCardName(cardName);
// Look for the correct card zone of where to place the new card
QString zoneName = getCardZoneFromName(cardName, isSideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN);
// get zone name based on if it's in sideboard
QString zoneName = getCardZoneFromName(cardName, sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN);
okRows++;
new DecklistCardNode(cardName, number, getZoneObjFromName(zoneName));
// make new entry in decklist
new DecklistCardNode(cardName, amount, getZoneObjFromName(zoneName));
}
updateDeckHash();
return (okRows > 0);
return true;
}
InnerDecklistNode *DeckList::getZoneObjFromName(const QString zoneName)

108
common/expression.cpp Normal file
View file

@ -0,0 +1,108 @@
#include "expression.h"
#include "./lib/peglib.h"
#include <QByteArray>
#include <QString>
#include <cmath>
#include <functional>
peg::parser math(R"(
EXPRESSION <- P0
P0 <- P1 (P1_OPERATOR P1)*
P1 <- P2 (P2_OPERATOR P2)*
P2 <- P3 (P3_OPERATOR P3)*
P3 <- NUMBER / FUNCTION / VARIABLE / '(' P0 ')'
P1_OPERATOR <- < [-+] >
P2_OPERATOR <- < [/*] >
P3_OPERATOR <- < '^' >
NUMBER <- < '-'? [0-9]+ >
NAME <- < [a-z][a-z0-9]* >
VARIABLE <- < [x] >
FUNCTION <- NAME '(' EXPRESSION ( [,\n] EXPRESSION )* ')'
%whitespace <- [ \t\r]*
)");
QMap<QString, std::function<double(double)>> *default_functions = nullptr;
Expression::Expression(double initial) : value(initial)
{
if (default_functions == nullptr) {
default_functions = new QMap<QString, std::function<double(double)>>();
default_functions->insert("sin", [](double a) { return sin(a); });
default_functions->insert("cos", [](double a) { return cos(a); });
default_functions->insert("tan", [](double a) { return tan(a); });
default_functions->insert("sqrt", [](double a) { return sqrt(a); });
default_functions->insert("log", [](double a) { return log(a); });
default_functions->insert("log10", [](double a) { return log(a); });
default_functions->insert("trunc", [](double a) { return trunc(a); });
default_functions->insert("abs", [](double a) { return abs(a); });
default_functions->insert("floor", [](double a) { return floor(a); });
default_functions->insert("ceil", [](double a) { return ceil(a); });
default_functions->insert("round", [](double a) { return round(a); });
default_functions->insert("trunc", [](double a) { return trunc(a); });
}
fns = QMap<QString, std::function<double(double)>>(*default_functions);
}
double Expression::eval(const peg::Ast &ast)
{
const auto &nodes = ast.nodes;
if (ast.name == "NUMBER") {
return stod(ast.token);
} else if (ast.name == "FUNCTION") {
QString name = QString::fromStdString(nodes[0]->token);
if (!fns.contains(name))
return 0;
return fns[name](eval(*nodes[1]));
} else if (ast.name == "VARIABLE") {
return value;
} else if (ast.name[0] == 'P') {
double result = eval(*nodes[0]);
for (int i = 1; i < nodes.size(); i += 2) {
double arg = eval(*nodes[i + 1]);
char operation = nodes[i]->token[0];
switch (operation) {
case '+':
result += arg;
break;
case '-':
result -= arg;
break;
case '*':
result *= arg;
break;
case '/':
result /= arg;
break;
case '^':
result = pow(result, arg);
break;
default:
result = 0;
break;
}
}
return result;
} else {
return -1;
}
}
double Expression::parse(const QString &expr)
{
QByteArray ba = expr.toLocal8Bit();
math.enable_ast();
std::shared_ptr<peg::Ast> ast;
if (math.parse(ba.data(), ast)) {
ast = peg::AstOptimizer(true).optimize(ast);
return eval(*ast);
}
return 0;
}

28
common/expression.h Normal file
View file

@ -0,0 +1,28 @@
#ifndef EXPRESSION_H
#define EXPRESSION_H
#include <QMap>
#include <QString>
#include <functional>
namespace peg
{
template <typename Annotation> struct AstBase;
struct EmptyType;
typedef AstBase<EmptyType> Ast;
} // namespace peg
class Expression
{
public:
double value;
explicit Expression(double initial = 0);
double parse(const QString &expr);
private:
double eval(const peg::Ast &ast);
QMap<QString, std::function<double(double)>> fns;
};
#endif

3293
common/lib/peglib.h Normal file

File diff suppressed because it is too large Load diff

View file

@ -169,3 +169,12 @@ if (UNIX)
set(cockatrice_protocol_LIBS ${cockatrice_protocol_LIBS} -lpthread)
endif (UNIX)
target_link_libraries(cockatrice_protocol ${cockatrice_protocol_LIBS})
# ubuntu uses an outdated package for protobuf, 3.1.0 is required
if(${Protobuf_VERSION} VERSION_LESS "3.1.0")
# remove unused parameter and misleading indentation warnings when compiling to avoid errors
set(CMAKE_CXX_FLAGS_DEBUG
"${CMAKE_CXX_FLAGS_DEBUG} -Wno-unused-parameter -Wno-misleading-indentation")
message(WARNING "Outdated protobuf version found (${Protobuf_VERSION} < 3.1.0), "
"disabled warnings to avoid compilation errors.")
endif()

View file

@ -5,3 +5,10 @@ message Command_Concede {
optional Command_Concede ext = 1017;
}
}
message Command_Unconcede {
extend GameCommand {
optional Command_Unconcede ext = 1032;
}
}

View file

@ -6,3 +6,9 @@ message Context_Concede {
optional Context_Concede ext = 1001;
}
}
message Context_Unconcede {
extend GameEventContext {
optional Context_Unconcede ext = 1009;
}
}

View file

@ -33,6 +33,8 @@ message GameCommand {
DECK_SELECT = 1029;
SET_SIDEBOARD_LOCK = 1030;
CHANGE_ZONE_PROPERTIES = 1031;
UNCONCEDE = 1032;
}
extensions 100 to max;
}

View file

@ -10,6 +10,7 @@ message GameEventContext {
PING_CHANGED = 1006;
CONNECTION_STATE_CHANGED = 1007;
SET_SIDEBOARD_LOCK = 1008;
UNCONCEDE = 1009;
}
extensions 100 to max;
}

View file

@ -77,7 +77,6 @@ private:
Server_Player *playerWhosAsking,
bool omniscient,
bool withUserInfo);
void sendGameStateToPlayers();
void storeGameInformation();
signals:
void sigStartGameIfReady();
@ -192,6 +191,7 @@ public:
prepareGameEvent(const ::google::protobuf::Message &gameEvent, int playerId, GameEventContext *context = 0);
GameEventContext prepareGameEventContext(const ::google::protobuf::Message &gameEventContext);
void sendGameStateToPlayers();
void sendGameEventContainer(GameEventContainer *cont,
GameEventStorageItem::EventRecipients recipients = GameEventStorageItem::SendToPrivate |
GameEventStorageItem::SendToOthers,

View file

@ -776,6 +776,30 @@ Server_Player::cmdConcede(const Command_Concede & /*cmd*/, ResponseContainer & /
return Response::RespOk;
}
Response::ResponseCode
Server_Player::cmdUnconcede(const Command_Unconcede & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges)
{
if (spectator)
return Response::RespFunctionNotAllowed;
if (!game->getGameStarted())
return Response::RespGameNotStarted;
if (!conceded)
return Response::RespContextError;
setConceded(false);
Event_PlayerPropertiesChanged event;
event.mutable_player_properties()->set_conceded(false);
ges.enqueueGameEvent(event, playerId);
ges.setGameEventContext(Context_Unconcede());
setupZones();
game->sendGameStateToPlayers();
return Response::RespOk;
}
Response::ResponseCode
Server_Player::cmdReadyStart(const Command_ReadyStart &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
{
@ -1826,6 +1850,10 @@ Server_Player::processGameCommand(const GameCommand &command, ResponseContainer
case GameCommand::CHANGE_ZONE_PROPERTIES:
return cmdChangeZoneProperties(command.GetExtension(Command_ChangeZoneProperties::ext), rc, ges);
break;
case GameCommand::UNCONCEDE:
return cmdUnconcede(command.GetExtension(Command_Unconcede::ext), rc, ges);
break;
default:
return Response::RespInvalidCommand;
}

View file

@ -46,6 +46,7 @@ class Command_SetCardCounter;
class Command_IncCardCounter;
class Command_ReadyStart;
class Command_Concede;
class Command_Unconcede;
class Command_IncCounter;
class Command_CreateCounter;
class Command_SetCounter;
@ -190,6 +191,7 @@ public:
Response::ResponseCode
cmdKickFromGame(const Command_KickFromGame &cmd, ResponseContainer &rc, GameEventStorage &ges);
Response::ResponseCode cmdConcede(const Command_Concede &cmd, ResponseContainer &rc, GameEventStorage &ges);
Response::ResponseCode cmdUnconcede(const Command_Unconcede &cmd, ResponseContainer &rc, GameEventStorage &ges);
Response::ResponseCode cmdReadyStart(const Command_ReadyStart &cmd, ResponseContainer &rc, GameEventStorage &ges);
Response::ResponseCode cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &rc, GameEventStorage &ges);
Response::ResponseCode