diff --git a/CMakeLists.txt b/CMakeLists.txt index 84a67b73..69d22d76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,11 @@ if (NOT WITHOUT_CLIENT) add_subdirectory(cockatrice) add_subdirectory(oracle) endif(NOT WITHOUT_CLIENT) +if (WITH_TESTCLIENT) + add_subdirectory(testclient) +endif(WITH_TESTCLIENT) FILE(GLOB sounds "${CMAKE_CURRENT_SOURCE_DIR}/sounds/*.raw") INSTALL(FILES ${sounds} DESTINATION share/cockatrice/sounds) FILE(GLOB zonebg "${CMAKE_CURRENT_SOURCE_DIR}/zonebg/*.*") -INSTALL(FILES ${zonebg} DESTINATION share/cockatrice/zonebg) \ No newline at end of file +INSTALL(FILES ${zonebg} DESTINATION share/cockatrice/zonebg) diff --git a/README.md b/README.md new file mode 100644 index 00000000..3502fd8a --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Cockatrice + +Cockatrice is an open-source multiplatform software for playing card games, +such as Magic: The Gathering, over a network. It is fully client-server based +to prevent any kind of cheating, though it supports single-player games without +a network interface as well. Both client and server are written in Qt 4. + +# License + +Cockatrice is free software, licensed under the GPLv2; see COPYING for details. + +# Building + +Dependencies: + +- [Qt](http://qt-project.org/) + +- [protobuf](http://code.google.com/p/protobuf/) + +- [CMake](http://www.cmake.org/) + +The server requires an additional dependency: + +- [libgcrypt](http://www.gnu.org/software/libgcrypt/) + +``` +mkdir build +cd build +cmake .. +make +make install +``` + +The following flags can be passed to `cmake`: + +- `-DWITH_SERVER=1` build the server + +- `-DWITHOUT_CLIENT=1` do not build the client + +# Running + +`oracle` fetches card data + +`cockatrice` is the game client + +`servatrice` is the server diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 135c888b..70de75a5 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -197,10 +197,16 @@ if (NOT QT_QTMULTIMEDIA_FOUND) FIND_PACKAGE(QtMobility REQUIRED) endif (NOT QT_QTMULTIMEDIA_FOUND) FIND_PACKAGE(Protobuf REQUIRED) +FIND_PACKAGE(Threads) set(CMAKE_CXX_FLAGS_DEBUG "-ggdb -O0") set(CMAKE_CXX_FLAGS_RELEASE "-s -O2") +# paths +set(ICONDIR share/icons CACHE STRING "icon dir") +set(DESKTOPDIR share/applications CACHE STRING "desktop file destination") + + QT4_WRAP_CPP(cockatrice_HEADERS_MOC ${cockatrice_HEADERS}) QT4_ADD_TRANSLATION(cockatrice_QM ${cockatrice_TS}) QT4_ADD_RESOURCES(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) @@ -214,13 +220,17 @@ INCLUDE_DIRECTORIES(${QT_MOBILITY_INCLUDE_DIR}) INCLUDE_DIRECTORIES(${QT_MOBILITY_MULTIMEDIAKIT_INCLUDE_DIR}) ADD_EXECUTABLE(cockatrice WIN32 MACOSX_BUNDLE ${cockatrice_SOURCES} ${cockatrice_QM} ${cockatrice_RESOURCES_RCC} ${cockatrice_HEADERS_MOC}) -TARGET_LINK_LIBRARIES(cockatrice cockatrice_common ${QT_LIBRARIES} ${QT_MOBILITY_MULTIMEDIAKIT_LIBRARY}) +TARGET_LINK_LIBRARIES(cockatrice cockatrice_common ${QT_LIBRARIES} ${QT_MOBILITY_MULTIMEDIAKIT_LIBRARY} ${CMAKE_THREAD_LIBS_INIT}) -INSTALL(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/cockatrice DESTINATION bin) +IF (NOT APPLE) + INSTALL(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/cockatrice DESTINATION bin) +ELSE (APPLE) + INSTALL(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/cockatrice.app DESTINATION bin) +ENDIF (NOT APPLE) if (NOT WIN32 AND NOT APPLE) - INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION share/icons/hicolor/48x48/apps) - INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION share/icons/hicolor/scalable/apps) - INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION share/applications) + INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps) + INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps) + INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR}) INSTALL(FILES ${cockatrice_QM} DESTINATION share/cockatrice/translations) ENDIF(NOT WIN32 AND NOT APPLE) diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index 07f2d580..451f9c1b 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -71,6 +71,7 @@ resources/countries/es.svg resources/countries/fi.svg resources/countries/fr.svg + resources/countries/ge.svg resources/countries/gr.svg resources/countries/gt.svg resources/countries/hr.svg diff --git a/cockatrice/resources/countries/ge.svg b/cockatrice/resources/countries/ge.svg new file mode 100644 index 00000000..8e3e0189 --- /dev/null +++ b/cockatrice/resources/countries/ge.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/cockatrice/src/carddatabasemodel.cpp b/cockatrice/src/carddatabasemodel.cpp index 67b8404e..d63f6bde 100644 --- a/cockatrice/src/carddatabasemodel.cpp +++ b/cockatrice/src/carddatabasemodel.cpp @@ -117,7 +117,7 @@ bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex { CardInfo const *info = static_cast(sourceModel())->getCard(sourceRow); - if (((isToken == ShowTrue) && !info->getIsToken()) || (isToken == ShowFalse) && info->getIsToken()) + if (((isToken == ShowTrue) && !info->getIsToken()) || ((isToken == ShowFalse) && info->getIsToken())) return false; if (!cardNameBeginning.isEmpty()) diff --git a/cockatrice/src/chatview.cpp b/cockatrice/src/chatview.cpp index 99b8d939..376a6fd1 100644 --- a/cockatrice/src/chatview.cpp +++ b/cockatrice/src/chatview.cpp @@ -54,6 +54,35 @@ void ChatView::appendHtml(const QString &html) verticalScrollBar()->setValue(verticalScrollBar()->maximum()); } +void ChatView::appendCardTag(QTextCursor &cursor, const QString &cardName) +{ + QTextCharFormat oldFormat = cursor.charFormat(); + QTextCharFormat anchorFormat = oldFormat; + anchorFormat.setForeground(Qt::blue); + anchorFormat.setAnchor(true); + anchorFormat.setAnchorHref("card://" + cardName); + + cursor.setCharFormat(anchorFormat); + cursor.insertText(cardName); + cursor.setCharFormat(oldFormat); +} + +void ChatView::appendUrlTag(QTextCursor &cursor, QString url) +{ + if (!url.contains("://")) + url.prepend("http://"); + + QTextCharFormat oldFormat = cursor.charFormat(); + QTextCharFormat anchorFormat = oldFormat; + anchorFormat.setForeground(Qt::blue); + anchorFormat.setAnchor(true); + anchorFormat.setAnchorHref(url); + + cursor.setCharFormat(anchorFormat); + cursor.insertText(url); + cursor.setCharFormat(oldFormat); +} + void ChatView::appendMessage(QString message, QString sender, UserLevelFlags userLevel, bool playerBold) { bool atBottom = verticalScrollBar()->value() >= verticalScrollBar()->maximum(); @@ -103,7 +132,7 @@ void ChatView::appendMessage(QString message, QString sender, UserLevelFlags use message = message.mid(index); if (message.isEmpty()) break; - + if (message.startsWith("[card]")) { message = message.mid(6); int closeTagIndex = message.indexOf("[/card]"); @@ -113,14 +142,17 @@ void ChatView::appendMessage(QString message, QString sender, UserLevelFlags use else message = message.mid(closeTagIndex + 7); - QTextCharFormat tempFormat = messageFormat; - tempFormat.setForeground(Qt::blue); - tempFormat.setAnchor(true); - tempFormat.setAnchorHref("card://" + cardName); + appendCardTag(cursor, cardName); + } else if (message.startsWith("[[")) { + message = message.mid(2); + int closeTagIndex = message.indexOf("]]"); + QString cardName = message.left(closeTagIndex); + if (closeTagIndex == -1) + message.clear(); + else + message = message.mid(closeTagIndex + 2); - cursor.setCharFormat(tempFormat); - cursor.insertText(cardName); - cursor.setCharFormat(messageFormat); + appendCardTag(cursor, cardName); } else if (message.startsWith("[url]")) { message = message.mid(5); int closeTagIndex = message.indexOf("[/url]"); @@ -130,17 +162,7 @@ void ChatView::appendMessage(QString message, QString sender, UserLevelFlags use else message = message.mid(closeTagIndex + 6); - if (!url.contains("://")) - url.prepend("http://"); - - QTextCharFormat tempFormat = messageFormat; - tempFormat.setForeground(Qt::blue); - tempFormat.setAnchor(true); - tempFormat.setAnchorHref(url); - - cursor.setCharFormat(tempFormat); - cursor.insertText(url); - cursor.setCharFormat(messageFormat); + appendUrlTag(cursor, url); } else from = 1; } diff --git a/cockatrice/src/chatview.h b/cockatrice/src/chatview.h index 27e80f8c..37685e10 100644 --- a/cockatrice/src/chatview.h +++ b/cockatrice/src/chatview.h @@ -28,6 +28,8 @@ private: QString hoveredContent; QTextFragment getFragmentUnderMouse(const QPoint &pos) const; QTextCursor prepareBlock(bool same = false); + void appendCardTag(QTextCursor &cursor, const QString &cardName); + void appendUrlTag(QTextCursor &cursor, QString url); private slots: void openLink(const QUrl &link); public: diff --git a/cockatrice/src/dlg_filter_games.cpp b/cockatrice/src/dlg_filter_games.cpp index 45895678..721a3d20 100644 --- a/cockatrice/src/dlg_filter_games.cpp +++ b/cockatrice/src/dlg_filter_games.cpp @@ -14,6 +14,7 @@ DlgFilterGames::DlgFilterGames(const QMap &allGameTypes, QWidget * : QDialog(parent) { unavailableGamesVisibleCheckBox = new QCheckBox(tr("Show &unavailable games")); + passwordProtectedGamesVisibleCheckBox = new QCheckBox(tr("Show &password protected games")); QLabel *gameNameFilterLabel = new QLabel(tr("Game &description:")); gameNameFilterEdit = new QLineEdit; @@ -68,6 +69,7 @@ DlgFilterGames::DlgFilterGames(const QMap &allGameTypes, QWidget * leftGrid->addWidget(creatorNameFilterEdit, 1, 1); leftGrid->addWidget(maxPlayersGroupBox, 2, 0, 1, 2); leftGrid->addWidget(unavailableGamesVisibleCheckBox, 3, 0, 1, 2); + leftGrid->addWidget(passwordProtectedGamesVisibleCheckBox, 4, 0, 1, 2); QVBoxLayout *leftColumn = new QVBoxLayout; leftColumn->addLayout(leftGrid); @@ -102,6 +104,16 @@ void DlgFilterGames::setUnavailableGamesVisible(bool _unavailableGamesVisible) unavailableGamesVisibleCheckBox->setChecked(_unavailableGamesVisible); } +bool DlgFilterGames::getPasswordProtectedGamesVisible() const +{ + return passwordProtectedGamesVisibleCheckBox->isChecked(); +} + +void DlgFilterGames::setPasswordProtectedGamesVisible(bool _passwordProtectedGamesVisible) +{ + passwordProtectedGamesVisibleCheckBox->setChecked(_passwordProtectedGamesVisible); +} + QString DlgFilterGames::getGameNameFilter() const { return gameNameFilterEdit->text(); diff --git a/cockatrice/src/dlg_filter_games.h b/cockatrice/src/dlg_filter_games.h index d79c1841..5c768bc1 100644 --- a/cockatrice/src/dlg_filter_games.h +++ b/cockatrice/src/dlg_filter_games.h @@ -13,6 +13,7 @@ class DlgFilterGames : public QDialog { Q_OBJECT private: QCheckBox *unavailableGamesVisibleCheckBox; + QCheckBox *passwordProtectedGamesVisibleCheckBox; QLineEdit *gameNameFilterEdit; QLineEdit *creatorNameFilterEdit; QMap gameTypeFilterCheckBoxes; @@ -23,6 +24,8 @@ public: bool getUnavailableGamesVisible() const; void setUnavailableGamesVisible(bool _unavailableGamesVisible); + bool getPasswordProtectedGamesVisible() const; + void setPasswordProtectedGamesVisible(bool _passwordProtectedGamesVisible); QString getGameNameFilter() const; void setGameNameFilter(const QString &_gameNameFilter); QString getCreatorNameFilter() const; diff --git a/cockatrice/src/gameselector.cpp b/cockatrice/src/gameselector.cpp index 14350789..86f86df9 100644 --- a/cockatrice/src/gameselector.cpp +++ b/cockatrice/src/gameselector.cpp @@ -82,6 +82,7 @@ void GameSelector::actSetFilter() gameTypeMap = gameListModel->getGameTypes().value(room->getRoomId()); DlgFilterGames dlg(gameTypeMap, this); dlg.setUnavailableGamesVisible(gameListProxyModel->getUnavailableGamesVisible()); + dlg.setPasswordProtectedGamesVisible(gameListProxyModel->getPasswordProtectedGamesVisible()); dlg.setGameNameFilter(gameListProxyModel->getGameNameFilter()); dlg.setCreatorNameFilter(gameListProxyModel->getCreatorNameFilter()); dlg.setGameTypeFilter(gameListProxyModel->getGameTypeFilter()); @@ -93,6 +94,7 @@ void GameSelector::actSetFilter() clearFilterButton->setEnabled(true); gameListProxyModel->setUnavailableGamesVisible(dlg.getUnavailableGamesVisible()); + gameListProxyModel->setPasswordProtectedGamesVisible(dlg.getPasswordProtectedGamesVisible()); gameListProxyModel->setGameNameFilter(dlg.getGameNameFilter()); gameListProxyModel->setCreatorNameFilter(dlg.getCreatorNameFilter()); gameListProxyModel->setGameTypeFilter(dlg.getGameTypeFilter()); diff --git a/cockatrice/src/gamesmodel.cpp b/cockatrice/src/gamesmodel.cpp index 5a1d6e6c..cbffc86d 100644 --- a/cockatrice/src/gamesmodel.cpp +++ b/cockatrice/src/gamesmodel.cpp @@ -105,6 +105,12 @@ void GamesProxyModel::setUnavailableGamesVisible(bool _unavailableGamesVisible) invalidateFilter(); } +void GamesProxyModel::setPasswordProtectedGamesVisible(bool _passwordProtectedGamesVisible) +{ + passwordProtectedGamesVisible = _passwordProtectedGamesVisible; + invalidateFilter(); +} + void GamesProxyModel::setGameNameFilter(const QString &_gameNameFilter) { gameNameFilter = _gameNameFilter; @@ -133,6 +139,7 @@ void GamesProxyModel::setMaxPlayersFilter(int _maxPlayersFilterMin, int _maxPlay void GamesProxyModel::resetFilterParameters() { unavailableGamesVisible = false; + passwordProtectedGamesVisible = false; gameNameFilter = QString(); creatorNameFilter = QString(); gameTypeFilter.clear(); @@ -158,6 +165,8 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &/*sourc if (game.only_registered()) return false; } + if (!passwordProtectedGamesVisible && game.with_password()) + return false; if (!gameNameFilter.isEmpty()) if (!QString::fromStdString(game.description()).contains(gameNameFilter, Qt::CaseInsensitive)) return false; diff --git a/cockatrice/src/gamesmodel.h b/cockatrice/src/gamesmodel.h index 977bf75d..843fb043 100644 --- a/cockatrice/src/gamesmodel.h +++ b/cockatrice/src/gamesmodel.h @@ -33,6 +33,7 @@ class GamesProxyModel : public QSortFilterProxyModel { private: ServerInfo_User *ownUser; bool unavailableGamesVisible; + bool passwordProtectedGamesVisible; QString gameNameFilter, creatorNameFilter; QSet gameTypeFilter; int maxPlayersFilterMin, maxPlayersFilterMax; @@ -41,6 +42,8 @@ public: bool getUnavailableGamesVisible() const { return unavailableGamesVisible; } void setUnavailableGamesVisible(bool _unavailableGamesVisible); + bool getPasswordProtectedGamesVisible() const { return passwordProtectedGamesVisible; } + void setPasswordProtectedGamesVisible(bool _passwordProtectedGamesVisible); QString getGameNameFilter() const { return gameNameFilter; } void setGameNameFilter(const QString &_gameNameFilter); QString getCreatorNameFilter() const { return creatorNameFilter; } diff --git a/cockatrice/src/tab_admin.cpp b/cockatrice/src/tab_admin.cpp index 6e02758e..53189707 100644 --- a/cockatrice/src/tab_admin.cpp +++ b/cockatrice/src/tab_admin.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -24,24 +25,16 @@ ShutdownDialog::ShutdownDialog(QWidget *parent) minutesEdit->setMinimum(0); minutesEdit->setValue(5); - QPushButton *okButton = new QPushButton(tr("&OK")); - okButton->setAutoDefault(true); - okButton->setDefault(true); - connect(okButton, SIGNAL(clicked()), this, SLOT(accept())); - QPushButton *cancelButton = new QPushButton(tr("&Cancel")); - connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); - - QHBoxLayout *buttonLayout = new QHBoxLayout; - buttonLayout->addStretch(); - buttonLayout->addWidget(okButton); - buttonLayout->addWidget(cancelButton); + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); QGridLayout *mainLayout = new QGridLayout; mainLayout->addWidget(reasonLabel, 0, 0); mainLayout->addWidget(reasonEdit, 0, 1); mainLayout->addWidget(minutesLabel, 1, 0); mainLayout->addWidget(minutesEdit, 1, 1); - mainLayout->addLayout(buttonLayout, 2, 0, 1, 2); + mainLayout->addWidget(buttonBox, 2, 0, 1, 2); setLayout(mainLayout); setWindowTitle(tr("Shut down server")); diff --git a/cockatrice/src/tab_deck_storage.cpp b/cockatrice/src/tab_deck_storage.cpp index b19e370b..10ddb354 100644 --- a/cockatrice/src/tab_deck_storage.cpp +++ b/cockatrice/src/tab_deck_storage.cpp @@ -306,6 +306,8 @@ void TabDeckStorage::actDeleteRemoteDeck() QString path = dir->getPath(); if (path.isEmpty()) return; + if (QMessageBox::warning(this, tr("Delete remote folder"), tr("Are you sure you want to delete \"%1\"?").arg(path), QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) + return; Command_DeckDelDir cmd; cmd.set_path(path.toStdString()); pend = client->prepareSessionCommand(cmd); diff --git a/cockatrice/src/tab_server.cpp b/cockatrice/src/tab_server.cpp index f6b9448f..0ad57ccd 100644 --- a/cockatrice/src/tab_server.cpp +++ b/cockatrice/src/tab_server.cpp @@ -76,8 +76,10 @@ void RoomSelector::processListRoomsEvent(const Event_ListRooms &event) twi->setData(0, Qt::DisplayRole, QString::fromStdString(room.name())); if (room.has_description()) twi->setData(1, Qt::DisplayRole, QString::fromStdString(room.description())); - twi->setData(2, Qt::DisplayRole, room.player_count()); - twi->setData(3, Qt::DisplayRole, room.game_count()); + if (room.has_player_count()) + twi->setData(2, Qt::DisplayRole, room.player_count()); + if (room.has_game_count()) + twi->setData(3, Qt::DisplayRole, room.game_count()); return; } } @@ -91,6 +93,7 @@ void RoomSelector::processListRoomsEvent(const Event_ListRooms &event) twi->setData(3, Qt::DisplayRole, room.game_count()); twi->setTextAlignment(2, Qt::AlignRight); twi->setTextAlignment(3, Qt::AlignRight); + roomList->addTopLevelItem(twi); if (room.has_auto_join()) if (room.auto_join()) diff --git a/common/server.cpp b/common/server.cpp index 149805f2..f0530497 100644 --- a/common/server.cpp +++ b/common/server.cpp @@ -36,7 +36,7 @@ #include Server::Server(bool _threaded, QObject *parent) - : QObject(parent), threaded(_threaded), clientsLock(QReadWriteLock::Recursive), nextLocalGameId(0) + : QObject(parent), threaded(_threaded), nextLocalGameId(0) { qRegisterMetaType("ServerInfo_Game"); qRegisterMetaType("ServerInfo_Room"); @@ -79,10 +79,9 @@ void Server::prepareDestroy() clientsLock.unlock(); } while (!done); } else { - clientsLock.lockForWrite(); + // no locking is needed in unthreaded mode while (!clients.isEmpty()) clients.first()->prepareDestroy(); - clientsLock.unlock(); } roomsLock.lockForWrite(); @@ -141,7 +140,7 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, QString } users.insert(name, session); - qDebug() << "Server::loginUser: name=" << name; + qDebug() << "Server::loginUser:" << session << "name=" << name; data.set_session_id(databaseInterface->startSession(name, session->getAddress())); databaseInterface->unlockSessionTables(); @@ -159,7 +158,7 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, QString clients[i]->sendProtocolItem(*se); delete se; - event.mutable_user_info()->CopyFrom(session->copyUserInfo(true, true)); + event.mutable_user_info()->CopyFrom(session->copyUserInfo(true, true, true)); locker.unlock(); se = Server_ProtocolHandler::prepareSessionEvent(event); @@ -187,6 +186,17 @@ QList Server::getPersistentPlayerReferences(const QString &user return persistentPlayers.values(userName); } +Server_AbstractUserInterface *Server::findUser(const QString &userName) const +{ + // Call this only with clientsLock set. + + Server_AbstractUserInterface *userHandler = users.value(userName); + if (userHandler) + return userHandler; + else + return externalUsers.value(userName); +} + void Server::addClient(Server_ProtocolHandler *client) { QWriteLocker locker(&clientsLock); @@ -218,13 +228,13 @@ void Server::removeClient(Server_ProtocolHandler *client) qDebug() << "closed session id:" << sessionId; } } - qDebug() << "Server::removeClient:" << clients.size() << "clients; " << users.size() << "users left"; + qDebug() << "Server::removeClient: removed" << (void *) client << ";" << clients.size() << "clients; " << users.size() << "users left"; } void Server::externalUserJoined(const ServerInfo_User &userInfo) { // This function is always called from the main thread via signal/slot. - QWriteLocker locker(&clientsLock); + clientsLock.lockForWrite(); Server_RemoteUserInterface *newUser = new Server_RemoteUserInterface(this, ServerInfo_User_Container(userInfo)); externalUsers.insert(QString::fromStdString(userInfo.name()), newUser); @@ -263,7 +273,7 @@ void Server::externalUserLeft(const QString &userName) if (!room) continue; - QMutexLocker roomGamesLocker(&room->gamesMutex); + QReadLocker roomGamesLocker(&room->gamesLock); Server_Game *game = room->getGames().value(userGamesIterator.key()); if (!game) continue; @@ -389,7 +399,7 @@ void Server::externalGameCommandContainerReceived(const CommandContainer &cont, throw Response::RespNotInRoom; } - QMutexLocker roomGamesLocker(&room->gamesMutex); + QReadLocker roomGamesLocker(&room->gamesLock); Server_Game *game = room->getGames().value(cont.game_id()); if (!game) { qDebug() << "externalGameCommandContainerReceived: game id=" << cont.game_id() << "not found"; @@ -499,7 +509,7 @@ int Server::getGamesCount() const QMapIterator roomIterator(rooms); while (roomIterator.hasNext()) { Server_Room *room = roomIterator.next().value(); - QMutexLocker roomLocker(&room->gamesMutex); + QReadLocker roomLocker(&room->gamesLock); result += room->getGames().size(); } return result; diff --git a/common/server.h b/common/server.h index a09eb31c..631d2d2e 100644 --- a/common/server.h +++ b/common/server.h @@ -42,9 +42,11 @@ public: mutable QReadWriteLock clientsLock, roomsLock; // locking order: roomsLock before clientsLock Server(bool _threaded, QObject *parent = 0); ~Server(); + void setThreaded(bool _threaded) { threaded = _threaded; } AuthenticationResult loginUser(Server_ProtocolHandler *session, QString &name, const QString &password, QString &reason, int &secondsLeft); const QMap &getRooms() { return rooms; } + Server_AbstractUserInterface *findUser(const QString &userName) const; const QMap &getUsers() const { return users; } const QMap &getUsersBySessionId() const { return usersBySessionId; } void addClient(Server_ProtocolHandler *player); @@ -62,7 +64,6 @@ public: Server_DatabaseInterface *getDatabaseInterface() const; int getNextLocalGameId() { QMutexLocker locker(&nextLocalGameIdMutex); return ++nextLocalGameId; } - virtual void storeGameInformation(int secondsElapsed, const QSet &allPlayersEver, const QSet &allSpectatorsEver, const QList &replays) { } void sendIsl_Response(const Response &item, int serverId = -1, qint64 sessionId = -1); void sendIsl_SessionEvent(const SessionEvent &item, int serverId = -1, qint64 sessionId = -1); diff --git a/common/server_abstractuserinterface.cpp b/common/server_abstractuserinterface.cpp index 6ecc1c46..ea4b0f6b 100644 --- a/common/server_abstractuserinterface.cpp +++ b/common/server_abstractuserinterface.cpp @@ -78,7 +78,7 @@ void Server_AbstractUserInterface::joinPersistentGames(ResponseContainer &rc) Server_Room *room = server->getRooms().value(pr.getRoomId()); if (!room) continue; - QMutexLocker roomGamesLocker(&room->gamesMutex); + QReadLocker roomGamesLocker(&room->gamesLock); Server_Game *game = room->getGames().value(pr.getGameId()); if (!game) diff --git a/common/server_cardzone.cpp b/common/server_cardzone.cpp index c806dbca..018abae8 100644 --- a/common/server_cardzone.cpp +++ b/common/server_cardzone.cpp @@ -51,28 +51,78 @@ void Server_CardZone::shuffle() playersWithWritePermission.clear(); } +void Server_CardZone::removeCardFromCoordMap(Server_Card *card, int oldX, int oldY) +{ + if (oldX < 0) + return; + + const int baseX = (oldX / 3) * 3; + QMap &coordMap = coordinateMap[oldY]; + + if (coordMap.contains(baseX) && coordMap.contains(baseX + 1) && coordMap.contains(baseX + 2)) + // If the removal of this card has opened up a previously full pile... + freePilesMap[oldY].insert(coordMap.value(baseX)->getName(), baseX); + + coordMap.remove(oldX); + + if (!(coordMap.contains(baseX) && coordMap.value(baseX)->getName() == card->getName()) && !(coordMap.contains(baseX + 1) && coordMap.value(baseX + 1)->getName() == card->getName()) && !(coordMap.contains(baseX + 2) && coordMap.value(baseX + 2)->getName() == card->getName())) + // If this card was the last one with this name... + freePilesMap[oldY].remove(card->getName(), baseX); + + if (!coordMap.contains(baseX) && !coordMap.contains(baseX + 1) && !coordMap.contains(baseX + 2)) { + // If the removal of this card has freed a whole pile, i.e. it was the last card in it... + if (baseX < freeSpaceMap[oldY]) + freeSpaceMap[oldY] = baseX; + } +} + +void Server_CardZone::insertCardIntoCoordMap(Server_Card *card, int x, int y) +{ + if (x < 0) + return; + + coordinateMap[y].insert(x, card); + if (!(x % 3)) { + if (!freePilesMap[y].contains(card->getName(), x) && card->getAttachedCards().isEmpty()) + freePilesMap[y].insert(card->getName(), x); + if (freeSpaceMap[y] == x) { + int nextFreeX = x; + do { + nextFreeX += 3; + } while (coordinateMap[y].contains(nextFreeX) || coordinateMap[y].contains(nextFreeX + 1) || coordinateMap[y].contains(nextFreeX + 2)); + freeSpaceMap[y] = nextFreeX; + } + } else if (!((x - 2) % 3)) { + const int baseX = (x / 3) * 3; + freePilesMap[y].remove(coordinateMap[y].value(baseX)->getName(), baseX); + } +} + int Server_CardZone::removeCard(Server_Card *card) { int index = cards.indexOf(card); cards.removeAt(index); + if (has_coords) + removeCardFromCoordMap(card, card->getX(), card->getY()); card->setZone(0); return index; } -Server_Card *Server_CardZone::getCard(int id, int *position) +Server_Card *Server_CardZone::getCard(int id, int *position, bool remove) { if (type != ServerInfo_Zone::HiddenZone) { - QListIterator CardIterator(cards); - int i = 0; - while (CardIterator.hasNext()) { - Server_Card *tmp = CardIterator.next(); + for (int i = 0; i < cards.size(); ++i) { + Server_Card *tmp = cards[i]; if (tmp->getId() == id) { if (position) *position = i; + if (remove) { + cards.removeAt(i); + tmp->setZone(0); + } return tmp; } - i++; } return NULL; } else { @@ -81,30 +131,29 @@ Server_Card *Server_CardZone::getCard(int id, int *position) Server_Card *tmp = cards[id]; if (position) *position = id; + if (remove) { + cards.removeAt(id); + tmp->setZone(0); + } return tmp; } } int Server_CardZone::getFreeGridColumn(int x, int y, const QString &cardName) const { - QMap coordMap; - for (int i = 0; i < cards.size(); ++i) - if (cards[i]->getY() == y) - coordMap.insert(cards[i]->getX(), cards[i]); - - int resultX = 0; + const QMap &coordMap = coordinateMap.value(y); if (x == -1) { - for (int i = 0; i < cards.size(); ++i) - if ((cards[i]->getName() == cardName) && !(cards[i]->getX() % 3) && (cards[i]->getY() == y)) { - if (!cards[i]->getAttachedCards().isEmpty()) - continue; - if (!coordMap.value(cards[i]->getX() + 1)) - return cards[i]->getX() + 1; - if (!coordMap.value(cards[i]->getX() + 2)) - return cards[i]->getX() + 2; - } - } else if (x == -2) { - } else { + if (freePilesMap[y].contains(cardName)) { + x = (freePilesMap[y].value(cardName) / 3) * 3; + if (!coordMap.contains(x)) + return x; + else if (!coordMap.contains(x + 1)) + return x + 1; + else + return x + 2; + } + } else if (x >= 0) { + int resultX = 0; x = (x / 3) * 3; if (!coordMap.contains(x)) resultX = x; @@ -119,13 +168,14 @@ int Server_CardZone::getFreeGridColumn(int x, int y, const QString &cardName) co resultX = x; x = -1; } + if (x < 0) + while (coordMap.contains(resultX)) + resultX += 3; + + return resultX; } - if (x < 0) - while (coordMap.value(resultX)) - resultX += 3; - - return resultX; + return freeSpaceMap[y]; } bool Server_CardZone::isColumnStacked(int x, int y) const @@ -133,12 +183,7 @@ bool Server_CardZone::isColumnStacked(int x, int y) const if (!has_coords) return false; - QMap coordMap; - for (int i = 0; i < cards.size(); ++i) - if (cards[i]->getY() == y) - coordMap.insert(cards[i]->getX(), cards[i]); - - return coordMap.contains((x / 3) * 3 + 1); + return coordinateMap[y].contains((x / 3) * 3 + 1); } bool Server_CardZone::isColumnEmpty(int x, int y) const @@ -146,63 +191,68 @@ bool Server_CardZone::isColumnEmpty(int x, int y) const if (!has_coords) return true; - QMap coordMap; - for (int i = 0; i < cards.size(); ++i) - if (cards[i]->getY() == y) - coordMap.insert(cards[i]->getX(), cards[i]); - - return !coordMap.contains((x / 3) * 3); + return !coordinateMap[y].contains((x / 3) * 3); } -void Server_CardZone::moveCard(GameEventStorage &ges, QMap &coordMap, Server_Card *card, int x, int y) +void Server_CardZone::moveCardInRow(GameEventStorage &ges, Server_Card *card, int x, int y) { - coordMap.remove(card->getY() * 10000 + card->getX()); - CardToMove *cardToMove = new CardToMove; cardToMove->set_card_id(card->getId()); player->moveCard(ges, this, QList() << cardToMove, this, x, y, false, false); delete cardToMove; - - coordMap.insert(y * 10000 + x, card); } void Server_CardZone::fixFreeSpaces(GameEventStorage &ges) { - QMap coordMap; - QSet placesToLook; - for (int i = 0; i < cards.size(); ++i) { - coordMap.insert(cards[i]->getY() * 10000 + cards[i]->getX(), cards[i]); - placesToLook.insert(cards[i]->getY() * 10000 + (cards[i]->getX() / 3) * 3); - } + if (!has_coords) + return; - QSetIterator placeIterator(placesToLook); + QSet > placesToLook; + for (int i = 0; i < cards.size(); ++i) + placesToLook.insert(QPair((cards[i]->getX() / 3) * 3, cards[i]->getY())); + + QSetIterator > placeIterator(placesToLook); while (placeIterator.hasNext()) { - int foo = placeIterator.next(); - int y = foo / 10000; - int baseX = foo - y * 10000; + const QPair &foo = placeIterator.next(); + int baseX = foo.first; + int y = foo.second; - if (!coordMap.contains(y * 10000 + baseX)) { - if (coordMap.contains(y * 10000 + baseX + 1)) - moveCard(ges, coordMap, coordMap.value(y * 10000 + baseX + 1), baseX, y); - else if (coordMap.contains(y * 10000 + baseX + 2)) { - moveCard(ges, coordMap, coordMap.value(y * 10000 + baseX + 2), baseX, y); + if (!coordinateMap[y].contains(baseX)) { + if (coordinateMap[y].contains(baseX + 1)) + moveCardInRow(ges, coordinateMap[y].value(baseX + 1), baseX, y); + else if (coordinateMap[y].contains(baseX + 2)) { + moveCardInRow(ges, coordinateMap[y].value(baseX + 2), baseX, y); continue; } else continue; } - if (!coordMap.contains(y * 10000 + baseX + 1) && coordMap.contains(y * 10000 + baseX + 2)) - moveCard(ges, coordMap, coordMap.value(y * 10000 + baseX + 2), baseX + 1, y); + if (!coordinateMap[y].contains(baseX + 1) && coordinateMap[y].contains(baseX + 2)) + moveCardInRow(ges, coordinateMap[y].value(baseX + 2), baseX + 1, y); } } +void Server_CardZone::updateCardCoordinates(Server_Card *card, int oldX, int oldY) +{ + if (!has_coords) + return; + + if (oldX != -1) + removeCardFromCoordMap(card, oldX, oldY); + insertCardIntoCoordMap(card, card->getX(), card->getY()); +} + void Server_CardZone::insertCard(Server_Card *card, int x, int y) { if (hasCoords()) { card->setCoords(x, y); cards.append(card); + insertCardIntoCoordMap(card, x, y); } else { card->setCoords(0, 0); - cards.insert(x, card); + if (x == -1) + cards.append(card); + else + cards.insert(x, card); } card->setZone(this); } @@ -212,6 +262,9 @@ void Server_CardZone::clear() for (int i = 0; i < cards.size(); i++) delete cards.at(i); cards.clear(); + coordinateMap.clear(); + freePilesMap.clear(); + freeSpaceMap.clear(); playersWithWritePermission.clear(); } diff --git a/common/server_cardzone.h b/common/server_cardzone.h index 914ecc5b..1eeb9008 100644 --- a/common/server_cardzone.h +++ b/common/server_cardzone.h @@ -40,12 +40,19 @@ private: int cardsBeingLookedAt; QSet playersWithWritePermission; bool alwaysRevealTopCard; + QList cards; + QMap > coordinateMap; // y -> (x -> card) + QMap > freePilesMap; // y -> (cardName -> x) + QMap freeSpaceMap; // y -> x + void removeCardFromCoordMap(Server_Card *card, int oldX, int oldY); + void insertCardIntoCoordMap(Server_Card *card, int x, int y); public: Server_CardZone(Server_Player *_player, const QString &_name, bool _has_coords, ServerInfo_Zone::ZoneType _type); ~Server_CardZone(); - + + const QList &getCards() const { return cards; } int removeCard(Server_Card *card); - Server_Card *getCard(int id, int *position = NULL); + Server_Card *getCard(int id, int *position = NULL, bool remove = false); int getCardsBeingLookedAt() const { return cardsBeingLookedAt; } void setCardsBeingLookedAt(int _cardsBeingLookedAt) { cardsBeingLookedAt = _cardsBeingLookedAt; } @@ -59,9 +66,9 @@ public: bool isColumnEmpty(int x, int y) const; bool isColumnStacked(int x, int y) const; void fixFreeSpaces(GameEventStorage &ges); - void moveCard(GameEventStorage &ges, QMap &coordMap, Server_Card *card, int x, int y); - QList cards; + void moveCardInRow(GameEventStorage &ges, Server_Card *card, int x, int y); void insertCard(Server_Card *card, int x, int y); + void updateCardCoordinates(Server_Card *card, int oldX, int oldY); void shuffle(); void clear(); void addWritePermission(int playerId); diff --git a/common/server_database_interface.h b/common/server_database_interface.h index c12d69f0..10ce082a 100644 --- a/common/server_database_interface.h +++ b/common/server_database_interface.h @@ -19,7 +19,7 @@ public: virtual bool isInIgnoreList(const QString &whoseList, const QString &who) { return false; } virtual ServerInfo_User getUserData(const QString &name, bool withId = false) = 0; virtual void storeGameInformation(const QString &roomName, const QStringList &roomGameTypes, const ServerInfo_Game &gameInfo, const QSet &allPlayersEver, const QSet &allSpectatorsEver, const QList &replayList) { } - virtual DeckList *getDeckFromDatabase(int deckId, const QString &userName) { return 0; } + virtual DeckList *getDeckFromDatabase(int deckId, int userId) { return 0; } virtual qint64 startSession(const QString &userName, const QString &address) { return 0; } public slots: diff --git a/common/server_game.cpp b/common/server_game.cpp index 2cdd65ca..94eb03ae 100644 --- a/common/server_game.cpp +++ b/common/server_game.cpp @@ -42,6 +42,7 @@ #include "pb/event_set_active_phase.pb.h" #include "pb/serverinfo_playerping.pb.h" #include "pb/game_replay.pb.h" +#include "pb/event_replay_added.pb.h" #include #include #include @@ -90,7 +91,7 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, int _gameId, const Server_Game::~Server_Game() { - room->gamesMutex.lock(); + room->gamesLock.lockForWrite(); gameMutex.lock(); gameClosed = true; @@ -106,11 +107,11 @@ Server_Game::~Server_Game() creatorInfo = 0; gameMutex.unlock(); - room->gamesMutex.unlock(); + room->gamesLock.unlock(); currentReplay->set_duration_seconds(secondsElapsed - startTimeOfThisGame); replayList.append(currentReplay); - room->getServer()->storeGameInformation(secondsElapsed, allPlayersEver, allSpectatorsEver, replayList); + storeGameInformation(); for (int i = 0; i < replayList.size(); ++i) delete replayList[i]; @@ -118,6 +119,51 @@ Server_Game::~Server_Game() qDebug() << "Server_Game destructor: gameId=" << gameId; } +void Server_Game::storeGameInformation() +{ + const ServerInfo_Game &gameInfo = replayList.first()->game_info(); + + Event_ReplayAdded replayEvent; + ServerInfo_ReplayMatch *replayMatchInfo = replayEvent.mutable_match_info(); + replayMatchInfo->set_game_id(gameInfo.game_id()); + replayMatchInfo->set_room_name(room->getName().toStdString()); + replayMatchInfo->set_time_started(QDateTime::currentDateTime().addSecs(-secondsElapsed).toTime_t()); + replayMatchInfo->set_length(secondsElapsed); + replayMatchInfo->set_game_name(gameInfo.description()); + + const QStringList &allGameTypes = room->getGameTypes(); + QStringList gameTypes; + for (int i = gameInfo.game_types_size() - 1; i >= 0; --i) + gameTypes.append(allGameTypes[gameInfo.game_types(i)]); + + QSetIterator playerIterator(allPlayersEver); + while (playerIterator.hasNext()) + replayMatchInfo->add_player_names(playerIterator.next().toStdString()); + + for (int i = 0; i < replayList.size(); ++i) { + ServerInfo_Replay *replayInfo = replayMatchInfo->add_replay_list(); + replayInfo->set_replay_id(replayList[i]->replay_id()); + replayInfo->set_replay_name(gameInfo.description()); + replayInfo->set_duration(replayList[i]->duration_seconds()); + } + + QSet allUsersInGame = allPlayersEver + allSpectatorsEver; + QSetIterator allUsersIterator(allUsersInGame); + + SessionEvent *sessionEvent = Server_ProtocolHandler::prepareSessionEvent(replayEvent); + Server *server = room->getServer(); + server->clientsLock.lockForRead(); + while (allUsersIterator.hasNext()) { + Server_AbstractUserInterface *userHandler = server->findUser(allUsersIterator.next()); + if (userHandler) + userHandler->sendProtocolItem(*sessionEvent); + } + server->clientsLock.unlock(); + delete sessionEvent; + + server->getDatabaseInterface()->storeGameInformation(room->getName(), gameTypes, gameInfo, allPlayersEver, allSpectatorsEver, replayList); +} + void Server_Game::pingClockTimeout() { QMutexLocker locker(&gameMutex); @@ -359,7 +405,7 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, const QStri return Response::RespWrongPassword; if (!(user->user_level() & ServerInfo_User::IsRegistered) && onlyRegistered) return Response::RespUserLevelTooLow; - if (onlyBuddies) + if (onlyBuddies && (user->name() != creatorInfo->name())) if (!databaseInterface->isInBuddyList(QString::fromStdString(creatorInfo->name()), QString::fromStdString(user->name()))) return Response::RespOnlyBuddies; if (databaseInterface->isInIgnoreList(QString::fromStdString(creatorInfo->name()), QString::fromStdString(user->name()))) @@ -390,7 +436,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, Respons { QMutexLocker locker(&gameMutex); - Server_Player *newPlayer = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true), spectator, userInterface); + Server_Player *newPlayer = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true), spectator, userInterface); newPlayer->moveToThread(thread()); Event_Join joinEvent; @@ -516,8 +562,8 @@ void Server_Game::unattachCards(GameEventStorage &ges, Server_Player *player) QMapIterator zoneIterator(player->getZones()); while (zoneIterator.hasNext()) { Server_CardZone *zone = zoneIterator.next().value(); - for (int i = 0; i < zone->cards.size(); ++i) { - Server_Card *card = zone->cards.at(i); + for (int i = 0; i < zone->getCards().size(); ++i) { + Server_Card *card = zone->getCards().at(i); // Make a copy of the list because the original one gets modified during the loop QList attachedCards = card->getAttachedCards(); diff --git a/common/server_game.h b/common/server_game.h index 829d60a8..f6ddbf92 100644 --- a/common/server_game.h +++ b/common/server_game.h @@ -73,6 +73,7 @@ private: void createGameStateChangedEvent(Event_GameStateChanged *event, Server_Player *playerWhosAsking, bool omniscient, bool withUserInfo); void sendGameStateToPlayers(); + void storeGameInformation(); signals: void sigStartGameIfReady(); void gameInfoChanged(ServerInfo_Game gameInfo); diff --git a/common/server_player.cpp b/common/server_player.cpp index bfa53805..489c3dcb 100644 --- a/common/server_player.cpp +++ b/common/server_player.cpp @@ -84,7 +84,7 @@ #include Server_Player::Server_Player(Server_Game *_game, int _playerId, const ServerInfo_User &_userInfo, bool _spectator, Server_AbstractUserInterface *_userInterface) - : game(_game), userInterface(_userInterface), userInfo(new ServerInfo_User(_userInfo)), deck(0), pingTime(0), playerId(_playerId), spectator(_spectator), nextCardId(0), readyStart(false), conceded(false), sideboardLocked(true) + : ServerInfo_User_Container(_userInfo), game(_game), userInterface(_userInterface), deck(0), pingTime(0), playerId(_playerId), spectator(_spectator), nextCardId(0), readyStart(false), conceded(false), sideboardLocked(true) { } @@ -101,9 +101,6 @@ void Server_Player::prepareDestroy() userInterface->playerRemovedFromGame(game); playerMutex.unlock(); - delete userInfo; - userInfo = 0; - clearZones(); deleteLater(); @@ -185,7 +182,7 @@ void Server_Player::setupZones() if (!currentCard) continue; for (int k = 0; k < currentCard->getNumber(); ++k) - z->cards.append(new Server_Card(currentCard->getName(), nextCardId++, 0, 0, z)); + z->insertCard(new Server_Card(currentCard->getName(), nextCardId++, 0, 0, z), -1, 0); } } @@ -209,11 +206,10 @@ void Server_Player::setupZones() else continue; - for (int j = 0; j < start->cards.size(); ++j) - if (start->cards[j]->getName() == QString::fromStdString(m.card_name())) { - Server_Card *card = start->cards[j]; - start->cards.removeAt(j); - target->cards.append(card); + for (int j = 0; j < start->getCards().size(); ++j) + if (start->getCards()[j]->getName() == QString::fromStdString(m.card_name())) { + Server_Card *card = start->getCard(j, NULL, true); + target->insertCard(card, -1, 0); break; } } @@ -245,7 +241,7 @@ void Server_Player::getProperties(ServerInfo_PlayerProperties &result, bool with { result.set_player_id(playerId); if (withUserInfo) - result.mutable_user_info()->CopyFrom(*userInfo); + copyUserInfo(*(result.mutable_user_info()), true); result.set_spectator(spectator); if (!spectator) { result.set_conceded(conceded); @@ -286,16 +282,16 @@ Response::ResponseCode Server_Player::drawCards(GameEventStorage &ges, int numbe { Server_CardZone *deckZone = zones.value("deck"); Server_CardZone *handZone = zones.value("hand"); - if (deckZone->cards.size() < number) - number = deckZone->cards.size(); + if (deckZone->getCards().size() < number) + number = deckZone->getCards().size(); Event_DrawCards eventOthers; eventOthers.set_number(number); Event_DrawCards eventPrivate(eventOthers); for (int i = 0; i < number; ++i) { - Server_Card *card = deckZone->cards.takeFirst(); - handZone->cards.append(card); + Server_Card *card = deckZone->getCard(0, NULL, true); + handZone->insertCard(card, -1, 0); lastDrawList.append(card->getId()); ServerInfo_Card *cardInfo = eventPrivate.add_cards(); @@ -306,11 +302,11 @@ Response::ResponseCode Server_Player::drawCards(GameEventStorage &ges, int numbe ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId); ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); - if (deckZone->getAlwaysRevealTopCard() && !deckZone->cards.isEmpty()) { + if (deckZone->getAlwaysRevealTopCard() && !deckZone->getCards().isEmpty()) { Event_RevealCards revealEvent; revealEvent.set_zone_name(deckZone->getName().toStdString()); revealEvent.set_card_id(0); - deckZone->cards.first()->getInfo(revealEvent.add_cards()); + deckZone->getCards().first()->getInfo(revealEvent.add_cards()); ges.enqueueGameEvent(revealEvent, playerId); } @@ -346,7 +342,7 @@ Response::ResponseCode Server_Player::moveCard(GameEventStorage &ges, Server_Car return Response::RespContextError; if (!targetzone->hasCoords() && (x == -1)) - x = targetzone->cards.size(); + x = targetzone->getCards().size(); QList > cardsToMove; QMap cardProperties; @@ -357,15 +353,21 @@ Response::ResponseCode Server_Player::moveCard(GameEventStorage &ges, Server_Car continue; cardIdsToMove.insert(_cards[i]->card_id()); + // Consistency checks. In case the command contains illegal moves, try to resolve the legal ones still. int position; Server_Card *card = startzone->getCard(_cards[i]->card_id(), &position); if (!card) return Response::RespNameNotFound; + if (card->getParentCard()) + continue; if (!card->getAttachedCards().isEmpty() && !targetzone->isColumnEmpty(x, y)) - return Response::RespContextError; + continue; cardsToMove.append(QPair(card, position)); cardProperties.insert(card, _cards[i]); } + // In case all moves were filtered out, abort. + if (cardsToMove.isEmpty()) + return Response::RespContextError; MoveCardCompareFunctor cmp(startzone == targetzone ? -1 : x); qSort(cardsToMove.begin(), cardsToMove.end(), cmp); @@ -525,19 +527,19 @@ Response::ResponseCode Server_Player::moveCard(GameEventStorage &ges, Server_Car if (!ptString.isEmpty() && !faceDown) setCardAttrHelper(ges, targetzone->getName(), card->getId(), AttrPT, ptString); } - if (startzone->getAlwaysRevealTopCard() && !startzone->cards.isEmpty() && (originalPosition == 0)) { + if (startzone->getAlwaysRevealTopCard() && !startzone->getCards().isEmpty() && (originalPosition == 0)) { Event_RevealCards revealEvent; revealEvent.set_zone_name(startzone->getName().toStdString()); revealEvent.set_card_id(0); - startzone->cards.first()->getInfo(revealEvent.add_cards()); + startzone->getCards().first()->getInfo(revealEvent.add_cards()); ges.enqueueGameEvent(revealEvent, playerId); } - if (targetzone->getAlwaysRevealTopCard() && !targetzone->cards.isEmpty() && (newX == 0)) { + if (targetzone->getAlwaysRevealTopCard() && !targetzone->getCards().isEmpty() && (newX == 0)) { Event_RevealCards revealEvent; revealEvent.set_zone_name(targetzone->getName().toStdString()); revealEvent.set_card_id(0); - targetzone->cards.first()->getInfo(revealEvent.add_cards()); + targetzone->getCards().first()->getInfo(revealEvent.add_cards()); ges.enqueueGameEvent(revealEvent, playerId); } @@ -556,7 +558,7 @@ Response::ResponseCode Server_Player::moveCard(GameEventStorage &ges, Server_Car void Server_Player::unattachCard(GameEventStorage &ges, Server_Card *card) { Server_CardZone *zone = card->getZone(); - + Server_Card *parentCard = card->getParentCard(); card->setParentCard(0); Event_AttachCard event; @@ -568,6 +570,9 @@ void Server_Player::unattachCard(GameEventStorage &ges, Server_Card *card) cardToMove->set_card_id(card->getId()); moveCard(ges, zone, QList() << cardToMove, zone, -1, card->getY(), card->getFaceDown()); delete cardToMove; + + if (parentCard->getZone()) + parentCard->getZone()->updateCardCoordinates(parentCard, parentCard->getX(), parentCard->getY()); } Response::ResponseCode Server_Player::setCardAttrHelper(GameEventStorage &ges, const QString &zoneName, int cardId, CardAttribute attribute, const QString &attrValue) @@ -580,7 +585,7 @@ Response::ResponseCode Server_Player::setCardAttrHelper(GameEventStorage &ges, c QString result; if (cardId == -1) { - QListIterator CardIterator(zone->cards); + QListIterator CardIterator(zone->getCards()); while (CardIterator.hasNext()) { result = CardIterator.next()->setAttribute(attribute, attrValue, true); if (result.isNull()) @@ -631,7 +636,7 @@ Response::ResponseCode Server_Player::cmdDeckSelect(const Command_DeckSelect &cm DeckList *newDeck; if (cmd.has_deck_id()) { try { - newDeck = game->getRoom()->getServer()->getDatabaseInterface()->getDeckFromDatabase(cmd.deck_id(), QString::fromStdString(userInfo->name())); + newDeck = game->getRoom()->getServer()->getDatabaseInterface()->getDeckFromDatabase(cmd.deck_id(), userInfo->id()); } catch(Response::ResponseCode r) { return r; } @@ -782,11 +787,11 @@ Response::ResponseCode Server_Player::cmdShuffle(const Command_Shuffle & /*cmd*/ event.set_zone_name("deck"); ges.enqueueGameEvent(event, playerId); - if (deckZone->getAlwaysRevealTopCard() && !deckZone->cards.isEmpty()) { + if (deckZone->getAlwaysRevealTopCard() && !deckZone->getCards().isEmpty()) { Event_RevealCards revealEvent; revealEvent.set_zone_name(deckZone->getName().toStdString()); revealEvent.set_card_id(0); - deckZone->cards.first()->getInfo(revealEvent.add_cards()); + deckZone->getCards().first()->getInfo(revealEvent.add_cards()); ges.enqueueGameEvent(revealEvent, playerId); } @@ -805,12 +810,12 @@ Response::ResponseCode Server_Player::cmdMulligan(const Command_Mulligan & /*cmd return Response::RespContextError; Server_CardZone *hand = zones.value("hand"); - int number = (hand->cards.size() <= 1) ? initialCards : hand->cards.size() - 1; + int number = (hand->getCards().size() <= 1) ? initialCards : hand->getCards().size() - 1; Server_CardZone *deck = zones.value("deck"); - while (!hand->cards.isEmpty()) { + while (!hand->getCards().isEmpty()) { CardToMove *cardToMove = new CardToMove; - cardToMove->set_card_id(hand->cards.first()->getId()); + cardToMove->set_card_id(hand->getCards().first()->getId()); moveCard(ges, hand, QList() << cardToMove, deck, 0, 0, false); delete cardToMove; } @@ -991,9 +996,11 @@ Response::ResponseCode Server_Player::cmdAttachCard(const Command_AttachCard &cm return Response::RespContextError; if (cmd.has_target_card_id()) targetCard = targetzone->getCard(cmd.target_card_id()); - if (targetCard) + if (targetCard) { if (targetCard->getParentCard()) return Response::RespContextError; + } else + return Response::RespNameNotFound; } if (!startzone->hasCoords()) return Response::RespContextError; @@ -1025,6 +1032,11 @@ Response::ResponseCode Server_Player::cmdAttachCard(const Command_AttachCard &cm for (int i = 0; i < attachedList.size(); ++i) attachedList[i]->getZone()->getPlayer()->unattachCard(ges, attachedList[i]); + card->setParentCard(targetCard); + const int oldX = card->getX(); + card->setCoords(-1, card->getY()); + startzone->updateCardCoordinates(card, oldX, card->getY()); + if (targetzone->isColumnStacked(targetCard->getX(), targetCard->getY())) { CardToMove *cardToMove = new CardToMove; cardToMove->set_card_id(targetCard->getId()); @@ -1032,9 +1044,6 @@ Response::ResponseCode Server_Player::cmdAttachCard(const Command_AttachCard &cm delete cardToMove; } - card->setParentCard(targetCard); - card->setCoords(-1, card->getY()); - Event_AttachCard event; event.set_start_zone(startzone->getName().toStdString()); event.set_card_id(card->getId()); @@ -1407,16 +1416,17 @@ Response::ResponseCode Server_Player::cmdDumpZone(const Command_DumpZone &cmd, R return Response::RespContextError; int numberCards = cmd.number_cards(); + const QList &cards = zone->getCards(); Response_DumpZone *re = new Response_DumpZone; ServerInfo_Zone *zoneInfo = re->mutable_zone_info(); zoneInfo->set_name(zone->getName().toStdString()); zoneInfo->set_type(zone->getType()); zoneInfo->set_with_coords(zone->hasCoords()); - zoneInfo->set_card_count(numberCards < zone->cards.size() ? zone->cards.size() : numberCards); + zoneInfo->set_card_count(numberCards < cards.size() ? cards.size() : numberCards); - for (int i = 0; (i < zone->cards.size()) && (i < numberCards || numberCards == -1); ++i) { - Server_Card *card = zone->cards[i]; + for (int i = 0; (i < cards.size()) && (i < numberCards || numberCards == -1); ++i) { + Server_Card *card = cards[i]; QString displayedName = card->getFaceDown() ? QString() : card->getName(); ServerInfo_Card *cardInfo = zoneInfo->add_card_list(); cardInfo->set_name(displayedName.toStdString()); @@ -1510,11 +1520,11 @@ Response::ResponseCode Server_Player::cmdRevealCards(const Command_RevealCards & QList cardsToReveal; if (!cmd.has_card_id()) - cardsToReveal = zone->cards; + cardsToReveal = zone->getCards(); else if (cmd.card_id() == -2) { - if (zone->cards.isEmpty()) + if (zone->getCards().isEmpty()) return Response::RespContextError; - cardsToReveal.append(zone->cards.at(rng->getNumber(0, zone->cards.size() - 1))); + cardsToReveal.append(zone->getCards().at(rng->getNumber(0, zone->getCards().size() - 1))); } else { Server_Card *card = zone->getCard(cmd.card_id()); if (!card) @@ -1600,11 +1610,11 @@ Response::ResponseCode Server_Player::cmdChangeZoneProperties(const Command_Chan ges.enqueueGameEvent(event, playerId); - if (!zone->cards.isEmpty() && cmd.always_reveal_top_card()) { + if (!zone->getCards().isEmpty() && cmd.always_reveal_top_card()) { Event_RevealCards revealEvent; revealEvent.set_zone_name(zone->getName().toStdString()); revealEvent.set_card_id(0); - zone->cards.first()->getInfo(revealEvent.add_cards()); + zone->getCards().first()->getInfo(revealEvent.add_cards()); ges.enqueueGameEvent(revealEvent, playerId); } diff --git a/common/server_player.h b/common/server_player.h index c136ef9d..61e29bab 100644 --- a/common/server_player.h +++ b/common/server_player.h @@ -2,6 +2,7 @@ #define PLAYER_H #include "server_arrowtarget.h" +#include "serverinfo_user_container.h" #include #include #include @@ -60,13 +61,12 @@ class Command_DeckSelect; class Command_SetSideboardLock; class Command_ChangeZoneProperties; -class Server_Player : public Server_ArrowTarget { +class Server_Player : public Server_ArrowTarget, public ServerInfo_User_Container { Q_OBJECT private: class MoveCardCompareFunctor; Server_Game *game; Server_AbstractUserInterface *userInterface; - ServerInfo_User *userInfo; DeckList *deck; QMap zones; QMap counters; @@ -96,7 +96,6 @@ public: bool getSpectator() const { return spectator; } bool getConceded() const { return conceded; } void setConceded(bool _conceded) { conceded = _conceded; } - ServerInfo_User *getUserInfo() const { return userInfo; } DeckList *getDeck() const { return deck; } Server_Game *getGame() const { return game; } const QMap &getZones() const { return zones; } diff --git a/common/server_protocolhandler.cpp b/common/server_protocolhandler.cpp index 5054bbc7..eb8322d0 100644 --- a/common/server_protocolhandler.cpp +++ b/common/server_protocolhandler.cpp @@ -38,6 +38,8 @@ Server_ProtocolHandler::~Server_ProtocolHandler() { } +// This function must only be called from the thread this object lives in. +// The thread must not hold any server locks when calling this (e.g. clientsLock, roomsLock). void Server_ProtocolHandler::prepareDestroy() { if (deleted) @@ -58,24 +60,24 @@ void Server_ProtocolHandler::prepareDestroy() Server_Room *r = server->getRooms().value(gameIterator.value().first); if (!r) continue; - r->gamesMutex.lock(); + r->gamesLock.lockForRead(); Server_Game *g = r->getGames().value(gameIterator.key()); if (!g) { - r->gamesMutex.unlock(); + r->gamesLock.unlock(); continue; } g->gameMutex.lock(); Server_Player *p = g->getPlayers().value(gameIterator.value().second); if (!p) { g->gameMutex.unlock(); - r->gamesMutex.unlock(); + r->gamesLock.unlock(); continue; } p->disconnectClient(); g->gameMutex.unlock(); - r->gamesMutex.unlock(); + r->gamesLock.unlock(); } server->roomsLock.unlock(); @@ -195,7 +197,7 @@ Response::ResponseCode Server_ProtocolHandler::processGameCommandContainer(const if (!room) return Response::RespNotInRoom; - QMutexLocker roomGamesLocker(&room->gamesMutex); + QReadLocker roomGamesLocker(&room->gamesLock); Server_Game *game = room->getGames().value(cont.game_id()); if (!game) { if (room->getExternalGames().contains(cont.game_id())) { @@ -275,6 +277,10 @@ Response::ResponseCode Server_ProtocolHandler::processAdminCommandContainer(cons void Server_ProtocolHandler::processCommandContainer(const CommandContainer &cont) { + // Command processing must be disabled after prepareDestroy() has been called. + if (deleted) + return; + lastDataReceived = timeRunning; ResponseContainer responseContainer(cont.has_cmd_id() ? cont.cmd_id() : -1); @@ -374,12 +380,9 @@ Response::ResponseCode Server_ProtocolHandler::cmdMessage(const Command_Message QReadLocker locker(&server->clientsLock); QString receiver = QString::fromStdString(cmd.user_name()); - Server_AbstractUserInterface *userInterface = server->getUsers().value(receiver); - if (!userInterface) { - userInterface = server->getExternalUsers().value(receiver); - if (!userInterface) - return Response::RespNameNotFound; - } + Server_AbstractUserInterface *userInterface = server->findUser(receiver); + if (!userInterface) + return Response::RespNameNotFound; if (databaseInterface->isInIgnoreList(receiver, QString::fromStdString(userInfo->name()))) return Response::RespInIgnoreList; @@ -400,22 +403,20 @@ Response::ResponseCode Server_ProtocolHandler::cmdGetGamesOfUser(const Command_G if (authState == NotLoggedIn) return Response::RespLoginNeeded; - server->clientsLock.lockForRead(); - if (!server->getUsers().contains(QString::fromStdString(cmd.user_name()))) - return Response::RespNameNotFound; - server->clientsLock.unlock(); + // We don't need to check whether the user is logged in; persistent games should also work. + // The client needs to deal with an empty result list. Response_GetGamesOfUser *re = new Response_GetGamesOfUser; server->roomsLock.lockForRead(); QMapIterator roomIterator(server->getRooms()); while (roomIterator.hasNext()) { Server_Room *room = roomIterator.next().value(); - room->gamesMutex.lock(); + room->gamesLock.lockForRead(); room->getInfo(*re->add_room_list(), false, true); QListIterator gameIterator(room->getGamesOfUser(QString::fromStdString(cmd.user_name()))); while (gameIterator.hasNext()) re->add_game_list()->CopyFrom(gameIterator.next()); - room->gamesMutex.unlock(); + room->gamesLock.unlock(); } server->roomsLock.unlock(); @@ -436,15 +437,11 @@ Response::ResponseCode Server_ProtocolHandler::cmdGetUserInfo(const Command_GetU QReadLocker locker(&server->clientsLock); - ServerInfo_User_Container *infoSource; - if (server->getUsers().contains(userName)) - infoSource = server->getUsers().value(userName); - else if (server->getExternalUsers().contains(userName)) - infoSource = server->getExternalUsers().value(userName); - else + ServerInfo_User_Container *infoSource = server->findUser(userName); + if (!infoSource) return Response::RespNameNotFound; - re->mutable_user_info()->CopyFrom(infoSource->copyUserInfo(true, userInfo->user_level() & ServerInfo_User::IsModerator)); + re->mutable_user_info()->CopyFrom(infoSource->copyUserInfo(true, false, userInfo->user_level() & ServerInfo_User::IsModerator)); } rc.setResponseExtension(re); @@ -556,8 +553,6 @@ Response::ResponseCode Server_ProtocolHandler::cmdCreateGame(const Command_Creat if (gameId == -1) return Response::RespInternalError; - QMutexLocker roomLocker(&room->gamesMutex); - if (server->getMaxGamesPerUser() > 0) if (room->getGamesCreatedByUser(QString::fromStdString(userInfo->name())) >= server->getMaxGamesPerUser()) return Response::RespContextError; diff --git a/common/server_room.cpp b/common/server_room.cpp index 5538a121..4369e129 100644 --- a/common/server_room.cpp +++ b/common/server_room.cpp @@ -13,7 +13,7 @@ #include Server_Room::Server_Room(int _id, const QString &_name, const QString &_description, bool _autoJoin, const QString &_joinMessage, const QStringList &_gameTypes, Server *parent) - : QObject(parent), id(_id), name(_name), description(_description), autoJoin(_autoJoin), joinMessage(_joinMessage), gameTypes(_gameTypes), gamesMutex(QMutex::Recursive) + : QObject(parent), id(_id), name(_name), description(_description), autoJoin(_autoJoin), joinMessage(_joinMessage), gameTypes(_gameTypes), gamesLock(QReadWriteLock::Recursive) { connect(this, SIGNAL(gameListChanged(ServerInfo_Game)), this, SLOT(broadcastGameListUpdate(ServerInfo_Game)), Qt::QueuedConnection); } @@ -22,15 +22,15 @@ Server_Room::~Server_Room() { qDebug("Server_Room destructor"); - gamesMutex.lock(); + gamesLock.lockForWrite(); const QList gameList = games.values(); for (int i = 0; i < gameList.size(); ++i) delete gameList[i]; games.clear(); - gamesMutex.unlock(); + gamesLock.unlock(); usersLock.lockForWrite(); - userList.clear(); + users.clear(); usersLock.unlock(); } @@ -39,17 +39,15 @@ Server *Server_Room::getServer() const return static_cast(parent()); } -const ServerInfo_Room &Server_Room::getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes, bool updating, bool includeExternalData) const +const ServerInfo_Room &Server_Room::getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes, bool includeExternalData) const { result.set_room_id(id); - if (!updating) { - result.set_name(name.toStdString()); - result.set_description(description.toStdString()); - result.set_auto_join(autoJoin); - } + result.set_name(name.toStdString()); + result.set_description(description.toStdString()); + result.set_auto_join(autoJoin); - gamesMutex.lock(); + gamesLock.lockForRead(); result.set_game_count(games.size() + externalGames.size()); if (complete) { QMapIterator gameIterator(games); @@ -61,13 +59,14 @@ const ServerInfo_Room &Server_Room::getInfo(ServerInfo_Room &result, bool comple result.add_game_list()->CopyFrom(externalGameIterator.next().value()); } } - gamesMutex.unlock(); + gamesLock.unlock(); usersLock.lockForRead(); - result.set_player_count(userList.size() + externalUsers.size()); + result.set_player_count(users.size() + externalUsers.size()); if (complete) { - for (int i = 0; i < userList.size(); ++i) - result.add_user_list()->CopyFrom(userList[i]->copyUserInfo(false)); + QMapIterator userIterator(users); + while (userIterator.hasNext()) + result.add_user_list()->CopyFrom(userIterator.next().value()->copyUserInfo(false)); if (includeExternalData) { QMapIterator externalUserIterator(externalUsers); while (externalUserIterator.hasNext()) @@ -100,26 +99,44 @@ void Server_Room::addClient(Server_ProtocolHandler *client) event.mutable_user_info()->CopyFrom(client->copyUserInfo(false)); sendRoomEvent(prepareRoomEvent(event)); + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + usersLock.lockForWrite(); - userList.append(client); + users.insert(QString::fromStdString(client->getUserInfo()->name()), client); + roomInfo.set_player_count(users.size() + externalUsers.size()); usersLock.unlock(); - ServerInfo_Room roomInfo; - emit roomInfoChanged(getInfo(roomInfo, false, false, true)); + // XXX This can be removed during the next client update. + gamesLock.lockForRead(); + roomInfo.set_game_count(games.size() + externalGames.size()); + gamesLock.unlock(); + // ----------- + + emit roomInfoChanged(roomInfo); } void Server_Room::removeClient(Server_ProtocolHandler *client) { usersLock.lockForWrite(); - userList.removeAt(userList.indexOf(client)); + users.remove(QString::fromStdString(client->getUserInfo()->name())); + + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + roomInfo.set_player_count(users.size() + externalUsers.size()); usersLock.unlock(); Event_LeaveRoom event; event.set_name(client->getUserInfo()->name()); sendRoomEvent(prepareRoomEvent(event)); - ServerInfo_Room roomInfo; - emit roomInfoChanged(getInfo(roomInfo, false, false, true)); + // XXX This can be removed during the next client update. + gamesLock.lockForRead(); + roomInfo.set_game_count(games.size() + externalGames.size()); + gamesLock.unlock(); + // ----------- + + emit roomInfoChanged(roomInfo); } void Server_Room::addExternalUser(const ServerInfo_User &userInfo) @@ -130,43 +147,52 @@ void Server_Room::addExternalUser(const ServerInfo_User &userInfo) event.mutable_user_info()->CopyFrom(userInfoContainer.copyUserInfo(false)); sendRoomEvent(prepareRoomEvent(event), false); + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + usersLock.lockForWrite(); externalUsers.insert(QString::fromStdString(userInfo.name()), userInfoContainer); + roomInfo.set_player_count(users.size() + externalUsers.size()); usersLock.unlock(); - ServerInfo_Room roomInfo; - emit roomInfoChanged(getInfo(roomInfo, false, false, true)); + emit roomInfoChanged(roomInfo); } void Server_Room::removeExternalUser(const QString &name) { // This function is always called from the Server thread with server->roomsMutex locked. + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + usersLock.lockForWrite(); if (externalUsers.contains(name)) externalUsers.remove(name); + roomInfo.set_player_count(users.size() + externalUsers.size()); usersLock.unlock(); Event_LeaveRoom event; event.set_name(name.toStdString()); sendRoomEvent(prepareRoomEvent(event), false); - ServerInfo_Room roomInfo; - emit roomInfoChanged(getInfo(roomInfo, false, false, true)); + emit roomInfoChanged(roomInfo); } void Server_Room::updateExternalGameList(const ServerInfo_Game &gameInfo) { // This function is always called from the Server thread with server->roomsMutex locked. - gamesMutex.lock(); + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + + gamesLock.lockForWrite(); if (!gameInfo.has_player_count() && externalGames.contains(gameInfo.game_id())) externalGames.remove(gameInfo.game_id()); else externalGames.insert(gameInfo.game_id(), gameInfo); - gamesMutex.unlock(); + roomInfo.set_game_count(games.size() + externalGames.size()); + gamesLock.unlock(); broadcastGameListUpdate(gameInfo, false); - ServerInfo_Room roomInfo; - emit roomInfoChanged(getInfo(roomInfo, false, false, true)); + emit roomInfoChanged(roomInfo); } Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGame &cmd, ResponseContainer &rc, Server_AbstractUserInterface *userInterface) @@ -174,7 +200,7 @@ Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGam // This function is called from the Server thread and from the S_PH thread. // server->roomsMutex is always locked. - QMutexLocker roomGamesLocker(&gamesMutex); + QReadLocker roomGamesLocker(&gamesLock); Server_Game *g = games.value(cmd.game_id()); if (!g) { if (externalGames.contains(cmd.game_id())) { @@ -210,8 +236,11 @@ void Server_Room::say(const QString &userName, const QString &s, bool sendToIsl) void Server_Room::sendRoomEvent(RoomEvent *event, bool sendToIsl) { usersLock.lockForRead(); - for (int i = 0; i < userList.size(); ++i) - userList[i]->sendProtocolItem(*event); + { + QMapIterator userIterator(users); + while (userIterator.hasNext()) + userIterator.next().value()->sendProtocolItem(*event); + } usersLock.unlock(); if (sendToIsl) @@ -229,24 +258,33 @@ void Server_Room::broadcastGameListUpdate(const ServerInfo_Game &gameInfo, bool void Server_Room::addGame(Server_Game *game) { - // Lock gamesMutex before calling this + ServerInfo_Room roomInfo; + roomInfo.set_room_id(id); + gamesLock.lockForWrite(); connect(game, SIGNAL(gameInfoChanged(ServerInfo_Game)), this, SLOT(broadcastGameListUpdate(ServerInfo_Game))); game->gameMutex.lock(); games.insert(game->getGameId(), game); ServerInfo_Game gameInfo; game->getInfo(gameInfo); + roomInfo.set_game_count(games.size() + externalGames.size()); game->gameMutex.unlock(); + gamesLock.unlock(); + // XXX This can be removed during the next client update. + usersLock.lockForRead(); + roomInfo.set_player_count(users.size() + externalUsers.size()); + usersLock.unlock(); + // ----------- + emit gameListChanged(gameInfo); - ServerInfo_Room roomInfo; - emit roomInfoChanged(getInfo(roomInfo, false, false, true)); + emit roomInfoChanged(roomInfo); } void Server_Room::removeGame(Server_Game *game) { - // No need to lock gamesMutex or gameMutex. This method is only + // No need to lock gamesLock or gameMutex. This method is only // called from ~Server_Game, which locks both mutexes anyway beforehand. disconnect(game, 0, this, 0); @@ -258,12 +296,21 @@ void Server_Room::removeGame(Server_Game *game) games.remove(game->getGameId()); ServerInfo_Room roomInfo; - emit roomInfoChanged(getInfo(roomInfo, false, false, true)); + roomInfo.set_room_id(id); + roomInfo.set_game_count(games.size() + externalGames.size()); + + // XXX This can be removed during the next client update. + usersLock.lockForRead(); + roomInfo.set_player_count(users.size() + externalUsers.size()); + usersLock.unlock(); + // ----------- + + emit roomInfoChanged(roomInfo); } int Server_Room::getGamesCreatedByUser(const QString &userName) const { - QMutexLocker locker(&gamesMutex); + QReadLocker locker(&gamesLock); QMapIterator gamesIterator(games); int result = 0; @@ -275,7 +322,7 @@ int Server_Room::getGamesCreatedByUser(const QString &userName) const QList Server_Room::getGamesOfUser(const QString &userName) const { - QMutexLocker locker(&gamesMutex); + QReadLocker locker(&gamesLock); QList result; QMapIterator gamesIterator(games); diff --git a/common/server_room.h b/common/server_room.h index 421a9836..355d89d2 100644 --- a/common/server_room.h +++ b/common/server_room.h @@ -37,13 +37,13 @@ private: QStringList gameTypes; QMap games; QMap externalGames; - QList userList; + QMap users; QMap externalUsers; private slots: void broadcastGameListUpdate(const ServerInfo_Game &gameInfo, bool sendToIsl = true); public: mutable QReadWriteLock usersLock; - mutable QMutex gamesMutex; + mutable QReadWriteLock gamesLock; Server_Room(int _id, const QString &_name, const QString &_description, bool _autoJoin, const QString &_joinMessage, const QStringList &_gameTypes, Server *parent); ~Server_Room(); int getId() const { return id; } @@ -55,7 +55,7 @@ public: const QMap &getGames() const { return games; } const QMap &getExternalGames() const { return externalGames; } Server *getServer() const; - const ServerInfo_Room &getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes = false, bool updating = false, bool includeExternalData = true) const; + const ServerInfo_Room &getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes = false, bool includeExternalData = true) const; int getGamesCreatedByUser(const QString &name) const; QList getGamesOfUser(const QString &name) const; diff --git a/common/serverinfo_user_container.cpp b/common/serverinfo_user_container.cpp index 8e50568c..deead4e0 100644 --- a/common/serverinfo_user_container.cpp +++ b/common/serverinfo_user_container.cpp @@ -26,22 +26,27 @@ ServerInfo_User_Container::~ServerInfo_User_Container() void ServerInfo_User_Container::setUserInfo(const ServerInfo_User &_userInfo) { - userInfo = new ServerInfo_User; - userInfo->CopyFrom(_userInfo); + userInfo = new ServerInfo_User(_userInfo); } -ServerInfo_User ServerInfo_User_Container::copyUserInfo(bool complete, bool moderatorInfo) const +ServerInfo_User &ServerInfo_User_Container::copyUserInfo(ServerInfo_User &result, bool complete, bool internalInfo, bool sessionInfo) const { - ServerInfo_User result; if (userInfo) { result.CopyFrom(*userInfo); - if (!moderatorInfo) { + if (!sessionInfo) { result.clear_session_id(); result.clear_address(); - result.clear_id(); } + if (!internalInfo) + result.clear_id(); if (!complete) result.clear_avatar_bmp(); } - return result; + return result; +} + +ServerInfo_User ServerInfo_User_Container::copyUserInfo(bool complete, bool internalInfo, bool sessionInfo) const +{ + ServerInfo_User result; + return copyUserInfo(result, complete, internalInfo, sessionInfo); } diff --git a/common/serverinfo_user_container.h b/common/serverinfo_user_container.h index 1786bd4d..6cf87ab1 100644 --- a/common/serverinfo_user_container.h +++ b/common/serverinfo_user_container.h @@ -13,7 +13,8 @@ public: virtual ~ServerInfo_User_Container(); ServerInfo_User *getUserInfo() const { return userInfo; } void setUserInfo(const ServerInfo_User &_userInfo); - ServerInfo_User copyUserInfo(bool complete, bool moderatorInfo = false) const; + ServerInfo_User ©UserInfo(ServerInfo_User &result, bool complete, bool internalInfo = false, bool sessionInfo = false) const; + ServerInfo_User copyUserInfo(bool complete, bool internalInfo = false, bool sessionInfo = false) const; }; #endif diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 4d82dff1..41338903 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -1,6 +1,9 @@ CMAKE_MINIMUM_REQUIRED(VERSION 2.6) PROJECT(oracle) +# paths +set(DESKTOPDIR share/applications CACHE STRING "path to .desktop files") + SET(oracle_SOURCES src/main.cpp src/oracleimporter.cpp src/window_main.cpp ../cockatrice/src/carddatabase.cpp ../cockatrice/src/settingscache.cpp) SET(oracle_HEADERS src/oracleimporter.h src/window_main.h ../cockatrice/src/carddatabase.h ../cockatrice/src/settingscache.h) @@ -19,7 +22,11 @@ INCLUDE_DIRECTORIES(../cockatrice/src) ADD_EXECUTABLE(oracle WIN32 MACOSX_BUNDLE ${oracle_SOURCES} ${oracle_HEADERS_MOC}) TARGET_LINK_LIBRARIES(oracle ${QT_LIBRARIES}) -INSTALL(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/oracle DESTINATION bin) +IF (NOT APPLE) + INSTALL(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/oracle DESTINATION bin) +ELSE (APPLE) + INSTALL(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/oracle.app DESTINATION bin) +ENDIF (NOT APPLE) IF (NOT WIN32 AND NOT APPLE) - INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/oracle.desktop DESTINATION share/applications) + INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/oracle.desktop DESTINATION ${DESKTOPDIR}) ENDIF (NOT WIN32 AND NOT APPLE) diff --git a/servatrice/CMakeLists.txt b/servatrice/CMakeLists.txt index 3f9d3800..8af8fc4a 100644 --- a/servatrice/CMakeLists.txt +++ b/servatrice/CMakeLists.txt @@ -2,6 +2,9 @@ CMAKE_MINIMUM_REQUIRED(VERSION 2.6) SET(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) PROJECT(servatrice) +# cmake modules +include(GNUInstallDirs) + SET(servatrice_SOURCES src/main.cpp src/passwordhasher.cpp @@ -28,6 +31,7 @@ SET(QT_USE_QTSQL TRUE) FIND_PACKAGE(Qt4 REQUIRED) FIND_PACKAGE(Protobuf REQUIRED) FIND_PACKAGE(Libgcrypt REQUIRED) +FIND_PACKAGE(Threads) #set(CMAKE_BUILD_TYPE Release) set(CMAKE_CXX_FLAGS_DEBUG "-ggdb -O2") @@ -43,7 +47,7 @@ INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR}/../common) INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR}) ADD_EXECUTABLE(servatrice ${servatrice_SOURCES} ${servatrice_HEADERS_MOC}) -TARGET_LINK_LIBRARIES(servatrice cockatrice_common ${QT_LIBRARIES} ${LIBGCRYPT_LIBRARY}) +TARGET_LINK_LIBRARIES(servatrice cockatrice_common ${QT_LIBRARIES} ${LIBGCRYPT_LIBRARY} ${CMAKE_THREAD_LIBS_INIT}) #add_custom_target(versionheader ALL DEPENDS version_header) add_custom_command( @@ -51,3 +55,5 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/../common/getversion.cmake ) +# install rules +INSTALL(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/servatrice DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index 7646158d..6e83f1f2 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -28,12 +28,12 @@ SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; CREATE TABLE IF NOT EXISTS `cockatrice_decklist_files` ( `id` int(7) unsigned zerofill NOT NULL auto_increment, `id_folder` int(7) unsigned zerofill NOT NULL, - `user` varchar(35) NOT NULL, + `id_user` int(7) unsigned NULL, `name` varchar(50) NOT NULL, `upload_time` datetime NOT NULL, `content` text NOT NULL, PRIMARY KEY (`id`), - KEY `FolderPlusUser` (`id_folder`,`user`) + KEY `FolderPlusUser` (`id_folder`,`id_user`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- -------------------------------------------------------- @@ -45,10 +45,10 @@ CREATE TABLE IF NOT EXISTS `cockatrice_decklist_files` ( CREATE TABLE IF NOT EXISTS `cockatrice_decklist_folders` ( `id` int(7) unsigned zerofill NOT NULL auto_increment, `id_parent` int(7) unsigned zerofill NOT NULL, - `user` varchar(35) NOT NULL, + `id_user` int(7) unsigned NULL, `name` varchar(30) NOT NULL, PRIMARY KEY (`id`), - KEY `ParentPlusUser` (`id_parent`,`user`) + KEY `ParentPlusUser` (`id_parent`,`id_user`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- -------------------------------------------------------- @@ -115,9 +115,11 @@ CREATE TABLE IF NOT EXISTS `cockatrice_users` ( `avatar_bmp` blob NOT NULL, `registrationDate` datetime NOT NULL, `active` tinyint(1) NOT NULL, - `token` char(32) NOT NULL, + `token` binary(16) NOT NULL, PRIMARY KEY (`id`), - UNIQUE KEY `name` (`name`) + UNIQUE KEY `name` (`name`), + KEY `token` (`token`), + KEY `email` (`email`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE `cockatrice_uptime` ( @@ -163,7 +165,8 @@ CREATE TABLE `cockatrice_bans` ( `reason` text NOT NULL, `visible_reason` text NOT NULL, PRIMARY KEY (`user_name`,`time_from`), - KEY `time_from` (`time_from`,`ip_address`) + KEY `time_from` (`time_from`,`ip_address`), + KEY `ip_address` (`ip_address`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE `cockatrice_sessions` ( @@ -201,6 +204,7 @@ CREATE TABLE `cockatrice_replays_access` ( `id_player` int(7) NOT NULL, `replay_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `do_not_hide` tinyint(1) NOT NULL, - KEY `id_player` (`id_player`) + KEY `id_player` (`id_player`), + KEY `id_game` (`id_game`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; diff --git a/servatrice/src/isl_interface.cpp b/servatrice/src/isl_interface.cpp index 52968422..2a9ea41b 100644 --- a/servatrice/src/isl_interface.cpp +++ b/servatrice/src/isl_interface.cpp @@ -128,8 +128,8 @@ void IslInterface::initServer() while (roomIterator.hasNext()) { Server_Room *room = roomIterator.next().value(); room->usersLock.lockForRead(); - room->gamesMutex.lock(); - room->getInfo(*event.add_room_list(), true, true, false, false); + room->gamesLock.lockForRead(); + room->getInfo(*event.add_room_list(), true, true, false); } IslMessage message; @@ -150,7 +150,7 @@ void IslInterface::initServer() roomIterator.toFront(); while (roomIterator.hasNext()) { roomIterator.next(); - roomIterator.value()->gamesMutex.unlock(); + roomIterator.value()->gamesLock.unlock(); roomIterator.value()->usersLock.unlock(); } server->roomsLock.unlock(); diff --git a/servatrice/src/main.cpp b/servatrice/src/main.cpp index c71477fe..0f7f0112 100644 --- a/servatrice/src/main.cpp +++ b/servatrice/src/main.cpp @@ -127,6 +127,7 @@ int main(int argc, char *argv[]) QSettings *settings = new QSettings("servatrice.ini", QSettings::IniFormat); loggerThread = new QThread; + loggerThread->setObjectName("logger"); logger = new ServerLogger(logToConsole); logger->moveToThread(loggerThread); @@ -151,12 +152,16 @@ int main(int argc, char *argv[]) sigemptyset(&segv.sa_mask); sigaction(SIGSEGV, &segv, 0); sigaction(SIGABRT, &segv, 0); + + signal(SIGPIPE, SIG_IGN); #endif rng = new RNG_SFMT; std::cerr << "Servatrice " << VERSION_STRING << " starting." << std::endl; std::cerr << "-------------------------" << std::endl; + PasswordHasher::initialize(); + if (testRandom) testRNG(); if (testHashFunction) diff --git a/servatrice/src/passwordhasher.cpp b/servatrice/src/passwordhasher.cpp index 48fe7b66..1cc8528c 100644 --- a/servatrice/src/passwordhasher.cpp +++ b/servatrice/src/passwordhasher.cpp @@ -3,6 +3,14 @@ #include #include +void PasswordHasher::initialize() +{ + // These calls are required by libgcrypt before we use any of its functions. + gcry_check_version(0); + gcry_control(GCRYCTL_DISABLE_SECMEM, 0); + gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); +} + QString PasswordHasher::computeHash(const QString &password, const QString &salt) { const int algo = GCRY_MD_SHA512; diff --git a/servatrice/src/passwordhasher.h b/servatrice/src/passwordhasher.h index 2487b322..0cb6744c 100644 --- a/servatrice/src/passwordhasher.h +++ b/servatrice/src/passwordhasher.h @@ -5,6 +5,7 @@ class PasswordHasher { public: + static void initialize(); static QString computeHash(const QString &password, const QString &salt); }; diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index ae53f634..ee513339 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -33,8 +33,6 @@ #include "server_logger.h" #include "main.h" #include "decklist.h" -#include "pb/game_replay.pb.h" -#include "pb/event_replay_added.pb.h" #include "pb/event_server_message.pb.h" #include "pb/event_server_shutdown.pb.h" #include "pb/event_connection_closed.pb.h" @@ -44,6 +42,7 @@ Servatrice_GameServer::Servatrice_GameServer(Servatrice *_server, int _numberPoo server(_server) { if (_numberPools == 0) { + server->setThreaded(false); Servatrice_DatabaseInterface *newDatabaseInterface = new Servatrice_DatabaseInterface(0, server); Servatrice_ConnectionPool *newPool = new Servatrice_ConnectionPool(newDatabaseInterface); @@ -57,6 +56,7 @@ Servatrice_GameServer::Servatrice_GameServer(Servatrice *_server, int _numberPoo Servatrice_ConnectionPool *newPool = new Servatrice_ConnectionPool(newDatabaseInterface); QThread *newThread = new QThread; + newThread->setObjectName("pool_" + QString::number(i)); newPool->moveToThread(newThread); newDatabaseInterface->moveToThread(newThread); server->addDatabaseInterface(newThread, newDatabaseInterface); @@ -253,6 +253,7 @@ bool Servatrice::initServer() } QThread *thread = new QThread; + thread->setObjectName("isl_" + QString::number(prop.id)); connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); IslInterface *interface = new IslInterface(prop.id, prop.hostname, prop.address.toString(), prop.controlPort, prop.cert, cert, key, this); @@ -290,6 +291,7 @@ bool Servatrice::initServer() const int numberPools = settings->value("server/number_pools", 1).toInt(); gameServer = new Servatrice_GameServer(this, numberPools, servatriceDatabaseInterface->getDatabase(), this); + gameServer->setMaxPendingConnections(1000); const int gamePort = settings->value("server/port", 4747).toInt(); qDebug() << "Starting server on port" << gamePort; if (gameServer->listen(QHostAddress::Any, gamePort)) @@ -354,55 +356,6 @@ QList Servatrice::getUsersWithAddressAsList(const QHost return result; } -void Servatrice::storeGameInformation(int secondsElapsed, const QSet &allPlayersEver, const QSet &allSpectatorsEver, const QList &replayList) -{ - const ServerInfo_Game &gameInfo = replayList.first()->game_info(); - - Server_Room *room = rooms.value(gameInfo.room_id()); - - Event_ReplayAdded replayEvent; - ServerInfo_ReplayMatch *replayMatchInfo = replayEvent.mutable_match_info(); - replayMatchInfo->set_game_id(gameInfo.game_id()); - replayMatchInfo->set_room_name(room->getName().toStdString()); - replayMatchInfo->set_time_started(QDateTime::currentDateTime().addSecs(-secondsElapsed).toTime_t()); - replayMatchInfo->set_length(secondsElapsed); - replayMatchInfo->set_game_name(gameInfo.description()); - - const QStringList &allGameTypes = room->getGameTypes(); - QStringList gameTypes; - for (int i = gameInfo.game_types_size() - 1; i >= 0; --i) - gameTypes.append(allGameTypes[gameInfo.game_types(i)]); - - QSetIterator playerIterator(allPlayersEver); - while (playerIterator.hasNext()) - replayMatchInfo->add_player_names(playerIterator.next().toStdString()); - - for (int i = 0; i < replayList.size(); ++i) { - ServerInfo_Replay *replayInfo = replayMatchInfo->add_replay_list(); - replayInfo->set_replay_id(replayList[i]->replay_id()); - replayInfo->set_replay_name(gameInfo.description()); - replayInfo->set_duration(replayList[i]->duration_seconds()); - } - - QSet allUsersInGame = allPlayersEver + allSpectatorsEver; - QSetIterator allUsersIterator(allUsersInGame); - - SessionEvent *sessionEvent = Server_ProtocolHandler::prepareSessionEvent(replayEvent); - clientsLock.lockForRead(); - while (allUsersIterator.hasNext()) { - const QString userName = allUsersIterator.next(); - Server_AbstractUserInterface *userHandler = users.value(userName); - if (!userHandler) - userHandler = externalUsers.value(userName); - if (userHandler) - userHandler->sendProtocolItem(*sessionEvent); - } - clientsLock.unlock(); - delete sessionEvent; - - getDatabaseInterface()->storeGameInformation(room->getName(), gameTypes, gameInfo, allPlayersEver, allSpectatorsEver, replayList); -} - void Servatrice::updateLoginMessage() { if (!servatriceDatabaseInterface->checkSql()) diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index 70acb96d..fbfac035 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -122,6 +122,7 @@ private: QMap islInterfaces; public slots: void scheduleShutdown(const QString &reason, int minutes); + void updateLoginMessage(); public: Servatrice(QSettings *_settings, QObject *parent = 0); ~Servatrice(); @@ -139,12 +140,10 @@ public: AuthenticationMethod getAuthenticationMethod() const { return authenticationMethod; } QString getDbPrefix() const { return dbPrefix; } int getServerId() const { return serverId; } - void updateLoginMessage(); int getUsersWithAddress(const QHostAddress &address) const; QList getUsersWithAddressAsList(const QHostAddress &address) const; void incTxBytes(quint64 num); void incRxBytes(quint64 num); - void storeGameInformation(int secondsElapsed, const QSet &allPlayersEver, const QSet &allSpectatorsEver, const QList &replays); void addDatabaseInterface(QThread *thread, Servatrice_DatabaseInterface *databaseInterface); bool islConnectionExists(int serverId) const; diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 9ef6f094..58856f4f 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -507,15 +507,15 @@ void Servatrice_DatabaseInterface::storeGameInformation(const QString &roomName, } } -DeckList *Servatrice_DatabaseInterface::getDeckFromDatabase(int deckId, const QString &userName) +DeckList *Servatrice_DatabaseInterface::getDeckFromDatabase(int deckId, int userId) { checkSql(); QSqlQuery query(sqlDatabase); - query.prepare("select content from " + server->getDbPrefix() + "_decklist_files where id = :id and user = :user"); + query.prepare("select content from " + server->getDbPrefix() + "_decklist_files where id = :id and id_user = :id_user"); query.bindValue(":id", deckId); - query.bindValue(":user", userName); + query.bindValue(":id_user", userId); execSqlQuery(query); if (!query.next()) throw Response::RespNameNotFound; diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index 814432cc..e1d6d813 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -38,7 +38,7 @@ public: bool isInIgnoreList(const QString &whoseList, const QString &who); ServerInfo_User getUserData(const QString &name, bool withId = false); void storeGameInformation(const QString &roomName, const QStringList &roomGameTypes, const ServerInfo_Game &gameInfo, const QSet &allPlayersEver, const QSet &allSpectatorsEver, const QList &replayList); - DeckList *getDeckFromDatabase(int deckId, const QString &userName); + DeckList *getDeckFromDatabase(int deckId, int userId); int getNextGameId(); int getNextReplayId(); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 03060eee..f91029d7 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -72,18 +72,25 @@ ServerSocketInterface::ServerSocketInterface(Servatrice *_server, Servatrice_Dat socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); connect(socket, SIGNAL(readyRead()), this, SLOT(readClient())); connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(catchSocketError(QAbstractSocket::SocketError))); - connect(this, SIGNAL(outputBufferChanged()), this, SLOT(flushOutputBuffer()), Qt::QueuedConnection); + + // Never call flushOutputQueue directly from outputQueueChanged. In case of a socket error, + // it could lead to this object being destroyed while another function is still on the call stack. -> mutex deadlocks etc. + connect(this, SIGNAL(outputQueueChanged()), this, SLOT(flushOutputQueue()), Qt::QueuedConnection); } ServerSocketInterface::~ServerSocketInterface() { logger->logMessage("ServerSocketInterface destructor", this); - flushOutputBuffer(); + flushOutputQueue(); } void ServerSocketInterface::initConnection(int socketDescriptor) { + // Add this object to the server's list of connections before it can receive socket events. + // Otherwise, in case a of a socket error, it could be removed from the list before it is added. + server->addClient(this); + socket->setSocketDescriptor(socketDescriptor); logger->logMessage(QString("Incoming connection: %1").arg(socket->peerAddress().toString()), this); initSessionDeprecated(); @@ -93,11 +100,10 @@ void ServerSocketInterface::initSessionDeprecated() { // dirty hack to make v13 client display the correct error message - outputBufferMutex.lock(); - outputBuffer = ""; - outputBufferMutex.unlock(); - - emit outputBufferChanged(); + QByteArray buf; + buf.append(""); + socket->write(buf); + socket->flush(); } bool ServerSocketInterface::initSession() @@ -121,21 +127,9 @@ bool ServerSocketInterface::initSession() return false; } - server->addClient(this); return true; } -void ServerSocketInterface::flushOutputBuffer() -{ - QMutexLocker locker(&outputBufferMutex); - if (outputBuffer.isEmpty()) - return; - servatrice->incTxBytes(outputBuffer.size()); - socket->write(outputBuffer); - socket->flush(); - outputBuffer.clear(); -} - void ServerSocketInterface::readClient() { QByteArray data = socket->readAll(); @@ -183,18 +177,42 @@ void ServerSocketInterface::catchSocketError(QAbstractSocket::SocketError socket void ServerSocketInterface::transmitProtocolItem(const ServerMessage &item) { - QByteArray buf; - unsigned int size = item.ByteSize(); - buf.resize(size + 4); - item.SerializeToArray(buf.data() + 4, size); - buf.data()[3] = (unsigned char) size; - buf.data()[2] = (unsigned char) (size >> 8); - buf.data()[1] = (unsigned char) (size >> 16); - buf.data()[0] = (unsigned char) (size >> 24); + outputQueueMutex.lock(); + outputQueue.append(item); + outputQueueMutex.unlock(); - QMutexLocker locker(&outputBufferMutex); - outputBuffer.append(buf); - emit outputBufferChanged(); + emit outputQueueChanged(); +} + +void ServerSocketInterface::flushOutputQueue() +{ + QMutexLocker locker(&outputQueueMutex); + if (outputQueue.isEmpty()) + return; + + int totalBytes = 0; + while (!outputQueue.isEmpty()) { + ServerMessage item = outputQueue.takeFirst(); + locker.unlock(); + + QByteArray buf; + unsigned int size = item.ByteSize(); + buf.resize(size + 4); + item.SerializeToArray(buf.data() + 4, size); + buf.data()[3] = (unsigned char) size; + buf.data()[2] = (unsigned char) (size >> 8); + buf.data()[1] = (unsigned char) (size >> 16); + buf.data()[0] = (unsigned char) (size >> 24); + // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. + socket->write(buf); + + totalBytes += size + 4; + locker.relock(); + } + locker.unlock(); + servatrice->incTxBytes(totalBytes); + // see above wrt mutex + socket->flush(); } void ServerSocketInterface::logDebugMessage(const QString &message) @@ -324,10 +342,10 @@ int ServerSocketInterface::getDeckPathId(int basePathId, QStringList path) return 0; QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("select id from " + servatrice->getDbPrefix() + "_decklist_folders where id_parent = :id_parent and name = :name and user = :user"); + query.prepare("select id from " + servatrice->getDbPrefix() + "_decklist_folders where id_parent = :id_parent and name = :name and id_user = :id_user"); query.bindValue(":id_parent", basePathId); query.bindValue(":name", path.takeFirst()); - query.bindValue(":user", QString::fromStdString(userInfo->name())); + query.bindValue(":id_user", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) return -1; if (!query.next()) @@ -347,9 +365,9 @@ int ServerSocketInterface::getDeckPathId(const QString &path) bool ServerSocketInterface::deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder) { QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("select id, name from " + servatrice->getDbPrefix() + "_decklist_folders where id_parent = :id_parent and user = :user"); + query.prepare("select id, name from " + servatrice->getDbPrefix() + "_decklist_folders where id_parent = :id_parent and id_user = :id_user"); query.bindValue(":id_parent", folderId); - query.bindValue(":user", QString::fromStdString(userInfo->name())); + query.bindValue(":id_user", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) return false; @@ -362,9 +380,9 @@ bool ServerSocketInterface::deckListHelper(int folderId, ServerInfo_DeckStorage_ return false; } - query.prepare("select id, name, upload_time from " + servatrice->getDbPrefix() + "_decklist_files where id_folder = :id_folder and user = :user"); + query.prepare("select id, name, upload_time from " + servatrice->getDbPrefix() + "_decklist_files where id_folder = :id_folder and id_user = :id_user"); query.bindValue(":id_folder", folderId); - query.bindValue(":user", QString::fromStdString(userInfo->name())); + query.bindValue(":id_user", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) return false; @@ -412,9 +430,9 @@ Response::ResponseCode ServerSocketInterface::cmdDeckNewDir(const Command_DeckNe return Response::RespNameNotFound; QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("insert into " + servatrice->getDbPrefix() + "_decklist_folders (id_parent, user, name) values(:id_parent, :user, :name)"); + query.prepare("insert into " + servatrice->getDbPrefix() + "_decklist_folders (id_parent, id_user, name) values(:id_parent, :id_user, :name)"); query.bindValue(":id_parent", folderId); - query.bindValue(":user", QString::fromStdString(userInfo->name())); + query.bindValue(":id_user", userInfo->id()); query.bindValue(":name", QString::fromStdString(cmd.dir_name())); if (!sqlInterface->execSqlQuery(query)) return Response::RespContextError; @@ -463,9 +481,9 @@ Response::ResponseCode ServerSocketInterface::cmdDeckDel(const Command_DeckDel & sqlInterface->checkSql(); QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("select id from " + servatrice->getDbPrefix() + "_decklist_files where id = :id and user = :user"); + query.prepare("select id from " + servatrice->getDbPrefix() + "_decklist_files where id = :id and id_user = :id_user"); query.bindValue(":id", cmd.deck_id()); - query.bindValue(":user", QString::fromStdString(userInfo->name())); + query.bindValue(":id_user", userInfo->id()); sqlInterface->execSqlQuery(query); if (!query.next()) return Response::RespNameNotFound; @@ -500,9 +518,9 @@ Response::ResponseCode ServerSocketInterface::cmdDeckUpload(const Command_DeckUp return Response::RespNameNotFound; QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("insert into " + servatrice->getDbPrefix() + "_decklist_files (id_folder, user, name, upload_time, content) values(:id_folder, :user, :name, NOW(), :content)"); + query.prepare("insert into " + servatrice->getDbPrefix() + "_decklist_files (id_folder, id_user, name, upload_time, content) values(:id_folder, :id_user, :name, NOW(), :content)"); query.bindValue(":id_folder", folderId); - query.bindValue(":user", QString::fromStdString(userInfo->name())); + query.bindValue(":id_user", userInfo->id()); query.bindValue(":name", deckName); query.bindValue(":content", deckStr); sqlInterface->execSqlQuery(query); @@ -515,9 +533,9 @@ Response::ResponseCode ServerSocketInterface::cmdDeckUpload(const Command_DeckUp rc.setResponseExtension(re); } else if (cmd.has_deck_id()) { QSqlQuery query(sqlInterface->getDatabase()); - query.prepare("update " + servatrice->getDbPrefix() + "_decklist_files set name=:name, upload_time=NOW(), content=:content where id = :id_deck and user = :user"); + query.prepare("update " + servatrice->getDbPrefix() + "_decklist_files set name=:name, upload_time=NOW(), content=:content where id = :id_deck and id_user = :id_user"); query.bindValue(":id_deck", cmd.deck_id()); - query.bindValue(":user", QString::fromStdString(userInfo->name())); + query.bindValue(":id_user", userInfo->id()); query.bindValue(":name", deckName); query.bindValue(":content", deckStr); sqlInterface->execSqlQuery(query); @@ -544,7 +562,7 @@ Response::ResponseCode ServerSocketInterface::cmdDeckDownload(const Command_Deck DeckList *deck; try { - deck = sqlInterface->getDeckFromDatabase(cmd.deck_id(), QString::fromStdString(userInfo->name())); + deck = sqlInterface->getDeckFromDatabase(cmd.deck_id(), userInfo->id()); } catch(Response::ResponseCode r) { return r; } @@ -701,6 +719,7 @@ Response::ResponseCode ServerSocketInterface::cmdBanFromServer(const Command_Ban query.bindValue(":visible_reason", QString::fromStdString(cmd.visible_reason())); sqlInterface->execSqlQuery(query); + servatrice->clientsLock.lockForRead(); QList userList = servatrice->getUsersWithAddressAsList(QHostAddress(address)); ServerSocketInterface *user = static_cast(server->getUsers().value(userName)); if (user && !userList.contains(user)) @@ -715,10 +734,11 @@ Response::ResponseCode ServerSocketInterface::cmdBanFromServer(const Command_Ban for (int i = 0; i < userList.size(); ++i) { SessionEvent *se = userList[i]->prepareSessionEvent(event); userList[i]->sendProtocolItem(*se); - userList[i]->prepareDestroy(); delete se; + QMetaObject::invokeMethod(userList[i], "prepareDestroy", Qt::QueuedConnection); } } + servatrice->clientsLock.unlock(); return Response::RespOk; } @@ -728,7 +748,7 @@ Response::ResponseCode ServerSocketInterface::cmdBanFromServer(const Command_Ban Response::ResponseCode ServerSocketInterface::cmdUpdateServerMessage(const Command_UpdateServerMessage & /*cmd*/, ResponseContainer & /*rc*/) { - servatrice->updateLoginMessage(); + QMetaObject::invokeMethod(server, "updateLoginMessage"); return Response::RespOk; } diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index 184f9d76..6a2a41f8 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -54,18 +54,19 @@ class ServerSocketInterface : public Server_ProtocolHandler private slots: void readClient(); void catchSocketError(QAbstractSocket::SocketError socketError); - void flushOutputBuffer(); + void flushOutputQueue(); signals: - void outputBufferChanged(); + void outputQueueChanged(); protected: void logDebugMessage(const QString &message); private: - QMutex outputBufferMutex; + QMutex outputQueueMutex; Servatrice *servatrice; Servatrice_DatabaseInterface *sqlInterface; QTcpSocket *socket; - QByteArray inputBuffer, outputBuffer; + QByteArray inputBuffer; + QList outputQueue; bool messageInProgress; bool handshakeStarted; int messageLength;