1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-24 08:32:34 +02:00

Partially unified handling of pregame and in-game chats

This commit is contained in:
Ivan Savenko 2024-03-18 19:52:53 +02:00
parent 0b89d3804c
commit 4ed961fb96
14 changed files with 248 additions and 72 deletions

View File

@ -162,6 +162,7 @@ set(client_SRCS
CVideoHandler.cpp
Client.cpp
ClientCommandManager.cpp
GameChatHandler.cpp
HeroMovementController.cpp
NetPacksClient.cpp
NetPacksLobbyClient.cpp
@ -348,6 +349,7 @@ set(client_HEADERS
ClientCommandManager.h
ClientNetPackVisitors.h
HeroMovementController.h
GameChatHandler.h
LobbyClientNetPackVisitors.h
ServerRunner.h
resource.h

View File

@ -13,6 +13,7 @@
#include "Client.h"
#include "CGameInfo.h"
#include "ServerRunner.h"
#include "GameChatHandler.h"
#include "CPlayerInterface.h"
#include "gui/CGuiHandler.h"
#include "gui/WindowHandler.h"
@ -128,6 +129,7 @@ CServerHandler::~CServerHandler()
CServerHandler::CServerHandler()
: networkHandler(INetworkHandler::createHandler())
, lobbyClient(std::make_unique<GlobalLobbyClient>())
, gameChat(std::make_unique<GameChatHandler>())
, applier(std::make_unique<CApplier<CBaseForLobbyApply>>())
, threadNetwork(&CServerHandler::threadRunNetwork, this)
, state(EClientState::NONE)
@ -168,6 +170,15 @@ void CServerHandler::resetStateForLobby(EStartMode mode, ESelectionScreen screen
localPlayerNames = playerNames;
else
localPlayerNames.push_back(settings["general"]["playerName"].String());
gameChat->resetMatchState();
if (lobbyClient)
lobbyClient->resetMatchState();
}
GameChatHandler & CServerHandler::getGameChat()
{
return *gameChat;
}
GlobalLobbyClient & CServerHandler::getGlobalLobby()
@ -532,10 +543,7 @@ void CServerHandler::sendMessage(const std::string & txt) const
}
else
{
LobbyChatMessage lcm;
lcm.message = txt;
lcm.playerName = playerNames.find(myFirstId())->second.name;
sendLobbyPack(lcm);
gameChat->sendMessageLobby(playerNames.find(myFirstId())->second.name, txt);
}
}

View File

@ -36,6 +36,7 @@ VCMI_LIB_NAMESPACE_END
class CClient;
class CBaseForLobbyApply;
class GlobalLobbyClient;
class GameChatHandler;
class IServerRunner;
class HighScoreCalculation;
@ -100,6 +101,7 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
std::unique_ptr<INetworkHandler> networkHandler;
std::shared_ptr<INetworkConnection> networkConnection;
std::unique_ptr<GlobalLobbyClient> lobbyClient;
std::unique_ptr<GameChatHandler> gameChat;
std::unique_ptr<CApplier<CBaseForLobbyApply>> applier;
std::unique_ptr<IServerRunner> serverRunner;
std::shared_ptr<CMapInfo> mapToStart;
@ -113,8 +115,6 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
void threadRunNetwork();
void waitForServerShutdown();
void sendLobbyPack(const CPackForLobby & pack) const override;
void onPacketReceived(const NetworkConnectionPtr &, const std::vector<std::byte> & message) override;
void onConnectionFailed(const std::string & errorMessage) override;
void onConnectionEstablished(const NetworkConnectionPtr &) override;
@ -153,6 +153,7 @@ public:
void startLocalServerAndConnect(bool connectToLobby);
void connectToServer(const std::string & addr, const ui16 port);
GameChatHandler & getGameChat();
GlobalLobbyClient & getGlobalLobby();
INetworkHandler & getNetworkHandler();
@ -179,6 +180,8 @@ public:
ui16 getRemotePort() const;
// Lobby server API for UI
void sendLobbyPack(const CPackForLobby & pack) const override;
void sendClientConnecting() const override;
void sendClientDisconnecting() override;
void setCampaignState(std::shared_ptr<CampaignState> newCampaign) override;

