From ea8201af5ce79305389375dd98460e826b6b9802 Mon Sep 17 00:00:00 2001 From: Rob Blanckaert Date: Thu, 21 Feb 2019 11:00:00 -0800 Subject: [PATCH] Judge mode (#3531) * Judge mode * Use seperate judge icon * Fix clang init ordering complaint * Create gavel.svg * Add judge level * Adjust judge permissions. * - Tag events caused by judges - Allow judges access to card right click menus. * Allow judges to change phase / turn. * Remove gavel from pawn * Make judge action text black. * Create scales * Rename scales to scales.svg * Use scales * remove gavel * - Address PR feedback - Fix sort order * Zach * add option to servatrice.ini --- cockatrice/cockatrice.qrc | 1 + cockatrice/resources/icons/scales.svg | 6 + cockatrice/src/abstractcounter.cpp | 4 +- cockatrice/src/carditem.cpp | 4 +- cockatrice/src/chatview/chatview.h | 5 +- cockatrice/src/dlg_creategame.cpp | 2 + cockatrice/src/gameselector.cpp | 2 + cockatrice/src/messagelogwidget.cpp | 15 +++ cockatrice/src/messagelogwidget.h | 5 + cockatrice/src/pixmapgenerator.cpp | 1 + cockatrice/src/player.cpp | 82 ++++++++++--- cockatrice/src/player.h | 12 +- cockatrice/src/playerlistwidget.cpp | 13 +- cockatrice/src/playerlistwidget.h | 2 +- cockatrice/src/tab_game.cpp | 21 +++- cockatrice/src/tab_game.h | 1 + cockatrice/src/user_context_menu.cpp | 26 +++- cockatrice/src/user_context_menu.h | 1 + cockatrice/src/userinfobox.cpp | 3 + cockatrice/src/userlist.cpp | 2 +- common/pb/admin_commands.proto | 3 +- common/pb/event_game_joined.proto | 1 + common/pb/game_commands.proto | 10 +- common/pb/game_event_container.proto | 1 + common/pb/room_commands.proto | 2 + common/pb/serverinfo_playerproperties.proto | 1 + common/pb/serverinfo_user.proto | 1 + common/server.h | 4 + common/server_game.cpp | 16 ++- common/server_game.h | 3 +- common/server_player.cpp | 64 +++++++--- common/server_player.h | 8 ++ common/server_protocolhandler.cpp | 8 +- common/server_response_containers.cpp | 4 + common/server_response_containers.h | 5 + common/server_room.cpp | 4 +- servatrice/servatrice.ini.example | 5 + servatrice/src/servatrice.cpp | 5 + servatrice/src/servatrice.h | 1 + .../src/servatrice_database_interface.cpp | 8 +- servatrice/src/serversocketinterface.cpp | 115 +++++++++++------- servatrice/src/serversocketinterface.h | 3 + 42 files changed, 375 insertions(+), 105 deletions(-) create mode 100644 cockatrice/resources/icons/scales.svg diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index aff2f002..a5cded2c 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -24,6 +24,7 @@ resources/icons/player.svg resources/icons/ready_start.svg resources/icons/remove_row.svg + resources/icons/scales.svg resources/icons/search.svg resources/icons/settings.svg resources/icons/spectator.svg diff --git a/cockatrice/resources/icons/scales.svg b/cockatrice/resources/icons/scales.svg new file mode 100644 index 00000000..a410921d --- /dev/null +++ b/cockatrice/resources/icons/scales.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/cockatrice/src/abstractcounter.cpp b/cockatrice/src/abstractcounter.cpp index d9095138..247f5b91 100644 --- a/cockatrice/src/abstractcounter.cpp +++ b/cockatrice/src/abstractcounter.cpp @@ -26,7 +26,7 @@ AbstractCounter::AbstractCounter(Player *_player, shortcutActive = false; - if (player->getLocal()) { + if (player->getLocalOrJudge()) { menu = new QMenu(name); aSet = new QAction(this); connect(aSet, SIGNAL(triggered()), this, SLOT(setCounter())); @@ -115,7 +115,7 @@ void AbstractCounter::setValue(int _value) void AbstractCounter::mousePressEvent(QGraphicsSceneMouseEvent *event) { - if (isUnderMouse() && player->getLocal()) { + if (isUnderMouse() && player->getLocalOrJudge()) { if (event->button() == Qt::MidButton || (QApplication::keyboardModifiers() & Qt::ShiftModifier)) { if (menu) menu->exec(event->screenPos()); diff --git a/cockatrice/src/carditem.cpp b/cockatrice/src/carditem.cpp index 06b4b362..00b42bd9 100644 --- a/cockatrice/src/carditem.cpp +++ b/cockatrice/src/carditem.cpp @@ -321,7 +321,7 @@ void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) const ZoneViewZone *view = static_cast(zone); if (view->getRevealZone() && !view->getWriteableRevealZone()) return; - } else if (!owner->getLocal()) + } else if (!owner->getLocalOrJudge()) return; bool forceFaceDown = event->modifiers().testFlag(Qt::ShiftModifier); @@ -352,7 +352,7 @@ void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) void CardItem::playCard(bool faceDown) { // Do nothing if the card belongs to another player - if (!owner->getLocal()) + if (!owner->getLocalOrJudge()) return; TableZone *tz = qobject_cast(zone); diff --git a/cockatrice/src/chatview/chatview.h b/cockatrice/src/chatview/chatview.h index 9fdceb15..38b560b1 100644 --- a/cockatrice/src/chatview/chatview.h +++ b/cockatrice/src/chatview/chatview.h @@ -71,8 +71,9 @@ public: QWidget *parent = 0); void retranslateUi(); void appendHtml(const QString &html); - void - appendHtmlServerMessage(const QString &html, bool optionalIsBold = false, QString optionalFontColor = QString()); + void virtual appendHtmlServerMessage(const QString &html, + bool optionalIsBold = false, + QString optionalFontColor = QString()); void appendMessage(QString message, RoomMessageTypeFlags messageType = 0, QString sender = QString(), diff --git a/cockatrice/src/dlg_creategame.cpp b/cockatrice/src/dlg_creategame.cpp index 2173eb74..df0280f1 100644 --- a/cockatrice/src/dlg_creategame.cpp +++ b/cockatrice/src/dlg_creategame.cpp @@ -1,6 +1,7 @@ #include "dlg_creategame.h" #include "settingscache.h" #include "tab_room.h" +#include #include #include #include @@ -223,6 +224,7 @@ void DlgCreateGame::actOK() cmd.set_spectators_need_password(spectatorsNeedPasswordCheckBox->isChecked()); cmd.set_spectators_can_talk(spectatorsCanTalkCheckBox->isChecked()); cmd.set_spectators_see_everything(spectatorsSeeEverythingCheckBox->isChecked()); + cmd.set_join_as_judge(QApplication::keyboardModifiers() & Qt::ShiftModifier); QString gameTypes = QString(); QMapIterator gameTypeCheckBoxIterator(gameTypeCheckBoxes); diff --git a/cockatrice/src/gameselector.cpp b/cockatrice/src/gameselector.cpp index 1651f271..1777830a 100644 --- a/cockatrice/src/gameselector.cpp +++ b/cockatrice/src/gameselector.cpp @@ -8,6 +8,7 @@ #include "pending_command.h" #include "tab_room.h" #include "tab_supervisor.h" +#include #include #include #include @@ -205,6 +206,7 @@ void GameSelector::actJoin() cmd.set_password(password.toStdString()); cmd.set_spectator(spectator); cmd.set_override_restrictions(overrideRestrictions); + cmd.set_join_as_judge((QApplication::keyboardModifiers() & Qt::ShiftModifier) != 0); TabRoom *r = tabSupervisor->getRoomTabs().value(game.room_id()); if (!r) { diff --git a/cockatrice/src/messagelogwidget.cpp b/cockatrice/src/messagelogwidget.cpp index 2d257bb8..59799ae0 100644 --- a/cockatrice/src/messagelogwidget.cpp +++ b/cockatrice/src/messagelogwidget.cpp @@ -121,9 +121,11 @@ MessageLogWidget::getFromStr(CardZone *zone, QString cardName, int position, boo void MessageLogWidget::containerProcessingDone() { + if (currentContext == MessageContext_MoveCard) { for (auto &i : moveCardQueue) logDoMoveCard(i); + moveCardQueue.clear(); moveCardTapped.clear(); moveCardExtras.clear(); @@ -134,6 +136,7 @@ void MessageLogWidget::containerProcessingDone() } currentContext = MessageContext_None; + messageSuffix = messagePrefix = QString(); } void MessageLogWidget::containerProcessingStarted(const GameEventContext &context) @@ -815,6 +818,18 @@ void MessageLogWidget::logUndoDraw(Player *player, QString cardName) .arg(QString("%2").arg(sanitizeHtml(cardName)).arg(sanitizeHtml(cardName)))); } +void MessageLogWidget::setContextJudgeName(QString name) +{ + messagePrefix = QString(""); + messageSuffix = QString(" [ %1]").arg(sanitizeHtml(name)); +} + +void MessageLogWidget::appendHtmlServerMessage(const QString &html, bool optionalIsBold, QString optionalFontColor) +{ + + ChatView::appendHtmlServerMessage(messagePrefix + html + messageSuffix, optionalIsBold, optionalFontColor); +} + void MessageLogWidget::connectToPlayer(Player *player) { connect(player, SIGNAL(logSay(Player *, QString)), this, SLOT(logSay(Player *, QString))); diff --git a/cockatrice/src/messagelogwidget.h b/cockatrice/src/messagelogwidget.h index 7bbccb24..61a93bf4 100644 --- a/cockatrice/src/messagelogwidget.h +++ b/cockatrice/src/messagelogwidget.h @@ -38,6 +38,7 @@ private: QList moveCardQueue; QMap moveCardTapped; QList moveCardExtras; + QString messagePrefix, messageSuffix; const QString tableConstant() const; const QString graveyardConstant() const; @@ -108,6 +109,10 @@ public slots: void logStopDumpZone(Player *player, CardZone *zone); void logUnattachCard(Player *player, QString cardName); void logUndoDraw(Player *player, QString cardName); + void setContextJudgeName(QString player); + void appendHtmlServerMessage(const QString &html, + bool optionalIsBold = false, + QString optionalFontColor = QString()) override; public: void connectToPlayer(Player *player); diff --git a/cockatrice/src/pixmapgenerator.cpp b/cockatrice/src/pixmapgenerator.cpp index c65ce12c..77a0bca0 100644 --- a/cockatrice/src/pixmapgenerator.cpp +++ b/cockatrice/src/pixmapgenerator.cpp @@ -143,6 +143,7 @@ QPixmap UserLevelPixmapGenerator::generatePixmap(int height, UserLevelFlags user QPixmap pixmap = QPixmap("theme:userlevels/" + levelString) .scaled(height, height, Qt::KeepAspectRatio, Qt::SmoothTransformation); + pmCache.insert(key, pixmap); return pixmap; } diff --git a/cockatrice/src/player.cpp b/cockatrice/src/player.cpp index b69ac9a1..69115706 100644 --- a/cockatrice/src/player.cpp +++ b/cockatrice/src/player.cpp @@ -30,6 +30,7 @@ #include "pb/command_attach_card.pb.h" #include "pb/command_change_zone_properties.pb.h" +#include "pb/command_concede.pb.h" #include "pb/command_create_token.pb.h" #include "pb/command_draw_cards.pb.h" #include "pb/command_flip_card.pb.h" @@ -91,10 +92,11 @@ void PlayerArea::setSize(qreal width, qreal height) bRect = QRectF(0, 0, width, height); } -Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_parent) +Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, TabGame *_parent) : QObject(_parent), game(_parent), shortcutsActive(false), defaultNumberTopCards(1), defaultNumberTopCardsToPlaceBelow(1), lastTokenDestroy(true), lastTokenTableRow(0), id(_id), active(false), - local(_local), mirrored(false), handVisible(false), conceded(false), dialogSemaphore(false), deck(nullptr) + local(_local), judge(_judge), mirrored(false), handVisible(false), conceded(false), dialogSemaphore(false), + deck(nullptr) { userInfo = new ServerInfo_User; userInfo->CopyFrom(info); @@ -140,10 +142,12 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare updateBoundingRect(); - if (local) { + if (local || judge) { connect(_parent, SIGNAL(playerAdded(Player *)), this, SLOT(addPlayer(Player *))); connect(_parent, SIGNAL(playerRemoved(Player *)), this, SLOT(removePlayer(Player *))); + } + if (local || judge) { aMoveHandToTopLibrary = new QAction(this); aMoveHandToTopLibrary->setData(QList() << "deck" << 0); aMoveHandToBottomLibrary = new QAction(this); @@ -204,7 +208,7 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare aViewRfg = new QAction(this); connect(aViewRfg, SIGNAL(triggered()), this, SLOT(actViewRfg())); - if (local) { + if (local || judge) { aViewSideboard = new QAction(this); connect(aViewSideboard, SIGNAL(triggered()), this, SLOT(actViewSideboard())); @@ -237,7 +241,7 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare playerMenu = new QMenu(QString()); table->setMenu(playerMenu); - if (local) { + if (local || judge) { handMenu = playerMenu->addMenu(QString()); playerLists.append(mRevealHand = handMenu->addMenu(QString())); playerLists.append(mRevealRandomHandCard = handMenu->addMenu(QString())); @@ -286,7 +290,7 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare graveMenu = playerMenu->addMenu(QString()); graveMenu->addAction(aViewGraveyard); - if (local) { + if (local || judge) { mRevealRandomGraveyardCard = graveMenu->addMenu(QString()); QAction *newAction = mRevealRandomGraveyardCard->addAction(QString()); newAction->setData(-1); @@ -300,7 +304,7 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare rfgMenu->addAction(aViewRfg); rfg->setMenu(rfgMenu, aViewRfg); - if (local) { + if (local || judge) { graveMenu->addSeparator(); moveGraveMenu = graveMenu->addMenu(QString()); moveGraveMenu->addAction(aMoveGraveToTopLibrary); @@ -349,12 +353,22 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare playerMenu->addAction(aCreateAnotherToken); playerMenu->addMenu(createPredefinedTokenMenu); playerMenu->addSeparator(); + } + + if (local) { sayMenu = playerMenu->addMenu(QString()); initSayMenu(); + } + if (local || judge) { aCardMenu = new QAction(this); playerMenu->addSeparator(); playerMenu->addAction(aCardMenu); + } else { + aCardMenu = nullptr; + } + + if (local || judge) { for (auto &playerList : playerLists) { QAction *newAction = playerList->addAction(QString()); @@ -363,12 +377,13 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare allPlayersActions.append(newAction); playerList->addSeparator(); } - } else { + } + + if (!local && !judge) { countersMenu = nullptr; sbMenu = nullptr; aCreateAnotherToken = nullptr; createPredefinedTokenMenu = nullptr; - aCardMenu = nullptr; } aTap = new QAction(this); @@ -632,7 +647,7 @@ void Player::retranslateUi() graveMenu->setTitle(tr("&Graveyard")); rfgMenu->setTitle(tr("&Exile")); - if (local) { + if (local || judge) { moveHandMenu->setTitle(tr("&Move hand to...")); aMoveHandToTopLibrary->setText(tr("&Top of library")); aMoveHandToBottomLibrary->setText(tr("&Bottom of library")); @@ -684,7 +699,6 @@ void Player::retranslateUi() aCreateToken->setText(tr("&Create token...")); aCreateAnotherToken->setText(tr("C&reate another token")); createPredefinedTokenMenu->setTitle(tr("Cr&eate predefined token")); - sayMenu->setTitle(tr("S&ay")); QMapIterator counterIterator(counters); while (counterIterator.hasNext()) @@ -696,6 +710,10 @@ void Player::retranslateUi() allPlayersAction->setText(tr("&All players")); } + if (local) { + sayMenu->setTitle(tr("S&ay")); + } + aPlay->setText(tr("&Play")); aHide->setText(tr("&Hide")); aPlayFacedown->setText(tr("Play &Face Down")); @@ -2122,7 +2140,7 @@ AbstractCounter *Player::addCounter(int counterId, const QString &name, QColor c ctr = new GeneralCounter(this, counterId, name, color, radius, value, true, this); } counters.insert(counterId, ctr); - if (countersMenu) { + if (countersMenu && ctr->getMenu()) { countersMenu->addMenu(ctr->getMenu()); } if (shortcutsActive) { @@ -2254,17 +2272,47 @@ void Player::rearrangeCounters() PendingCommand *Player::prepareGameCommand(const google::protobuf::Message &cmd) { - return game->prepareGameCommand(cmd); + + if (judge && !local) { + Command_Judge base; + GameCommand *c = base.add_game_command(); + base.set_target_id(id); + c->GetReflection()->MutableMessage(c, cmd.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(cmd); + return game->prepareGameCommand(base); + } else { + return game->prepareGameCommand(cmd); + } } PendingCommand *Player::prepareGameCommand(const QList &cmdList) { - return game->prepareGameCommand(cmdList); + if (judge && !local) { + Command_Judge base; + base.set_target_id(id); + for (int i = 0; i < cmdList.size(); ++i) { + GameCommand *c = base.add_game_command(); + c->GetReflection() + ->MutableMessage(c, cmdList[i]->GetDescriptor()->FindExtensionByName("ext")) + ->CopyFrom(*cmdList[i]); + delete cmdList[i]; + } + return game->prepareGameCommand(base); + } else { + return game->prepareGameCommand(cmdList); + } } void Player::sendGameCommand(const google::protobuf::Message &command) { - game->sendGameCommand(command, id); + if (judge && !local) { + Command_Judge base; + GameCommand *c = base.add_game_command(); + base.set_target_id(id); + c->GetReflection()->MutableMessage(c, command.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(command); + game->sendGameCommand(base, id); + } else { + game->sendGameCommand(command, id); + } } void Player::sendGameCommand(PendingCommand *pend) @@ -2806,7 +2854,7 @@ void Player::refreshShortcuts() void Player::updateCardMenu(const CardItem *card) { // If bad card OR is a spectator (as spectators don't need card menus), return - if (card == nullptr || game->isSpectator()) { + if (card == nullptr || (game->isSpectator() && !judge)) { return; } @@ -2817,7 +2865,7 @@ void Player::updateCardMenu(const CardItem *card) cardMenu->clear(); bool revealedCard = false; - bool writeableCard = getLocal(); + bool writeableCard = getLocalOrJudge(); if (card->getZone() && card->getZone()->getIsView()) { auto *view = dynamic_cast(card->getZone()); if (view->getRevealZone()) { diff --git a/cockatrice/src/player.h b/cockatrice/src/player.h index b88338b1..f8a5b2fa 100644 --- a/cockatrice/src/player.h +++ b/cockatrice/src/player.h @@ -228,6 +228,7 @@ private: int id; bool active; bool local; + bool judge; bool mirrored; bool handVisible; bool conceded; @@ -339,8 +340,9 @@ public: return playerTarget; } - Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_parent); + Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, TabGame *_parent); ~Player() override; + void retranslateUi(); void clear(); TabGame *getGame() const @@ -365,6 +367,14 @@ public: { return local; } + bool getLocalOrJudge() const + { + return local || judge; + } + bool getJudge() const + { + return judge; + } bool getMirrored() const { return mirrored; diff --git a/cockatrice/src/playerlistwidget.cpp b/cockatrice/src/playerlistwidget.cpp index 657c1ca3..177d238b 100644 --- a/cockatrice/src/playerlistwidget.cpp +++ b/cockatrice/src/playerlistwidget.cpp @@ -58,6 +58,7 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor, notReadyIcon = QPixmap("theme:icons/not_ready_start"); concededIcon = QPixmap("theme:icons/conceded"); playerIcon = QPixmap("theme:icons/player"); + judgeIcon = QPixmap("theme:icons/scales"); spectatorIcon = QPixmap("theme:icons/spectator"); lockIcon = QPixmap("theme:icons/lock"); @@ -111,8 +112,16 @@ void PlayerListWidget::updatePlayerProperties(const ServerInfo_PlayerProperties return; bool isSpectator = prop.has_spectator() && prop.spectator(); - player->setIcon(1, isSpectator ? spectatorIcon : playerIcon); - player->setData(1, Qt::UserRole, !isSpectator); + if (prop.has_judge() || prop.has_spectator()) { + if (prop.has_judge() && prop.judge()) { + player->setIcon(1, judgeIcon); + } else if (isSpectator) { + player->setIcon(1, spectatorIcon); + } else { + player->setIcon(1, playerIcon); + } + player->setData(1, Qt::UserRole, !isSpectator); + } if (!isSpectator) { if (prop.has_conceded()) diff --git a/cockatrice/src/playerlistwidget.h b/cockatrice/src/playerlistwidget.h index 278bb3c8..a9c37c63 100644 --- a/cockatrice/src/playerlistwidget.h +++ b/cockatrice/src/playerlistwidget.h @@ -37,7 +37,7 @@ private: AbstractClient *client; TabGame *game; UserContextMenu *userContextMenu; - QIcon readyIcon, notReadyIcon, concededIcon, playerIcon, spectatorIcon, lockIcon; + QIcon readyIcon, notReadyIcon, concededIcon, playerIcon, judgeIcon, spectatorIcon, lockIcon; bool gameStarted; signals: void openMessageDialog(const QString &userName, bool focus); diff --git a/cockatrice/src/tab_game.cpp b/cockatrice/src/tab_game.cpp index 735c74d1..fec4beb3 100644 --- a/cockatrice/src/tab_game.cpp +++ b/cockatrice/src/tab_game.cpp @@ -332,8 +332,8 @@ void DeckViewContainer::setDeck(const DeckLoader &deck) TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) : Tab(_tabSupervisor), secondsElapsed(0), hostId(-1), localPlayerId(-1), - isLocalGame(_tabSupervisor->getIsLocalGame()), spectator(true), gameStateKnown(false), resuming(false), - currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(_replay), currentReplayStep(0), + isLocalGame(_tabSupervisor->getIsLocalGame()), spectator(true), judge(false), gameStateKnown(false), + resuming(false), currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(_replay), currentReplayStep(0), sayLabel(nullptr), sayEdit(nullptr) { // THIS CTOR IS USED ON REPLAY @@ -393,8 +393,8 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, const QMap &_roomGameTypes) : Tab(_tabSupervisor), clients(_clients), gameInfo(event.game_info()), roomGameTypes(_roomGameTypes), hostId(event.host_id()), localPlayerId(event.player_id()), isLocalGame(_tabSupervisor->getIsLocalGame()), - spectator(event.spectator()), gameStateKnown(false), resuming(event.resuming()), currentPhase(-1), - activeCard(nullptr), gameClosed(false), replay(nullptr), replayDock(nullptr) + spectator(event.spectator()), judge(event.judge()), gameStateKnown(false), resuming(event.resuming()), + currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(nullptr), replayDock(nullptr) { // THIS CTOR IS USED ON GAMES gameInfo.set_started(false); @@ -751,7 +751,7 @@ void TabGame::actCompleterChanged() Player *TabGame::addPlayer(int playerId, const ServerInfo_User &info) { bool local = ((clients.size() > 1) || (playerId == localPlayerId)); - auto *newPlayer = new Player(info, playerId, local, this); + auto *newPlayer = new Player(info, playerId, local, judge, this); connect(newPlayer, SIGNAL(openDeckEditor(const DeckLoader *)), this, SIGNAL(openDeckEditor(const DeckLoader *))); QString newPlayerName = "@" + newPlayer->getName(); if (sayEdit && !autocompleteUserList.contains(newPlayerName)) { @@ -789,6 +789,17 @@ void TabGame::processGameEventContainer(const GameEventContainer &cont, Abstract const GameEvent &event = cont.event_list(i); const int playerId = event.player_id(); const auto eventType = static_cast(getPbExtension(event)); + + if (cont.has_forced_by_judge()) { + auto id = cont.forced_by_judge(); + Player *judgep = players.value(id, nullptr); + if (judgep) { + messageLog->setContextJudgeName(judgep->getName()); + } else if (spectators.contains(id)) { + messageLog->setContextJudgeName(QString::fromStdString(spectators.value(id).name())); + } + } + if (spectators.contains(playerId)) { switch (eventType) { case GameEvent::GAME_SAY: diff --git a/cockatrice/src/tab_game.h b/cockatrice/src/tab_game.h index 41f56ddc..36c7c856 100644 --- a/cockatrice/src/tab_game.h +++ b/cockatrice/src/tab_game.h @@ -124,6 +124,7 @@ private: int localPlayerId; const bool isLocalGame; bool spectator; + bool judge; QMap players; QMap spectators; bool gameStateKnown; diff --git a/cockatrice/src/user_context_menu.cpp b/cockatrice/src/user_context_menu.cpp index 7968c1b2..c5a45e17 100644 --- a/cockatrice/src/user_context_menu.cpp +++ b/cockatrice/src/user_context_menu.cpp @@ -44,6 +44,8 @@ UserContextMenu::UserContextMenu(const TabSupervisor *_tabSupervisor, QWidget *p aBanHistory = new QAction(QString(), this); aPromoteToMod = new QAction(QString(), this); aDemoteFromMod = new QAction(QString(), this); + aPromoteToJudge = new QAction(QString(), this); + aDemoteFromJudge = new QAction(QString(), this); retranslateUi(); } @@ -64,6 +66,8 @@ void UserContextMenu::retranslateUi() aBanHistory->setText(tr("View user's &ban history")); aPromoteToMod->setText(tr("&Promote user to moderator")); aDemoteFromMod->setText(tr("Dem&ote user from moderator")); + aPromoteToJudge->setText(tr("Promote user to &juge")); + aDemoteFromJudge->setText(tr("Demote user from judge")); } void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer) @@ -220,7 +224,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext); if (resp.response_code() == Response::RespOk) { - if (cmd.should_be_mod()) { + if (cmd.should_be_mod() || cmd.should_be_judge()) { QMessageBox::information(static_cast(parent()), tr("Success"), tr("Successfully promoted user.")); } else { @@ -228,7 +232,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const } } else { - if (cmd.should_be_mod()) { + if (cmd.should_be_mod() || cmd.should_be_judge()) { QMessageBox::information(static_cast(parent()), tr("Failed"), tr("Failed to promote user.")); } else { QMessageBox::information(static_cast(parent()), tr("Failed"), tr("Failed to demote user.")); @@ -312,6 +316,15 @@ void UserContextMenu::showContextMenu(const QPoint &pos, (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) { menu->addAction(aPromoteToMod); } + + if (userLevel.testFlag(ServerInfo_User::IsJudge) && + (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) { + menu->addAction(aDemoteFromJudge); + + } else if (userLevel.testFlag(ServerInfo_User::IsRegistered) && + (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) { + menu->addAction(aPromoteToJudge); + } } bool anotherUser = userName != tabSupervisor->getOwnUsername(); aDetails->setEnabled(true); @@ -389,6 +402,15 @@ void UserContextMenu::showContextMenu(const QPoint &pos, cmd.set_user_name(userName.toStdString()); cmd.set_should_be_mod(actionClicked == aPromoteToMod); + PendingCommand *pend = client->prepareAdminCommand(cmd); + connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, + SLOT(adjustMod_processUserResponse(Response, CommandContainer))); + client->sendCommand(pend); + } else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) { + Command_AdjustMod cmd; + cmd.set_user_name(userName.toStdString()); + cmd.set_should_be_judge(actionClicked == aPromoteToJudge); + PendingCommand *pend = client->prepareAdminCommand(cmd); connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(adjustMod_processUserResponse(Response, CommandContainer))); diff --git a/cockatrice/src/user_context_menu.h b/cockatrice/src/user_context_menu.h index 894ba617..884d67ba 100644 --- a/cockatrice/src/user_context_menu.h +++ b/cockatrice/src/user_context_menu.h @@ -30,6 +30,7 @@ private: QAction *aKick; QAction *aBan, *aBanHistory; QAction *aPromoteToMod, *aDemoteFromMod; + QAction *aPromoteToJudge, *aDemoteFromJudge; QAction *aWarnUser, *aWarnHistory; signals: void openMessageDialog(const QString &userName, bool focus); diff --git a/cockatrice/src/userinfobox.cpp b/cockatrice/src/userinfobox.cpp index d4d22e76..ebb063b2 100644 --- a/cockatrice/src/userinfobox.cpp +++ b/cockatrice/src/userinfobox.cpp @@ -109,6 +109,9 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user) else userLevelText = tr("Unregistered user"); + if (userLevel.testFlag(ServerInfo_User::IsJudge)) + userLevelText += " | " + tr("Judge"); + if (user.has_privlevel() && user.privlevel() != "NONE") { userLevelText += " | " + QString("%1").arg(user.privlevel().c_str()); } diff --git a/cockatrice/src/userlist.cpp b/cockatrice/src/userlist.cpp index 4e5600bc..0af5cb3d 100644 --- a/cockatrice/src/userlist.cpp +++ b/cockatrice/src/userlist.cpp @@ -307,7 +307,7 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const // Sort by user level if (data(0, Qt::UserRole) != other.data(0, Qt::UserRole)) - return data(0, Qt::UserRole).toInt() > other.data(0, Qt::UserRole).toInt(); + return (data(0, Qt::UserRole).toInt() & 15) > (other.data(0, Qt::UserRole).toInt() & 15); // Sort by name return QString::localeAwareCompare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0; diff --git a/common/pb/admin_commands.proto b/common/pb/admin_commands.proto index 9d5e5414..6f974d92 100644 --- a/common/pb/admin_commands.proto +++ b/common/pb/admin_commands.proto @@ -34,6 +34,7 @@ message Command_AdjustMod { optional Command_AdjustMod ext = 1003; } required string user_name = 1; - required bool should_be_mod = 2; + optional bool should_be_mod = 2; + optional bool should_be_judge = 3; } diff --git a/common/pb/event_game_joined.proto b/common/pb/event_game_joined.proto index eaf2894c..5d90b0b7 100644 --- a/common/pb/event_game_joined.proto +++ b/common/pb/event_game_joined.proto @@ -13,4 +13,5 @@ message Event_GameJoined { optional sint32 player_id = 4; optional bool spectator = 5; optional bool resuming = 6; + optional bool judge = 7; } diff --git a/common/pb/game_commands.proto b/common/pb/game_commands.proto index 266c218f..1544f00c 100644 --- a/common/pb/game_commands.proto +++ b/common/pb/game_commands.proto @@ -34,7 +34,15 @@ message GameCommand { SET_SIDEBOARD_LOCK = 1030; CHANGE_ZONE_PROPERTIES = 1031; UNCONCEDE = 1032; - + JUDGE = 1033; } extensions 100 to max; } + +message Command_Judge { + extend GameCommand { + optional Command_Judge ext = 1033; + } + optional sint32 target_id = 1 [default = -1]; + repeated GameCommand game_command = 2; +} diff --git a/common/pb/game_event_container.proto b/common/pb/game_event_container.proto index fd4ca430..239da4fd 100644 --- a/common/pb/game_event_container.proto +++ b/common/pb/game_event_container.proto @@ -7,4 +7,5 @@ message GameEventContainer { repeated GameEvent event_list = 2; optional GameEventContext context = 3; optional uint32 seconds_elapsed = 4; + optional uint32 forced_by_judge = 5; } diff --git a/common/pb/room_commands.proto b/common/pb/room_commands.proto index d6872810..d1dfde9f 100644 --- a/common/pb/room_commands.proto +++ b/common/pb/room_commands.proto @@ -36,6 +36,7 @@ message Command_CreateGame { optional bool spectators_can_talk = 8; optional bool spectators_see_everything = 9; repeated uint32 game_type_ids = 10; + optional bool join_as_judge = 11; } message Command_JoinGame { @@ -46,4 +47,5 @@ message Command_JoinGame { optional string password = 2; optional bool spectator = 3; optional bool override_restrictions = 4; + optional bool join_as_judge = 5; } diff --git a/common/pb/serverinfo_playerproperties.proto b/common/pb/serverinfo_playerproperties.proto index 96f3644a..cdd0ee42 100644 --- a/common/pb/serverinfo_playerproperties.proto +++ b/common/pb/serverinfo_playerproperties.proto @@ -10,4 +10,5 @@ message ServerInfo_PlayerProperties { optional string deck_hash = 6; optional sint32 ping_seconds = 7; optional bool sideboard_locked = 8; + optional bool judge = 9; } diff --git a/common/pb/serverinfo_user.proto b/common/pb/serverinfo_user.proto index 5baf091c..908d2032 100644 --- a/common/pb/serverinfo_user.proto +++ b/common/pb/serverinfo_user.proto @@ -6,6 +6,7 @@ message ServerInfo_User { IsRegistered = 2; IsModerator = 4; IsAdmin = 8; + IsJudge = 16; }; enum Gender { GenderUnknown = -1; diff --git a/common/server.h b/common/server.h index 6b299279..b7f5e85f 100644 --- a/common/server.h +++ b/common/server.h @@ -172,6 +172,10 @@ public: { return 0; } + virtual bool permitCreateGameAsJudge() const + { + return false; + } Server_DatabaseInterface *getDatabaseInterface() const; int getNextLocalGameId() diff --git a/common/server_game.cpp b/common/server_game.cpp index 859ae81f..2fff28a4 100644 --- a/common/server_game.cpp +++ b/common/server_game.cpp @@ -390,8 +390,11 @@ void Server_Game::stopGameIfFinished() emit gameInfoChanged(gameInfo); } -Response::ResponseCode -Server_Game::checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions) +Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, + const QString &_password, + bool spectator, + bool overrideRestrictions, + bool asJudge) { Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface(); { @@ -400,6 +403,10 @@ Server_Game::checkJoin(ServerInfo_User *user, const QString &_password, bool spe if (playerIterator.next().value()->getUserInfo()->name() == user->name()) return Response::RespContextError; } + + if (asJudge && !(user->user_level() & ServerInfo_User::IsJudge)) { + return Response::RespUserLevelTooLow; + } if (!(overrideRestrictions && (user->user_level() & ServerInfo_User::IsModerator))) { if ((_password != password) && !(spectator && !spectatorsNeedPassword)) return Response::RespWrongPassword; @@ -437,12 +444,14 @@ bool Server_Game::containsUser(const QString &userName) const void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, ResponseContainer &rc, bool spectator, + bool judge, bool broadcastUpdate) { QMutexLocker locker(&gameMutex); Server_Player *newPlayer = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), - spectator, userInterface); + spectator, judge, userInterface); + newPlayer->moveToThread(thread()); Event_Join joinEvent; @@ -663,6 +672,7 @@ void Server_Game::createGameJoinedEvent(Server_Player *player, ResponseContainer event1.set_host_id(hostId); event1.set_player_id(player->getPlayerId()); event1.set_spectator(player->getSpectator()); + event1.set_judge(player->getJudge()); event1.set_resuming(resuming); if (resuming) { const QStringList &allGameTypes = room->getGameTypes(); diff --git a/common/server_game.h b/common/server_game.h index 89ad6e31..39470b52 100644 --- a/common/server_game.h +++ b/common/server_game.h @@ -157,11 +157,12 @@ public: return spectatorsSeeEverything; } Response::ResponseCode - checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions); + checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions, bool asJudge); bool containsUser(const QString &userName) const; void addPlayer(Server_AbstractUserInterface *userInterface, ResponseContainer &rc, bool spectator, + bool judge, bool broadcastUpdate = true); void removePlayer(Server_Player *player, Event_Leave::LeaveReason reason); void removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_Player *player); diff --git a/common/server_player.cpp b/common/server_player.cpp index 8ccfc62b..8289c640 100644 --- a/common/server_player.cpp +++ b/common/server_player.cpp @@ -87,10 +87,11 @@ Server_Player::Server_Player(Server_Game *_game, int _playerId, const ServerInfo_User &_userInfo, bool _spectator, + bool _judge, Server_AbstractUserInterface *_userInterface) : ServerInfo_User_Container(_userInfo), game(_game), userInterface(_userInterface), deck(nullptr), pingTime(0), - playerId(_playerId), spectator(_spectator), initialCards(0), nextCardId(0), readyStart(false), conceded(false), - sideboardLocked(true) + playerId(_playerId), spectator(_spectator), judge(_judge), initialCards(0), nextCardId(0), readyStart(false), + conceded(false), sideboardLocked(true) { } @@ -251,6 +252,7 @@ void Server_Player::getProperties(ServerInfo_PlayerProperties &result, bool with result.set_sideboard_locked(sideboardLocked); result.set_ready_start(readyStart); } + result.set_judge(judge); if (deck) result.set_deck_hash(deck->getDeckHash().toStdString()); result.set_ping_seconds(pingTime); @@ -353,7 +355,7 @@ Response::ResponseCode Server_Player::moveCard(GameEventStorage &ges, { // Disallow controller change to other zones than the table. if (((targetzone->getType() != ServerInfo_Zone::PublicZone) || !targetzone->hasCoords()) && - (startzone->getPlayer() != targetzone->getPlayer())) + (startzone->getPlayer() != targetzone->getPlayer()) && !judge) return Response::RespContextError; if (!targetzone->hasCoords() && (x <= -1)) @@ -797,6 +799,24 @@ Server_Player::cmdUnconcede(const Command_Unconcede & /*cmd*/, ResponseContainer return Response::RespOk; } +Response::ResponseCode Server_Player::cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges) +{ + if (!judge) + return Response::RespFunctionNotAllowed; + + Server_Player *player = this->game->getPlayers().value(cmd.target_id()); + + ges.setForcedByJudge(playerId); + if (player == nullptr) + return Response::RespContextError; + + for (int i = 0; i < cmd.game_command_size(); ++i) { + player->processGameCommand(cmd.game_command(i), rc, ges); + } + + return Response::RespOk; +} + Response::ResponseCode Server_Player::cmdReadyStart(const Command_ReadyStart &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) { @@ -988,7 +1008,7 @@ Server_Player::cmdMoveCard(const Command_MoveCard &cmd, ResponseContainer & /*rc if (!startZone) return Response::RespNameNotFound; - if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId))) + if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !judge) return Response::RespContextError; Server_Player *targetPlayer = game->getPlayers().value(cmd.target_player_id()); @@ -998,7 +1018,7 @@ Server_Player::cmdMoveCard(const Command_MoveCard &cmd, ResponseContainer & /*rc if (!targetZone) return Response::RespNameNotFound; - if ((startPlayer != this) && (targetPlayer != this)) + if ((startPlayer != this) && (targetPlayer != this) && !judge) return Response::RespContextError; QList cardsToMove; @@ -1491,13 +1511,16 @@ Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer & Response::ResponseCode Server_Player::cmdNextTurn(const Command_NextTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage & /*ges*/) { - if (spectator) - return Response::RespFunctionNotAllowed; - if (!game->getGameStarted()) return Response::RespGameNotStarted; - if (conceded) - return Response::RespContextError; + + if (!judge) { + if (spectator) + return Response::RespFunctionNotAllowed; + + if (conceded) + return Response::RespContextError; + } game->nextTurn(); return Response::RespOk; @@ -1507,16 +1530,20 @@ Response::ResponseCode Server_Player::cmdSetActivePhase(const Command_SetActiveP ResponseContainer & /*rc*/, GameEventStorage & /*ges*/) { - if (spectator) - return Response::RespFunctionNotAllowed; - if (!game->getGameStarted()) return Response::RespGameNotStarted; - if (conceded) - return Response::RespContextError; - if (game->getActivePlayer() != playerId) - return Response::RespContextError; + if (!judge) { + if (spectator) + return Response::RespFunctionNotAllowed; + + if (conceded) + return Response::RespContextError; + + if (game->getActivePlayer() != playerId) + return Response::RespContextError; + } + game->setActivePhase(cmd.phase()); return Response::RespOk; @@ -1858,6 +1885,9 @@ Server_Player::processGameCommand(const GameCommand &command, ResponseContainer case GameCommand::UNCONCEDE: return cmdUnconcede(command.GetExtension(Command_Unconcede::ext), rc, ges); break; + case GameCommand::JUDGE: + return cmdJudge(command.GetExtension(Command_Judge::ext), rc, ges); + break; default: return Response::RespInvalidCommand; diff --git a/common/server_player.h b/common/server_player.h index 9ff4ebde..79b8c9ee 100644 --- a/common/server_player.h +++ b/common/server_player.h @@ -47,6 +47,7 @@ class Command_IncCardCounter; class Command_ReadyStart; class Command_Concede; class Command_Unconcede; +class Command_Judge; class Command_IncCounter; class Command_CreateCounter; class Command_SetCounter; @@ -77,6 +78,7 @@ private: int pingTime; int playerId; bool spectator; + bool judge; int initialCards; int nextCardId; bool readyStart; @@ -89,6 +91,7 @@ public: int _playerId, const ServerInfo_User &_userInfo, bool _spectator, + bool _judge, Server_AbstractUserInterface *_handler); ~Server_Player() override; void prepareDestroy(); @@ -115,6 +118,10 @@ public: { return spectator; } + bool getJudge() const + { + return judge; + } bool getConceded() const { return conceded; @@ -185,6 +192,7 @@ public: 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 cmdJudge(const Command_Judge &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 diff --git a/common/server_protocolhandler.cpp b/common/server_protocolhandler.cpp index e27be317..e49c5b0a 100644 --- a/common/server_protocolhandler.cpp +++ b/common/server_protocolhandler.cpp @@ -762,6 +762,11 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room if (room->getGamesCreatedByUser(QString::fromStdString(userInfo->name())) >= server->getMaxGamesPerUser()) return Response::RespContextError; + if (cmd.join_as_judge() && !server->permitCreateGameAsJudge() && + !(userInfo->user_level() & ServerInfo_User::IsJudge)) { + return Response::RespContextError; + } + QList gameTypes; for (int i = cmd.game_type_ids_size() - 1; i >= 0; --i) gameTypes.append(cmd.game_type_ids(i)); @@ -776,7 +781,8 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()), cmd.max_players(), gameTypes, cmd.only_buddies(), onlyRegisteredUsers, cmd.spectators_allowed(), cmd.spectators_need_password(), cmd.spectators_can_talk(), cmd.spectators_see_everything(), room); - game->addPlayer(this, rc, false, false); + + game->addPlayer(this, rc, false, cmd.join_as_judge(), false); room->addGame(game); return Response::RespOk; diff --git a/common/server_response_containers.cpp b/common/server_response_containers.cpp index a9b38a01..c487e7f2 100644 --- a/common/server_response_containers.cpp +++ b/common/server_response_containers.cpp @@ -53,6 +53,10 @@ void GameEventStorage::sendToGame(Server_Game *game) GameEventContainer *contPrivate = new GameEventContainer; GameEventContainer *contOthers = new GameEventContainer; + if (forcedByJudge != -1) { + contPrivate->set_forced_by_judge(forcedByJudge); + contOthers->set_forced_by_judge(forcedByJudge); + } for (int i = 0; i < gameEventList.size(); ++i) { const GameEvent &event = gameEventList[i]->getGameEvent(); const GameEventStorageItem::EventRecipients recipients = gameEventList[i]->getRecipients(); diff --git a/common/server_response_containers.h b/common/server_response_containers.h index b2e77755..404e8773 100644 --- a/common/server_response_containers.h +++ b/common/server_response_containers.h @@ -48,6 +48,7 @@ private: ::google::protobuf::Message *gameEventContext; QList gameEventList; int privatePlayerId; + int forcedByJudge = -1; public: GameEventStorage(); @@ -66,6 +67,10 @@ public: { return privatePlayerId; } + void setForcedByJudge(int playerId) + { + forcedByJudge = playerId; + } void enqueueGameEvent(const ::google::protobuf::Message &event, int playerId, diff --git a/common/server_room.cpp b/common/server_room.cpp index 5f1ccd3c..a95f4eb6 100644 --- a/common/server_room.cpp +++ b/common/server_room.cpp @@ -262,9 +262,9 @@ Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGam QMutexLocker gameLocker(&g->gameMutex); Response::ResponseCode result = g->checkJoin(userInterface->getUserInfo(), QString::fromStdString(cmd.password()), - cmd.spectator(), cmd.override_restrictions()); + cmd.spectator(), cmd.override_restrictions(), cmd.join_as_judge()); if (result == Response::RespOk) - g->addPlayer(userInterface, rc, cmd.spectator()); + g->addPlayer(userInterface, rc, cmd.spectator(), cmd.join_as_judge()); return result; } diff --git a/servatrice/servatrice.ini.example b/servatrice/servatrice.ini.example index c2aee7c0..b35208c7 100644 --- a/servatrice/servatrice.ini.example +++ b/servatrice/servatrice.ini.example @@ -304,6 +304,11 @@ max_game_inactivity_time=120 ; the database. Default value is true. store_replays=true +; Allow users to create a new game and join it as a judge. The host will be able to execute any action on +; the cards of every player. This is needed in order to support some games (eg. Werewolf). +; Default off to prevent abuse on servers that are mostly running other games. +allow_create_as_judge=false + [security] ; You may want to restrict the number of users that can connect to your server at any given time. enable_max_user_limit=false diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index d9d08880..1991aa7c 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -950,6 +950,11 @@ int Servatrice::getNumberOfTCPPools() const return settingsCache->value("server/number_pools", 1).toInt(); } +bool Servatrice::permitCreateGameAsJudge() const +{ + return settingsCache->value("game/allow_create_as_judge", false).toBool(); +} + QHostAddress Servatrice::getServerTCPHost() const { QString host = settingsCache->value("server/host", "any").toString(); diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index 6d2788c0..9b06329c 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -268,6 +268,7 @@ public: int getCommandCountingInterval() const override; int getMaxCommandCountPerInterval() const override; int getMaxUserTotal() const override; + bool permitCreateGameAsJudge() const override; int getMaxTcpUserLimit() const; int getMaxWebSocketUserLimit() const; int getUsersWithAddress(const QHostAddress &address) const; diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 31f74339..c7498667 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -549,10 +549,14 @@ ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuer const int is_admin = query->value(2).toInt(); int userLevel = ServerInfo_User::IsUser | ServerInfo_User::IsRegistered; - if (is_admin == 1) + if (is_admin & 1) userLevel |= ServerInfo_User::IsAdmin | ServerInfo_User::IsModerator; - else if (is_admin == 2) + else if (is_admin & 2) userLevel |= ServerInfo_User::IsModerator; + + if (is_admin & 4) + userLevel |= ServerInfo_User::IsJudge; + result.set_user_level(userLevel); const QString country = query->value(3).toString(); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 7ea74ba0..51c8a6d0 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -1415,57 +1415,88 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReloadConfig(const Comm return Response::RespOk; } +bool AbstractServerSocketInterface::addAdminFlagToUser(const QString &userName, int flag) +{ + QSqlQuery *query = + sqlInterface->prepareQuery("update {prefix}_users set admin = (admin | :adminlevel) where name = :username"); + query->bindValue(":adminlevel", flag); + query->bindValue(":username", userName); + if (!sqlInterface->execSqlQuery(query)) { + logger->logMessage( + QString::fromStdString("Failed to promote user %1: %2").arg(userName).arg(query->lastError().text())); + return false; + } + + AbstractServerSocketInterface *user = + static_cast(server->getUsers().value(userName)); + if (user) { + Event_NotifyUser event; + event.set_type(Event_NotifyUser::PROMOTED); + SessionEvent *se = user->prepareSessionEvent(event); + user->sendProtocolItem(*se); + delete se; + } + + return true; +} + +bool AbstractServerSocketInterface::removeAdminFlagFromUser(const QString &userName, int flag) +{ + QSqlQuery *query = + sqlInterface->prepareQuery("update {prefix}_users set admin = (admin & ~ :adminlevel) where name = :username"); + query->bindValue(":adminlevel", flag); + query->bindValue(":username", userName); + if (!sqlInterface->execSqlQuery(query)) { + logger->logMessage( + QString::fromStdString("Failed to demote user %1: %2").arg(userName).arg(query->lastError().text())); + return false; + } + + AbstractServerSocketInterface *user = + static_cast(server->getUsers().value(userName)); + if (user) { + Event_ConnectionClosed event; + event.set_reason(Event_ConnectionClosed::DEMOTED); + event.set_reason_str("Your moderator and/or judge status has been revoked."); + event.set_end_time(QDateTime::currentDateTime().toTime_t()); + + SessionEvent *se = user->prepareSessionEvent(event); + user->sendProtocolItem(*se); + delete se; + } + + QMetaObject::invokeMethod(user, "prepareDestroy", Qt::QueuedConnection); + return true; +} + Response::ResponseCode AbstractServerSocketInterface::cmdAdjustMod(const Command_AdjustMod &cmd, ResponseContainer & /*rc*/) { QString userName = QString::fromStdString(cmd.user_name()); - if (cmd.should_be_mod()) { - QSqlQuery *query = - sqlInterface->prepareQuery("update {prefix}_users set admin = :adminlevel where name = :username"); - query->bindValue(":adminlevel", 2); - query->bindValue(":username", userName); - if (!sqlInterface->execSqlQuery(query)) { - logger->logMessage( - QString::fromStdString("Failed to promote user %1: %2").arg(userName).arg(query->lastError().text())); - return Response::RespInternalError; + if (cmd.has_should_be_mod()) { + if (cmd.should_be_mod()) { + if (!addAdminFlagToUser(userName, 2)) { + return Response::RespInternalError; + } + } else { + if (!removeAdminFlagFromUser(userName, 2)) { + return Response::RespInternalError; + } } + } - AbstractServerSocketInterface *user = - static_cast(server->getUsers().value(userName)); - if (user) { - Event_NotifyUser event; - event.set_type(Event_NotifyUser::PROMOTED); - SessionEvent *se = user->prepareSessionEvent(event); - user->sendProtocolItem(*se); - delete se; + if (cmd.has_should_be_judge()) { + if (cmd.should_be_judge()) { + if (!addAdminFlagToUser(userName, 4)) { + return Response::RespInternalError; + } + } else { + if (!removeAdminFlagFromUser(userName, 4)) { + return Response::RespInternalError; + } } - } else { - QSqlQuery *query = - sqlInterface->prepareQuery("update {prefix}_users set admin = :adminlevel where name = :username"); - query->bindValue(":adminlevel", 0); - query->bindValue(":username", userName); - if (!sqlInterface->execSqlQuery(query)) { - logger->logMessage( - QString::fromStdString("Failed to demote user %1: %2").arg(userName).arg(query->lastError().text())); - return Response::RespInternalError; - } - - AbstractServerSocketInterface *user = - static_cast(server->getUsers().value(userName)); - if (user) { - Event_ConnectionClosed event; - event.set_reason(Event_ConnectionClosed::DEMOTED); - event.set_reason_str("Your moderator status has been revoked."); - event.set_end_time(QDateTime::currentDateTime().toTime_t()); - - SessionEvent *se = user->prepareSessionEvent(event); - user->sendProtocolItem(*se); - delete se; - } - - QMetaObject::invokeMethod(user, "prepareDestroy", Qt::QueuedConnection); } return Response::RespOk; diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index 6a78cfa1..69b239c3 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -119,6 +119,9 @@ private: Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountPassword(const Command_AccountPassword &cmd, ResponseContainer &rc); + bool addAdminFlagToUser(const QString &user, int flag); + bool removeAdminFlagFromUser(const QString &user, int flag); + public: AbstractServerSocketInterface(Servatrice *_server, Servatrice_DatabaseInterface *_databaseInterface,