mirror of
https://github.com/vcmi/vcmi.git
synced 2025-11-25 22:42:04 +02:00
Implemented matches history in lobby
This commit is contained in:
@@ -78,6 +78,11 @@
|
|||||||
"vcmi.lobby.login.error" : "Connection error: %s",
|
"vcmi.lobby.login.error" : "Connection error: %s",
|
||||||
"vcmi.lobby.login.create" : "New Account",
|
"vcmi.lobby.login.create" : "New Account",
|
||||||
"vcmi.lobby.login.login" : "Login",
|
"vcmi.lobby.login.login" : "Login",
|
||||||
|
"vcmi.lobby.header.rooms" : "Game Rooms",
|
||||||
|
"vcmi.lobby.header.channels" : "Chat Channels",
|
||||||
|
"vcmi.lobby.header.chat" : "Game Chat",
|
||||||
|
"vcmi.lobby.header.history" : "Your Games",
|
||||||
|
"vcmi.lobby.header.players" : "Players Online",
|
||||||
"vcmi.lobby.room.create" : "Create New Room",
|
"vcmi.lobby.room.create" : "Create New Room",
|
||||||
"vcmi.lobby.room.players.limit" : "Players Limit",
|
"vcmi.lobby.room.players.limit" : "Players Limit",
|
||||||
"vcmi.lobby.room.description.public" : "Any player can join public room.",
|
"vcmi.lobby.room.description.public" : "Any player can join public room.",
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
"vcmi.lobby.filepath" : "Назва файлу",
|
"vcmi.lobby.filepath" : "Назва файлу",
|
||||||
"vcmi.lobby.creationDate" : "Дата створення",
|
"vcmi.lobby.creationDate" : "Дата створення",
|
||||||
"vcmi.lobby.scenarioName" : "Scenario name",
|
"vcmi.lobby.scenarioName" : "Назва сценарію",
|
||||||
"vcmi.lobby.mapPreview" : "Огляд мапи",
|
"vcmi.lobby.mapPreview" : "Огляд мапи",
|
||||||
"vcmi.lobby.noPreview" : "огляд недоступний",
|
"vcmi.lobby.noPreview" : "огляд недоступний",
|
||||||
"vcmi.lobby.noUnderground" : "немає підземелля",
|
"vcmi.lobby.noUnderground" : "немає підземелля",
|
||||||
@@ -78,6 +78,11 @@
|
|||||||
"vcmi.lobby.login.error" : "Помилка з'єднання: %s",
|
"vcmi.lobby.login.error" : "Помилка з'єднання: %s",
|
||||||
"vcmi.lobby.login.create" : "Створити акаунт",
|
"vcmi.lobby.login.create" : "Створити акаунт",
|
||||||
"vcmi.lobby.login.login" : "Увійти",
|
"vcmi.lobby.login.login" : "Увійти",
|
||||||
|
"vcmi.lobby.header.rooms" : "Кімнати",
|
||||||
|
"vcmi.lobby.header.channels" : "Канали чату",
|
||||||
|
"vcmi.lobby.header.chat" : "Чат гри",
|
||||||
|
"vcmi.lobby.header.history" : "Ваші попередні ігри",
|
||||||
|
"vcmi.lobby.header.players" : "Гравці в мережі",
|
||||||
"vcmi.lobby.room.create" : "Створити нову кімнату",
|
"vcmi.lobby.room.create" : "Створити нову кімнату",
|
||||||
"vcmi.lobby.room.players.limit" : "Максимум гравців",
|
"vcmi.lobby.room.players.limit" : "Максимум гравців",
|
||||||
"vcmi.lobby.room.description.public" : "Будь-хто з гравців може приєднатися до публічної кімнати.",
|
"vcmi.lobby.room.description.public" : "Будь-хто з гравців може приєднатися до публічної кімнати.",
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ void CServerHandler::resetStateForLobby(EStartMode mode, ESelectionScreen screen
|
|||||||
localPlayerNames.push_back(settings["general"]["playerName"].String());
|
localPlayerNames.push_back(settings["general"]["playerName"].String());
|
||||||
|
|
||||||
gameChat->resetMatchState();
|
gameChat->resetMatchState();
|
||||||
if (lobbyClient)
|
|
||||||
lobbyClient->resetMatchState();
|
lobbyClient->resetMatchState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "CServerHandler.h"
|
#include "CServerHandler.h"
|
||||||
#include "CPlayerInterface.h"
|
#include "CPlayerInterface.h"
|
||||||
#include "PlayerLocalState.h"
|
#include "PlayerLocalState.h"
|
||||||
|
#include "globalLobby/GlobalLobbyClient.h"
|
||||||
#include "lobby/CLobbyScreen.h"
|
#include "lobby/CLobbyScreen.h"
|
||||||
|
|
||||||
#include "adventureMap/CInGameConsole.h"
|
#include "adventureMap/CInGameConsole.h"
|
||||||
@@ -34,7 +35,7 @@ static std::string getCurrentTimeFormatted(int timeOffsetSeconds = 0)
|
|||||||
return TextOperations::getFormattedTimeLocal(std::chrono::system_clock::to_time_t(timeNowChrono));
|
return TextOperations::getFormattedTimeLocal(std::chrono::system_clock::to_time_t(timeNowChrono));
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<GameChatMessage> GameChatHandler::getChatHistory()
|
const std::vector<GameChatMessage> & GameChatHandler::getChatHistory()
|
||||||
{
|
{
|
||||||
return chatHistory;
|
return chatHistory;
|
||||||
}
|
}
|
||||||
@@ -47,6 +48,7 @@ void GameChatHandler::resetMatchState()
|
|||||||
void GameChatHandler::sendMessageGameplay(const std::string & messageText)
|
void GameChatHandler::sendMessageGameplay(const std::string & messageText)
|
||||||
{
|
{
|
||||||
LOCPLINT->cb->sendMessage(messageText, LOCPLINT->localState->getCurrentArmy());
|
LOCPLINT->cb->sendMessage(messageText, LOCPLINT->localState->getCurrentArmy());
|
||||||
|
CSH->getGlobalLobby().sendMatchChatMessage(messageText);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameChatHandler::sendMessageLobby(const std::string & senderName, const std::string & messageText)
|
void GameChatHandler::sendMessageLobby(const std::string & senderName, const std::string & messageText)
|
||||||
@@ -55,11 +57,22 @@ void GameChatHandler::sendMessageLobby(const std::string & senderName, const std
|
|||||||
lcm.message = messageText;
|
lcm.message = messageText;
|
||||||
lcm.playerName = senderName;
|
lcm.playerName = senderName;
|
||||||
CSH->sendLobbyPack(lcm);
|
CSH->sendLobbyPack(lcm);
|
||||||
|
CSH->getGlobalLobby().sendMatchChatMessage(messageText);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameChatHandler::onNewLobbyMessageReceived(const std::string & senderName, const std::string & messageText)
|
void GameChatHandler::onNewLobbyMessageReceived(const std::string & senderName, const std::string & messageText)
|
||||||
{
|
{
|
||||||
auto lobby = dynamic_cast<CLobbyScreen*>(SEL);
|
if (!SEL)
|
||||||
|
{
|
||||||
|
logGlobal->debug("Received chat message for lobby but lobby not yet exists!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto * lobby = dynamic_cast<CLobbyScreen*>(SEL);
|
||||||
|
|
||||||
|
// FIXME: when can this happen?
|
||||||
|
assert(lobby);
|
||||||
|
assert(lobby->card);
|
||||||
|
|
||||||
if(lobby && lobby->card)
|
if(lobby && lobby->card)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class GameChatHandler : boost::noncopyable
|
|||||||
std::vector<GameChatMessage> chatHistory;
|
std::vector<GameChatMessage> chatHistory;
|
||||||
public:
|
public:
|
||||||
/// Returns all message history for current match
|
/// Returns all message history for current match
|
||||||
const std::vector<GameChatMessage> getChatHistory();
|
const std::vector<GameChatMessage> & getChatHistory();
|
||||||
|
|
||||||
/// Erases any local state, must be called when client disconnects from match server
|
/// Erases any local state, must be called when client disconnects from match server
|
||||||
void resetMatchState();
|
void resetMatchState();
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ void GlobalLobbyClient::onPacketReceived(const std::shared_ptr<INetworkConnectio
|
|||||||
if(json["type"].String() == "inviteReceived")
|
if(json["type"].String() == "inviteReceived")
|
||||||
return receiveInviteReceived(json);
|
return receiveInviteReceived(json);
|
||||||
|
|
||||||
|
if(json["type"].String() == "matchesHistory")
|
||||||
|
return receiveMatchesHistory(json);
|
||||||
|
|
||||||
logGlobal->error("Received unexpected message from lobby server: %s", json["type"].String());
|
logGlobal->error("Received unexpected message from lobby server: %s", json["type"].String());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +214,13 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
|
|||||||
room.hostAccountDisplayName = jsonEntry["hostAccountDisplayName"].String();
|
room.hostAccountDisplayName = jsonEntry["hostAccountDisplayName"].String();
|
||||||
room.description = jsonEntry["description"].String();
|
room.description = jsonEntry["description"].String();
|
||||||
room.statusID = jsonEntry["status"].String();
|
room.statusID = jsonEntry["status"].String();
|
||||||
room.playersCount = jsonEntry["playersCount"].Integer();
|
for (auto const & jsonParticipant : jsonEntry["participants"].Vector())
|
||||||
|
{
|
||||||
|
GlobalLobbyAccount account;
|
||||||
|
account.accountID = jsonParticipant["accountID"].String();
|
||||||
|
account.displayName = jsonParticipant["displayName"].String();
|
||||||
|
room.participants.push_back(account);
|
||||||
|
}
|
||||||
room.playerLimit = jsonEntry["playerLimit"].Integer();
|
room.playerLimit = jsonEntry["playerLimit"].Integer();
|
||||||
|
|
||||||
activeRooms.push_back(room);
|
activeRooms.push_back(room);
|
||||||
@@ -222,6 +231,36 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
|
|||||||
lobbyWindowPtr->onActiveRooms(activeRooms);
|
lobbyWindowPtr->onActiveRooms(activeRooms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GlobalLobbyClient::receiveMatchesHistory(const JsonNode & json)
|
||||||
|
{
|
||||||
|
matchesHistory.clear();
|
||||||
|
|
||||||
|
for (auto const & jsonEntry : json["matchesHistory"].Vector())
|
||||||
|
{
|
||||||
|
GlobalLobbyRoom room;
|
||||||
|
|
||||||
|
room.gameRoomID = jsonEntry["gameRoomID"].String();
|
||||||
|
room.hostAccountID = jsonEntry["hostAccountID"].String();
|
||||||
|
room.hostAccountDisplayName = jsonEntry["hostAccountDisplayName"].String();
|
||||||
|
room.description = jsonEntry["description"].String();
|
||||||
|
room.statusID = jsonEntry["status"].String();
|
||||||
|
for (auto const & jsonParticipant : jsonEntry["participants"].Vector())
|
||||||
|
{
|
||||||
|
GlobalLobbyAccount account;
|
||||||
|
account.accountID = jsonParticipant["accountID"].String();
|
||||||
|
account.displayName = jsonParticipant["displayName"].String();
|
||||||
|
room.participants.push_back(account);
|
||||||
|
}
|
||||||
|
room.playerLimit = jsonEntry["playerLimit"].Integer();
|
||||||
|
|
||||||
|
matchesHistory.push_back(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto lobbyWindowPtr = lobbyWindow.lock();
|
||||||
|
if(lobbyWindowPtr)
|
||||||
|
lobbyWindowPtr->onMatchesHistory(activeRooms);
|
||||||
|
}
|
||||||
|
|
||||||
void GlobalLobbyClient::receiveInviteReceived(const JsonNode & json)
|
void GlobalLobbyClient::receiveInviteReceived(const JsonNode & json)
|
||||||
{
|
{
|
||||||
assert(0); //TODO
|
assert(0); //TODO
|
||||||
@@ -372,7 +411,7 @@ const std::vector<std::string> & GlobalLobbyClient::getActiveChannels() const
|
|||||||
return activeChannels;
|
return activeChannels;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<GlobalLobbyHistoryMatch> & GlobalLobbyClient::getMatchesHistory() const
|
const std::vector<GlobalLobbyRoom> & GlobalLobbyClient::getMatchesHistory() const
|
||||||
{
|
{
|
||||||
return matchesHistory;
|
return matchesHistory;
|
||||||
}
|
}
|
||||||
@@ -435,3 +474,20 @@ void GlobalLobbyClient::resetMatchState()
|
|||||||
{
|
{
|
||||||
currentGameRoomUUID.clear();
|
currentGameRoomUUID.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GlobalLobbyClient::sendMatchChatMessage(const std::string & messageText)
|
||||||
|
{
|
||||||
|
if (!isConnected())
|
||||||
|
return; // we are not playing with lobby
|
||||||
|
|
||||||
|
if (currentGameRoomUUID.empty())
|
||||||
|
return; // we are not playing through lobby
|
||||||
|
|
||||||
|
JsonNode toSend;
|
||||||
|
toSend["type"].String() = "sendChatMessage";
|
||||||
|
toSend["channelType"].String() = "match";
|
||||||
|
toSend["channelName"].String() = currentGameRoomUUID;
|
||||||
|
toSend["messageText"].String() = messageText;
|
||||||
|
|
||||||
|
CSH->getGlobalLobby().sendMessage(toSend);
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyabl
|
|||||||
std::vector<GlobalLobbyAccount> activeAccounts;
|
std::vector<GlobalLobbyAccount> activeAccounts;
|
||||||
std::vector<GlobalLobbyRoom> activeRooms;
|
std::vector<GlobalLobbyRoom> activeRooms;
|
||||||
std::vector<std::string> activeChannels;
|
std::vector<std::string> activeChannels;
|
||||||
std::vector<GlobalLobbyHistoryMatch> matchesHistory;
|
std::vector<GlobalLobbyRoom> matchesHistory;
|
||||||
|
|
||||||
/// Contains known history of each channel
|
/// Contains known history of each channel
|
||||||
/// Key: concatenated channel type and channel name
|
/// Key: concatenated channel type and channel name
|
||||||
@@ -50,6 +50,7 @@ class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyabl
|
|||||||
void receiveChatMessage(const JsonNode & json);
|
void receiveChatMessage(const JsonNode & json);
|
||||||
void receiveActiveAccounts(const JsonNode & json);
|
void receiveActiveAccounts(const JsonNode & json);
|
||||||
void receiveActiveGameRooms(const JsonNode & json);
|
void receiveActiveGameRooms(const JsonNode & json);
|
||||||
|
void receiveMatchesHistory(const JsonNode & json);
|
||||||
void receiveJoinRoomSuccess(const JsonNode & json);
|
void receiveJoinRoomSuccess(const JsonNode & json);
|
||||||
void receiveInviteReceived(const JsonNode & json);
|
void receiveInviteReceived(const JsonNode & json);
|
||||||
|
|
||||||
@@ -63,12 +64,13 @@ public:
|
|||||||
const std::vector<GlobalLobbyAccount> & getActiveAccounts() const;
|
const std::vector<GlobalLobbyAccount> & getActiveAccounts() const;
|
||||||
const std::vector<GlobalLobbyRoom> & getActiveRooms() const;
|
const std::vector<GlobalLobbyRoom> & getActiveRooms() const;
|
||||||
const std::vector<std::string> & getActiveChannels() const;
|
const std::vector<std::string> & getActiveChannels() const;
|
||||||
const std::vector<GlobalLobbyHistoryMatch> & getMatchesHistory() const;
|
const std::vector<GlobalLobbyRoom> & getMatchesHistory() const;
|
||||||
const std::vector<GlobalLobbyChannelMessage> & getChannelHistory(const std::string & channelType, const std::string & channelName) const;
|
const std::vector<GlobalLobbyChannelMessage> & getChannelHistory(const std::string & channelType, const std::string & channelName) const;
|
||||||
|
|
||||||
/// Activate interface and pushes lobby UI as top window
|
/// Activate interface and pushes lobby UI as top window
|
||||||
void activateInterface();
|
void activateInterface();
|
||||||
|
|
||||||
|
void sendMatchChatMessage(const std::string & messageText);
|
||||||
void sendMessage(const JsonNode & data);
|
void sendMessage(const JsonNode & data);
|
||||||
void sendClientRegister(const std::string & accountName);
|
void sendClientRegister(const std::string & accountName);
|
||||||
void sendClientLogin();
|
void sendClientLogin();
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ struct GlobalLobbyRoom
|
|||||||
std::string hostAccountDisplayName;
|
std::string hostAccountDisplayName;
|
||||||
std::string description;
|
std::string description;
|
||||||
std::string statusID;
|
std::string statusID;
|
||||||
int playersCount;
|
std::string startDateFormatted;
|
||||||
|
std::vector<GlobalLobbyAccount> participants;
|
||||||
int playerLimit;
|
int playerLimit;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,10 +35,3 @@ struct GlobalLobbyChannelMessage
|
|||||||
std::string displayName;
|
std::string displayName;
|
||||||
std::string messageText;
|
std::string messageText;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct GlobalLobbyHistoryMatch
|
|
||||||
{
|
|
||||||
std::string gameRoomID;
|
|
||||||
std::string startDateFormatted;
|
|
||||||
std::vector<std::string> opponentDisplayNames;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ std::shared_ptr<CListBox> GlobalLobbyWidget::getRoomList()
|
|||||||
return widget<CListBox>("roomList");
|
return widget<CListBox>("roomList");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<CListBox> GlobalLobbyWidget::getMatchList()
|
||||||
|
{
|
||||||
|
return widget<CListBox>("matchList");
|
||||||
|
}
|
||||||
|
|
||||||
GlobalLobbyChannelCardBase::GlobalLobbyChannelCardBase(GlobalLobbyWindow * window, const std::string & channelType, const std::string & channelName)
|
GlobalLobbyChannelCardBase::GlobalLobbyChannelCardBase(GlobalLobbyWindow * window, const std::string & channelType, const std::string & channelName)
|
||||||
: window(window)
|
: window(window)
|
||||||
, channelType(channelType)
|
, channelType(channelType)
|
||||||
@@ -169,7 +174,7 @@ GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const Globa
|
|||||||
};
|
};
|
||||||
|
|
||||||
auto roomSizeText = MetaString::createFromRawString("%d/%d");
|
auto roomSizeText = MetaString::createFromRawString("%d/%d");
|
||||||
roomSizeText.replaceNumber(roomDescription.playersCount);
|
roomSizeText.replaceNumber(roomDescription.participants.size());
|
||||||
roomSizeText.replaceNumber(roomDescription.playerLimit);
|
roomSizeText.replaceNumber(roomDescription.playerLimit);
|
||||||
|
|
||||||
auto roomStatusText = MetaString::createFromTextID("vcmi.lobby.room.state." + roomDescription.statusID);
|
auto roomStatusText = MetaString::createFromTextID("vcmi.lobby.room.state." + roomDescription.statusID);
|
||||||
@@ -203,7 +208,7 @@ GlobalLobbyChannelCard::GlobalLobbyChannelCard(GlobalLobbyWindow * window, const
|
|||||||
labelName = std::make_shared<CLabel>(5, 20, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, Languages::getLanguageOptions(channelName).nameNative);
|
labelName = std::make_shared<CLabel>(5, 20, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, Languages::getLanguageOptions(channelName).nameNative);
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalLobbyMatchCard::GlobalLobbyMatchCard(GlobalLobbyWindow * window, const GlobalLobbyHistoryMatch & matchDescription)
|
GlobalLobbyMatchCard::GlobalLobbyMatchCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & matchDescription)
|
||||||
: GlobalLobbyChannelCardBase(window, "match", matchDescription.gameRoomID)
|
: GlobalLobbyChannelCardBase(window, "match", matchDescription.gameRoomID)
|
||||||
{
|
{
|
||||||
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
|
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
|
||||||
@@ -216,16 +221,16 @@ GlobalLobbyMatchCard::GlobalLobbyMatchCard(GlobalLobbyWindow * window, const Glo
|
|||||||
|
|
||||||
MetaString opponentDescription;
|
MetaString opponentDescription;
|
||||||
|
|
||||||
if (matchDescription.opponentDisplayNames.empty())
|
if (matchDescription.participants.size() == 1)
|
||||||
opponentDescription.appendRawString("Solo game"); // or "Singleplayer game" is better?
|
opponentDescription.appendRawString("Solo game"); // or "Singleplayer game" is better?
|
||||||
|
|
||||||
if (matchDescription.opponentDisplayNames.size() == 1)
|
if (matchDescription.participants.size() == 2)
|
||||||
opponentDescription.appendRawString(matchDescription.opponentDisplayNames[0]);
|
opponentDescription.appendRawString(matchDescription.participants[0].displayName);//FIXME: find opponent - not our player
|
||||||
|
|
||||||
if (matchDescription.opponentDisplayNames.size() > 1)
|
if (matchDescription.participants.size() > 2)
|
||||||
{
|
{
|
||||||
opponentDescription.appendRawString("%d players");
|
opponentDescription.appendRawString("%d players");
|
||||||
opponentDescription.replaceNumber(matchDescription.opponentDisplayNames.size());
|
opponentDescription.replaceNumber(matchDescription.participants.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
labelMatchOpponent = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, opponentDescription.toString());
|
labelMatchOpponent = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, opponentDescription.toString());
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
class GlobalLobbyWindow;
|
class GlobalLobbyWindow;
|
||||||
struct GlobalLobbyAccount;
|
struct GlobalLobbyAccount;
|
||||||
struct GlobalLobbyRoom;
|
struct GlobalLobbyRoom;
|
||||||
struct GlobalLobbyHistoryMatch;
|
|
||||||
class CListBox;
|
class CListBox;
|
||||||
|
|
||||||
class GlobalLobbyWidget : public InterfaceObjectConfigurable
|
class GlobalLobbyWidget : public InterfaceObjectConfigurable
|
||||||
@@ -90,5 +89,5 @@ class GlobalLobbyMatchCard : public GlobalLobbyChannelCardBase
|
|||||||
std::shared_ptr<CLabel> labelMatchOpponent;
|
std::shared_ptr<CLabel> labelMatchOpponent;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
GlobalLobbyMatchCard(GlobalLobbyWindow * window, const GlobalLobbyHistoryMatch & matchDescription);
|
GlobalLobbyMatchCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & matchDescription);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -113,12 +113,20 @@ void GlobalLobbyWindow::onActiveAccounts(const std::vector<GlobalLobbyAccount> &
|
|||||||
|
|
||||||
void GlobalLobbyWindow::onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms)
|
void GlobalLobbyWindow::onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms)
|
||||||
{
|
{
|
||||||
if (rooms.size() == widget->getAccountList()->size())
|
if (rooms.size() == widget->getRoomList()->size())
|
||||||
widget->getRoomList()->reset();
|
widget->getRoomList()->reset();
|
||||||
else
|
else
|
||||||
widget->getRoomList()->resize(rooms.size());
|
widget->getRoomList()->resize(rooms.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GlobalLobbyWindow::onMatchesHistory(const std::vector<GlobalLobbyRoom> & history)
|
||||||
|
{
|
||||||
|
if (history.size() == widget->getMatchList()->size())
|
||||||
|
widget->getMatchList()->reset();
|
||||||
|
else
|
||||||
|
widget->getMatchList()->resize(history.size());
|
||||||
|
}
|
||||||
|
|
||||||
void GlobalLobbyWindow::onJoinedRoom()
|
void GlobalLobbyWindow::onJoinedRoom()
|
||||||
{
|
{
|
||||||
widget->getAccountList()->reset();
|
widget->getAccountList()->reset();
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public:
|
|||||||
void onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when, const std::string & channelType, const std::string & channelName);
|
void onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when, const std::string & channelType, const std::string & channelName);
|
||||||
void onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts);
|
void onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts);
|
||||||
void onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms);
|
void onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms);
|
||||||
|
void onMatchesHistory(const std::vector<GlobalLobbyRoom> & history);
|
||||||
void onJoinedRoom();
|
void onJoinedRoom();
|
||||||
void onLeftRoom();
|
void onLeftRoom();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
{
|
{
|
||||||
"type" : "object",
|
"type" : "object",
|
||||||
"additionalProperties" : false,
|
"additionalProperties" : false,
|
||||||
"required" : [ "gameRoomID", "hostAccountID", "hostAccountDisplayName", "description", "playersCount", "playerLimit", "status" ],
|
"required" : [ "gameRoomID", "hostAccountID", "hostAccountDisplayName", "description", "participants", "playerLimit", "status" ],
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"gameRoomID" :
|
"gameRoomID" :
|
||||||
{
|
{
|
||||||
@@ -42,10 +42,28 @@
|
|||||||
"type" : "string",
|
"type" : "string",
|
||||||
"description" : "Auto-generated description of this room"
|
"description" : "Auto-generated description of this room"
|
||||||
},
|
},
|
||||||
"playersCount" :
|
"participants" :
|
||||||
{
|
{
|
||||||
"type" : "number",
|
"type" : "array",
|
||||||
"description" : "Current number of players in this room, including host"
|
"description" : "List of accounts in the room, including host",
|
||||||
|
"items" :
|
||||||
|
{
|
||||||
|
"type" : "object",
|
||||||
|
"additionalProperties" : false,
|
||||||
|
"required" : [ "accountID", "displayName" ],
|
||||||
|
"properties" : {
|
||||||
|
"accountID" :
|
||||||
|
{
|
||||||
|
"type" : "string",
|
||||||
|
"description" : "Unique ID of an account"
|
||||||
|
},
|
||||||
|
"displayName" :
|
||||||
|
{
|
||||||
|
"type" : "string",
|
||||||
|
"description" : "Display name of an account"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"status" :
|
"status" :
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
{
|
{
|
||||||
"type": "labelTitle",
|
"type": "labelTitle",
|
||||||
"position": {"x": 15, "y": 53},
|
"position": {"x": 15, "y": 53},
|
||||||
"text" : "Room List"
|
"text" : "vcmi.lobby.header.rooms"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type" : "lobbyItemList",
|
"type" : "lobbyItemList",
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
{
|
{
|
||||||
"type": "labelTitle",
|
"type": "labelTitle",
|
||||||
"position": {"x": 280, "y": 53},
|
"position": {"x": 280, "y": 53},
|
||||||
"text" : "Channel List"
|
"text" : "vcmi.lobby.header.channels"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type" : "lobbyItemList",
|
"type" : "lobbyItemList",
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
{
|
{
|
||||||
"type": "labelTitle",
|
"type": "labelTitle",
|
||||||
"position": {"x": 280, "y": 213},
|
"position": {"x": 280, "y": 213},
|
||||||
"text" : "Games History"
|
"text" : "vcmi.lobby.header.history"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type" : "lobbyItemList",
|
"type" : "lobbyItemList",
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
{
|
{
|
||||||
"type": "labelTitle",
|
"type": "labelTitle",
|
||||||
"position": {"x": 440, "y": 53},
|
"position": {"x": 440, "y": 53},
|
||||||
"text" : "Game Chat"
|
"text" : "vcmi.lobby.header.chat"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "textBox",
|
"type": "textBox",
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
{
|
{
|
||||||
"type": "labelTitle",
|
"type": "labelTitle",
|
||||||
"position": {"x": 880, "y": 53},
|
"position": {"x": 880, "y": 53},
|
||||||
"text" : "Account List"
|
"text" : "vcmi.lobby.header.players"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type" : "lobbyItemList",
|
"type" : "lobbyItemList",
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ void LobbyDatabase::clearOldData()
|
|||||||
WHERE online <> 0
|
WHERE online <> 0
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
//FIXME: set different status for rooms that never reached in game state
|
||||||
static const std::string removeActiveRooms = R"(
|
static const std::string removeActiveRooms = R"(
|
||||||
UPDATE gameRooms
|
UPDATE gameRooms
|
||||||
SET status = 5
|
SET status = 5
|
||||||
@@ -201,6 +202,14 @@ void LobbyDatabase::prepareStatements()
|
|||||||
WHERE roomID = ?
|
WHERE roomID = ?
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
static const std::string getAccountGameHistoryText = R"(
|
||||||
|
SELECT gr.roomID, hostAccountID, displayName, description, status, playerLimit
|
||||||
|
FROM gameRoomPlayers grp
|
||||||
|
LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID
|
||||||
|
LEFT JOIN accounts a ON gr.hostAccountID = a.accountID
|
||||||
|
WHERE grp.accountID = ? AND status IN (4,5)
|
||||||
|
)";
|
||||||
|
|
||||||
static const std::string getAccountGameRoomText = R"(
|
static const std::string getAccountGameRoomText = R"(
|
||||||
SELECT grp.roomID
|
SELECT grp.roomID
|
||||||
FROM gameRoomPlayers grp
|
FROM gameRoomPlayers grp
|
||||||
@@ -223,8 +232,9 @@ void LobbyDatabase::prepareStatements()
|
|||||||
)";
|
)";
|
||||||
|
|
||||||
static const std::string countRoomUsedSlotsText = R"(
|
static const std::string countRoomUsedSlotsText = R"(
|
||||||
SELECT COUNT(accountID)
|
SELECT a.accountID, a.displayName
|
||||||
FROM gameRoomPlayers
|
FROM gameRoomPlayers grp
|
||||||
|
LEFT JOIN accounts a ON a.accountID = grp.accountID
|
||||||
WHERE roomID = ?
|
WHERE roomID = ?
|
||||||
)";
|
)";
|
||||||
|
|
||||||
@@ -298,6 +308,7 @@ void LobbyDatabase::prepareStatements()
|
|||||||
getFullMessageHistoryStatement = database->prepare(getFullMessageHistoryText);
|
getFullMessageHistoryStatement = database->prepare(getFullMessageHistoryText);
|
||||||
getIdleGameRoomStatement = database->prepare(getIdleGameRoomText);
|
getIdleGameRoomStatement = database->prepare(getIdleGameRoomText);
|
||||||
getGameRoomStatusStatement = database->prepare(getGameRoomStatusText);
|
getGameRoomStatusStatement = database->prepare(getGameRoomStatusText);
|
||||||
|
getAccountGameHistoryStatement = database->prepare(getAccountGameHistoryText);
|
||||||
getAccountGameRoomStatement = database->prepare(getAccountGameRoomText);
|
getAccountGameRoomStatement = database->prepare(getAccountGameRoomText);
|
||||||
getActiveAccountsStatement = database->prepare(getActiveAccountsText);
|
getActiveAccountsStatement = database->prepare(getActiveAccountsText);
|
||||||
getActiveGameRoomsStatement = database->prepare(getActiveGameRoomsText);
|
getActiveGameRoomsStatement = database->prepare(getActiveGameRoomsText);
|
||||||
@@ -545,8 +556,39 @@ std::vector<LobbyGameRoom> LobbyDatabase::getActiveGameRooms()
|
|||||||
for (auto & room : result)
|
for (auto & room : result)
|
||||||
{
|
{
|
||||||
countRoomUsedSlotsStatement->setBinds(room.roomID);
|
countRoomUsedSlotsStatement->setBinds(room.roomID);
|
||||||
if(countRoomUsedSlotsStatement->execute())
|
while(countRoomUsedSlotsStatement->execute())
|
||||||
countRoomUsedSlotsStatement->getColumns(room.playersCount);
|
{
|
||||||
|
LobbyAccount account;
|
||||||
|
countRoomUsedSlotsStatement->getColumns(account.accountID, account.displayName);
|
||||||
|
room.participants.push_back(account);
|
||||||
|
}
|
||||||
|
countRoomUsedSlotsStatement->reset();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<LobbyGameRoom> LobbyDatabase::getAccountGameHistory(const std::string & accountID)
|
||||||
|
{
|
||||||
|
std::vector<LobbyGameRoom> result;
|
||||||
|
|
||||||
|
getAccountGameHistoryStatement->setBinds(accountID);
|
||||||
|
while(getAccountGameHistoryStatement->execute())
|
||||||
|
{
|
||||||
|
LobbyGameRoom entry;
|
||||||
|
getAccountGameHistoryStatement->getColumns(entry.roomID, entry.hostAccountID, entry.hostAccountDisplayName, entry.description, entry.roomState, entry.playerLimit);
|
||||||
|
result.push_back(entry);
|
||||||
|
}
|
||||||
|
getAccountGameHistoryStatement->reset();
|
||||||
|
|
||||||
|
for (auto & room : result)
|
||||||
|
{
|
||||||
|
countRoomUsedSlotsStatement->setBinds(room.roomID);
|
||||||
|
while(countRoomUsedSlotsStatement->execute())
|
||||||
|
{
|
||||||
|
LobbyAccount account;
|
||||||
|
countRoomUsedSlotsStatement->getColumns(account.accountID, account.displayName);
|
||||||
|
room.participants.push_back(account);
|
||||||
|
}
|
||||||
countRoomUsedSlotsStatement->reset();
|
countRoomUsedSlotsStatement->reset();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class LobbyDatabase
|
|||||||
SQLiteStatementPtr getFullMessageHistoryStatement;
|
SQLiteStatementPtr getFullMessageHistoryStatement;
|
||||||
SQLiteStatementPtr getIdleGameRoomStatement;
|
SQLiteStatementPtr getIdleGameRoomStatement;
|
||||||
SQLiteStatementPtr getGameRoomStatusStatement;
|
SQLiteStatementPtr getGameRoomStatusStatement;
|
||||||
|
SQLiteStatementPtr getAccountGameHistoryStatement;
|
||||||
SQLiteStatementPtr getActiveGameRoomsStatement;
|
SQLiteStatementPtr getActiveGameRoomsStatement;
|
||||||
SQLiteStatementPtr getActiveAccountsStatement;
|
SQLiteStatementPtr getActiveAccountsStatement;
|
||||||
SQLiteStatementPtr getAccountGameRoomStatement;
|
SQLiteStatementPtr getAccountGameRoomStatement;
|
||||||
@@ -81,6 +82,7 @@ public:
|
|||||||
void updateRoomPlayerLimit(const std::string & gameRoomID, int playerLimit);
|
void updateRoomPlayerLimit(const std::string & gameRoomID, int playerLimit);
|
||||||
void updateRoomDescription(const std::string & gameRoomID, const std::string & description);
|
void updateRoomDescription(const std::string & gameRoomID, const std::string & description);
|
||||||
|
|
||||||
|
std::vector<LobbyGameRoom> getAccountGameHistory(const std::string & accountID);
|
||||||
std::vector<LobbyGameRoom> getActiveGameRooms();
|
std::vector<LobbyGameRoom> getActiveGameRooms();
|
||||||
std::vector<LobbyAccount> getActiveAccounts();
|
std::vector<LobbyAccount> getActiveAccounts();
|
||||||
std::vector<LobbyChatMessage> getRecentMessageHistory(const std::string & channelType, const std::string & channelName);
|
std::vector<LobbyChatMessage> getRecentMessageHistory(const std::string & channelType, const std::string & channelName);
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ struct LobbyGameRoom
|
|||||||
std::string hostAccountID;
|
std::string hostAccountID;
|
||||||
std::string hostAccountDisplayName;
|
std::string hostAccountDisplayName;
|
||||||
std::string description;
|
std::string description;
|
||||||
|
std::vector<LobbyAccount> participants;
|
||||||
LobbyRoomState roomState;
|
LobbyRoomState roomState;
|
||||||
uint32_t playersCount;
|
|
||||||
uint32_t playerLimit;
|
uint32_t playerLimit;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,16 @@ void LobbyServer::broadcastActiveAccounts()
|
|||||||
sendMessage(connection.first, reply);
|
sendMessage(connection.first, reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static JsonNode loadLobbyAccountToJson(const LobbyAccount & account)
|
||||||
|
{
|
||||||
|
JsonNode jsonEntry;
|
||||||
|
jsonEntry["accountID"].String() = account.accountID;
|
||||||
|
jsonEntry["displayName"].String() = account.displayName;
|
||||||
|
return jsonEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JsonNode loadLobbyGameRoomToJson(const LobbyGameRoom & gameRoom)
|
||||||
|
{
|
||||||
static constexpr std::array LOBBY_ROOM_STATE_NAMES = {
|
static constexpr std::array LOBBY_ROOM_STATE_NAMES = {
|
||||||
"idle",
|
"idle",
|
||||||
"public",
|
"public",
|
||||||
@@ -176,6 +186,35 @@ static constexpr std::array LOBBY_ROOM_STATE_NAMES = {
|
|||||||
"closed"
|
"closed"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
JsonNode jsonEntry;
|
||||||
|
jsonEntry["gameRoomID"].String() = gameRoom.roomID;
|
||||||
|
jsonEntry["hostAccountID"].String() = gameRoom.hostAccountID;
|
||||||
|
jsonEntry["hostAccountDisplayName"].String() = gameRoom.hostAccountDisplayName;
|
||||||
|
jsonEntry["description"].String() = gameRoom.description;
|
||||||
|
jsonEntry["status"].String() = LOBBY_ROOM_STATE_NAMES[vstd::to_underlying(gameRoom.roomState)];
|
||||||
|
jsonEntry["playerLimit"].Integer() = gameRoom.playerLimit;
|
||||||
|
|
||||||
|
for (auto const & account : gameRoom.participants)
|
||||||
|
jsonEntry["participants"].Vector().push_back(loadLobbyAccountToJson(account));
|
||||||
|
|
||||||
|
return jsonEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LobbyServer::sendMatchesHistory(const NetworkConnectionPtr & target)
|
||||||
|
{
|
||||||
|
std::string accountID = activeAccounts.at(target);
|
||||||
|
|
||||||
|
auto matchesHistory = database->getAccountGameHistory(accountID);
|
||||||
|
JsonNode reply;
|
||||||
|
reply["type"].String() = "matchesHistory";
|
||||||
|
reply["matchesHistory"].Vector(); // force creation of empty vector
|
||||||
|
|
||||||
|
for(const auto & gameRoom : matchesHistory)
|
||||||
|
reply["matchesHistory"].Vector().push_back(loadLobbyGameRoomToJson(gameRoom));
|
||||||
|
|
||||||
|
sendMessage(target, reply);
|
||||||
|
}
|
||||||
|
|
||||||
JsonNode LobbyServer::prepareActiveGameRooms()
|
JsonNode LobbyServer::prepareActiveGameRooms()
|
||||||
{
|
{
|
||||||
auto activeGameRoomStats = database->getActiveGameRooms();
|
auto activeGameRoomStats = database->getActiveGameRooms();
|
||||||
@@ -184,17 +223,7 @@ JsonNode LobbyServer::prepareActiveGameRooms()
|
|||||||
reply["gameRooms"].Vector(); // force creation of empty vector
|
reply["gameRooms"].Vector(); // force creation of empty vector
|
||||||
|
|
||||||
for(const auto & gameRoom : activeGameRoomStats)
|
for(const auto & gameRoom : activeGameRoomStats)
|
||||||
{
|
reply["gameRooms"].Vector().push_back(loadLobbyGameRoomToJson(gameRoom));
|
||||||
JsonNode jsonEntry;
|
|
||||||
jsonEntry["gameRoomID"].String() = gameRoom.roomID;
|
|
||||||
jsonEntry["hostAccountID"].String() = gameRoom.hostAccountID;
|
|
||||||
jsonEntry["hostAccountDisplayName"].String() = gameRoom.hostAccountDisplayName;
|
|
||||||
jsonEntry["description"].String() = gameRoom.description;
|
|
||||||
jsonEntry["status"].String() = LOBBY_ROOM_STATE_NAMES[vstd::to_underlying(gameRoom.roomState)];
|
|
||||||
jsonEntry["playersCount"].Integer() = gameRoom.playersCount;
|
|
||||||
jsonEntry["playerLimit"].Integer() = gameRoom.playerLimit;
|
|
||||||
reply["gameRooms"].Vector().push_back(jsonEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
@@ -252,7 +281,12 @@ void LobbyServer::onDisconnected(const NetworkConnectionPtr & connection, const
|
|||||||
|
|
||||||
if(activeGameRooms.count(connection))
|
if(activeGameRooms.count(connection))
|
||||||
{
|
{
|
||||||
database->setGameRoomStatus(activeGameRooms.at(connection), LobbyRoomState::CLOSED);
|
std::string gameRoomID = activeGameRooms.at(connection);
|
||||||
|
database->setGameRoomStatus(gameRoomID, LobbyRoomState::CLOSED);
|
||||||
|
|
||||||
|
for(const auto & connection : activeAccounts)
|
||||||
|
if (database->isPlayerInGameRoom(connection.second, gameRoomID))
|
||||||
|
sendMatchesHistory(connection.first);
|
||||||
activeGameRooms.erase(connection);
|
activeGameRooms.erase(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,9 +492,10 @@ void LobbyServer::receiveSendChatMessage(const NetworkConnectionPtr & connection
|
|||||||
|
|
||||||
database->insertChatMessage(senderAccountID, channelType, channelName, messageText);
|
database->insertChatMessage(senderAccountID, channelType, channelName, messageText);
|
||||||
|
|
||||||
|
// TODO: Don't report match messages if room is still active - players in room will receive these messages via match server
|
||||||
for(const auto & otherConnection : activeAccounts)
|
for(const auto & otherConnection : activeAccounts)
|
||||||
{
|
{
|
||||||
if (database->isPlayerInGameRoom(senderAccountID, otherConnection.second))
|
if (database->isPlayerInGameRoom(otherConnection.second, channelName))
|
||||||
sendChatMessage(otherConnection.first, channelType, channelName, senderAccountID, displayName, messageText);
|
sendChatMessage(otherConnection.first, channelType, channelName, senderAccountID, displayName, messageText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,6 +571,7 @@ void LobbyServer::receiveClientLogin(const NetworkConnectionPtr & connection, co
|
|||||||
// and update acount list to everybody else including new account
|
// and update acount list to everybody else including new account
|
||||||
broadcastActiveAccounts();
|
broadcastActiveAccounts();
|
||||||
sendMessage(connection, prepareActiveGameRooms());
|
sendMessage(connection, prepareActiveGameRooms());
|
||||||
|
sendMatchesHistory(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LobbyServer::receiveServerLogin(const NetworkConnectionPtr & connection, const JsonNode & json)
|
void LobbyServer::receiveServerLogin(const NetworkConnectionPtr & connection, const JsonNode & json)
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class LobbyServer final : public INetworkServerListener
|
|||||||
void sendAccountJoinsRoom(const NetworkConnectionPtr & target, const std::string & accountID);
|
void sendAccountJoinsRoom(const NetworkConnectionPtr & target, const std::string & accountID);
|
||||||
void sendJoinRoomSuccess(const NetworkConnectionPtr & target, const std::string & gameRoomID, bool proxyMode);
|
void sendJoinRoomSuccess(const NetworkConnectionPtr & target, const std::string & gameRoomID, bool proxyMode);
|
||||||
void sendInviteReceived(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & gameRoomID);
|
void sendInviteReceived(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & gameRoomID);
|
||||||
|
void sendMatchesHistory(const NetworkConnectionPtr & target);
|
||||||
|
|
||||||
void receiveClientRegister(const NetworkConnectionPtr & connection, const JsonNode & json);
|
void receiveClientRegister(const NetworkConnectionPtr & connection, const JsonNode & json);
|
||||||
void receiveClientLogin(const NetworkConnectionPtr & connection, const JsonNode & json);
|
void receiveClientLogin(const NetworkConnectionPtr & connection, const JsonNode & json);
|
||||||
|
|||||||
Reference in New Issue
Block a user