Initial commit of mtgonline project

This commit is contained in:
2026-07-18 04:57:40 +00:00
commit 86c12376f8
1870 changed files with 547994 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
cmake_minimum_required(VERSION 3.16)
project(Utility VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp
libcockatrice/utility/passwordhasher.cpp
)
set(UTILITY_HEADERS
libcockatrice/utility/color.h
libcockatrice/utility/expression.h
libcockatrice/utility/levenshtein.h
libcockatrice/utility/macros.h
libcockatrice/utility/passwordhasher.h
libcockatrice/utility/string_limits.h
libcockatrice/utility/dice_limits.h
libcockatrice/utility/counter_limits.h
libcockatrice/utility/clamped_arithmetic.h
libcockatrice/utility/zone_names.h
libcockatrice/utility/days_years_between.h
)
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})
set(ORACLE_LIBS)
include_directories(${${COCKATRICE_QT_VERSION_NAME}Core_INCLUDE_DIRS})
@@ -0,0 +1,29 @@
#ifndef CARD_REF_H
#define CARD_REF_H
#include <QString>
/**
* The information passed over the server that is required to identify the exact card to display.
*
* @param name The name of the card. Should not be empty, unless to indicate the lack of a card.
* @param providerId Determines which printing of the card to use. Can be empty, in which case Cockatrice should default
* to using the preferred set.
*/
struct CardRef
{
QString name;
QString providerId = QString();
bool operator==(const CardRef &other) const
{
return name == other.name && providerId == other.providerId;
}
bool isEmpty() const
{
return name.isEmpty() && providerId.isEmpty();
}
};
#endif // CARD_REF_H
@@ -0,0 +1,22 @@
#ifndef CLAMPED_ARITHMETIC_H
#define CLAMPED_ARITHMETIC_H
#include <QtGlobal>
#include <cstdint>
/**
* @brief Overflow-safe clamped addition: returns value + delta bounded to [minValue, maxValue].
*
* Uses a 64-bit intermediate so the addition cannot overflow int. Shared by the bounded
* counter arithmetic in both the client and the server.
*
* @note Requires minValue <= maxValue. Bounds come from trusted compile-time call sites;
* qBound() asserts this internally in debug builds.
*/
inline int addClamped(int value, int delta, int minValue, int maxValue)
{
const auto result = static_cast<int64_t>(value) + static_cast<int64_t>(delta);
return static_cast<int>(qBound(static_cast<int64_t>(minValue), result, static_cast<int64_t>(maxValue)));
}
#endif // CLAMPED_ARITHMETIC_H
@@ -0,0 +1,103 @@
#ifndef COLOR_H
#define COLOR_H
#ifdef QT_GUI_LIB
#include <QColor>
#endif
#include <libcockatrice/protocol/pb/color.pb.h>
#ifdef QT_GUI_LIB
inline QColor convertColorToQColor(const color &c)
{
return QColor(c.r(), c.g(), c.b());
}
inline color convertQColorToColor(const QColor &c)
{
color result;
result.set_r(c.red());
result.set_g(c.green());
result.set_b(c.blue());
return result;
}
#include <QMap>
#include <QSet>
namespace GameSpecificColors
{
namespace MTG
{
inline QColor colorHelper(const QString &name)
{
static const QMap<QString, QColor> colorMap = {
{"W", QColor(245, 245, 220)},
{"U", QColor(80, 140, 255)},
{"B", QColor(60, 60, 60)},
{"R", QColor(220, 60, 50)},
{"G", QColor(70, 160, 70)},
{"Creature", QColor(70, 130, 180)},
{"Instant", QColor(138, 43, 226)},
{"Sorcery", QColor(199, 21, 133)},
{"Enchantment", QColor(218, 165, 32)},
{"Artifact", QColor(169, 169, 169)},
{"Planeswalker", QColor(210, 105, 30)},
{"Land", QColor(110, 80, 50)},
};
if (colorMap.contains(name)) {
return colorMap[name];
}
if (name.length() == 1 && colorMap.contains(name.toUpper())) {
return colorMap[name.toUpper()];
}
uint h = qHash(name);
int r = 100 + (h % 120);
int g = 100 + ((h >> 8) % 120);
int b = 100 + ((h >> 16) % 120);
return QColor(r, g, b);
}
inline QList<QPair<QString, int>> sortManaMapWUBRGCFirst(const QMap<QString, int> &input)
{
static const QStringList priorityOrder = {"W", "U", "B", "R", "G", "C"};
QList<QPair<QString, int>> result;
QSet<QString> consumed;
// 1. Add priority colors in fixed order
for (const QString &key : priorityOrder) {
auto it = input.find(key);
if (it != input.end()) {
result.append({it.key(), it.value()});
consumed.insert(it.key());
}
}
// 2. Add remaining keys (QMap iteration is already sorted)
for (auto it = input.begin(); it != input.end(); ++it) {
if (!consumed.contains(it.key())) {
result.append({it.key(), it.value()});
}
}
return result;
}
} // namespace MTG
} // namespace GameSpecificColors
#endif
inline color makeColor(int r, int g, int b)
{
color result;
result.set_r(r);
result.set_g(g);
result.set_b(b);
return result;
}
#endif
@@ -0,0 +1,17 @@
#ifndef COUNTER_LIMITS_H
#define COUNTER_LIMITS_H
/**
* @brief Upper bound for a bounded counter's value: [0, MAX_COUNTER_VALUE].
*
* Caps an individual counter's VALUE (e.g. a +1/+1 counter at 999), not how many counters
* something holds. Applies to counters that are constrained to a non-negative display range,
* such as card counters and commander tax. Unbounded counters (e.g. a player's life total)
* do not use this limit and may go negative, saturating only at the int range.
*
* The max of 999 is a display constraint (3-digit rendering) and a reasonable gameplay limit.
* The server enforces these bounds; the client may also check them for UX optimization.
*/
constexpr int MAX_COUNTER_VALUE = 999;
#endif // COUNTER_LIMITS_H
@@ -0,0 +1,13 @@
#ifndef COCKATRICE_DAYS_YEARS_BETWEEN_H
#define COCKATRICE_DAYS_YEARS_BETWEEN_H
#include <QDateTime>
inline static QPair<int, int> getDaysAndYearsBetween(const QDate &then, const QDate &now)
{
int years = now.addDays(1 - then.dayOfYear()).year() - then.year(); // there is no yearsTo
int days = then.addYears(years).daysTo(now);
return {days, years};
}
#endif // COCKATRICE_DAYS_YEARS_BETWEEN_H
@@ -0,0 +1,15 @@
#ifndef DICE_LIMITS_H
#define DICE_LIMITS_H
#include <QtGlobal> // for uint
/** @brief Fewest sides a rollable die may have. */
constexpr uint MINIMUM_DIE_SIDES = 2;
/** @brief Most sides a rollable die may have. */
constexpr uint MAXIMUM_DIE_SIDES = 1000000;
/** @brief Fewest dice that may be rolled at once. */
constexpr uint MINIMUM_DICE_TO_ROLL = 1;
/** @brief Most dice that may be rolled at once. */
constexpr uint MAXIMUM_DICE_TO_ROLL = 100;
#endif // DICE_LIMITS_H
@@ -0,0 +1,108 @@
#include "expression.h"
#include "peglib.h"
#include <QByteArray>
#include <QString>
#include <QtMath>
#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 <- < [xX] >
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("abs", [](double a) { return qFabs(a); });
default_functions->insert("ceil", [](double a) { return qCeil(a); });
default_functions->insert("cos", [](double a) { return qCos(a); });
default_functions->insert("floor", [](double a) { return qFloor(a); });
default_functions->insert("log", [](double a) { return qLn(a); });
default_functions->insert("log10", [](double a) { return qLn(a); });
default_functions->insert("round", [](double a) { return qRound(a); });
default_functions->insert("sin", [](double a) { return qSin(a); });
default_functions->insert("sqrt", [](double a) { return qSqrt(a); });
default_functions->insert("tan", [](double a) { return qTan(a); });
default_functions->insert("trunc", [](double a) { return std::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(std::string(ast.token));
} else if (ast.name == "FUNCTION") {
QString name = QString::fromStdString(std::string(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 (unsigned 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 = qPow(result, arg);
break;
default:
result = 0;
break;
}
}
return result;
} else {
return -1;
}
}
double Expression::parse(const QString &expr)
{
QByteArray ba = expr.toUtf8();
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;
}
@@ -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
@@ -0,0 +1,27 @@
#include "levenshtein.h"
#include <algorithm>
#include <vector>
int levenshteinDistance(const QString &s1, const QString &s2)
{
int len1 = s1.size();
int len2 = s2.size();
std::vector<std::vector<int>> dp(len1 + 1, std::vector<int>(len2 + 1));
for (int i = 0; i <= len1; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= len2; j++) {
dp[0][j] = j;
}
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1;
dp[i][j] = std::min({dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost});
}
}
return dp[len1][len2];
}
@@ -0,0 +1,14 @@
/**
* @file levenshtein.h
* @ingroup Core
*/
//! \todo Document this file.
#ifndef LEVENSHTEIN_H
#define LEVENSHTEIN_H
#include <QString>
int levenshteinDistance(const QString &s1, const QString &s2);
#endif // LEVENSHTEIN_H
@@ -0,0 +1,17 @@
#ifndef COCKATRICE_MACROS_H
#define COCKATRICE_MACROS_H
#include <QtGlobal>
// Qt6.7 changed how stateChanged functionality
// of QCheckBoxes work.
// See https://doc.qt.io/qt-6/qcheckbox.html#checkStateChanged
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
#define QT_STATE_CHANGED checkStateChanged
#define QT_STATE_CHANGED_T Qt::CheckState
#else
#define QT_STATE_CHANGED stateChanged
#define QT_STATE_CHANGED_T int
#endif
#endif // COCKATRICE_MACROS_H
@@ -0,0 +1,38 @@
#include "passwordhasher.h"
#include <QCryptographicHash>
#include <libcockatrice/rng/rng_sfmt.h>
QString PasswordHasher::computeHash(const QString &password, const QString &salt)
{
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha512;
const int rounds = 1000;
QByteArray hash = (salt + password).toUtf8();
for (int i = 0; i < rounds; ++i) {
hash = QCryptographicHash::hash(hash, algo);
}
QString hashedPass = salt + QString(hash.toBase64());
return hashedPass;
}
QString PasswordHasher::generateRandomSalt(const int len)
{
static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
QString ret;
int size = sizeof(alphanum) - 1;
for (int i = 0; i < len; ++i) {
ret.append(alphanum[rng->rand(0, size)]);
}
return ret;
}
QString PasswordHasher::generateActivationToken()
{
return QCryptographicHash::hash(generateRandomSalt().toUtf8(), QCryptographicHash::Md5).toBase64().left(16);
}
@@ -0,0 +1,14 @@
#ifndef PASSWORDHASHER_H
#define PASSWORDHASHER_H
#include <QObject>
class PasswordHasher
{
public:
static QString computeHash(const QString &password, const QString &salt);
static QString generateRandomSalt(const int len = 16);
static QString generateActivationToken();
};
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,38 @@
#ifndef COCKATRICE_QT_UTILS_H
#define COCKATRICE_QT_UTILS_H
#include <QLayout>
#include <QObject>
namespace QtUtils
{
template <typename T> T *findParentOfType(const QObject *obj)
{
const QObject *p = obj ? obj->parent() : nullptr;
while (p) {
if (auto casted = qobject_cast<T *>(const_cast<QObject *>(p))) {
return casted;
}
p = p->parent();
}
return nullptr;
}
static inline void clearLayoutRec(QLayout *l)
{
if (!l) {
return;
}
QLayoutItem *it;
while ((it = l->takeAt(0)) != nullptr) {
if (QWidget *w = it->widget()) {
w->deleteLater();
}
if (QLayout *sub = it->layout()) {
clearLayoutRec(sub);
}
delete it;
}
}
} // namespace QtUtils
#endif // COCKATRICE_QT_UTILS_H
@@ -0,0 +1,31 @@
#ifndef STRING_LIMITS_H
#define STRING_LIMITS_H
#include <QString>
#include <algorithm>
#include <string>
/** @brief Max size for short strings, like names and things that are generally a single phrase. */
constexpr int MAX_NAME_LENGTH = 0xff;
/** @brief Max size for chat messages and text contents. */
constexpr int MAX_TEXT_LENGTH = 0xfff;
/** @brief Max size for deck files and pictures (about 2 megabytes). */
constexpr int MAX_FILE_LENGTH = 0x1fffff;
/** @brief Returns a QString from a std::string, truncated to at most MAX_NAME_LENGTH bytes. */
inline QString nameFromStdString(const std::string &_string)
{
return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_NAME_LENGTH));
}
/** @brief Returns a QString from a std::string, truncated to at most MAX_TEXT_LENGTH bytes. */
inline QString textFromStdString(const std::string &_string)
{
return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_TEXT_LENGTH));
}
/** @brief Returns a QString from a std::string, truncated to at most MAX_FILE_LENGTH bytes. */
inline QString fileFromStdString(const std::string &_string)
{
return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_FILE_LENGTH));
}
#endif // STRING_LIMITS_H
@@ -0,0 +1,19 @@
#ifndef ZONE_NAMES_H
#define ZONE_NAMES_H
namespace ZoneNames
{
// Protocol-level zone identifiers shared between client and server.
// These must match exactly across all components.
constexpr const char *TABLE = "table";
constexpr const char *GRAVE = "grave";
constexpr const char *EXILE = "rfg"; // "removed from game"
constexpr const char *HAND = "hand";
constexpr const char *DECK = "deck";
constexpr const char *SIDEBOARD = "sb";
constexpr const char *STACK = "stack";
} // namespace ZoneNames
#endif // ZONE_NAMES_H