Przeglądaj źródła

Partially unified handling of pregame and in-game chats

Ivan Savenko 1 rok temu
rodzic
commit
4ed961fb96

+ 2 - 0
client/CMakeLists.txt

@@ -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

+ 12 - 4
client/CServerHandler.cpp

@@ -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);
 	}
 }
 

+ 5 - 2
client/CServerHandler.h

@@ -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;

+ 1 - 1
client/ClientCommandManager.cpp

@@ -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);
 		}
 	}
 }

+ 96 - 0
client/GameChatHandler.cpp

@@ -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 - 0
client/GameChatHandler.h

@@ -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);
+};

+ 5 - 12
client/NetPacksClient.cpp

@@ -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)

+ 2 - 7
client/NetPacksLobbyClient.cpp

@@ -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)

+ 54 - 29
client/adventureMap/CInGameConsole.cpp

@@ -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();
-	{
-		boost::unique_lock<boost::mutex> lock(texts_mx);
 
-		for(auto & text : texts)
-			text.timeOnScreen += msPassed;
+	for(auto & text : texts)
+		text.timeOnScreen += msPassed;
 
-		vstd::erase_if(
-			texts,
-			[&](const auto & value)
-			{
-				return value.timeOnScreen > defaultTimeout;
-			}
-		);
+	vstd::erase_if(
+				texts,
+				[&](const auto & value)
+	{
+		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();
 

+ 3 - 4
client/adventureMap/CInGameConsole.h

@@ -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;

+ 7 - 3
client/globalLobby/GlobalLobbyClient.cpp

@@ -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();
+}

+ 3 - 0
client/globalLobby/GlobalLobbyClient.h

@@ -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;

+ 1 - 5
config/schemas/settings.json

@@ -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" : ""
 				}
 			}
 		},

+ 9 - 4
lobby/LobbyServer.cpp

@@ -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);
 		}
 	}
 }