View File

@ -468,7 +468,7 @@ void ClientCommandManager::printCommandMessage(const std::string &commandMessage
boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
if(LOCPLINT && LOCPLINT->cingconsole)
{
LOCPLINT->cingconsole->print(commandMessage);
LOCPLINT->cingconsole->addMessage("", "System", commandMessage);
}
}
}

View File

@ -0,0 +1,96 @@
/*
* GameChatHandler.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "GameChatHandler.h"
#include "CServerHandler.h"
#include "CPlayerInterface.h"
#include "PlayerLocalState.h"
#include "lobby/CLobbyScreen.h"
#include "adventureMap/CInGameConsole.h"
#include "../CCallback.h"
#include "../lib/networkPacks/PacksForLobby.h"
#include "../lib/TextOperations.h"
#include "../lib/mapObjects/CArmedInstance.h"
#include "../lib/CConfigHandler.h"
#include "../lib/MetaString.h"
static std::string getCurrentTimeFormatted(int timeOffsetSeconds = 0)
{
// FIXME: better/unified way to format date
auto timeNowChrono = std::chrono::system_clock::now();
timeNowChrono += std::chrono::seconds(timeOffsetSeconds);
return TextOperations::getFormattedTimeLocal(std::chrono::system_clock::to_time_t(timeNowChrono));
}
const std::vector<GameChatMessage> GameChatHandler::getChatHistory()
{
return chatHistory;
}
void GameChatHandler::resetMatchState()
{
chatHistory.clear();
}
void GameChatHandler::sendMessageGameplay(const std::string & messageText)
{
LOCPLINT->cb->sendMessage(messageText, LOCPLINT->localState->getCurrentArmy());
}
void GameChatHandler::sendMessageLobby(const std::string & senderName, const std::string & messageText)
{
LobbyChatMessage lcm;
lcm.message = messageText;
lcm.playerName = senderName;
CSH->sendLobbyPack(lcm);
}
void GameChatHandler::onNewLobbyMessageReceived(const std::string & senderName, const std::string & messageText)
{
auto lobby = dynamic_cast<CLobbyScreen*>(SEL);
if(lobby && lobby->card)
{
MetaString formatted = MetaString::createFromRawString("[%s] %s: %s");
formatted.replaceRawString(getCurrentTimeFormatted());
formatted.replaceRawString(senderName);
formatted.replaceRawString(messageText);
lobby->card->chat->addNewMessage(formatted.toString());
if (!lobby->card->showChat)
lobby->toggleChat();
}
chatHistory.push_back({senderName, messageText, getCurrentTimeFormatted()});
}
void GameChatHandler::onNewGameMessageReceived(PlayerColor sender, const std::string & messageText)
{
std::string timeFormatted = getCurrentTimeFormatted();
std::string playerName = sender.isSpectator() ? "Spectator" : sender.toString(); //FIXME: should actually be player nickname, at least in MP
chatHistory.push_back({playerName, messageText, timeFormatted});
LOCPLINT->cingconsole->addMessage(timeFormatted, playerName, messageText);
}
void GameChatHandler::onNewSystemMessageReceived(const std::string & messageText)
{
chatHistory.push_back({"System", messageText, getCurrentTimeFormatted()});
if(LOCPLINT && !settings["session"]["hideSystemMessages"].Bool())
LOCPLINT->cingconsole->addMessage(getCurrentTimeFormatted(), "System", messageText);
}

47
client/GameChatHandler.h Normal file
View File

@ -0,0 +1,47 @@
/*
* GameChatHandler.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../lib/constants/EntityIdentifiers.h"
struct GameChatMessage
{
std::string senderName;
std::string messageText;
std::string dateFormatted;
};
/// Class that manages game chat for current game match
/// Used for all matches - singleplayer, local multiplayer, online multiplayer
class GameChatHandler : boost::noncopyable
{
std::vector<GameChatMessage> chatHistory;
public:
/// Returns all message history for current match
const std::vector<GameChatMessage> getChatHistory();
/// Erases any local state, must be called when client disconnects from match server
void resetMatchState();
/// Must be called when local player sends new message into chat from gameplay mode (adventure map)
void sendMessageGameplay(const std::string & messageText);
/// Must be called when local player sends new message into chat from pregame mode (match lobby)
void sendMessageLobby(const std::string & senderName, const std::string & messageText);
/// Must be called when client receives new chat message from server
void onNewLobbyMessageReceived(const std::string & senderName, const std::string & messageText);
/// Must be called when client receives new chat message from server
void onNewGameMessageReceived(PlayerColor sender, const std::string & messageText);
/// Must be called when client receives new message from "system" sender
void onNewSystemMessageReceived(const std::string & messageText);
};

View File

@ -22,6 +22,7 @@
#include "gui/WindowHandler.h"
#include "widgets/MiscWidgets.h"
#include "CMT.h"
#include "GameChatHandler.h"
#include "CServerHandler.h"
#include "../CCallback.h"
@ -881,12 +882,10 @@ void ApplyClientNetPackVisitor::visitPackageApplied(PackageApplied & pack)
void ApplyClientNetPackVisitor::visitSystemMessage(SystemMessage & pack)
{
std::ostringstream str;
str << "System message: " << pack.text;
// usually used to receive error messages from server
logNetwork->error("System message: %s", pack.text);
logNetwork->error(str.str()); // usually used to receive error messages from server
if(LOCPLINT && !settings["session"]["hideSystemMessages"].Bool())
LOCPLINT->cingconsole->print(str.str());
CSH->getGameChat().onNewSystemMessageReceived(pack.text);
}
void ApplyClientNetPackVisitor::visitPlayerBlocked(PlayerBlocked & pack)
@ -918,13 +917,7 @@ void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & p
{
logNetwork->debug("pack.player %s sends a message: %s", pack.player.toString(), pack.text);
std::ostringstream str;
if(pack.player.isSpectator())
str << "Spectator: " << pack.text;
else
str << cl.getPlayerState(pack.player)->nodeName() <<": " << pack.text;
if(LOCPLINT)
LOCPLINT->cingconsole->print(str.str());
CSH->getGameChat().onNewGameMessageReceived(pack.player, pack.text);
}
void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack)

View File

@ -24,6 +24,7 @@
#include "globalLobby/GlobalLobbyClient.h"
#include "CServerHandler.h"
#include "GameChatHandler.h"
#include "CGameInfo.h"
#include "Client.h"
#include "gui/CGuiHandler.h"
@ -98,13 +99,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyClientDisconnected(LobbyClientD
void ApplyOnLobbyScreenNetPackVisitor::visitLobbyChatMessage(LobbyChatMessage & pack)
{
if(lobby && lobby->card)
{
lobby->card->chat->addNewMessage(pack.playerName + ": " + pack.message);
lobby->card->setChat(true);
if(lobby->buttonChat)
lobby->buttonChat->setTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL, Colors::WHITE);
}
handler.getGameChat().onNewLobbyMessageReceived(pack.playerName, pack.message);
}
void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack)

View File

@ -14,7 +14,8 @@
#include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CPlayerInterface.h"
#include "../PlayerLocalState.h"
#include "../CServerHandler.h"
#include "../GameChatHandler.h"
#include "../ClientCommandManager.h"
#include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h"
@ -31,6 +32,7 @@
#include "../../lib/CThreadHelper.h"
#include "../../lib/TextOperations.h"
#include "../../lib/mapObjects/CArmedInstance.h"
#include "../../lib/MetaString.h"
CInGameConsole::CInGameConsole()
: CIntObject(KEYBOARD | TIME | TEXTINPUT)
@ -51,7 +53,6 @@ void CInGameConsole::show(Canvas & to)
int number = 0;
boost::unique_lock<boost::mutex> lock(texts_mx);
for(auto & text : texts)
{
Point leftBottomCorner(0, pos.h);
@ -64,46 +65,53 @@ void CInGameConsole::show(Canvas & to)
void CInGameConsole::tick(uint32_t msPassed)
{
// Check whether text input is active - we want to keep recent messages visible during this period
// FIXME: better check?
if(enteredText != "")
return;
size_t sizeBefore = texts.size();
for(auto & text : texts)
text.timeOnScreen += msPassed;
vstd::erase_if(
texts,
[&](const auto & value)
{
boost::unique_lock<boost::mutex> lock(texts_mx);
for(auto & text : texts)
text.timeOnScreen += msPassed;
vstd::erase_if(
texts,
[&](const auto & value)
{
return value.timeOnScreen > defaultTimeout;
}
);
return value.timeOnScreen > defaultTimeout;
}
);
if(sizeBefore != texts.size())
GH.windows().totalRedraw(); // FIXME: ingame console has no parent widget set
}
void CInGameConsole::print(const std::string & txt)
void CInGameConsole::addMessageSilent(const std::string & timeFormatted, const std::string & senderName, const std::string & messageText)
{
// boost::unique_lock scope
{
boost::unique_lock<boost::mutex> lock(texts_mx);
MetaString formatted = MetaString::createFromRawString("[%s] %s: %s");
formatted.replaceRawString(timeFormatted);
formatted.replaceRawString(senderName);
formatted.replaceRawString(messageText);
// Maximum width for a text line is limited by:
// 1) width of adventure map terrain area, for when in-game console is on top of advmap
// 2) width of castle/battle window (fixed to 800) when this window is open
// 3) arbitrary selected left and right margins
int maxWidth = std::min( 800, adventureInt->terrainAreaPixels().w) - 100;
// Maximum width for a text line is limited by:
// 1) width of adventure map terrain area, for when in-game console is on top of advmap
// 2) width of castle/battle window (fixed to 800) when this window is open
// 3) arbitrary selected left and right margins
int maxWidth = std::min( 800, adventureInt->terrainAreaPixels().w) - 100;
auto splitText = CMessage::breakText(txt, maxWidth, FONT_MEDIUM);
auto splitText = CMessage::breakText(formatted.toString(), maxWidth, FONT_MEDIUM);
for(const auto & entry : splitText)
texts.push_back({entry, 0});
for(const auto & entry : splitText)
texts.push_back({entry, 0});
while(texts.size() > maxDisplayedTexts)
texts.erase(texts.begin());
}
while(texts.size() > maxDisplayedTexts)
texts.erase(texts.begin());
}
void CInGameConsole::addMessage(const std::string & timeFormatted, const std::string & senderName, const std::string & messageText)
{
addMessageSilent(timeFormatted, senderName, messageText);
GH.windows().totalRedraw(); // FIXME: ingame console has no parent widget set
@ -238,6 +246,21 @@ void CInGameConsole::textEdited(const std::string & inputtedText)
//do nothing here
}
void CInGameConsole::showRecentChatHistory()
{
auto const & history = CSH->getGameChat().getChatHistory();
texts.clear();
int entriesToShow = std::min<int>(maxDisplayedTexts, history.size());
int firstEntryToShow = history.size() - entriesToShow;
for (int i = firstEntryToShow; i < history.size(); ++i)
addMessageSilent(history[i].dateFormatted, history[i].senderName, history[i].messageText);
GH.windows().totalRedraw();
}
void CInGameConsole::startEnteringText()
{
if (!isActive())
@ -254,6 +277,8 @@ void CInGameConsole::startEnteringText()
GH.statusbar()->setEnteringMode(true);
GH.statusbar()->setEnteredText(enteredText);
showRecentChatHistory();
}
void CInGameConsole::endEnteringText(bool processEnteredText)
@ -278,7 +303,7 @@ void CInGameConsole::endEnteringText(bool processEnteredText)
clientCommandThread.detach();
}
else
LOCPLINT->cb->sendMessage(txt, LOCPLINT->localState->getCurrentArmy());
CSH->getGameChat().sendMessageGameplay(txt);
}
enteredText.clear();

View File

@ -23,9 +23,6 @@ private:
/// Currently visible texts in the overlay
std::vector<TextState> texts;
/// protects texts
boost::mutex texts_mx;
/// previously entered texts, for up/down arrows to work
std::vector<std::string> previouslyEntered;
@ -41,8 +38,10 @@ private:
std::weak_ptr<IStatusBar> currentStatusBar;
std::string enteredText;
void showRecentChatHistory();
void addMessageSilent(const std::string & timeFormatted, const std::string & senderName, const std::string & messageText);
public:
void print(const std::string & txt);
void addMessage(const std::string & timeFormatted, const std::string & senderName, const std::string & messageText);
void tick(uint32_t msPassed) override;
void show(Canvas & to) override;

View File

@ -229,8 +229,7 @@ void GlobalLobbyClient::receiveInviteReceived(const JsonNode & json)
void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json)
{
Settings configRoom = settings.write["lobby"]["roomID"];
configRoom->String() = json["gameRoomID"].String();
currentGameRoomUUID = json["gameRoomID"].String();
if (json["proxyMode"].Bool())
{
@ -426,8 +425,13 @@ void GlobalLobbyClient::sendProxyConnectionLogin(const NetworkConnectionPtr & ne
toSend["type"].String() = "clientProxyLogin";
toSend["accountID"] = settings["lobby"]["accountID"];
toSend["accountCookie"] = settings["lobby"]["accountCookie"];
toSend["gameRoomID"] = settings["lobby"]["roomID"];
toSend["gameRoomID"] = settings["session"]["lobby"]["roomID"];
assert(JsonUtils::validate(toSend, "vcmi:lobbyProtocol/" + toSend["type"].String(), toSend["type"].String() + " pack"));
netConnection->sendPacket(toSend.toBytes());
}
void GlobalLobbyClient::resetMatchState()
{
currentGameRoomUUID.clear();
}

View File

@ -32,6 +32,7 @@ class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyabl
std::map<std::string, std::vector<GlobalLobbyChannelMessage>> chatHistory;
std::shared_ptr<INetworkConnection> networkConnection;
std::string currentGameRoomUUID;
std::weak_ptr<GlobalLobbyLoginWindow> loginWindow;
std::weak_ptr<GlobalLobbyWindow> lobbyWindow;
@ -67,12 +68,14 @@ public:
/// Activate interface and pushes lobby UI as top window
void activateInterface();
void sendMessage(const JsonNode & data);
void sendClientRegister(const std::string & accountName);
void sendClientLogin();
void sendOpenRoom(const std::string & mode, int playerLimit);
void sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection);
void resetMatchState();
void connect();
bool isConnected() const;

View File

@ -570,7 +570,7 @@
"type" : "object",
"additionalProperties" : false,
"default" : {},
"required" : [ "mapPreview", "accountID", "accountCookie", "displayName", "hostname", "port", "roomPlayerLimit", "roomType", "roomMode", "roomID" ],
"required" : [ "mapPreview", "accountID", "accountCookie", "displayName", "hostname", "port", "roomPlayerLimit", "roomType", "roomMode" ],
"properties" : {
"mapPreview" : {
"type" : "boolean",
@ -607,10 +607,6 @@
"roomMode" : {
"type" : "number",
"default" : 0
},
"roomID" : {
"type" : "string",
"default" : ""
}
}
},

View File

@ -37,7 +37,10 @@ bool LobbyServer::isAccountNameValid(const std::string & accountName) const
std::string LobbyServer::sanitizeChatMessage(const std::string & inputString) const
{
// TODO: sanitize message and remove any "weird" symbols from it
// TODO: sanitize message and remove any "weird" symbols from it:
// - control characters ('\0' ... ' ')
// - '{' and '}' symbols to avoid formatting
// - other non-printable characters?
return inputString;
}
@ -472,10 +475,12 @@ void LobbyServer::receiveSendChatMessage(const NetworkConnectionPtr & connection
database->insertChatMessage(senderAccountID, channelType, roomID, messageText);
for(const auto & otherConnection : activeAccounts)
sendChatMessage(connection, channelType, receiverAccountID, senderAccountID, displayName, messageText);
if (senderAccountID != receiverAccountID)
{
if (otherConnection.second == receiverAccountID)
sendChatMessage(otherConnection.first, channelType, senderAccountID, senderAccountID, displayName, messageText);
for(const auto & otherConnection : activeAccounts)
if (otherConnection.second == receiverAccountID)
sendChatMessage(otherConnection.first, channelType, senderAccountID, senderAccountID, displayName, messageText);
}
}
}