ソースを参照

Merge remote-tracking branch 'origin/develop' into dimension-door-changes

Dydzio 1 年間 前
コミット
fe42fab2d6
100 ファイル変更3045 行追加769 行削除
  1. BIN
      Mods/vcmi/Data/lobby/iconEnter.png
  2. BIN
      Mods/vcmi/Data/lobby/iconFolder.png
  3. BIN
      Mods/vcmi/Data/lobby/iconPlayer.png
  4. BIN
      Mods/vcmi/Data/lobby/iconSend.png
  5. BIN
      Mods/vcmi/Data/lobby/selectionTabSortDate.png
  6. BIN
      Mods/vcmi/Data/lobby/townBorderBig.png
  7. BIN
      Mods/vcmi/Data/lobby/townBorderBigActivated.png
  8. BIN
      Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png
  9. BIN
      Mods/vcmi/Data/lobby/townBorderSmallActivated.png
  10. 22 2
      Mods/vcmi/config/vcmi/chinese.json
  11. 19 5
      Mods/vcmi/config/vcmi/english.json
  12. 32 1
      Mods/vcmi/config/vcmi/ukrainian.json
  13. 4 0
      client/CMakeLists.txt
  14. 11 4
      client/CServerHandler.cpp
  15. 5 2
      client/CServerHandler.h
  16. 1 1
      client/ClientCommandManager.cpp
  17. 107 0
      client/GameChatHandler.cpp
  18. 47 0
      client/GameChatHandler.h
  19. 18 12
      client/NetPacksClient.cpp
  20. 5 9
      client/NetPacksLobbyClient.cpp
  21. 61 33
      client/adventureMap/CInGameConsole.cpp
  22. 6 4
      client/adventureMap/CInGameConsole.h
  23. 6 0
      client/eventsSDL/InputSourceTouch.cpp
  24. 191 47
      client/globalLobby/GlobalLobbyClient.cpp
  25. 20 3
      client/globalLobby/GlobalLobbyClient.h
  26. 13 3
      client/globalLobby/GlobalLobbyDefines.h
  27. 78 0
      client/globalLobby/GlobalLobbyInviteWindow.cpp
  28. 46 0
      client/globalLobby/GlobalLobbyInviteWindow.h
  29. 2 2
      client/globalLobby/GlobalLobbyServerSetup.cpp
  30. 183 46
      client/globalLobby/GlobalLobbyWidget.cpp
  31. 51 9
      client/globalLobby/GlobalLobbyWidget.h
  32. 93 2
      client/globalLobby/GlobalLobbyWindow.cpp
  33. 17 2
      client/globalLobby/GlobalLobbyWindow.h
  34. 2 1
      client/gui/InterfaceObjectConfigurable.cpp
  35. 14 1
      client/gui/TextAlignment.h
  36. 5 0
      client/lobby/CLobbyScreen.cpp
  37. 33 26
      client/lobby/CSelectionBase.cpp
  38. 3 2
      client/lobby/CSelectionBase.h
  39. 7 1
      client/widgets/ObjectLists.cpp
  40. 15 18
      client/widgets/TextControls.cpp
  41. 8 1
      config/schemas/lobbyProtocol/activateGameRoom.json
  42. 36 5
      config/schemas/lobbyProtocol/activeGameRooms.json
  43. 21 0
      config/schemas/lobbyProtocol/changeRoomDescription.json
  44. 14 1
      config/schemas/lobbyProtocol/chatHistory.json
  45. 6 7
      config/schemas/lobbyProtocol/chatMessage.json
  46. 2 2
      config/schemas/lobbyProtocol/clientLoginSuccess.json
  47. 1 1
      config/schemas/lobbyProtocol/clientProxyLogin.json
  48. 0 21
      config/schemas/lobbyProtocol/declineInvite.json
  49. 17 0
      config/schemas/lobbyProtocol/gameStarted.json
  50. 90 0
      config/schemas/lobbyProtocol/matchesHistory.json
  51. 28 0
      config/schemas/lobbyProtocol/requestChatHistory.json
  52. 13 2
      config/schemas/lobbyProtocol/sendChatMessage.json
  53. 21 0
      config/schemas/lobbyProtocol/serverLoginSuccess.json
  54. 1 5
      config/schemas/settings.json
  55. 118 0
      config/widgets/buttons/lobbyCreateRoom.json
  56. 118 0
      config/widgets/buttons/lobbyHideWindow.json
  57. 118 0
      config/widgets/buttons/lobbyJoinRoom.json
  58. 118 0
      config/widgets/buttons/lobbySendMessage.json
  59. 118 0
      config/widgets/buttons/pregameInvitePlayers.json
  60. 118 0
      config/widgets/buttons/pregameReturnToLobby.json
  61. 59 47
      config/widgets/lobbyWindow.json
  62. 2 2
      docs/developers/Serialization.md
  63. 2 5
      lib/Languages.h
  64. 12 0
      lib/TextOperations.cpp
  65. 8 0
      lib/TextOperations.h
  66. 1 1
      lib/bonuses/Bonus.h
  67. 1 3
      lib/bonuses/BonusList.cpp
  68. 8 0
      lib/constants/EntityIdentifiers.cpp
  69. 1 0
      lib/mapObjects/CGCreature.cpp
  70. 1 1
      lib/minizip/minizip.c
  71. 7 0
      lib/minizip/mztools.c
  72. 2 0
      lib/modding/IdentifierStorage.cpp
  73. 1 1
      lib/rmg/CMapGenerator.cpp
  74. 7 7
      lib/rmg/CZonePlacer.cpp
  75. 2 1
      lib/rmg/Functions.cpp
  76. 38 23
      lib/rmg/Zone.cpp
  77. 46 6
      lib/rmg/Zone.h
  78. 57 31
      lib/rmg/modificators/ConnectionsPlacer.cpp
  79. 1 0
      lib/rmg/modificators/ConnectionsPlacer.h
  80. 8 6
      lib/rmg/modificators/Modificator.cpp
  81. 75 39
      lib/rmg/modificators/ObjectManager.cpp
  82. 16 13
      lib/rmg/modificators/ObstaclePlacer.cpp
  83. 14 9
      lib/rmg/modificators/RiverPlacer.cpp
  84. 2 2
      lib/rmg/modificators/RoadPlacer.cpp
  85. 1 1
      lib/rmg/modificators/RockFiller.cpp
  86. 14 11
      lib/rmg/modificators/RockPlacer.cpp
  87. 1 1
      lib/rmg/modificators/TerrainPainter.cpp
  88. 3 3
      lib/rmg/modificators/TownPlacer.cpp
  89. 33 35
      lib/rmg/modificators/TreasurePlacer.cpp
  90. 7 7
      lib/rmg/modificators/WaterAdopter.cpp
  91. 25 19
      lib/rmg/modificators/WaterProxy.cpp
  92. 17 11
      lib/rmg/modificators/WaterRoutes.cpp
  93. 1 1
      lib/serializer/BinaryDeserializer.cpp
  94. 4 4
      lib/serializer/BinaryDeserializer.h
  95. 2 2
      lib/serializer/CLoadFile.cpp
  96. 9 1
      lobby/EntryPoint.cpp
  97. 190 119
      lobby/LobbyDatabase.cpp
  98. 11 2
      lobby/LobbyDatabase.h
  99. 28 26
      lobby/LobbyDefines.h
  100. 245 48
      lobby/LobbyServer.cpp

BIN
Mods/vcmi/Data/lobby/iconEnter.png


BIN
Mods/vcmi/Data/lobby/iconFolder.png


BIN
Mods/vcmi/Data/lobby/iconPlayer.png


BIN
Mods/vcmi/Data/lobby/iconSend.png


BIN
Mods/vcmi/Data/lobby/selectionTabSortDate.png


BIN
Mods/vcmi/Data/lobby/townBorderBig.png


BIN
Mods/vcmi/Data/lobby/townBorderBigActivated.png


BIN
Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png


BIN
Mods/vcmi/Data/lobby/townBorderSmallActivated.png


+ 22 - 2
Mods/vcmi/config/vcmi/chinese.json

@@ -63,7 +63,6 @@
 	"vcmi.mainMenu.serverClosing" : "关闭中...",
 	"vcmi.mainMenu.hostTCP" : "创建TCP/IP游戏",
 	"vcmi.mainMenu.joinTCP" : "加入TCP/IP游戏",
-	"vcmi.mainMenu.playerName" : "Player",
 
 	"vcmi.lobby.filepath" : "文件路径",
 	"vcmi.lobby.creationDate" : "创建时间",
@@ -73,12 +72,33 @@
 	"vcmi.lobby.noUnderground" : "无地下部分",
 	"vcmi.lobby.sortDate" : "以修改时间排序地图",
 
+	"vcmi.lobby.login.title" : "VCMI大厅",
+	"vcmi.lobby.login.username" : "用户名:",
+	"vcmi.lobby.login.connecting" : "连接中...",
+	"vcmi.lobby.login.error" : "连接错误: %s",
+	"vcmi.lobby.login.create" : "新账号",
+	"vcmi.lobby.login.login" : "登录",
+
+	"vcmi.lobby.room.create" : "创建房间",
+	"vcmi.lobby.room.players.limit" : "玩家限制",
+	"vcmi.lobby.room.public" : "公开",
+	"vcmi.lobby.room.private" : "私有",
+	"vcmi.lobby.room.description.public" : "任何玩家都可以加入公开房间。",
+	"vcmi.lobby.room.description.private" : "只有被邀请的玩家能加入私有房间。",
+	"vcmi.lobby.room.description.new" : "选择一个新场景或设置一个随机地图开始游戏。",
+	"vcmi.lobby.room.description.load" : "使用你的一个存档开始游戏。",
+	"vcmi.lobby.room.description.limit" : "最多%d个玩家能加入你的房间,包括你在内。",
+	"vcmi.lobby.room.new" : "新建游戏",
+	"vcmi.lobby.room.load" : "加载游戏",
+	"vcmi.lobby.room.type" : "房间类型",
+	"vcmi.lobby.room.mode" : "游戏模式",
+
 	"vcmi.client.errors.invalidMap" : "{非法地图或战役}\n\n启动游戏失败,选择的地图或者战役,无效或被污染。原因:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{找不到数据文件}\n\n没有找到战役数据文件!你可能使用了不完整或损坏的英雄无敌3数据文件,请重新安装数据文件。",
+	"vcmi.server.errors.disconnected" : "{网络错误}\n\n与游戏服务器的连接已断开!",
 	"vcmi.server.errors.existingProcess"     : "一个VCMI进程已经在运行,启动新进程前请结束它。",
 	"vcmi.server.errors.modsToEnable"    : "{需要启用的mod列表}",
 	"vcmi.server.errors.modsToDisable"   : "{需要禁用的mod列表}",
-	"vcmi.server.confirmReconnect"           : "您想要重连上一个会话么?",
 	"vcmi.server.errors.modNoDependency" : "读取mod包 {'%s'}失败!\n 需要的mod {'%s'} 没有安装或无效!\n",
 	"vcmi.server.errors.modConflict" : "读取的mod包 {'%s'}无法运行!\n 与另一个mod {'%s'}冲突!\n",
 	"vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!",

+ 19 - 5
Mods/vcmi/config/vcmi/english.json

@@ -71,27 +71,41 @@
 	"vcmi.lobby.noPreview" : "no preview",
 	"vcmi.lobby.noUnderground" : "no underground",
 	"vcmi.lobby.sortDate" : "Sorts maps by change date",
+	"vcmi.lobby.backToLobby" : "Return to lobby",
 	
-	"vcmi.lobby.login.title" : "VCMI Lobby",
+	"vcmi.lobby.login.title" : "VCMI Online Lobby",
 	"vcmi.lobby.login.username" : "Username:",
 	"vcmi.lobby.login.connecting" : "Connecting...",
 	"vcmi.lobby.login.error" : "Connection error: %s",
 	"vcmi.lobby.login.create" : "New Account",
 	"vcmi.lobby.login.login" : "Login",
-
-	"vcmi.lobby.room.create" : "Create Room",
+	"vcmi.lobby.header.rooms" : "Game Rooms - %d",
+	"vcmi.lobby.header.channels" : "Chat Channels",
+	"vcmi.lobby.header.chat.global" : "Global Game Chat - %s", // %s -> language name
+	"vcmi.lobby.header.chat.match" : "Chat from previous game on %s", // %s -> game start date & time
+	"vcmi.lobby.header.chat.player" : "Private chat with %s", // %s -> nickname of another player
+	"vcmi.lobby.header.history" : "Your Previous Games",
+	"vcmi.lobby.header.players" : "Players Online - %d",
+	"vcmi.lobby.match.solo" : "Singleplayer Game",
+	"vcmi.lobby.match.duel" : "Game with %s", // %s -> nickname of another player
+	"vcmi.lobby.match.multi" : "%d players",
+	"vcmi.lobby.room.create" : "Create New Room",
 	"vcmi.lobby.room.players.limit" : "Players Limit",
-	"vcmi.lobby.room.public" : "Public",
-	"vcmi.lobby.room.private" : "Private",
 	"vcmi.lobby.room.description.public" : "Any player can join public room.",
 	"vcmi.lobby.room.description.private" : "Only invited players can join private room.",
 	"vcmi.lobby.room.description.new" : "To start the game, select a scenario or set up a random map.",
 	"vcmi.lobby.room.description.load" : "To start the game, use one of your saved games.",
 	"vcmi.lobby.room.description.limit" : "Up to %d players can enter your room, including you.",
+	"vcmi.lobby.invite.header" : "Invite Players",
+	"vcmi.lobby.invite.notification" : "Player has invited you to their game room. You can now join their private room.",
 	"vcmi.lobby.room.new" : "New Game",
 	"vcmi.lobby.room.load" : "Load Game",
 	"vcmi.lobby.room.type" : "Room Type",
 	"vcmi.lobby.room.mode" : "Game Mode",
+	"vcmi.lobby.room.state.public" : "Public",
+	"vcmi.lobby.room.state.private" : "Private",
+	"vcmi.lobby.room.state.busy" : "In Game",
+	"vcmi.lobby.room.state.invited" : "Invited",
 
 	"vcmi.client.errors.invalidMap" : "{Invalid map or campaign}\n\nFailed to start game! Selected map or campaign might be invalid or corrupted. Reason:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.",

+ 32 - 1
Mods/vcmi/config/vcmi/ukrainian.json

@@ -66,12 +66,43 @@
 	
 	"vcmi.lobby.filepath" : "Назва файлу",
 	"vcmi.lobby.creationDate" : "Дата створення",
-	"vcmi.lobby.scenarioName" : "Scenario name",
+	"vcmi.lobby.scenarioName" : "Назва сценарію",
 	"vcmi.lobby.mapPreview" : "Огляд мапи",
 	"vcmi.lobby.noPreview" : "огляд недоступний",
 	"vcmi.lobby.noUnderground" : "немає підземелля",
 	"vcmi.lobby.sortDate" : "Сортувати мапи за датою зміни",
 
+	"vcmi.lobby.login.title" : "Онлайн лобі VCMI",
+	"vcmi.lobby.login.username" : "Логін:",
+	"vcmi.lobby.login.connecting" : "Підключення...",
+	"vcmi.lobby.login.error" : "Помилка з'єднання: %s",
+	"vcmi.lobby.login.create" : "Створити акаунт",
+	"vcmi.lobby.login.login" : "Увійти",
+	"vcmi.lobby.header.rooms" : "Активні кімнати - %d",
+	"vcmi.lobby.header.channels" : "Канали чату",
+	"vcmi.lobby.header.chat.global" : "Глобальний ігровий чат - %s", // %s -> language name
+	"vcmi.lobby.header.chat.match" : "Чат минулої гри від %s", // %s -> game start date & time
+	"vcmi.lobby.header.chat.player" : "Приватний чат з %s", // %s -> nickname of another player
+	"vcmi.lobby.header.history" : "Ваші попередні ігри",
+	"vcmi.lobby.header.players" : "Гравці в мережі - %d",
+	"vcmi.lobby.match.solo" : "Одиночна гра",
+	"vcmi.lobby.match.duel" : "Гра з %s", // %s -> nickname of another player
+	"vcmi.lobby.match.multi" : "%d гравців",
+	"vcmi.lobby.room.create" : "Створити нову кімнату",
+	"vcmi.lobby.room.players.limit" : "Максимум гравців",
+	"vcmi.lobby.room.description.public" : "Будь-хто з гравців може приєднатися до публічної кімнати.",
+	"vcmi.lobby.room.description.private" : "Тільки запрошені гравці можуть приєднатися до приватної кімнати.",
+	"vcmi.lobby.room.description.new" : "Щоб почати гру, виберіть сценарій або налаштуйте випадкову карту.",
+	"vcmi.lobby.room.description.load" : "Щоб почати гру, виберіть одну з ваших збережених ігор.",
+	"vcmi.lobby.room.description.limit" : "До %d гравців можуть зайти у вашу кімнату, включаючи вас.",
+	"vcmi.lobby.room.new" : "Нова гра",
+	"vcmi.lobby.room.load" : "Завантажити гру",
+	"vcmi.lobby.room.type" : "Тип кімнати",
+	"vcmi.lobby.room.mode" : "Режим гри",
+	"vcmi.lobby.room.state.public" : "Публічна",
+	"vcmi.lobby.room.state.private" : "Приватна",
+	"vcmi.lobby.room.state.busy" : "У грі",
+
 	"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
 	"vcmi.server.errors.modsToEnable"    : "{Потрібні модифікації для завантаження гри}",

+ 4 - 0
client/CMakeLists.txt

@@ -96,6 +96,7 @@ set(client_SRCS
 	renderSDL/SDL_Extensions.cpp
 
 	globalLobby/GlobalLobbyClient.cpp
+	globalLobby/GlobalLobbyInviteWindow.cpp
 	globalLobby/GlobalLobbyLoginWindow.cpp
 	globalLobby/GlobalLobbyServerSetup.cpp
 	globalLobby/GlobalLobbyWidget.cpp
@@ -162,6 +163,7 @@ set(client_SRCS
 	CVideoHandler.cpp
 	Client.cpp
 	ClientCommandManager.cpp
+	GameChatHandler.cpp
 	HeroMovementController.cpp
 	NetPacksClient.cpp
 	NetPacksLobbyClient.cpp
@@ -280,6 +282,7 @@ set(client_HEADERS
 
 	globalLobby/GlobalLobbyClient.h
 	globalLobby/GlobalLobbyDefines.h
+	globalLobby/GlobalLobbyInviteWindow.h
 	globalLobby/GlobalLobbyLoginWindow.h
 	globalLobby/GlobalLobbyServerSetup.h
 	globalLobby/GlobalLobbyWidget.h
@@ -348,6 +351,7 @@ set(client_HEADERS
 	ClientCommandManager.h
 	ClientNetPackVisitors.h
 	HeroMovementController.h
+	GameChatHandler.h
 	LobbyClientNetPackVisitors.h
 	ServerRunner.h
 	resource.h

+ 11 - 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,14 @@ void CServerHandler::resetStateForLobby(EStartMode mode, ESelectionScreen screen
 		localPlayerNames = playerNames;
 	else
 		localPlayerNames.push_back(settings["general"]["playerName"].String());
+
+	gameChat->resetMatchState();
+	lobbyClient->resetMatchState();
+}
+
+GameChatHandler & CServerHandler::getGameChat()
+{
+	return *gameChat;
 }
 
 GlobalLobbyClient & CServerHandler::getGlobalLobby()
@@ -532,10 +542,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);
 		}
 	}
 }

+ 107 - 0
client/GameChatHandler.cpp

@@ -0,0 +1,107 @@
+/*
+ * 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 "globalLobby/GlobalLobbyClient.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"
+
+const std::vector<GameChatMessage> & GameChatHandler::getChatHistory() const
+{
+	return chatHistory;
+}
+
+void GameChatHandler::resetMatchState()
+{
+	chatHistory.clear();
+}
+
+void GameChatHandler::sendMessageGameplay(const std::string & messageText)
+{
+	LOCPLINT->cb->sendMessage(messageText, LOCPLINT->localState->getCurrentArmy());
+	CSH->getGlobalLobby().sendMatchChatMessage(messageText);
+}
+
+void GameChatHandler::sendMessageLobby(const std::string & senderName, const std::string & messageText)
+{
+	LobbyChatMessage lcm;
+	lcm.message = messageText;
+	lcm.playerName = senderName;
+	CSH->sendLobbyPack(lcm);
+	CSH->getGlobalLobby().sendMatchChatMessage(messageText);
+}
+
+void GameChatHandler::onNewLobbyMessageReceived(const std::string & senderName, const std::string & messageText)
+{
+	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)
+	{
+		MetaString formatted = MetaString::createFromRawString("[%s] %s: %s");
+		formatted.replaceRawString(TextOperations::getCurrentFormattedTimeLocal());
+		formatted.replaceRawString(senderName);
+		formatted.replaceRawString(messageText);
+
+		lobby->card->chat->addNewMessage(formatted.toString());
+		if (!lobby->card->showChat)
+				lobby->toggleChat();
+	}
+
+	chatHistory.push_back({senderName, messageText, TextOperations::getCurrentFormattedTimeLocal()});
+}
+
+void GameChatHandler::onNewGameMessageReceived(PlayerColor sender, const std::string & messageText)
+{
+
+	std::string timeFormatted = TextOperations::getCurrentFormattedTimeLocal();
+	std::string playerName = "<UNKNOWN>";
+
+	if (sender.isValidPlayer())
+		playerName = LOCPLINT->cb->getStartInfo()->playerInfos.at(sender).name;
+
+	if (sender.isSpectator())
+		playerName = "Spectator"; // FIXME: translate? Provide nickname somewhere?
+
+	chatHistory.push_back({playerName, messageText, timeFormatted});
+
+	LOCPLINT->cingconsole->addMessage(timeFormatted, playerName, messageText);
+}
+
+void GameChatHandler::onNewSystemMessageReceived(const std::string & messageText)
+{
+	chatHistory.push_back({"System", messageText, TextOperations::getCurrentFormattedTimeLocal()});
+
+	if(LOCPLINT && !settings["session"]["hideSystemMessages"].Bool())
+		LOCPLINT->cingconsole->addMessage(TextOperations::getCurrentFormattedTimeLocal(), "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() const;
+
+	/// 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);
+};

+ 18 - 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"
@@ -228,21 +229,31 @@ void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack)
 void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack)
 {
 	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
+	cl.invalidatePaths(); //it is possible to remove last non-native unit for current terrain and lose movement penalty
 }
 
 void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack)
 {
 	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
+
+	if(pack.srcArmy != pack.dstArmy)
+		cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains
 }
 
 void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack)
 {
 	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
+
+	if(gs.getHero(pack.army))
+		cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains
 }
 
 void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack)
 {
 	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
+
+	if(pack.srcArmy != pack.dstArmy)
+		cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains
 }
 
 void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack)
@@ -253,6 +264,9 @@ void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & p
 			? ObjectInstanceID()
 			: pack.moves[0].dstArmy;
 		dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy);
+
+		if(pack.moves[0].srcArmy != destArmy)
+			cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains
 	}
 }
 
@@ -881,12 +895,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 +930,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)

+ 5 - 9
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"
@@ -58,11 +59,12 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientCon
 				// announce opened game room
 				// TODO: find better approach?
 				int roomType = settings["lobby"]["roomType"].Integer();
+				int roomPlayerLimit = settings["lobby"]["roomPlayerLimit"].Integer();
 
 				if (roomType != 0)
-					handler.getGlobalLobby().sendOpenPrivateRoom();
+					handler.getGlobalLobby().sendOpenRoom("private", roomPlayerLimit);
 				else
-					handler.getGlobalLobby().sendOpenPublicRoom();
+					handler.getGlobalLobby().sendOpenRoom("public", roomPlayerLimit);
 			}
 
 			while (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
@@ -97,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)

+ 61 - 33
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,52 @@ 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
+	if(isEnteringText())
+		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
 
@@ -117,7 +124,7 @@ void CInGameConsole::print(const std::string & txt)
 
 bool CInGameConsole::captureThisKey(EShortcut key)
 {
-	if (enteredText.empty())
+	if (!isEnteringText())
 		return false;
 
 	switch (key)
@@ -140,7 +147,7 @@ void CInGameConsole::keyPressed (EShortcut key)
 	if (LOCPLINT->cingconsole != this)
 		return;
 
-	if(enteredText.empty() && key != EShortcut::GAME_ACTIVATE_CONSOLE)
+	if(!isEnteringText() && key != EShortcut::GAME_ACTIVATE_CONSOLE)
 		return; //because user is not entering any text
 
 	switch(key)
@@ -222,7 +229,7 @@ void CInGameConsole::textInputed(const std::string & inputtedText)
 	if (LOCPLINT->cingconsole != this)
 		return;
 
-	if(enteredText.empty())
+	if(!isEnteringText())
 		return;
 
 	enteredText.resize(enteredText.size()-1);
@@ -238,12 +245,27 @@ 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())
 		return;
 
-	if(enteredText != "")
+	if(isEnteringText())
 		return;
 		
 	assert(currentStatusBar.expired());//effectively, nullptr check
@@ -254,6 +276,8 @@ void CInGameConsole::startEnteringText()
 
 	GH.statusbar()->setEnteringMode(true);
 	GH.statusbar()->setEnteredText(enteredText);
+
+	showRecentChatHistory();
 }
 
 void CInGameConsole::endEnteringText(bool processEnteredText)
@@ -278,7 +302,7 @@ void CInGameConsole::endEnteringText(bool processEnteredText)
 			clientCommandThread.detach();
 		}
 		else
-			LOCPLINT->cb->sendMessage(txt, LOCPLINT->localState->getCurrentArmy());
+			CSH->getGameChat().sendMessageGameplay(txt);
 	}
 	enteredText.clear();
 
@@ -300,3 +324,7 @@ void CInGameConsole::refreshEnteredText()
 		statusbar->setEnteredText(enteredText);
 }
 
+bool CInGameConsole::isEnteringText() const
+{
+	return !enteredText.empty();
+}

+ 6 - 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,13 @@ private:
 	std::weak_ptr<IStatusBar> currentStatusBar;
 	std::string enteredText;
 
+	/// Returns true if console is active and player is currently entering text
+	bool isEnteringText() const;
+
+	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;

+ 6 - 0
client/eventsSDL/InputSourceTouch.cpp

@@ -21,6 +21,8 @@
 #include "../gui/EventDispatcher.h"
 #include "../gui/MouseButton.h"
 #include "../gui/WindowHandler.h"
+#include "../CServerHandler.h"
+#include "../globalLobby/GlobalLobbyClient.h"
 
 #if defined(VCMI_ANDROID)
 #include "../../lib/CAndroidVMHelper.h"
@@ -149,6 +151,10 @@ void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinge
 			break;
 		}
 		case TouchState::TAP_DOWN_DOUBLE:
+		{
+			CSH->getGlobalLobby().activateInterface();
+			break;
+		}
 		case TouchState::TAP_DOWN_LONG:
 		case TouchState::TAP_DOWN_LONG_AWAIT:
 		{

+ 191 - 47
client/globalLobby/GlobalLobbyClient.cpp

@@ -11,15 +11,17 @@
 #include "StdInc.h"
 #include "GlobalLobbyClient.h"
 
+#include "GlobalLobbyInviteWindow.h"
 #include "GlobalLobbyLoginWindow.h"
 #include "GlobalLobbyWindow.h"
 
+#include "../CGameInfo.h"
+#include "../CMusicHandler.h"
+#include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
-#include "../windows/InfoWindows.h"
-#include "../CServerHandler.h"
 #include "../mainmenu/CMainMenu.h"
-#include "../CGameInfo.h"
+#include "../windows/InfoWindows.h"
 
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/MetaString.h"
@@ -27,18 +29,15 @@
 #include "../../lib/TextOperations.h"
 #include "../../lib/CGeneralTextHandler.h"
 
-GlobalLobbyClient::GlobalLobbyClient() = default;
-GlobalLobbyClient::~GlobalLobbyClient() = default;
-
-static std::string getCurrentTimeFormatted(int timeOffsetSeconds = 0)
+GlobalLobbyClient::GlobalLobbyClient()
 {
-	// 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));
+	activeChannels.emplace_back("english");
+	if (CGI->generaltexth->getPreferredLanguage() != "english")
+		activeChannels.emplace_back(CGI->generaltexth->getPreferredLanguage());
 }
 
+GlobalLobbyClient::~GlobalLobbyClient() = default;
+
 void GlobalLobbyClient::onPacketReceived(const std::shared_ptr<INetworkConnection> &, const std::vector<std::byte> & message)
 {
 	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
@@ -51,8 +50,8 @@ void GlobalLobbyClient::onPacketReceived(const std::shared_ptr<INetworkConnectio
 	if(json["type"].String() == "operationFailed")
 		return receiveOperationFailed(json);
 
-	if(json["type"].String() == "loginSuccess")
-		return receiveLoginSuccess(json);
+	if(json["type"].String() == "clientLoginSuccess")
+		return receiveClientLoginSuccess(json);
 
 	if(json["type"].String() == "chatHistory")
 		return receiveChatHistory(json);
@@ -72,6 +71,9 @@ void GlobalLobbyClient::onPacketReceived(const std::shared_ptr<INetworkConnectio
 	if(json["type"].String() == "inviteReceived")
 		return receiveInviteReceived(json);
 
+	if(json["type"].String() == "matchesHistory")
+		return receiveMatchesHistory(json);
+
 	logGlobal->error("Received unexpected message from lobby server: %s", json["type"].String());
 }
 
@@ -106,7 +108,7 @@ void GlobalLobbyClient::receiveOperationFailed(const JsonNode & json)
 	// TODO: handle errors in lobby menu
 }
 
-void GlobalLobbyClient::receiveLoginSuccess(const JsonNode & json)
+void GlobalLobbyClient::receiveClientLoginSuccess(const JsonNode & json)
 {
 	{
 		Settings configCookie = settings.write["lobby"]["accountCookie"];
@@ -126,36 +128,59 @@ void GlobalLobbyClient::receiveLoginSuccess(const JsonNode & json)
 
 void GlobalLobbyClient::receiveChatHistory(const JsonNode & json)
 {
+	std::string channelType = json["channelType"].String();
+	std::string channelName = json["channelName"].String();
+	std::string channelKey = channelType + '_' + channelName;
+
+	// create empty entry, potentially replacing any pre-existing data
+	chatHistory[channelKey] = {};
+
+	auto lobbyWindowPtr = lobbyWindow.lock();
+
 	for(const auto & entry : json["messages"].Vector())
 	{
-		std::string accountID = entry["accountID"].String();
-		std::string displayName = entry["displayName"].String();
-		std::string messageText = entry["messageText"].String();
-		int ageSeconds = entry["ageSeconds"].Integer();
-		std::string timeFormatted = getCurrentTimeFormatted(-ageSeconds);
+		GlobalLobbyChannelMessage message;
+
+		message.accountID = entry["accountID"].String();
+		message.displayName = entry["displayName"].String();
+		message.messageText = entry["messageText"].String();
+		std::chrono::seconds ageSeconds (entry["ageSeconds"].Integer());
+		message.timeFormatted = TextOperations::getCurrentFormattedTimeLocal(-ageSeconds);
+
+		chatHistory[channelKey].push_back(message);
 
-		auto lobbyWindowPtr = lobbyWindow.lock();
 		if(lobbyWindowPtr)
-			lobbyWindowPtr->onGameChatMessage(displayName, messageText, timeFormatted);
+			lobbyWindowPtr->onGameChatMessage(message.displayName, message.messageText, message.timeFormatted, channelType, channelName);
 	}
 }
 
 void GlobalLobbyClient::receiveChatMessage(const JsonNode & json)
 {
-	std::string accountID = json["accountID"].String();
-	std::string displayName = json["displayName"].String();
-	std::string messageText = json["messageText"].String();
-	std::string timeFormatted = getCurrentTimeFormatted();
+	GlobalLobbyChannelMessage message;
+
+	message.accountID = json["accountID"].String();
+	message.displayName = json["displayName"].String();
+	message.messageText = json["messageText"].String();
+	message.timeFormatted = TextOperations::getCurrentFormattedTimeLocal();
+
+	std::string channelType = json["channelType"].String();
+	std::string channelName = json["channelName"].String();
+	std::string channelKey = channelType + '_' + channelName;
+
+	chatHistory[channelKey].push_back(message);
+
 	auto lobbyWindowPtr = lobbyWindow.lock();
 	if(lobbyWindowPtr)
-		lobbyWindowPtr->onGameChatMessage(displayName, messageText, timeFormatted);
+		lobbyWindowPtr->onGameChatMessage(message.displayName, message.messageText, message.timeFormatted, channelType, channelName);
+
+	CCS->soundh->playSound(AudioPath::builtin("CHAT"));
 }
 
 void GlobalLobbyClient::receiveActiveAccounts(const JsonNode & json)
 {
 	activeAccounts.clear();
 
-	for (auto const & jsonEntry : json["accounts"].Vector())
+	for(const auto & jsonEntry : json["accounts"].Vector())
 	{
 		GlobalLobbyAccount account;
 
@@ -175,7 +200,7 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
 {
 	activeRooms.clear();
 
-	for (auto const & jsonEntry : json["gameRooms"].Vector())
+	for(const auto & jsonEntry : json["gameRooms"].Vector())
 	{
 		GlobalLobbyRoom room;
 
@@ -183,8 +208,18 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
 		room.hostAccountID = jsonEntry["hostAccountID"].String();
 		room.hostAccountDisplayName = jsonEntry["hostAccountDisplayName"].String();
 		room.description = jsonEntry["description"].String();
-		room.playersCount = jsonEntry["playersCount"].Integer();
-		room.playersLimit = jsonEntry["playersLimit"].Integer();
+		room.statusID = jsonEntry["status"].String();
+		std::chrono::seconds ageSeconds (jsonEntry["ageSeconds"].Integer());
+		room.startDateFormatted = TextOperations::getCurrentFormattedDateTimeLocal(-ageSeconds);
+
+		for(const auto & 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();
 
 		activeRooms.push_back(room);
 	}
@@ -194,16 +229,60 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
 		lobbyWindowPtr->onActiveRooms(activeRooms);
 }
 
+void GlobalLobbyClient::receiveMatchesHistory(const JsonNode & json)
+{
+	matchesHistory.clear();
+
+	for(const auto & 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();
+		std::chrono::seconds ageSeconds (jsonEntry["ageSeconds"].Integer());
+		room.startDateFormatted = TextOperations::getCurrentFormattedDateTimeLocal(-ageSeconds);
+
+		for(const auto & 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(matchesHistory);
+}
+
 void GlobalLobbyClient::receiveInviteReceived(const JsonNode & json)
 {
-	assert(0); //TODO
+	auto lobbyWindowPtr = lobbyWindow.lock();
+	std::string gameRoomID = json["gameRoomID"].String();
+	std::string accountID = json["accountID"].String();
+
+	activeInvites.insert(gameRoomID);
+	if(lobbyWindowPtr)
+	{
+		std::string message = MetaString::createFromTextID("vcmi.lobby.invite.notification").toString();
+		std::string time = TextOperations::getCurrentFormattedTimeLocal();
+
+		lobbyWindowPtr->onGameChatMessage("System", message, time, "player", accountID);
+		lobbyWindowPtr->onInviteReceived(gameRoomID);
+	}
+
+	CCS->soundh->playSound(AudioPath::builtin("CHAT"));
 }
 
 void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json)
 {
-	Settings configRoom = settings.write["lobby"]["roomID"];
-	configRoom->String() = json["gameRoomID"].String();
-
 	if (json["proxyMode"].Bool())
 	{
 		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, {});
@@ -213,6 +292,9 @@ void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json)
 		int16_t port = settings["lobby"]["port"].Integer();
 		CSH->connectToServer(hostname, port);
 	}
+
+	// NOTE: must be set after CSH->resetStateForLobby call
+	currentGameRoomUUID = json["gameRoomID"].String();
 }
 
 void GlobalLobbyClient::onConnectionEstablished(const std::shared_ptr<INetworkConnection> & connection)
@@ -284,21 +366,13 @@ void GlobalLobbyClient::sendMessage(const JsonNode & data)
 	networkConnection->sendPacket(data.toBytes());
 }
 
-void GlobalLobbyClient::sendOpenPublicRoom()
-{
-	JsonNode toSend;
-	toSend["type"].String() = "activateGameRoom";
-	toSend["hostAccountID"] = settings["lobby"]["accountID"];
-	toSend["roomType"].String() = "public";
-	sendMessage(toSend);
-}
-
-void GlobalLobbyClient::sendOpenPrivateRoom()
+void GlobalLobbyClient::sendOpenRoom(const std::string & mode, int playerLimit)
 {
 	JsonNode toSend;
 	toSend["type"].String() = "activateGameRoom";
 	toSend["hostAccountID"] = settings["lobby"]["accountID"];
-	toSend["roomType"].String() = "private";
+	toSend["roomType"].String() = mode;
+	toSend["playerLimit"].Integer() = playerLimit;
 	sendMessage(toSend);
 }
 
@@ -348,8 +422,46 @@ const std::vector<GlobalLobbyRoom> & GlobalLobbyClient::getActiveRooms() const
 	return activeRooms;
 }
 
+const std::vector<std::string> & GlobalLobbyClient::getActiveChannels() const
+{
+	return activeChannels;
+}
+
+const std::vector<GlobalLobbyRoom> & GlobalLobbyClient::getMatchesHistory() const
+{
+	return matchesHistory;
+}
+
+const std::vector<GlobalLobbyChannelMessage> & GlobalLobbyClient::getChannelHistory(const std::string & channelType, const std::string & channelName) const
+{
+	static const std::vector<GlobalLobbyChannelMessage> emptyVector;
+
+	std::string keyToTest = channelType + '_' + channelName;
+
+	if (chatHistory.count(keyToTest) == 0)
+	{
+		if (channelType != "global")
+		{
+			JsonNode toSend;
+			toSend["type"].String() = "requestChatHistory";
+			toSend["channelType"].String() = channelType;
+			toSend["channelName"].String() = channelName;
+			CSH->getGlobalLobby().sendMessage(toSend);
+		}
+		return emptyVector;
+	}
+	else
+		return chatHistory.at(keyToTest);
+}
+
 void GlobalLobbyClient::activateInterface()
 {
+	if (GH.windows().topWindow<GlobalLobbyWindow>() != nullptr)
+	{
+		GH.windows().popWindows(1);
+		return;
+	}
+
 	if (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
 		return;
 
@@ -362,14 +474,46 @@ void GlobalLobbyClient::activateInterface()
 		GH.windows().pushWindow(createLoginWindow());
 }
 
+void GlobalLobbyClient::activateRoomInviteInterface()
+{
+	GH.windows().createAndPushWindow<GlobalLobbyInviteWindow>();
+}
+
 void GlobalLobbyClient::sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection)
 {
 	JsonNode toSend;
 	toSend["type"].String() = "clientProxyLogin";
 	toSend["accountID"] = settings["lobby"]["accountID"];
 	toSend["accountCookie"] = settings["lobby"]["accountCookie"];
-	toSend["gameRoomID"] = settings["lobby"]["roomID"];
+	toSend["gameRoomID"].String() = currentGameRoomUUID;
 
 	assert(JsonUtils::validate(toSend, "vcmi:lobbyProtocol/" + toSend["type"].String(), toSend["type"].String() + " pack"));
 	netConnection->sendPacket(toSend.toBytes());
 }
+
+void GlobalLobbyClient::resetMatchState()
+{
+	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);
+}
+
+bool GlobalLobbyClient::isInvitedToRoom(const std::string & gameRoomID)
+{
+	return activeInvites.count(gameRoomID) > 0;
+}

+ 20 - 3
client/globalLobby/GlobalLobbyClient.h

@@ -23,8 +23,17 @@ class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyabl
 {
 	std::vector<GlobalLobbyAccount> activeAccounts;
 	std::vector<GlobalLobbyRoom> activeRooms;
+	std::vector<std::string> activeChannels;
+	std::set<std::string> activeInvites;
+	std::vector<GlobalLobbyRoom> matchesHistory;
+
+	/// Contains known history of each channel
+	/// Key: concatenated channel type and channel name
+	/// Value: list of known chat messages
+	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;
@@ -37,11 +46,12 @@ class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyabl
 
 	void receiveAccountCreated(const JsonNode & json);
 	void receiveOperationFailed(const JsonNode & json);
-	void receiveLoginSuccess(const JsonNode & json);
+	void receiveClientLoginSuccess(const JsonNode & json);
 	void receiveChatHistory(const JsonNode & json);
 	void receiveChatMessage(const JsonNode & json);
 	void receiveActiveAccounts(const JsonNode & json);
 	void receiveActiveGameRooms(const JsonNode & json);
+	void receiveMatchesHistory(const JsonNode & json);
 	void receiveJoinRoomSuccess(const JsonNode & json);
 	void receiveInviteReceived(const JsonNode & json);
 
@@ -54,17 +64,24 @@ public:
 
 	const std::vector<GlobalLobbyAccount> & getActiveAccounts() const;
 	const std::vector<GlobalLobbyRoom> & getActiveRooms() const;
+	const std::vector<std::string> & getActiveChannels() const;
+	const std::vector<GlobalLobbyRoom> & getMatchesHistory() const;
+	const std::vector<GlobalLobbyChannelMessage> & getChannelHistory(const std::string & channelType, const std::string & channelName) const;
 
 	/// Activate interface and pushes lobby UI as top window
 	void activateInterface();
+	void activateRoomInviteInterface();
+
+	void sendMatchChatMessage(const std::string & messageText);
 	void sendMessage(const JsonNode & data);
 	void sendClientRegister(const std::string & accountName);
 	void sendClientLogin();
-	void sendOpenPublicRoom();
-	void sendOpenPrivateRoom();
+	void sendOpenRoom(const std::string & mode, int playerLimit);
 
 	void sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection);
+	void resetMatchState();
 
 	void connect();
 	bool isConnected() const;
+	bool isInvitedToRoom(const std::string & gameRoomID);
 };

+ 13 - 3
client/globalLobby/GlobalLobbyDefines.h

@@ -1,5 +1,5 @@
 /*
- * GlobalLobbyClient.h, part of VCMI engine
+ * GlobalLobbyDefines.h, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *
@@ -22,6 +22,16 @@ struct GlobalLobbyRoom
 	std::string hostAccountID;
 	std::string hostAccountDisplayName;
 	std::string description;
-	int playersCount;
-	int playersLimit;
+	std::string statusID;
+	std::string startDateFormatted;
+	std::vector<GlobalLobbyAccount> participants;
+	int playerLimit;
+};
+
+struct GlobalLobbyChannelMessage
+{
+	std::string timeFormatted;
+	std::string accountID;
+	std::string displayName;
+	std::string messageText;
 };

+ 78 - 0
client/globalLobby/GlobalLobbyInviteWindow.cpp

@@ -0,0 +1,78 @@
+/*
+ * GlobalLobbyInviteWindow.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 "GlobalLobbyInviteWindow.h"
+
+#include "GlobalLobbyClient.h"
+
+#include "../CServerHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/Images.h"
+#include "../widgets/ObjectLists.h"
+#include "../widgets/TextControls.h"
+
+#include "../../lib/MetaString.h"
+#include "../../lib/json/JsonNode.h"
+
+GlobalLobbyInviteAccountCard::GlobalLobbyInviteAccountCard(const GlobalLobbyAccount & accountDescription)
+	: accountID(accountDescription.accountID)
+{
+	pos.w = 200;
+	pos.h = 40;
+	addUsedEvents(LCLICK);
+
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
+	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName);
+	labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status);
+}
+
+void GlobalLobbyInviteAccountCard::clickPressed(const Point & cursorPosition)
+{
+	JsonNode message;
+	message["type"].String() = "sendInvite";
+	message["accountID"].String() = accountID;
+
+	CSH->getGlobalLobby().sendMessage(message);
+}
+
+GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
+	: CWindowObject(BORDERED)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	pos.w = 236;
+	pos.h = 400;
+
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	filledBackground->playerColored(PlayerColor(1));
+	labelTitle = std::make_shared<CLabel>(
+		pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.invite.header").toString()
+	);
+
+	const auto & createAccountCardCallback = [](size_t index) -> std::shared_ptr<CIntObject>
+	{
+		const auto & accounts = CSH->getGlobalLobby().getActiveAccounts();
+
+		if(index < accounts.size())
+			return std::make_shared<GlobalLobbyInviteAccountCard>(accounts[index]);
+		return std::make_shared<CIntObject>();
+	};
+
+	listBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 48, 220, 304), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
+	accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, 0, 0, 1 | 4, Rect(200, 0, 300, 300));
+
+	buttonClose = std::make_shared<CButton>(Point(86, 364), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); } );
+
+	center();
+}

+ 46 - 0
client/globalLobby/GlobalLobbyInviteWindow.h

@@ -0,0 +1,46 @@
+/*
+ * GlobalLobbyInviteWindow.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 "../windows/CWindowObject.h"
+
+class CLabel;
+class FilledTexturePlayerColored;
+class TransparentFilledRectangle;
+class CListBox;
+class CButton;
+struct GlobalLobbyAccount;
+
+class GlobalLobbyInviteWindow : public CWindowObject
+{
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+	std::shared_ptr<CLabel> labelTitle;
+	std::shared_ptr<CListBox> accountList;
+	std::shared_ptr<TransparentFilledRectangle> listBackground;
+	std::shared_ptr<CButton> buttonClose;
+
+public:
+	GlobalLobbyInviteWindow();
+};
+
+class GlobalLobbyInviteAccountCard : public CIntObject
+{
+	std::string accountID;
+
+	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
+	std::shared_ptr<CLabel> labelName;
+	std::shared_ptr<CLabel> labelStatus;
+	std::shared_ptr<CLabel> labelInviteStatus;
+	std::shared_ptr<CButton> buttonInvite;
+
+	void clickPressed(const Point & cursorPosition) override;
+public:
+	GlobalLobbyInviteAccountCard(const GlobalLobbyAccount & accountDescription);
+};

+ 2 - 2
client/globalLobby/GlobalLobbyServerSetup.cpp

@@ -50,8 +50,8 @@ GlobalLobbyServerSetup::GlobalLobbyServerSetup()
 
 	auto buttonPublic  = std::make_shared<CToggleButton>(Point(10, 120),  AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0);
 	auto buttonPrivate = std::make_shared<CToggleButton>(Point(146, 120), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0);
-	buttonPublic->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.public"), EFonts::FONT_SMALL, Colors::YELLOW);
-	buttonPrivate->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.private"), EFonts::FONT_SMALL, Colors::YELLOW);
+	buttonPublic->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.state.public"), EFonts::FONT_SMALL, Colors::YELLOW);
+	buttonPrivate->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.state.private"), EFonts::FONT_SMALL, Colors::YELLOW);
 
 	toggleRoomType = std::make_shared<CToggleGroup>(nullptr);
 	toggleRoomType->addToggle(0, buttonPublic);

+ 183 - 46
client/globalLobby/GlobalLobbyWidget.cpp

@@ -14,33 +14,38 @@
 #include "GlobalLobbyClient.h"
 #include "GlobalLobbyWindow.h"
 
+#include "../CGameInfo.h"
+#include "../CMusicHandler.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/Images.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
 #include "../widgets/TextControls.h"
 
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/Languages.h"
 #include "../../lib/MetaString.h"
+
 GlobalLobbyWidget::GlobalLobbyWidget(GlobalLobbyWindow * window)
 	: window(window)
 {
 	addCallback("closeWindow", [](int) { GH.windows().popWindows(1); });
 	addCallback("sendMessage", [this](int) { this->window->doSendChatMessage(); });
-	addCallback("createGameRoom", [this](int) { this->window->doCreateGameRoom(); });
+	addCallback("createGameRoom", [this](int) { if (!CSH->inGame()) this->window->doCreateGameRoom(); });//TODO: button should be blocked instead
 
-	REGISTER_BUILDER("accountList", &GlobalLobbyWidget::buildAccountList);
-	REGISTER_BUILDER("roomList", &GlobalLobbyWidget::buildRoomList);
+	REGISTER_BUILDER("lobbyItemList", &GlobalLobbyWidget::buildItemList);
 
 	const JsonNode config(JsonPath::builtin("config/widgets/lobbyWindow.json"));
 	build(config);
 }
 
-std::shared_ptr<CIntObject> GlobalLobbyWidget::buildAccountList(const JsonNode & config) const
+GlobalLobbyWidget::CreateFunc GlobalLobbyWidget::getItemListConstructorFunc(const std::string & callbackName) const
 {
-	const auto & createCallback = [this](size_t index) -> std::shared_ptr<CIntObject>
+	const auto & createAccountCardCallback = [this](size_t index) -> std::shared_ptr<CIntObject>
 	{
 		const auto & accounts = CSH->getGlobalLobby().getActiveAccounts();
 
@@ -49,21 +54,7 @@ std::shared_ptr<CIntObject> GlobalLobbyWidget::buildAccountList(const JsonNode &
 		return std::make_shared<CIntObject>();
 	};
 
-	auto position = readPosition(config["position"]);
-	auto itemOffset = readPosition(config["itemOffset"]);
-	auto sliderPosition = readPosition(config["sliderPosition"]);
-	auto sliderSize = readPosition(config["sliderSize"]);
-	size_t visibleSize = 4; // FIXME: how many items can fit into UI?
-	size_t totalSize = 4; //FIXME: how many items are there in total
-	int sliderMode = 1 | 4; //  present, vertical, blue
-	int initialPos = 0;
-
-	return std::make_shared<CListBox>(createCallback, position, itemOffset, visibleSize, totalSize, initialPos, sliderMode, Rect(sliderPosition, sliderSize) );
-}
-
-std::shared_ptr<CIntObject> GlobalLobbyWidget::buildRoomList(const JsonNode & config) const
-{
-	const auto & createCallback = [this](size_t index) -> std::shared_ptr<CIntObject>
+	const auto & createRoomCardCallback = [this](size_t index) -> std::shared_ptr<CIntObject>
 	{
 		const auto & rooms = CSH->getGlobalLobby().getActiveRooms();
 
@@ -72,16 +63,55 @@ std::shared_ptr<CIntObject> GlobalLobbyWidget::buildRoomList(const JsonNode & co
 		return std::make_shared<CIntObject>();
 	};
 
+	const auto & createChannelCardCallback = [this](size_t index) -> std::shared_ptr<CIntObject>
+	{
+		const auto & channels = CSH->getGlobalLobby().getActiveChannels();
+
+		if(index < channels.size())
+			return std::make_shared<GlobalLobbyChannelCard>(this->window, channels[index]);
+		return std::make_shared<CIntObject>();
+	};
+
+	const auto & createMatchCardCallback = [this](size_t index) -> std::shared_ptr<CIntObject>
+	{
+		const auto & matches = CSH->getGlobalLobby().getMatchesHistory();
+
+		if(index < matches.size())
+			return std::make_shared<GlobalLobbyMatchCard>(this->window, matches[index]);
+		return std::make_shared<CIntObject>();
+	};
+
+	if (callbackName == "room")
+		return createRoomCardCallback;
+
+	if (callbackName == "account")
+		return createAccountCardCallback;
+
+	if (callbackName == "channel")
+		return createChannelCardCallback;
+
+	if (callbackName == "match")
+		return createMatchCardCallback;
+
+	throw std::runtime_error("Unknown item type in lobby widget constructor: " + callbackName);
+}
+
+std::shared_ptr<CIntObject> GlobalLobbyWidget::buildItemList(const JsonNode & config) const
+{
+	auto callback = getItemListConstructorFunc(config["itemType"].String());
 	auto position = readPosition(config["position"]);
 	auto itemOffset = readPosition(config["itemOffset"]);
 	auto sliderPosition = readPosition(config["sliderPosition"]);
 	auto sliderSize = readPosition(config["sliderSize"]);
-	size_t visibleSize = 4; // FIXME: how many items can fit into UI?
-	size_t totalSize = 4; //FIXME: how many items are there in total
-	int sliderMode = 1 | 4; //  present, vertical, blue
+	size_t visibleAmount = config["visibleAmount"].Integer();
+	size_t totalAmount = 0; // Will be set later, on server netpack
+	int sliderMode = config["sliderSize"].isNull() ? 0 : (1 | 4); //  present, vertical, blue
 	int initialPos = 0;
 
-	return std::make_shared<CListBox>(createCallback, position, itemOffset, visibleSize, totalSize, initialPos, sliderMode, Rect(sliderPosition, sliderSize) );
+	auto result = std::make_shared<CListBox>(callback, position, itemOffset, visibleAmount, totalAmount, initialPos, sliderMode, Rect(sliderPosition, sliderSize));
+
+	result->setRedrawParent(true);
+	return result;
 }
 
 std::shared_ptr<CLabel> GlobalLobbyWidget::getAccountNameLabel()
@@ -109,47 +139,154 @@ std::shared_ptr<CListBox> GlobalLobbyWidget::getRoomList()
 	return widget<CListBox>("roomList");
 }
 
-GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription)
+std::shared_ptr<CListBox> GlobalLobbyWidget::getChannelList()
 {
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	return widget<CListBox>("channelList");
+}
 
-	const auto & onInviteClicked = [window, accountID=accountDescription.accountID]()
-	{
-		window->doInviteAccount(accountID);
-	};
+std::shared_ptr<CListBox> GlobalLobbyWidget::getMatchList()
+{
+	return widget<CListBox>("matchList");
+}
 
-	pos.w = 130;
-	pos.h = 40;
+std::shared_ptr<CLabel> GlobalLobbyWidget::getGameChatHeader()
+{
+	return widget<CLabel>("headerGameChat");
+}
+
+std::shared_ptr<CLabel> GlobalLobbyWidget::getAccountListHeader()
+{
+	return widget<CLabel>("headerAccountList");
+}
 
-	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
-	labelName = std::make_shared<CLabel>( 5, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, accountDescription.displayName);
-	labelStatus = std::make_shared<CLabel>( 5, 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, accountDescription.status);
+std::shared_ptr<CLabel> GlobalLobbyWidget::getRoomListHeader()
+{
+	return widget<CLabel>("headerRoomList");
+}
+
+std::shared_ptr<CLabel> GlobalLobbyWidget::getChannelListHeader()
+{
+	return widget<CLabel>("headerChannelList");
+}
+
+std::shared_ptr<CLabel> GlobalLobbyWidget::getMatchListHeader()
+{
+	return widget<CLabel>("headerMatchList");
+}
+
+GlobalLobbyChannelCardBase::GlobalLobbyChannelCardBase(GlobalLobbyWindow * window, const Point & dimensions, const std::string & channelType, const std::string & channelName, const std::string & channelDescription)
+	: window(window)
+	, channelType(channelType)
+	, channelName(channelName)
+	, channelDescription(channelDescription)
+{
+	pos.w = dimensions.x;
+	pos.h = dimensions.y;
+	addUsedEvents(LCLICK);
 
-	if (CSH->inLobbyRoom())
-		buttonInvite = std::make_shared<CButton>(Point(95, 8), AnimationPath::builtin("settingsWindow/button32"), CButton::tooltip(), onInviteClicked);
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	if (window->isChannelOpen(channelType, channelName))
+		backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), Colors::YELLOW, 2);
+	else if (window->isChannelUnread(channelType, channelName))
+		backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), Colors::WHITE, 1);
+	else
+		backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
+}
+
+void GlobalLobbyChannelCardBase::clickPressed(const Point & cursorPosition)
+{
+	CCS->soundh->playSound(soundBase::button);
+	window->doOpenChannel(channelType, channelName, channelDescription);
+}
+
+GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription)
+	: GlobalLobbyChannelCardBase(window, Point(130, 40), "player", accountDescription.accountID, accountDescription.displayName)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName);
+	labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status);
 }
 
 GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription)
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 
-	const auto & onJoinClicked = [window, roomID=roomDescription.gameRoomID]()
+	const auto & onJoinClicked = [window, roomID = roomDescription.gameRoomID]()
 	{
 		window->doJoinRoom(roomID);
 	};
 
+	bool publicRoom = roomDescription.statusID == "public";
+	bool hasInvite = CSH->getGlobalLobby().isInvitedToRoom(roomDescription.gameRoomID);
+	bool canJoin = publicRoom || hasInvite;
+
 	auto roomSizeText = MetaString::createFromRawString("%d/%d");
-	roomSizeText.replaceNumber(roomDescription.playersCount);
-	roomSizeText.replaceNumber(roomDescription.playersLimit);
+	roomSizeText.replaceNumber(roomDescription.participants.size());
+	roomSizeText.replaceNumber(roomDescription.playerLimit);
+
+	MetaString roomStatusText;
+	if (roomDescription.statusID == "private" && hasInvite)
+		roomStatusText.appendTextID("vcmi.lobby.room.state.invited");
+	else
+		roomStatusText.appendTextID("vcmi.lobby.room.state." + roomDescription.statusID);
 
 	pos.w = 230;
 	pos.h = 40;
 
-	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
-	labelName = std::make_shared<CLabel>( 5, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName);
-	labelStatus = std::make_shared<CLabel>( 5, 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, roomDescription.description);
-	labelRoomSize = std::make_shared<CLabel>( 160, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, roomSizeText.toString());
+	if (window->isInviteUnread(roomDescription.gameRoomID))
+		backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), Colors::WHITE, 1);
+	else
+		backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
+
+	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName);
+	labelDescription = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, roomDescription.description);
+	labelRoomSize = std::make_shared<CLabel>(178, 10, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomSizeText.toString());
+	labelRoomStatus = std::make_shared<CLabel>(190, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomStatusText.toString());
+	iconRoomSize = std::make_shared<CPicture>(ImagePath::builtin("lobby/iconPlayer"), Point(180, 5));
+
+	if(!CSH->inGame() && canJoin)
+	{
+		buttonJoin = std::make_shared<CButton>(Point(194, 4), AnimationPath::builtin("lobbyJoinRoom"), CButton::tooltip(), onJoinClicked);
+		buttonJoin->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("lobby/iconEnter")));
+	}
+}
+
+GlobalLobbyChannelCard::GlobalLobbyChannelCard(GlobalLobbyWindow * window, const std::string & channelName)
+	: GlobalLobbyChannelCardBase(window, Point(146, 40), "global", channelName, Languages::getLanguageOptions(channelName).nameNative)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	labelName = std::make_shared<CLabel>(5, 20, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, Languages::getLanguageOptions(channelName).nameNative);
+}
+
+GlobalLobbyMatchCard::GlobalLobbyMatchCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & matchDescription)
+	: GlobalLobbyChannelCardBase(window, Point(130, 40), "match", matchDescription.gameRoomID, matchDescription.startDateFormatted)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	labelMatchDate = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, matchDescription.startDateFormatted);
+
+	MetaString opponentDescription;
+
+	if (matchDescription.participants.size() == 1)
+		opponentDescription.appendTextID("vcmi.lobby.match.solo");
+
+	if (matchDescription.participants.size() == 2)
+	{
+		std::string ourAccountID = settings["lobby"]["accountID"].String();
+
+		opponentDescription.appendTextID("vcmi.lobby.match.duel");
+		// Find display name of our one and only opponent in this game
+		if (matchDescription.participants[0].accountID == ourAccountID)
+			opponentDescription.replaceRawString(matchDescription.participants[1].displayName);
+		else
+			opponentDescription.replaceRawString(matchDescription.participants[0].displayName);
+	}
+
+	if (matchDescription.participants.size() > 2)
+	{
+		opponentDescription.appendTextID("vcmi.lobby.match.multi");
+		opponentDescription.replaceNumber(matchDescription.participants.size());
+	}
 
-	if (!CSH->inGame())
-		buttonJoin = std::make_shared<CButton>(Point(195, 8), AnimationPath::builtin("settingsWindow/button32"), CButton::tooltip(), onJoinClicked);
+	labelMatchOpponent = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, opponentDescription.toString());
 }

+ 51 - 9
client/globalLobby/GlobalLobbyWidget.h

@@ -20,8 +20,10 @@ class GlobalLobbyWidget : public InterfaceObjectConfigurable
 {
 	GlobalLobbyWindow * window;
 
-	std::shared_ptr<CIntObject> buildAccountList(const JsonNode &) const;
-	std::shared_ptr<CIntObject> buildRoomList(const JsonNode &) const;
+	using CreateFunc = std::function<std::shared_ptr<CIntObject>(size_t)>;
+
+	std::shared_ptr<CIntObject> buildItemList(const JsonNode &) const;
+	CreateFunc getItemListConstructorFunc(const std::string & callbackName) const;
 
 public:
 	explicit GlobalLobbyWidget(GlobalLobbyWindow * window);
@@ -31,27 +33,67 @@ public:
 	std::shared_ptr<CTextBox> getGameChat();
 	std::shared_ptr<CListBox> getAccountList();
 	std::shared_ptr<CListBox> getRoomList();
+	std::shared_ptr<CListBox> getChannelList();
+	std::shared_ptr<CListBox> getMatchList();
+
+	std::shared_ptr<CLabel> getGameChatHeader();
+	std::shared_ptr<CLabel> getAccountListHeader();
+	std::shared_ptr<CLabel> getRoomListHeader();
+	std::shared_ptr<CLabel> getChannelListHeader();
+	std::shared_ptr<CLabel> getMatchListHeader();
 };
 
-class GlobalLobbyAccountCard : public CIntObject
+class GlobalLobbyChannelCardBase : public CIntObject
 {
+	GlobalLobbyWindow * window;
+	std::string channelType;
+	std::string channelName;
+	std::string channelDescription;
+
+	void clickPressed(const Point & cursorPosition) override;
+
+	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
 public:
-	GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription);
+	GlobalLobbyChannelCardBase(GlobalLobbyWindow * window, const Point & dimensions, const std::string & channelType, const std::string & channelName, const std::string & channelDescription);
+};
 
+class GlobalLobbyAccountCard : public GlobalLobbyChannelCardBase
+{
 	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
 	std::shared_ptr<CLabel> labelName;
 	std::shared_ptr<CLabel> labelStatus;
-	std::shared_ptr<CButton> buttonInvite;
+
+public:
+	GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription);
 };
 
 class GlobalLobbyRoomCard : public CIntObject
 {
-public:
-	GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription);
-
 	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
 	std::shared_ptr<CLabel> labelName;
 	std::shared_ptr<CLabel> labelRoomSize;
-	std::shared_ptr<CLabel> labelStatus;
+	std::shared_ptr<CLabel> labelRoomStatus;
+	std::shared_ptr<CLabel> labelDescription;
 	std::shared_ptr<CButton> buttonJoin;
+	std::shared_ptr<CPicture> iconRoomSize;
+
+public:
+	GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription);
+};
+
+class GlobalLobbyChannelCard : public GlobalLobbyChannelCardBase
+{
+	std::shared_ptr<CLabel> labelName;
+
+public:
+	GlobalLobbyChannelCard(GlobalLobbyWindow * window, const std::string & channelName);
+};
+
+class GlobalLobbyMatchCard : public GlobalLobbyChannelCardBase
+{
+	std::shared_ptr<CLabel> labelMatchDate;
+	std::shared_ptr<CLabel> labelMatchOpponent;
+
+public:
+	GlobalLobbyMatchCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & matchDescription);
 };

+ 93 - 2
client/globalLobby/GlobalLobbyWindow.cpp

@@ -22,6 +22,7 @@
 #include "../widgets/ObjectLists.h"
 
 #include "../../lib/CConfigHandler.h"
+#include "../../lib/Languages.h"
 #include "../../lib/MetaString.h"
 
 GlobalLobbyWindow::GlobalLobbyWindow()
@@ -33,6 +34,40 @@ GlobalLobbyWindow::GlobalLobbyWindow()
 	center();
 
 	widget->getAccountNameLabel()->setText(settings["lobby"]["displayName"].String());
+	doOpenChannel("global", "english", Languages::getLanguageOptions("english").nameNative);
+
+	widget->getChannelListHeader()->setText(MetaString::createFromTextID("vcmi.lobby.header.channels").toString());
+}
+
+bool GlobalLobbyWindow::isChannelOpen(const std::string & testChannelType, const std::string & testChannelName) const
+{
+	return testChannelType == currentChannelType && testChannelName == currentChannelName;
+}
+
+void GlobalLobbyWindow::doOpenChannel(const std::string & channelType, const std::string & channelName, const std::string & roomDescription)
+{
+	currentChannelType = channelType;
+	currentChannelName = channelName;
+	chatHistory.clear();
+	unreadChannels.erase(channelType + "_" + channelName);
+	widget->getGameChat()->setText("");
+
+	auto history = CSH->getGlobalLobby().getChannelHistory(channelType, channelName);
+
+	for (auto const & entry : history)
+		onGameChatMessage(entry.displayName, entry.messageText, entry.timeFormatted, channelType, channelName);
+
+	MetaString text;
+	text.appendTextID("vcmi.lobby.header.chat." + channelType);
+	text.replaceRawString(roomDescription);
+	widget->getGameChatHeader()->setText(text.toString());
+
+	// Update currently selected item in UI
+	widget->getAccountList()->reset();
+	widget->getChannelList()->reset();
+	widget->getMatchList()->reset();
+
+	redraw();
 }
 
 void GlobalLobbyWindow::doSendChatMessage()
@@ -41,6 +76,8 @@ void GlobalLobbyWindow::doSendChatMessage()
 
 	JsonNode toSend;
 	toSend["type"].String() = "sendChatMessage";
+	toSend["channelType"].String() = currentChannelType;
+	toSend["channelName"].String() = currentChannelName;
 	toSend["messageText"].String() = messageText;
 
 	CSH->getGlobalLobby().sendMessage(toSend);
@@ -64,6 +101,8 @@ void GlobalLobbyWindow::doInviteAccount(const std::string & accountID)
 
 void GlobalLobbyWindow::doJoinRoom(const std::string & roomID)
 {
+	unreadInvites.erase(roomID);
+
 	JsonNode toSend;
 	toSend["type"].String() = "joinGameRoom";
 	toSend["gameRoomID"].String() = roomID;
@@ -71,8 +110,18 @@ void GlobalLobbyWindow::doJoinRoom(const std::string & roomID)
 	CSH->getGlobalLobby().sendMessage(toSend);
 }
 
-void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when)
+void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when, const std::string & channelType, const std::string & channelName)
 {
+	if (channelType != currentChannelType || channelName != currentChannelName)
+	{
+		// mark channel as unread
+		unreadChannels.insert(channelType + "_" + channelName);
+		widget->getAccountList()->reset();
+		widget->getChannelList()->reset();
+		widget->getMatchList()->reset();
+		return;
+	}
+
 	MetaString chatMessageFormatted;
 	chatMessageFormatted.appendRawString("[%s] {%s}: %s\n");
 	chatMessageFormatted.replaceRawString(when);
@@ -84,16 +133,58 @@ void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std:
 	widget->getGameChat()->setText(chatHistory);
 }
 
+bool GlobalLobbyWindow::isChannelUnread(const std::string & channelType, const std::string & channelName) const
+{
+	return unreadChannels.count(channelType + "_" + channelName) > 0;
+}
+
 void GlobalLobbyWindow::onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts)
 {
-	widget->getAccountList()->reset();
+	if (accounts.size() == widget->getAccountList()->size())
+		widget->getAccountList()->reset();
+	else
+		widget->getAccountList()->resize(accounts.size());
+
+	MetaString text = MetaString::createFromTextID("vcmi.lobby.header.players");
+	text.replaceNumber(accounts.size());
+	widget->getAccountListHeader()->setText(text.toString());
 }
 
 void GlobalLobbyWindow::onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms)
 {
+	if (rooms.size() == widget->getRoomList()->size())
+		widget->getRoomList()->reset();
+	else
+		widget->getRoomList()->resize(rooms.size());
+
+	MetaString text = MetaString::createFromTextID("vcmi.lobby.header.rooms");
+	text.replaceNumber(rooms.size());
+	widget->getRoomListHeader()->setText(text.toString());
+}
+
+void GlobalLobbyWindow::onMatchesHistory(const std::vector<GlobalLobbyRoom> & history)
+{
+	if (history.size() == widget->getMatchList()->size())
+		widget->getMatchList()->reset();
+	else
+		widget->getMatchList()->resize(history.size());
+
+	MetaString text = MetaString::createFromTextID("vcmi.lobby.header.history");
+	text.replaceNumber(history.size());
+	widget->getMatchListHeader()->setText(text.toString());
+}
+
+void GlobalLobbyWindow::onInviteReceived(const std::string & invitedRoomID)
+{
+	unreadInvites.insert(invitedRoomID);
 	widget->getRoomList()->reset();
 }
 
+bool GlobalLobbyWindow::isInviteUnread(const std::string & gameRoomID) const
+{
+	return unreadInvites.count(gameRoomID) > 0;
+}
+
 void GlobalLobbyWindow::onJoinedRoom()
 {
 	widget->getAccountList()->reset();

+ 17 - 2
client/globalLobby/GlobalLobbyWindow.h

@@ -18,21 +18,36 @@ struct GlobalLobbyRoom;
 class GlobalLobbyWindow : public CWindowObject
 {
 	std::string chatHistory;
+	std::string currentChannelType;
+	std::string currentChannelName;
 
 	std::shared_ptr<GlobalLobbyWidget> widget;
+	std::set<std::string> unreadChannels;
+	std::set<std::string> unreadInvites;
 
 public:
 	GlobalLobbyWindow();
 
+	// Callbacks for UI
+
 	void doSendChatMessage();
 	void doCreateGameRoom();
-
+	void doOpenChannel(const std::string & channelType, const std::string & channelName, const std::string & roomDescription);
 	void doInviteAccount(const std::string & accountID);
 	void doJoinRoom(const std::string & roomID);
 
-	void onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when);
+	/// Returns true if provided chat channel is the one that is currently open in UI
+	bool isChannelOpen(const std::string & channelType, const std::string & channelName) const;
+	bool isChannelUnread(const std::string & channelType, const std::string & channelName) const;
+	bool isInviteUnread(const std::string & gameRoomID) const;
+
+	// Callbacks for network packs
+
+	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 onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms);
+	void onMatchesHistory(const std::vector<GlobalLobbyRoom> & history);
+	void onInviteReceived(const std::string & invitedRoomID);
 	void onJoinedRoom();
 	void onLeftRoom();
 };

+ 2 - 1
client/gui/InterfaceObjectConfigurable.cpp

@@ -768,8 +768,9 @@ std::shared_ptr<CTextBox> InterfaceObjectConfigurable::buildTextBox(const JsonNo
 	auto alignment = readTextAlignment(config["alignment"]);
 	auto color = readColor(config["color"]);
 	auto text = readText(config["text"]);
+	auto blueTheme = config["blueTheme"].Bool();
 
-	return std::make_shared<CTextBox>(text, rect, 0, font, alignment, color);
+	return std::make_shared<CTextBox>(text, rect, blueTheme ? 1 : 0, font, alignment, color);
 }
 
 std::shared_ptr<CIntObject> InterfaceObjectConfigurable::buildWidget(JsonNode config) const

+ 14 - 1
client/gui/TextAlignment.h

@@ -9,4 +9,17 @@
  */
 #pragma once
 
-enum class ETextAlignment {TOPLEFT, TOPCENTER, CENTER, BOTTOMRIGHT};
+enum class ETextAlignment
+{
+	TOPLEFT,
+	TOPCENTER,
+	TOPRIGHT,
+
+	CENTERLEFT,
+	CENTER,
+	CENTERRIGHT,
+
+	BOTTOMLEFT,
+	BOTTOMCENTER,
+	BOTTOMRIGHT
+};

+ 5 - 0
client/lobby/CLobbyScreen.cpp

@@ -23,6 +23,7 @@
 #include "../widgets/Buttons.h"
 #include "../windows/InfoWindows.h"
 #include "../render/Colors.h"
+#include "../globalLobby/GlobalLobbyClient.h"
 
 #include "../../CCallback.h"
 
@@ -104,8 +105,12 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 
 	buttonBack = std::make_shared<CButton>(Point(581, 535), AnimationPath::builtin("SCNRBACK.DEF"), CGI->generaltexth->zelp[105], [&]()
 	{
+		bool wasInLobbyRoom = CSH->inLobbyRoom();
 		CSH->sendClientDisconnecting();
 		close();
+
+		if (wasInLobbyRoom)
+			CSH->getGlobalLobby().activateInterface();
 	}, EShortcut::GLOBAL_CANCEL);
 }
 

+ 33 - 26
client/lobby/CSelectionBase.cpp

@@ -26,6 +26,7 @@
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
+#include "../globalLobby/GlobalLobbyClient.h"
 #include "../mainmenu/CMainMenu.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/CComponent.h"
@@ -44,10 +45,10 @@
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/CRandomGenerator.h"
 #include "../../lib/CThreadHelper.h"
+#include "../../lib/MetaString.h"
 #include "../../lib/filesystem/Filesystem.h"
-#include "../../lib/mapping/CMapInfo.h"
 #include "../../lib/mapping/CMapHeader.h"
-#include "../../lib/CRandomGenerator.h"
+#include "../../lib/mapping/CMapInfo.h"
 
 ISelectionScreenInfo::ISelectionScreenInfo(ESelectionScreen ScreenType)
 	: screenType(ScreenType)
@@ -137,6 +138,12 @@ InfoCard::InfoCard()
 	playerListBg = std::make_shared<CPicture>(ImagePath::builtin("CHATPLUG.bmp"), 16, 276);
 	chat = std::make_shared<CChatBox>(Rect(18, 126, 335, 143));
 
+	buttonInvitePlayers = std::make_shared<CButton>(Point(20, 365), AnimationPath::builtin("pregameInvitePlayers"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateRoomInviteInterface(); } );
+	buttonOpenGlobalLobby = std::make_shared<CButton>(Point(188, 365), AnimationPath::builtin("pregameReturnToLobby"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateInterface(); });
+
+	buttonInvitePlayers->setTextOverlay  (MetaString::createFromTextID("vcmi.lobby.invite.header").toString(), EFonts::FONT_SMALL, Colors::WHITE);
+	buttonOpenGlobalLobby->setTextOverlay(MetaString::createFromTextID("vcmi.lobby.backToLobby").toString(), EFonts::FONT_SMALL, Colors::WHITE);
+
 	if(SEL->screenType == ESelectionScreen::campaignList)
 	{
 		labelCampaignDescription = std::make_shared<CLabel>(26, 132, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]);
@@ -183,11 +190,12 @@ InfoCard::InfoCard()
 		labelDifficulty = std::make_shared<CLabel>(62, 472, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 		labelDifficultyPercent = std::make_shared<CLabel>(311, 472, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 
-		labelGroupPlayersAssigned = std::make_shared<CLabelGroup>(FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
-		labelGroupPlayersUnassigned = std::make_shared<CLabelGroup>(FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
+		labelGroupPlayers = std::make_shared<CLabelGroup>(FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
 		disableLabelRedraws();
 	}
 	setChat(false);
+	if (CSH->inLobbyRoom())
+		setChat(true); // FIXME: less ugly version?
 }
 
 void InfoCard::disableLabelRedraws()
@@ -240,27 +248,21 @@ void InfoCard::changeSelection()
 
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	// FIXME: We recreate them each time because CLabelGroup don't use smart pointers
-	labelGroupPlayersAssigned = std::make_shared<CLabelGroup>(FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
-	labelGroupPlayersUnassigned = std::make_shared<CLabelGroup>(FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
+	labelGroupPlayers = std::make_shared<CLabelGroup>(FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
 	if(!showChat)
+		labelGroupPlayers->disable();
+
+	for(const auto & p : CSH->playerNames)
 	{
-		labelGroupPlayersAssigned->disable();
-		labelGroupPlayersUnassigned->disable();
-	}
-	for(auto & p : CSH->playerNames)
-	{
-		const auto pset = CSH->si->getPlayersSettings(p.first);
-		int pid = p.first;
-		if(pset)
-		{
-			auto name = boost::str(boost::format("%s (%d-%d %s)") % p.second.name % p.second.connection % pid % pset->color.toString());
-			labelGroupPlayersAssigned->add(24, 285 + (int)labelGroupPlayersAssigned->currentSize()*(int)graphics->fonts[FONT_SMALL]->getLineHeight(), name);
-		}
+		int slotsUsed = labelGroupPlayers->currentSize();
+		Point labelPosition;
+
+		if(slotsUsed < 4)
+			labelPosition = Point(24, 285 + slotsUsed * graphics->fonts[FONT_SMALL]->getLineHeight()); // left column
 		else
-		{
-			auto name = boost::str(boost::format("%s (%d-%d)") % p.second.name % p.second.connection % pid);
-			labelGroupPlayersUnassigned->add(193, 285 + (int)labelGroupPlayersUnassigned->currentSize()*(int)graphics->fonts[FONT_SMALL]->getLineHeight(), name);
-		}
+			labelPosition = Point(193, 285 + (slotsUsed - 4) * graphics->fonts[FONT_SMALL]->getLineHeight()); // right column
+
+		labelGroupPlayers->add(labelPosition.x, labelPosition.y, p.second.name);
 	}
 }
 
@@ -289,8 +291,12 @@ void InfoCard::setChat(bool activateChat)
 			labelVictoryConditionText->disable();
 			iconsLossCondition->disable();
 			labelLossConditionText->disable();
-			labelGroupPlayersAssigned->enable();
-			labelGroupPlayersUnassigned->enable();
+			labelGroupPlayers->enable();
+		}
+		if (CSH->inLobbyRoom())
+		{
+			buttonInvitePlayers->enable();
+			buttonOpenGlobalLobby->enable();
 		}
 		mapDescription->disable();
 		chat->enable();
@@ -298,6 +304,8 @@ void InfoCard::setChat(bool activateChat)
 	}
 	else
 	{
+		buttonInvitePlayers->disable();
+		buttonOpenGlobalLobby->disable();
 		mapDescription->enable();
 		chat->disable();
 		playerListBg->disable();
@@ -315,8 +323,7 @@ void InfoCard::setChat(bool activateChat)
 			iconsLossCondition->enable();
 			labelVictoryConditionText->enable();
 			labelLossConditionText->enable();
-			labelGroupPlayersAssigned->disable();
-			labelGroupPlayersUnassigned->disable();
+			labelGroupPlayers->disable();
 		}
 	}
 

+ 3 - 2
client/lobby/CSelectionBase.h

@@ -104,8 +104,9 @@ class InfoCard : public CIntObject
 	std::shared_ptr<CLabel> labelVictoryConditionText;
 	std::shared_ptr<CLabel> labelLossConditionText;
 
-	std::shared_ptr<CLabelGroup> labelGroupPlayersAssigned;
-	std::shared_ptr<CLabelGroup> labelGroupPlayersUnassigned;
+	std::shared_ptr<CLabelGroup> labelGroupPlayers;
+	std::shared_ptr<CButton> buttonInvitePlayers;
+	std::shared_ptr<CButton> buttonOpenGlobalLobby;
 public:
 
 	bool showChat;

+ 7 - 1
client/widgets/ObjectLists.cpp

@@ -66,6 +66,7 @@ size_t CTabbedInt::getActive() const
 
 void CTabbedInt::reset()
 {
+
 	deleteItem(activeTab);
 	activeTab = createItem(activeID);
 	activeTab->moveTo(pos.topLeft());
@@ -127,6 +128,11 @@ void CListBox::updatePositions()
 
 void CListBox::reset()
 {
+	// hack to ensure that all items will be recreated with new address
+	// save current item list so all shared_ptr's will be destroyed only on scope exit and not inside loop below
+	// see comment in EventDispatcher::handleLeftButtonClick for details on why this hack is needed
+	auto itemsCopy = items;
+
 	size_t current = first;
 	for (auto & elem : items)
 	{
@@ -279,4 +285,4 @@ void CListBoxWithCallback::moveToPrev()
 	CListBox::moveToPrev();
 	if(movedPosCallback)
 		movedPosCallback(getPos());
-}
+}

+ 15 - 18
client/widgets/TextControls.cpp

@@ -82,6 +82,8 @@ void CLabel::setAutoRedraw(bool value)
 
 void CLabel::setText(const std::string & Txt)
 {
+	assert(TextOperations::isValidUnicodeString(Txt));
+
 	text = Txt;
 	
 	trimText();
@@ -183,29 +185,24 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what)
 
 	// input is rect in which given text should be placed
 	// calculate proper position for top-left corner of the text
-	if(alignment == ETextAlignment::TOPLEFT)
-	{
+
+	if(alignment == ETextAlignment::TOPLEFT || alignment == ETextAlignment::CENTERLEFT  || alignment == ETextAlignment::BOTTOMLEFT)
 		where.x += getBorderSize().x;
-		where.y += getBorderSize().y;
-	}
 
-	if(alignment == ETextAlignment::TOPCENTER)
-	{
-		where.x += (int(destRect.w) - int(f->getStringWidth(what) - delimitersCount)) / 2;
+	if(alignment == ETextAlignment::CENTER || alignment == ETextAlignment::TOPCENTER || alignment == ETextAlignment::BOTTOMCENTER)
+		where.x += (destRect.w - (static_cast<int>(f->getStringWidth(what)) - delimitersCount)) / 2;
+
+	if(alignment == ETextAlignment::TOPRIGHT || alignment == ETextAlignment::BOTTOMRIGHT || alignment == ETextAlignment::CENTERRIGHT)
+		where.x += getBorderSize().x + destRect.w - (static_cast<int>(f->getStringWidth(what)) - delimitersCount);
+
+	if(alignment == ETextAlignment::TOPLEFT || alignment == ETextAlignment::TOPCENTER || alignment == ETextAlignment::TOPRIGHT)
 		where.y += getBorderSize().y;
-	}
 
-	if(alignment == ETextAlignment::CENTER)
-	{
-		where.x += (int(destRect.w) - int(f->getStringWidth(what) - delimitersCount)) / 2;
-		where.y += (int(destRect.h) - int(f->getLineHeight())) / 2;
-	}
+	if(alignment == ETextAlignment::CENTERLEFT || alignment == ETextAlignment::CENTER || alignment == ETextAlignment::CENTERRIGHT)
+		where.y += (destRect.h - static_cast<int>(f->getLineHeight())) / 2;
 
-	if(alignment == ETextAlignment::BOTTOMRIGHT)
-	{
-		where.x += getBorderSize().x + destRect.w - ((int)f->getStringWidth(what) - delimitersCount);
-		where.y += getBorderSize().y + destRect.h - (int)f->getLineHeight();
-	}
+	if(alignment == ETextAlignment::BOTTOMLEFT || alignment == ETextAlignment::BOTTOMCENTER || alignment == ETextAlignment::BOTTOMRIGHT)
+		where.y += getBorderSize().y + destRect.h - static_cast<int>(f->getLineHeight());
 
 	size_t begin = 0;
 	size_t currDelimeter = 0;

+ 8 - 1
config/schemas/lobbyProtocol/activateGameRoom.json

@@ -22,6 +22,13 @@
 			"type" : "string",
 			"enum" : [ "public", "private" ],
 			"description" : "Room type to use for activation"
-		}
+		},
+		"playerLimit" :
+		{
+			"type" : "number",
+			"minimum" : 1,
+			"maximum" : 8,
+			"description" : "Maximum number of players that can enter this room"
+		},
 	}
 }

+ 36 - 5
config/schemas/lobbyProtocol/activeGameRooms.json

@@ -20,7 +20,7 @@
 			{
 				"type" : "object",
 				"additionalProperties" : false,
-				"required" : [ "gameRoomID", "hostAccountID", "hostAccountDisplayName", "description", "playersCount", "playersLimit" ],
+				"required" : [ "gameRoomID", "hostAccountID", "hostAccountDisplayName", "description", "participants", "playerLimit", "status", "ageSeconds" ],
 				"properties" : {
 					"gameRoomID" :
 					{
@@ -42,15 +42,46 @@
 						"type" : "string",
 						"description" : "Auto-generated description of this room"
 					},
-					"playersCount" :
+					"participants" :
 					{
-						"type" : "number",
-						"description" : "Current number of players in this room, including host"
+						"type" : "array",
+						"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" :
+					{
+						"type" : "string",
+						"enum" : [ "idle", "public", "private", "busy", "cancelled", "closed" ],
+						"description" : "Current status of game room"
 					},
-					"playersLimit" :
+					"playerLimit" :
 					{
 						"type" : "number",
+						"minimum" : 1,
+						"maximum" : 8,
 						"description" : "Maximum number of players that can join this room, including host"
+					},
+					"ageSeconds" :
+					{
+						"type" : "number",
+						"description" : "Age of this room in seconds. For example, 10 means that this room was created 10 seconds ago"
 					}
 				}
 			}

+ 21 - 0
config/schemas/lobbyProtocol/changeRoomDescription.json

@@ -0,0 +1,21 @@
+{
+	"type" : "object",
+	"$schema" : "http://json-schema.org/draft-06/schema",
+	"title" : "Lobby protocol: changeRoomDescription",
+	"description" : "Sent by server when currently selected map changes",
+	"required" : [ "type", "description" ],
+	"additionalProperties" : false,
+
+	"properties" : {
+		"type" :
+		{
+			"type" : "string",
+			"const" : "changeRoomDescription"
+		},
+		"description" :
+		{
+			"type" : "string",
+			"description" : "Human-readable description of the room"
+		}
+	}
+}

+ 14 - 1
config/schemas/lobbyProtocol/chatHistory.json

@@ -3,7 +3,7 @@
 	"$schema" : "http://json-schema.org/draft-06/schema",
 	"title" : "Lobby protocol: chatHistory",
 	"description" : "Sent by server immediately after login to fill initial chat history",
-	"required" : [ "type", "messages" ],
+	"required" : [ "type", "messages", "channelType", "channelName" ],
 	"additionalProperties" : false,
 
 	"properties" : {
@@ -12,6 +12,19 @@
 			"type" : "string",
 			"const" : "chatHistory"
 		},
+
+		"channelType" :
+		{
+			"type" : "string",
+			"enum" : [ "global", "match", "player" ],
+			"description" : "Type of room to which these messages have been set."
+		},
+		"channelName" :
+		{
+			"type" : "string",
+			"description" : "Name of room to which these messages have been set. For 'global' this is language, for 'match' and 'player' this is receiver UUID"
+		},
+
 		"messages" :
 		{
 			"type" : "array",

+ 6 - 7
config/schemas/lobbyProtocol/chatMessage.json

@@ -3,7 +3,7 @@
 	"$schema" : "http://json-schema.org/draft-06/schema",
 	"title" : "Lobby protocol: chatMessage",
 	"description" : "Sent by server to all players in lobby whenever new message is sent to game chat",
-	"required" : [ "type", "messageText", "accountID", "displayName", "roomMode", "roomName" ],
+	"required" : [ "type", "messageText", "accountID", "displayName", "channelType", "channelName" ],
 	"additionalProperties" : false,
 
 	"properties" : {
@@ -27,17 +27,16 @@
 			"type" : "string",
 			"description" : "Display name of account that have sent this message"
 		},
-		"roomMode" :
+		"channelType" :
 		{
 			"type" : "string",
-			"const" : "global",
-			"description" : "Type of room to which this message has been set. Right now can only be 'general'"
+			"enum" : [ "global", "match", "player" ],
+			"description" : "Type of room to which this message has been set."
 		},
-		"roomName" :
+		"channelName" :
 		{
 			"type" : "string",
-			"const" : "english",
-			"description" : "Name of room to which this message has been set. Right now only 'english' is used"
+			"description" : "Name of room to which this message has been set. For 'global' this is language, for 'match' and 'player' this is receiver UUID"
 		}
 	}
 }

+ 2 - 2
config/schemas/lobbyProtocol/loginSuccess.json → config/schemas/lobbyProtocol/clientLoginSuccess.json

@@ -1,7 +1,7 @@
 {
 	"type" : "object",
 	"$schema" : "http://json-schema.org/draft-06/schema",
-	"title" : "Lobby protocol: loginSuccess",
+	"title" : "Lobby protocol: clientLoginSuccess",
 	"description" : "Sent by server once player sucesfully logs into the lobby",
 	"required" : [ "type", "accountCookie", "displayName" ],
 	"additionalProperties" : false,
@@ -10,7 +10,7 @@
 		"type" :
 		{
 			"type" : "string",
-			"const" : "loginSuccess"
+			"const" : "clientLoginSuccess"
 		},
 		"accountCookie" :
 		{

+ 1 - 1
config/schemas/lobbyProtocol/clientProxyLogin.json

@@ -10,7 +10,7 @@
 		"type" :
 		{
 			"type" : "string",
-			"const" : "clientLogin"
+			"const" : "clientProxyLogin"
 		},
 		"accountID" :
 		{

+ 0 - 21
config/schemas/lobbyProtocol/declineInvite.json

@@ -1,21 +0,0 @@
-{
-	"type" : "object",
-	"$schema" : "http://json-schema.org/draft-06/schema",
-	"title" : "Lobby protocol: declineInvite",
-	"description" : "Sent by client when player declines invite to join a room",
-	"required" : [ "type", "gameRoomID" ],
-	"additionalProperties" : false,
-
-	"properties" : {
-		"type" :
-		{
-			"type" : "string",
-			"const" : "declineInvite"
-		},
-		"gameRoomID" :
-		{
-			"type" : "string",
-			"description" : "ID of game room to decline invite"
-		}
-	}
-}

+ 17 - 0
config/schemas/lobbyProtocol/gameStarted.json

@@ -0,0 +1,17 @@
+{
+	"type" : "object",
+	"$schema" : "http://json-schema.org/draft-06/schema",
+	"title" : "Lobby protocol: gameStarted",
+	"description" : "Sent by match server to lobby on starting gameplay",
+
+	"required" : [ "type" ],
+	"additionalProperties" : false,
+
+	"properties" : {
+		"type" :
+		{
+			"type" : "string",
+			"const" : "gameStarted"
+		}
+	}
+}

+ 90 - 0
config/schemas/lobbyProtocol/matchesHistory.json

@@ -0,0 +1,90 @@
+{
+	"type" : "object",
+	"$schema" : "http://json-schema.org/draft-06/schema",
+	"title" : "Lobby protocol: matchesHistory",
+	"description" : "Sent by server to initialized or update list of previous matches by player",
+	"required" : [ "type", "matchesHistory" ],
+	"additionalProperties" : false,
+
+	"properties" : {
+		"type" :
+		{
+			"type" : "string",
+			"const" : "matchesHistory"
+		},
+		"matchesHistory" :
+		{
+			"type" : "array",
+			"description" : "List of previously played matches",
+			"items" :
+			{
+				"type" : "object",
+				"additionalProperties" : false,
+				"required" : [ "gameRoomID", "hostAccountID", "hostAccountDisplayName", "description", "participants", "status", "playerLimit", "ageSeconds" ],
+				"properties" : {
+					"gameRoomID" :
+					{
+						"type" : "string",
+						"description" : "Unique ID of game room"
+					},
+					"hostAccountID" :
+					{
+						"type" : "string",
+						"description" : "ID of account that created and hosts this game room"
+					},
+					"hostAccountDisplayName" :
+					{
+						"type" : "string",
+						"description" : "Display name of account that created and hosts this game room"
+					},
+					"description" :
+					{
+						"type" : "string",
+						"description" : "Auto-generated description of this room"
+					},
+					"participants" :
+					{
+						"type" : "array",
+						"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" :
+					{
+						"type" : "string",
+						"enum" : [ "idle", "public", "private", "busy", "cancelled", "closed" ],
+						"description" : "Current status of game room"
+					},
+					"playerLimit" :
+					{
+						"type" : "number",
+						"minimum" : 1,
+						"maximum" : 8,
+						"description" : "Maximum number of players that can join this room, including host"
+					},
+					"ageSeconds" :
+					{
+						"type" : "number",
+						"description" : "Age of this room in seconds. For example, 10 means that this room was created 10 seconds ago"
+					}
+				}
+			}
+		}
+	}
+}

+ 28 - 0
config/schemas/lobbyProtocol/requestChatHistory.json

@@ -0,0 +1,28 @@
+{
+	"type" : "object",
+	"$schema" : "http://json-schema.org/draft-06/schema",
+	"title" : "Lobby protocol: requestChatHistory",
+	"description" : "Sent by client when player opens chat channel for which he has no history",
+	"required" : [ "type", "channelType", "channelName" ],
+	"additionalProperties" : false,
+
+	"properties" : {
+		"type" :
+		{
+			"type" : "string",
+			"const" : "requestChatHistory"
+		},
+
+		"channelType" :
+		{
+			"type" : "string",
+			"enum" : [ "global", "match", "player" ],
+			"description" : "Type of room to which these messages have been set."
+		},
+		"channelName" :
+		{
+			"type" : "string",
+			"description" : "Name of room to which these messages have been set. For 'global' this is language, for 'match' and 'player' this is receiver UUID"
+		}
+	}
+}

+ 13 - 2
config/schemas/lobbyProtocol/sendChatMessage.json

@@ -2,8 +2,8 @@
 	"type" : "object",
 	"$schema" : "http://json-schema.org/draft-06/schema",
 	"title" : "Lobby protocol: sendChatMessage",
-	"description" : "Sent by client when player requests lobby login",
-	"required" : [ "type", "messageText" ],
+	"description" : "Sent by client when player enters a chat message",
+	"required" : [ "type", "messageText", "channelType", "channelName" ],
 	"additionalProperties" : false,
 
 	"properties" : {
@@ -16,6 +16,17 @@
 		{
 			"type" : "string",
 			"description" : "Text of sent message"
+		},
+		"channelType" :
+		{
+			"type" : "string",
+			"enum" : [ "global", "match", "player" ],
+			"description" : "Type of room to which this message has been set."
+		},
+		"channelName" :
+		{
+			"type" : "string",
+			"description" : "Name of room to which this message has been set. For 'global' this is language, for 'match' and 'player' this is receiver UUID"
 		}
 	}
 }

+ 21 - 0
config/schemas/lobbyProtocol/serverLoginSuccess.json

@@ -0,0 +1,21 @@
+{
+	"type" : "object",
+	"$schema" : "http://json-schema.org/draft-06/schema",
+	"title" : "Lobby protocol: serverLoginSuccess",
+	"description" : "Sent by server once server sucesfully logs into the lobby",
+	"required" : [ "type", "accountCookie" ],
+	"additionalProperties" : false,
+
+	"properties" : {
+		"type" :
+		{
+			"type" : "string",
+			"const" : "serverLoginSuccess"
+		},
+		"accountCookie" :
+		{
+			"type" : "string",
+			"description" : "Security cookie that should be stored by server and used for future operations"
+		}
+	}
+}

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

+ 118 - 0
config/widgets/buttons/lobbyCreateRoom.json

@@ -0,0 +1,118 @@
+{
+	"normal" : {
+		"width": 249,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 249, "h": 32}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 249, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"pressed" : {
+		"width": 249,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 1, "y": 1, "w": 248, "h": 31}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 249, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 160 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] },
+
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] },
+				]
+			}
+		]
+	},
+	"blocked" : {
+		"width": 249,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 249, "h": 32}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 249, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"highlighted" : {
+		"width": 249,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 249, "h": 32}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 249, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+				]
+			}
+		]
+	},
+}

+ 118 - 0
config/widgets/buttons/lobbyHideWindow.json

@@ -0,0 +1,118 @@
+{
+	"normal" : {
+		"width": 150,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 150, "h": 32}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 150, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"pressed" : {
+		"width": 150,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 1, "y": 1, "w": 149, "h": 31}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 150, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 160 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] },
+
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] },
+				]
+			}
+		]
+	},
+	"blocked" : {
+		"width": 150,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 150, "h": 32}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 150, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"highlighted" : {
+		"width": 150,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 150, "h": 32}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 150, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+				]
+			}
+		]
+	},
+}

+ 118 - 0
config/widgets/buttons/lobbyJoinRoom.json

@@ -0,0 +1,118 @@
+{
+	"normal" : {
+		"width": 32,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 32}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 32 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"pressed" : {
+		"width": 32,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 1, "y": 1, "w": 31, "h": 31}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 160 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] },
+
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 32 ] },
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] },
+				]
+			}
+		]
+	},
+	"blocked" : {
+		"width": 32,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 32}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 32 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"highlighted" : {
+		"width": 32,
+		"height": 32,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 32}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 32},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+				]
+			}
+		]
+	},
+}

+ 118 - 0
config/widgets/buttons/lobbySendMessage.json

@@ -0,0 +1,118 @@
+{
+	"normal" : {
+		"width": 32,
+		"height": 24,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 24}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 24},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 32 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"pressed" : {
+		"width": 32,
+		"height": 24,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 1, "y": 1, "w": 31, "h": 23}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 24},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 160 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] },
+
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 32 ] },
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] },
+				]
+			}
+		]
+	},
+	"blocked" : {
+		"width": 32,
+		"height": 24,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 24}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 24},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 32 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"highlighted" : {
+		"width": 32,
+		"height": 24,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 24}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 32, "h": 24},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+				]
+			}
+		]
+	},
+}

+ 118 - 0
config/widgets/buttons/pregameInvitePlayers.json

@@ -0,0 +1,118 @@
+{
+	"normal" : {
+		"width": 157,
+		"height": 20,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 157, "h": 20}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 157, "h": 20},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"pressed" : {
+		"width": 157,
+		"height": 20,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 1, "y": 1, "w": 156, "h": 19}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 157, "h": 20},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 160 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] },
+
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] },
+				]
+			}
+		]
+	},
+	"blocked" : {
+		"width": 157,
+		"height": 20,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 157, "h": 20}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 157, "h": 20},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"highlighted" : {
+		"width": 157,
+		"height": 20,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 157, "h": 20}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 157, "h": 20},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+				]
+			}
+		]
+	},
+}

+ 118 - 0
config/widgets/buttons/pregameReturnToLobby.json

@@ -0,0 +1,118 @@
+{
+	"normal" : {
+		"width": 163,
+		"height": 20,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 163, "h": 20}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 163, "h": 20},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"pressed" : {
+		"width": 163,
+		"height": 20,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 1, "y": 1, "w": 162, "h": 19}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 163, "h": 20},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 160 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] },
+
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] },
+				]
+			}
+		]
+	},
+	"blocked" : {
+		"width": 163,
+		"height": 20,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 163, "h": 20}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 163, "h": 20},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"highlighted" : {
+		"width": 163,
+		"height": 20,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"color" : "blue",
+				"rect": {"x": 0, "y": 0, "w": 163, "h": 20}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 163, "h": 20},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 128 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+				]
+			}
+		]
+	},
+}

+ 59 - 47
config/widgets/lobbyWindow.json

@@ -51,37 +51,67 @@
 			"rect": {"x": 5, "y": 50, "w": 250, "h": 500}
 		},
 		{
+			"name" : "headerRoomList",
 			"type": "labelTitle",
-			"position": {"x": 15, "y": 53},
-			"text" : "Room List"
+			"position": {"x": 15, "y": 53}
 		},
 		{
-			"type" : "roomList",
+			"type" : "lobbyItemList",
 			"name" : "roomList",
-			"position" : { "x" : 7, "y" : 69 },
+			"itemType" : "room",
+			"position" : { "x" : 7, "y" : 68 },
 			"itemOffset" : { "x" : 0, "y" : 40 },
 			"sliderPosition" : { "x" : 230, "y" : 0 },
-			"sliderSize" : { "x" : 450, "y" : 450 }
+			"sliderSize" : { "x" : 480, "y" : 480 },
+			"visibleAmount" : 12
 		},
 		
 		{
 			"type": "areaFilled",
-			"rect": {"x": 270, "y": 50, "w": 150, "h": 540}
+			"rect": {"x": 270, "y": 50, "w": 150, "h": 140}
 		},
 		{
+			"name" : "headerChannelList",
 			"type": "labelTitle",
-			"position": {"x": 280, "y": 53},
-			"text" : "Channel List"
+			"position": {"x": 280, "y": 53}
+		},
+		{
+			"type" : "lobbyItemList",
+			"name" : "channelList",
+			"itemType" : "channel",
+			"position" : { "x" : 272, "y" : 68 },
+			"itemOffset" : { "x" : 0, "y" : 40 },
+			"visibleAmount" : 3
 		},
 		
+		{
+			"type": "areaFilled",
+			"rect": {"x": 270, "y": 210, "w": 150, "h": 380}
+		},
+		{
+			"name" : "headerMatchList",
+			"type": "labelTitle",
+			"position": {"x": 280, "y": 213}
+		},
+		{
+			"type" : "lobbyItemList",
+			"name" : "matchList",
+			"itemType" : "match",
+			"position" : { "x" : 272, "y" : 228 },
+			"itemOffset" : { "x" : 0, "y" : 40 },
+			"sliderPosition" : { "x" : 130, "y" : 0 },
+			"sliderSize" : { "x" : 360, "y" : 360 },
+			"visibleAmount" : 9
+		},
+
 		{
 			"type": "areaFilled",
 			"rect": {"x": 430, "y": 50, "w": 430, "h": 515}
 		},
 		{
+			"name" : "headerGameChat",
 			"type": "labelTitle",
-			"position": {"x": 440, "y": 53},
-			"text" : "Game Chat"
+			"position": {"x": 440, "y": 53}
 		},
 		{
 			"type": "textBox",
@@ -89,18 +119,19 @@
 			"font": "small",
 			"alignment": "left",
 			"color": "white",
-			"rect": {"x": 440, "y": 70, "w": 430, "h": 495}
+			"blueTheme" : true,
+			"rect": {"x": 440, "y": 68, "w": 418, "h": 495}
 		},
 		
 		{
 			"type": "areaFilled",
-			"rect": {"x": 430, "y": 565, "w": 395, "h": 25}
+			"rect": {"x": 430, "y": 565, "w": 397, "h": 25}
 		},
 		{
 			"name" : "messageInput",
 			"type": "textInput",
 			"alignment" : "left",
-			"rect": {"x": 440, "y": 568, "w": 375, "h": 20}
+			"rect": {"x": 440, "y": 568, "w": 377, "h": 20}
 		},
 		
 		{
@@ -108,41 +139,25 @@
 			"rect": {"x": 870, "y": 50, "w": 150, "h": 540}
 		},
 		{
+			"name": "headerAccountList",
 			"type": "labelTitle",
-			"position": {"x": 880, "y": 53},
-			"text" : "Account List"
+			"position": {"x": 880, "y": 53}
 		},
 		{
-			"type" : "accountList",
+			"type" : "lobbyItemList",
 			"name" : "accountList",
-			"position" : { "x" : 872, "y" : 69 },
+			"itemType" : "account",
+			"position" : { "x" : 872, "y" : 68 },
 			"itemOffset" : { "x" : 0, "y" : 40 },
 			"sliderPosition" : { "x" : 130, "y" : 0 },
-			"sliderSize" : { "x" : 520, "y" : 520 }
+			"sliderSize" : { "x" : 520, "y" : 520 },
+			"visibleAmount" : 13
 		},
 
 		{
 			"type": "button",
-			"position": {"x": 840, "y": 10},
-			"image": "settingsWindow/button80",
-			"help": "core.help.288",
-			"callback": "closeWindow",
-			"items":
-			[
-				{
-					"type": "label",
-					"font": "medium",
-					"alignment": "center",
-					"color": "yellow",
-					"text": "Leave"
-				}
-			]
-		},
-		
-		{
-			"type": "button",
-			"position": {"x": 940, "y": 10},
-			"image": "settingsWindow/button80",
+			"position": {"x": 870, "y": 10},
+			"image": "lobbyHideWindow",
 			"help": "core.help.288",
 			"callback": "closeWindow",
 			"hotkey": "globalCancel",
@@ -153,7 +168,7 @@
 					"font": "medium",
 					"alignment": "center",
 					"color": "yellow",
-					"text": "Close"
+					"text": "core.help.561.hover" // Back
 				}
 			]
 		},
@@ -161,18 +176,15 @@
 		{
 			"type": "button",
 			"position": {"x": 828, "y": 565},
-			"image": "settingsWindow/button32",
+			"image": "lobbySendMessage",
 			"help": "core.help.288",
 			"callback": "sendMessage",
 			"hotkey": "globalAccept",
 			"items":
 			[
 				{
-					"type": "label",
-					"font": "medium",
-					"alignment": "center",
-					"color": "yellow",
-					"text": ">"
+					"type": "picture",
+					"image": "lobby/iconSend"
 				}
 			]
 		},
@@ -180,7 +192,7 @@
 		{
 			"type": "button",
 			"position": {"x": 10, "y": 555},
-			"image": "settingsWindow/button190",
+			"image": "lobbyCreateRoom",
 			"help": "core.help.288",
 			"callback": "createGameRoom",
 			"items":
@@ -190,7 +202,7 @@
 					"font": "medium",
 					"alignment": "center",
 					"color": "yellow",
-					"text": "Create Room"
+					"text": "vcmi.lobby.room.create"
 				}
 			]
 		},

+ 2 - 2
docs/developers/Serialization.md

@@ -25,7 +25,7 @@ Additionally, if your class is part of one of registered object hierarchies (bas
 They are simply stored in a binary form, as in memory. Compatibility is ensued through the following means:
 
 - VCMI uses internally types that have constant, defined size (like int32_t - has 32 bits on all platforms)
-- serializer stores information about its endianess
+- serializer stores information about its endianness
 
 It's not "really" portable, yet it works properly across all platforms we currently support.
 
@@ -132,7 +132,7 @@ struct DLL_LINKAGE Rumor
 
 ### Common information
 
-Serializer classes provide iostream-like interface with operator `<<` for serialization and operator `>>` for deserialization. Serializer upon creation will retrieve/store some metadata (version number, endianess), so even if no object is actually serialized, some data will be passed.
+Serializer classes provide iostream-like interface with operator `<<` for serialization and operator `>>` for deserialization. Serializer upon creation will retrieve/store some metadata (version number, endianness), so even if no object is actually serialized, some data will be passed.
 
 ### Serialization to file
 

+ 2 - 5
lib/Languages.h

@@ -111,8 +111,7 @@ inline const auto & getLanguageList()
 
 inline const Options & getLanguageOptions(ELanguages language)
 {
-	assert(language < ELanguages::COUNT);
-	return getLanguageList()[static_cast<size_t>(language)];
+	return getLanguageList().at(static_cast<size_t>(language));
 }
 
 inline const Options & getLanguageOptions(const std::string & language)
@@ -121,9 +120,7 @@ inline const Options & getLanguageOptions(const std::string & language)
 		if(entry.identifier == language)
 			return entry;
 
-	static const Options emptyValue;
-	assert(0);
-	return emptyValue;
+	throw std::out_of_range("Language " + language + " does not exists!");
 }
 
 template<typename Numeric>

+ 12 - 0
lib/TextOperations.cpp

@@ -224,5 +224,17 @@ std::string TextOperations::getFormattedTimeLocal(std::time_t dt)
 	return vstd::getFormattedDateTime(dt, "%H:%M");
 }
 
+std::string TextOperations::getCurrentFormattedTimeLocal(std::chrono::seconds timeOffset)
+{
+	auto timepoint = std::chrono::system_clock::now() + timeOffset;
+	return TextOperations::getFormattedTimeLocal(std::chrono::system_clock::to_time_t(timepoint));
+}
+
+std::string TextOperations::getCurrentFormattedDateTimeLocal(std::chrono::seconds timeOffset)
+{
+	auto timepoint = std::chrono::system_clock::now() + timeOffset;
+	return TextOperations::getFormattedDateTimeLocal(std::chrono::system_clock::to_time_t(timepoint));
+}
+
 
 VCMI_LIB_NAMESPACE_END

+ 8 - 0
lib/TextOperations.h

@@ -60,8 +60,16 @@ namespace TextOperations
 	/// get formatted DateTime depending on the language selected
 	DLL_LINKAGE std::string getFormattedDateTimeLocal(std::time_t dt);
 
+	/// get formatted current DateTime depending on the language selected
+	/// timeOffset - optional parameter to modify current time by specified time in seconds
+	DLL_LINKAGE std::string getCurrentFormattedDateTimeLocal(std::chrono::seconds timeOffset = {});
+
 	/// get formatted time (without date)
 	DLL_LINKAGE std::string getFormattedTimeLocal(std::time_t dt);
+
+	/// get formatted time (without date)
+	/// timeOffset - optional parameter to modify current time by specified time in seconds
+	DLL_LINKAGE std::string getCurrentFormattedTimeLocal(std::chrono::seconds timeOffset = {});
 };
 
 

+ 1 - 1
lib/bonuses/Bonus.h

@@ -70,7 +70,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>
 	std::string stacking; // bonuses with the same stacking value don't stack (e.g. Angel/Archangel morale bonus)
 
 	CAddInfo additionalInfo;
-	BonusLimitEffect effectRange = BonusLimitEffect::NO_LIMIT; //if not NO_LIMIT, bonus will be omitted by default
+	BonusLimitEffect effectRange = BonusLimitEffect::NO_LIMIT;
 
 	TLimiterPtr limiter;
 	TPropagatorPtr propagator;

+ 1 - 3
lib/bonuses/BonusList.cpp

@@ -190,9 +190,7 @@ void BonusList::getBonuses(BonusList & out, const CSelector &selector, const CSe
 	out.reserve(bonuses.size());
 	for(const auto & b : bonuses)
 	{
-		//add matching bonuses that matches limit predicate or have NO_LIMIT if no given predicate
-		auto noFightLimit = b->effectRange == BonusLimitEffect::NO_LIMIT;
-		if(selector(b.get()) && ((!limit && noFightLimit) || ((bool)limit && limit(b.get()))))
+		if(selector(b.get()) && (!limit || ((bool)limit && limit(b.get()))))
 			out.push_back(b);
 	}
 }

+ 8 - 0
lib/constants/EntityIdentifiers.cpp

@@ -362,6 +362,10 @@ const HeroType * HeroTypeID::toEntity(const Services * services) const
 
 si32 SpellID::decode(const std::string & identifier)
 {
+	if (identifier == "preset")
+		return SpellID::PRESET;
+	if (identifier == "spellbook_preset")
+		return SpellID::SPELLBOOK_PRESET;
 	return resolveIdentifier("spell", identifier);
 }
 
@@ -369,6 +373,10 @@ std::string SpellID::encode(const si32 index)
 {
 	if (index == -1)
 		return "";
+	if (index == SpellID::PRESET)
+		return "preset";
+	if (index == SpellID::SPELLBOOK_PRESET)
+		return "spellbook_preset";
 	return VLC->spells()->getByIndex(index)->getJsonKey();
 }
 

+ 1 - 0
lib/mapObjects/CGCreature.cpp

@@ -171,6 +171,7 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const
 			//ask if player agrees to pay gold
 			BlockingDialog ynd(true,false);
 			ynd.player = h->tempOwner;
+			ynd.components.emplace_back(ComponentType::RESOURCE, GameResID(GameResID::GOLD), action);
 			std::string tmp = VLC->generaltexth->advobtxt[90];
 			boost::algorithm::replace_first(tmp, "%d", std::to_string(getStackCount(SlotID(0))));
 			boost::algorithm::replace_first(tmp, "%d", std::to_string(action));

+ 1 - 1
lib/minizip/minizip.c

@@ -395,7 +395,7 @@ int main(argc,argv)
                    ((argv[i][1]>='0') || (argv[i][1]<='9'))) &&
                   (strlen(argv[i]) == 2)))
             {
-                FILE * fin;
+                FILE * fin = NULL;
                 int size_read;
                 const char* filenameinzip = argv[i];
                 const char *savefilenameinzip;

+ 7 - 0
lib/minizip/mztools.c

@@ -285,6 +285,13 @@ uLong* bytesRecovered;
       }
     }
   } else {
+    if (fpZip)
+      fclose(fpZip);
+    if (fpOut)
+      fclose(fpOut);
+    if (fpOutCD)
+      fclose(fpOutCD);
+
     err = Z_STREAM_ERROR;
   }
   return err;

+ 2 - 0
lib/modding/IdentifierStorage.cpp

@@ -83,6 +83,8 @@ CIdentifierStorage::CIdentifierStorage()
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel5", 5);
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel6", 6);
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel7", 7);
+	registerObject(ModScope::scopeBuiltin(), "spell", "preset", SpellID::PRESET);
+	registerObject(ModScope::scopeBuiltin(), "spell", "spellbook_preset", SpellID::SPELLBOOK_PRESET);
 }
 
 void CIdentifierStorage::checkIdentifier(std::string & ID)

+ 1 - 1
lib/rmg/CMapGenerator.cpp

@@ -418,7 +418,7 @@ void CMapGenerator::fillZones()
 	}
 	auto grailZone = *RandomGeneratorUtil::nextItem(treasureZones, rand);
 
-	map->getMap(this).grailPos = *RandomGeneratorUtil::nextItem(grailZone->freePaths().getTiles(), rand);
+	map->getMap(this).grailPos = *RandomGeneratorUtil::nextItem(grailZone->freePaths()->getTiles(), rand);
 
 	logGlobal->info("Zones filled successfully");
 

+ 7 - 7
lib/rmg/CZonePlacer.cpp

@@ -858,7 +858,7 @@ void CZonePlacer::assignZones(CRandomGenerator * rand)
 	auto moveZoneToCenterOfMass = [width, height](const std::shared_ptr<Zone> & zone) -> void
 	{
 		int3 total(0, 0, 0);
-		auto tiles = zone->area().getTiles();
+		auto tiles = zone->area()->getTiles();
 		for(const auto & tile : tiles)
 		{
 			total += tile;
@@ -892,14 +892,14 @@ void CZonePlacer::assignZones(CRandomGenerator * rand)
 				{
 					distances.emplace_back(zone.second, static_cast<float>(pos.dist2dSQ(zone.second->getPos())));
 				}
-				boost::min_element(distances, compareByDistance)->first->area().add(pos); //closest tile belongs to zone
+				boost::min_element(distances, compareByDistance)->first->area()->add(pos); //closest tile belongs to zone
 			}
 		}
 	}
 
 	for(const auto & zone : zones)
 	{
-		if(zone.second->area().empty())
+		if(zone.second->area()->empty())
 			throw rmgException("Empty zone is generated, probably RMG template is inappropriate for map size");
 		
 		moveZoneToCenterOfMass(zone.second);
@@ -948,14 +948,14 @@ void CZonePlacer::assignZones(CRandomGenerator * rand)
 
 				//Tile closest to vertex belongs to zone
 				auto closestZone = boost::min_element(distances, simpleCompareByDistance)->first;
-				closestZone->area().add(pos);
+				closestZone->area()->add(pos);
 				map.setZoneID(pos, closestZone->getId());
 			}
 		}
 
 		for(const auto & zone : zonesOnLevel[level])
 		{
-			if(zone.second->area().empty())
+			if(zone.second->area()->empty())
 			{
 				// FIXME: Some vertices are duplicated, but it's not a source of problem
 				logGlobal->error("Zone %d at %s is empty, dumping Penrose tiling", zone.second->getId(), zone.second->getCenter().toString());
@@ -981,12 +981,12 @@ void CZonePlacer::assignZones(CRandomGenerator * rand)
 			{
 				auto discardTiles = collectDistantTiles(*zone.second, zone.second->getSize() + 1.f);
 				for(const auto & t : discardTiles)
-					zone.second->area().erase(t);
+					zone.second->area()->erase(t);
 			}
 
 			//make sure that terrain inside zone is not a rock
 
-			auto v = zone.second->getArea().getTilesVector();
+			auto v = zone.second->area()->getTilesVector();
 			map.getMapProxy()->drawTerrain(*rand, v, ETerrainId::SUBTERRANEAN);
 		}
 	}

+ 2 - 1
lib/rmg/Functions.cpp

@@ -26,7 +26,8 @@ VCMI_LIB_NAMESPACE_BEGIN
 rmg::Tileset collectDistantTiles(const Zone& zone, int distance)
 {
 	uint32_t distanceSq = distance * distance;
-	auto subarea = zone.getArea().getSubarea([&zone, distanceSq](const int3 & t)
+
+	auto subarea = zone.area()->getSubarea([&zone, distanceSq](const int3 & t)
 	{
 		return t.dist2dSQ(zone.getPos()) > distanceSq;
 	});

+ 38 - 23
lib/rmg/Zone.cpp

@@ -75,30 +75,49 @@ void Zone::setPos(const int3 &Pos)
 	pos = Pos;
 }
 
-const rmg::Area & Zone::getArea() const
+ThreadSafeProxy<rmg::Area> Zone::area()
 {
-	return dArea;
+	return ThreadSafeProxy<rmg::Area>(dArea, areaMutex);
 }
 
-rmg::Area & Zone::area()
+ThreadSafeProxy<const rmg::Area> Zone::area() const
 {
-	return dArea;
+	return ThreadSafeProxy<const rmg::Area>(dArea, areaMutex);
 }
 
-rmg::Area & Zone::areaPossible()
+ThreadSafeProxy<rmg::Area> Zone::areaPossible()
 {
-	//FIXME: make const, only modify via mutex-protected interface
-	return dAreaPossible;
+	return ThreadSafeProxy<rmg::Area>(dAreaPossible, areaMutex);
 }
 
-rmg::Area & Zone::areaUsed()
+ThreadSafeProxy<const rmg::Area> Zone::areaPossible() const
 {
-	return dAreaUsed;
+	return ThreadSafeProxy<const rmg::Area>(dAreaPossible, areaMutex);
+}
+
+ThreadSafeProxy<rmg::Area> Zone::freePaths()
+{
+	return ThreadSafeProxy<rmg::Area>(dAreaFree, areaMutex);
+}
+
+ThreadSafeProxy<const rmg::Area> Zone::freePaths() const
+{
+	return ThreadSafeProxy<const rmg::Area>(dAreaFree, areaMutex);
+}
+
+ThreadSafeProxy<rmg::Area> Zone::areaUsed()
+{
+	return ThreadSafeProxy<rmg::Area>(dAreaUsed, areaMutex);
+}
+
+ThreadSafeProxy<const rmg::Area> Zone::areaUsed() const
+{
+	return ThreadSafeProxy<const rmg::Area>(dAreaUsed, areaMutex);
 }
 
 void Zone::clearTiles()
 {
-	//Lock lock(mx);
+	Lock lock(areaMutex);
 	dArea.clear();
 	dAreaPossible.clear();
 	dAreaFree.clear();
@@ -107,7 +126,6 @@ void Zone::clearTiles()
 void Zone::initFreeTiles()
 {
 	rmg::Tileset possibleTiles;
-	//Lock lock(mx);
 	vstd::copy_if(dArea.getTiles(), vstd::set_inserter(possibleTiles), [this](const int3 &tile) -> bool
 	{
 		return map.isPossible(tile);
@@ -122,11 +140,6 @@ void Zone::initFreeTiles()
 	}
 }
 
-rmg::Area & Zone::freePaths()
-{
-	return dAreaFree;
-}
-
 FactionID Zone::getTownType() const
 {
 	return townType;
@@ -225,18 +238,14 @@ TModificators Zone::getModificators()
 void Zone::connectPath(const rmg::Path & path)
 ///connect current tile to any other free tile within zone
 {
-	dAreaPossible.subtract(path.getPathArea());
-	dAreaFree.unite(path.getPathArea());
+	areaPossible()->subtract(path.getPathArea());
+	freePaths()->unite(path.getPathArea());
 	for(const auto & t : path.getPathArea().getTilesVector())
 		map.setOccupied(t, ETileType::FREE);
 }
 
 void Zone::fractalize()
 {
-	rmg::Area clearedTiles(dAreaFree);
-	rmg::Area possibleTiles(dAreaPossible);
-	rmg::Area tilesToIgnore; //will be erased in this iteration
-
 	//Squared
 	float minDistance = 9 * 9;
 	float freeDistance = pos.z ? (10 * 10) : (9 * 9);
@@ -282,6 +291,13 @@ void Zone::fractalize()
 	freeDistance = freeDistance * marginFactor;
 	vstd::amax(freeDistance, 4 * 4);
 	logGlobal->trace("Zone %d: treasureValue %d blockDistance: %2.f, freeDistance: %2.f", getId(), treasureValue, blockDistance, freeDistance);
+
+	Lock lock(areaMutex);
+	// FIXME: Do not access Area directly
+
+	rmg::Area clearedTiles(dAreaFree);
+	rmg::Area possibleTiles(dAreaPossible);
+	rmg::Area tilesToIgnore; //will be erased in this iteration
 	
 	if(type != ETemplateZoneType::JUNCTION)
 	{
@@ -336,7 +352,6 @@ void Zone::fractalize()
 		}
 	}
 
-	Lock lock(areaMutex);
 	//Connect with free areas
 	auto areas = connectedAreas(clearedTiles, true);
 	for(auto & area : areas)

+ 46 - 6
lib/rmg/Zone.h

@@ -34,6 +34,43 @@ extern const std::function<bool(const int3 &)> AREA_NO_FILTER;
 
 typedef std::list<std::shared_ptr<Modificator>> TModificators;
 
+template<typename T>
+class ThreadSafeProxy
+{
+public:
+	ThreadSafeProxy(T& resource, boost::recursive_mutex& mutex)
+		: resourceRef(resource), lock(mutex) {}
+
+	T* operator->() { return &resourceRef; }
+	const T* operator->() const { return &resourceRef; }
+	T& operator*() { return resourceRef; }
+	const T& operator*() const { return resourceRef; }
+	T& get() {return resourceRef;}
+	const T& get() const {return resourceRef;}
+
+
+	T operator+(const T & other)
+	{
+		return resourceRef + other;
+	}
+
+	template <typename U>
+	T operator+(ThreadSafeProxy<U> & other)
+	{
+		return get() + other.get();
+	}
+
+	template <typename U>
+	T operator+(ThreadSafeProxy<U> && other)
+	{
+		return get() + other.get();
+	}
+
+private:
+	T& resourceRef;
+	std::lock_guard<boost::recursive_mutex> lock;
+};
+
 class Zone : public rmg::ZoneOptions
 {
 public:
@@ -48,11 +85,14 @@ public:
 	int3 getPos() const;
 	void setPos(const int3 &pos);
 	
-	const rmg::Area & getArea() const;
-	rmg::Area & area();
-	rmg::Area & areaPossible();
-	rmg::Area & freePaths();
-	rmg::Area & areaUsed();
+	ThreadSafeProxy<rmg::Area> area(); 
+	ThreadSafeProxy<const rmg::Area> area() const;
+	ThreadSafeProxy<rmg::Area> areaPossible();
+	ThreadSafeProxy<const rmg::Area> areaPossible() const;
+	ThreadSafeProxy<rmg::Area> freePaths();
+	ThreadSafeProxy<const rmg::Area> freePaths() const;
+	ThreadSafeProxy<rmg::Area> areaUsed();
+	ThreadSafeProxy<const rmg::Area> areaUsed() const;
 
 	void initFreeTiles();
 	void clearTiles();
@@ -89,7 +129,7 @@ public:
 	
 	CRandomGenerator & getRand();
 public:
-	boost::recursive_mutex areaMutex;
+	mutable boost::recursive_mutex areaMutex;
 	using Lock = boost::unique_lock<boost::recursive_mutex>;
 	
 protected:

+ 57 - 31
lib/rmg/modificators/ConnectionsPlacer.cpp

@@ -28,6 +28,23 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+std::pair<Zone::Lock, Zone::Lock> ConnectionsPlacer::lockZones(std::shared_ptr<Zone> otherZone)
+{
+	if (zone.getId() == otherZone->getId())
+		return {};
+
+	while (true)
+	{
+		auto lock1 = Zone::Lock(zone.areaMutex, boost::try_to_lock);
+		auto lock2 = Zone::Lock(otherZone->areaMutex, boost::try_to_lock);
+
+		if (lock1.owns_lock() && lock2.owns_lock())
+		{
+			return { std::move(lock1), std::move(lock2) };
+		}
+	}
+}
+
 void ConnectionsPlacer::process()
 {
 	collectNeighbourZones();
@@ -36,16 +53,13 @@ void ConnectionsPlacer::process()
 	{
 		for (auto& c : dConnections)
 		{
-			if (c.getZoneA() != zone.getId() && c.getZoneB() != zone.getId())
-				continue;
-
 			auto otherZone = map.getZones().at(c.getZoneB());
 			auto* cp = otherZone->getModificator<ConnectionsPlacer>();
 
 			while (cp)
 			{
-				RecursiveLock lock1(externalAccessMutex, boost::try_to_lock_t{});
-				RecursiveLock lock2(cp->externalAccessMutex, boost::try_to_lock_t{});
+				RecursiveLock lock1(externalAccessMutex, boost::try_to_lock);
+				RecursiveLock lock2(cp->externalAccessMutex, boost::try_to_lock);
 				if (lock1.owns_lock() && lock2.owns_lock())
 				{
 					if (!vstd::contains(dCompleted, c))
@@ -78,8 +92,15 @@ void ConnectionsPlacer::init()
 	POSTFUNCTION(RoadPlacer);
 	POSTFUNCTION(ObjectManager);
 	
+	auto id = zone.getId();
 	for(auto c : map.getMapGenOptions().getMapTemplate()->getConnectedZoneIds())
-		addConnection(c);
+	{
+		// Only consider connected zones
+		if (c.getZoneA() == id || c.getZoneB() == id)
+		{
+			addConnection(c);
+		}
+	}
 }
 
 void ConnectionsPlacer::addConnection(const rmg::ZoneConnection& connection)
@@ -106,6 +127,9 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 
 	bool directProhibited = vstd::contains(ourTerrain->prohibitTransitions, otherZone->getTerrainType())
 						 || vstd::contains(otherTerrain->prohibitTransitions, zone.getTerrainType());
+
+	auto lock = lockZones(otherZone);
+
 	auto directConnectionIterator = dNeighbourZones.find(otherZoneId);
 
 	if (directConnectionIterator != dNeighbourZones.end())
@@ -115,19 +139,19 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 			for (auto borderPos : directConnectionIterator->second)
 			{
 				//TODO: Refactor common code with direct connection
-				int3 potentialPos = zone.areaPossible().nearest(borderPos);
+				int3 potentialPos = zone.areaPossible()->nearest(borderPos);
 				assert(borderPos != potentialPos);
 
 				auto safetyGap = rmg::Area({ potentialPos });
 				safetyGap.unite(safetyGap.getBorderOutside());
-				safetyGap.intersect(zone.areaPossible());
+				safetyGap.intersect(zone.areaPossible().get());
 				if (!safetyGap.empty())
 				{
-					safetyGap.intersect(otherZone->areaPossible());
+					safetyGap.intersect(otherZone->areaPossible().get());
 					if (safetyGap.empty())
 					{
-						rmg::Area border(zone.getArea().getBorder());
-						border.unite(otherZone->getArea().getBorder());
+						rmg::Area border(zone.area()->getBorder());
+						border.unite(otherZone->area()->getBorder());
 
 						auto costFunction = [&border](const int3& s, const int3& d)
 						{
@@ -139,9 +163,9 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 						theirArea.add(potentialPos);
 						rmg::Path ourPath(ourArea);
 						rmg::Path theirPath(theirArea);
-						ourPath.connect(zone.freePaths());
+						ourPath.connect(zone.freePaths().get());
 						ourPath = ourPath.search(potentialPos, true, costFunction);
-						theirPath.connect(otherZone->freePaths());
+						theirPath.connect(otherZone->freePaths().get());
 						theirPath = theirPath.search(potentialPos, true, costFunction);
 
 						if (ourPath.valid() && theirPath.valid())
@@ -174,7 +198,7 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 		int3 roadNode;
 		for (auto borderPos : directConnectionIterator->second)
 		{
-			int3 potentialPos = zone.areaPossible().nearest(borderPos);
+			int3 potentialPos = zone.areaPossible()->nearest(borderPos);
 			assert(borderPos != potentialPos);
 
 			//Check if guard pos doesn't touch any 3rd zone. This would create unwanted passage to 3rd zone
@@ -200,10 +224,10 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 
 				auto safetyGap = rmg::Area({ potentialPos });
 				safetyGap.unite(safetyGap.getBorderOutside());
-				safetyGap.intersect(zone.areaPossible());
+				safetyGap.intersect(zone.areaPossible().get());
 				if (!safetyGap.empty())
 				{
-					safetyGap.intersect(otherZone->areaPossible());
+					safetyGap.intersect(otherZone->areaPossible().get());
 					if (safetyGap.empty())
 					{
 						float distanceToCenter = zone.getPos().dist2d(potentialPos) * otherZone->getPos().dist2d(potentialPos);
@@ -228,8 +252,8 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 			auto & manager = *zone.getModificator<ObjectManager>();
 			auto * monsterType = manager.chooseGuard(connection.getGuardStrength(), true);
 			
-			rmg::Area border(zone.getArea().getBorder());
-			border.unite(otherZone->getArea().getBorder());
+			rmg::Area border(zone.area()->getBorder());
+			border.unite(otherZone->area()->getBorder());
 			
 			auto costFunction = [&border](const int3 & s, const int3 & d)
 			{
@@ -241,9 +265,9 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 			theirArea.add(guardPos);
 			rmg::Path ourPath(ourArea);
 			rmg::Path theirPath(theirArea);
-			ourPath.connect(zone.freePaths());
+			ourPath.connect(zone.freePaths().get());
 			ourPath = ourPath.search(guardPos, true, costFunction);
-			theirPath.connect(otherZone->freePaths());
+			theirPath.connect(otherZone->freePaths().get());
 			theirPath = theirPath.search(guardPos, true, costFunction);
 			
 			if(ourPath.valid() && theirPath.valid())
@@ -262,8 +286,8 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 				else
 				{
 					//Update distances from empty passage, too
-					zone.areaPossible().erase(guardPos);
-					zone.freePaths().add(guardPos);
+					zone.areaPossible()->erase(guardPos);
+					zone.freePaths()->add(guardPos);
 					map.setOccupied(guardPos, ETileType::FREE);
 					manager.updateDistances(guardPos);
 					otherZone->getModificator<ObjectManager>()->updateDistances(guardPos);
@@ -318,8 +342,10 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 	{
 		int3 zShift(0, 0, zone.getPos().z - otherZone->getPos().z);
 
+		auto lock = lockZones(otherZone);
+
 		std::scoped_lock doubleLock(zone.areaMutex, otherZone->areaMutex);
-		auto commonArea = zone.areaPossible() * (otherZone->areaPossible() + zShift);
+		auto commonArea = zone.areaPossible().get() * (otherZone->areaPossible().get() + zShift);
 		if(!commonArea.empty())
 		{
 			assert(zone.getModificator<ObjectManager>());
@@ -339,7 +365,7 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 			bool guarded2 = managerOther.addGuard(rmgGate2, connection.getGuardStrength(), true);
 			int minDist = 3;
 			
-			rmg::Path path2(otherZone->area());
+			rmg::Path path2(otherZone->area().get());
 			rmg::Path path1 = manager.placeAndConnectObject(commonArea, rmgGate1, [this, minDist, &path2, &rmgGate1, &zShift, guarded2, &managerOther, &rmgGate2	](const int3 & tile)
 			{
 				auto ti = map.getTileInfo(tile);
@@ -403,7 +429,7 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 
 void ConnectionsPlacer::collectNeighbourZones()
 {
-	auto border = zone.area().getBorderOutside();
+	auto border = zone.area()->getBorderOutside();
 	for(const auto & i : border)
 	{
 		if(!map.isOnMap(i))
@@ -423,8 +449,8 @@ bool ConnectionsPlacer::shouldGenerateRoad(const rmg::ZoneConnection& connection
 
 void ConnectionsPlacer::createBorder()
 {
-	rmg::Area borderArea(zone.getArea().getBorder());
-	rmg::Area borderOutsideArea(zone.getArea().getBorderOutside());
+	rmg::Area borderArea(zone.area()->getBorder());
+	rmg::Area borderOutsideArea(zone.area()->getBorderOutside());
 	auto blockBorder = borderArea.getSubarea([this, &borderOutsideArea](const int3 & t)
 	{
 		auto tile = borderOutsideArea.nearest(t);
@@ -448,21 +474,21 @@ void ConnectionsPlacer::createBorder()
 		}
 	};
 
-	Zone::Lock lock(zone.areaMutex); //Protect from erasing same tiles again
+	auto areaPossible = zone.areaPossible();
 	for(const auto & tile : blockBorder.getTilesVector())
 	{
 		if(map.isPossible(tile))
 		{
 			map.setOccupied(tile, ETileType::BLOCKED);
-			zone.areaPossible().erase(tile);
+			areaPossible->erase(tile);
 		}
 
-		map.foreachDirectNeighbour(tile, [this](int3 &nearbyPos)
+		map.foreachDirectNeighbour(tile, [this, &areaPossible](int3 &nearbyPos)
 		{
 			if(map.isPossible(nearbyPos) && map.getZoneID(nearbyPos) == zone.getId())
 			{
 				map.setOccupied(nearbyPos, ETileType::BLOCKED);
-				zone.areaPossible().erase(nearbyPos);
+				areaPossible->erase(nearbyPos);
 			}
 		});
 	}

+ 1 - 0
lib/rmg/modificators/ConnectionsPlacer.h

@@ -33,6 +33,7 @@ public:
 	
 protected:
 	void collectNeighbourZones();
+	std::pair<Zone::Lock, Zone::Lock> lockZones(std::shared_ptr<Zone> otherZone);
 
 protected:
 	std::vector<rmg::ZoneConnection> dConnections;

+ 8 - 6
lib/rmg/modificators/Modificator.cpp

@@ -35,7 +35,7 @@ const std::string & Modificator::getName() const
 
 bool Modificator::isReady()
 {
-	Lock lock(mx, boost::try_to_lock_t{});
+	Lock lock(mx, boost::try_to_lock);
 	if (!lock.owns_lock())
 	{
 		return false;
@@ -63,7 +63,7 @@ bool Modificator::isReady()
 
 bool Modificator::isFinished()
 {
-	Lock lock(mx, boost::try_to_lock_t{});
+	Lock lock(mx, boost::try_to_lock);
 	if (!lock.owns_lock())
 	{
 		return false;
@@ -119,6 +119,8 @@ void Modificator::postfunction(Modificator * modificator)
 
 void Modificator::dump()
 {
+	// TODO: Refactor to lock zone area only once
+
 	std::ofstream out(boost::str(boost::format("seed_%d_modzone_%d_%s.txt") % generator.getRandomSeed() % zone.getId() % getName()));
 	int levels = map.levels();
 	int width =  map.width();
@@ -140,13 +142,13 @@ void Modificator::dump()
 
 char Modificator::dump(const int3 & t)
 {
-	if(zone.freePaths().contains(t))
+	if(zone.freePaths()->contains(t))
 		return '.'; //free path
-	if(zone.areaPossible().contains(t))
+	if(zone.areaPossible()->contains(t))
 		return ' '; //possible
-	if(zone.areaUsed().contains(t))
+	if(zone.areaUsed()->contains(t))
 		return 'U'; //used
-	if(zone.area().contains(t))
+	if(zone.area()->contains(t))
 	{
 		if(map.shouldBeBlocked(t))
 			return '#'; //obstacle

+ 75 - 39
lib/rmg/modificators/ObjectManager.cpp

@@ -40,7 +40,30 @@ void ObjectManager::process()
 void ObjectManager::init()
 {
 	DEPENDENCY(WaterAdopter);
-	DEPENDENCY_ALL(ConnectionsPlacer); //Monoliths can be placed by other zone, too
+
+	//Monoliths can be placed by other zone, too
+	// Consider only connected zones
+	auto id = zone.getId();
+	std::set<TRmgTemplateZoneId> connectedZones;
+	for(auto c : map.getMapGenOptions().getMapTemplate()->getConnectedZoneIds())
+	{
+		// Only consider connected zones
+		if (c.getZoneA() == id || c.getZoneB() == id)
+		{
+			connectedZones.insert(c.getZoneA());
+			connectedZones.insert(c.getZoneB());
+		}
+	}
+	auto zones = map.getZones();
+	for (auto zoneId : connectedZones)
+	{		
+		auto * cp = zones.at(zoneId)->getModificator<ConnectionsPlacer>();
+		if (cp)
+		{
+			dependency(cp);
+		}
+	}
+
 	DEPENDENCY(TownPlacer); //Only secondary towns
 	DEPENDENCY(MinePlacer);
 	POSTFUNCTION(RoadPlacer);
@@ -49,9 +72,11 @@ void ObjectManager::init()
 
 void ObjectManager::createDistancesPriorityQueue()
 {
+	const auto tiles = zone.areaPossible()->getTilesVector();
+
 	RecursiveLock lock(externalAccessMutex);
 	tilesByDistance.clear();
-	for(const auto & tile : zone.areaPossible().getTilesVector())
+	for(const auto & tile : tiles)
 	{
 		tilesByDistance.push(std::make_pair(tile, map.getNearestObjectDistance(tile)));
 	}
@@ -93,9 +118,19 @@ void ObjectManager::updateDistances(const int3 & pos)
 
 void ObjectManager::updateDistances(std::function<ui32(const int3 & tile)> distanceFunction)
 {
-	RecursiveLock lock(externalAccessMutex);
+	// Workaround to avoid deadlock when accessed from other zone
+	RecursiveLock lock(zone.areaMutex, boost::try_to_lock);
+	if (!lock.owns_lock())
+	{
+		// Unsolvable problem of mutual access
+		return;
+	}
+
+	const auto tiles = zone.areaPossible()->getTilesVector();
+	//RecursiveLock lock(externalAccessMutex);
 	tilesByDistance.clear();
-	for (const auto & tile : zone.areaPossible().getTilesVector()) //don't need to mark distance for not possible tiles
+
+	for (const auto & tile : tiles) //don't need to mark distance for not possible tiles
 	{
 		ui32 d = distanceFunction(tile);
 		map.setNearestObjectDistance(tile, std::min(static_cast<float>(d), map.getNearestObjectDistance(tile)));
@@ -145,7 +180,10 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object
 	
 	if(optimizer & OptimizeType::DISTANCE)
 	{
+		// Do not add or remove tiles while we iterate on them
+		//RecursiveLock lock(externalAccessMutex);
 		auto open = tilesByDistance;
+
 		while(!open.empty())
 		{
 			auto node = open.top();
@@ -235,7 +273,6 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object
 
 rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg::Object & obj, si32 min_dist, bool isGuarded, bool onlyStraight, OptimizeType optimizer) const
 {
-	RecursiveLock lock(externalAccessMutex);
 	return placeAndConnectObject(searchArea, obj, [this, min_dist, &obj](const int3 & tile)
 	{
 		float bestDistance = 10e9;
@@ -372,7 +409,7 @@ bool ObjectManager::createMonoliths()
 		bool guarded = addGuard(rmgObject, objInfo.guardStrength, true);
 
 		Zone::Lock lock(zone.areaMutex);
-		auto path = placeAndConnectObject(zone.areaPossible(), rmgObject, 3, guarded, false, OptimizeType::DISTANCE);
+		auto path = placeAndConnectObject(zone.areaPossible().get(), rmgObject, 3, guarded, false, OptimizeType::DISTANCE);
 		
 		if(!path.valid())
 		{
@@ -395,7 +432,7 @@ bool ObjectManager::createRequiredObjects()
 {
 	logGlobal->trace("Creating required objects");
 	
-	//RecursiveLock lock(externalAccessMutex); //Why could requiredObjects be modified during the loop?
+	RecursiveLock lock(externalAccessMutex); //In case someone adds more objects
 	for(const auto & objInfo : requiredObjects)
 	{
 		rmg::Object rmgObject(*objInfo.obj);
@@ -403,7 +440,7 @@ bool ObjectManager::createRequiredObjects()
 		bool guarded = addGuard(rmgObject, objInfo.guardStrength, (objInfo.obj->ID == Obj::MONOLITH_TWO_WAY));
 
 		Zone::Lock lock(zone.areaMutex);
-		auto path = placeAndConnectObject(zone.areaPossible(), rmgObject, 3, guarded, false, OptimizeType::DISTANCE);
+		auto path = placeAndConnectObject(zone.areaPossible().get(), rmgObject, 3, guarded, false, OptimizeType::DISTANCE);
 		
 		if(!path.valid())
 		{
@@ -421,7 +458,7 @@ bool ObjectManager::createRequiredObjects()
 			
 			rmg::Object rmgNearObject(*nearby.obj);
 			rmg::Area possibleArea(rmgObject.instances().front()->getBlockedArea().getBorderOutside());
-			possibleArea.intersect(zone.areaPossible());
+			possibleArea.intersect(zone.areaPossible().get());
 			if(possibleArea.empty())
 			{
 				rmgNearObject.clear();
@@ -436,12 +473,11 @@ bool ObjectManager::createRequiredObjects()
 	for(const auto & objInfo : closeObjects)
 	{
 		Zone::Lock lock(zone.areaMutex);
-		auto possibleArea = zone.areaPossible();
 
 		rmg::Object rmgObject(*objInfo.obj);
 		rmgObject.setTemplate(zone.getTerrainType(), zone.getRand());
 		bool guarded = addGuard(rmgObject, objInfo.guardStrength, (objInfo.obj->ID == Obj::MONOLITH_TWO_WAY));
-		auto path = placeAndConnectObject(zone.areaPossible(), rmgObject,
+		auto path = placeAndConnectObject(zone.areaPossible().get(), rmgObject,
 										  [this, &rmgObject](const int3 & tile)
 		{
 			float dist = rmgObject.getArea().distanceSqr(zone.getPos());
@@ -470,15 +506,15 @@ bool ObjectManager::createRequiredObjects()
 
 		rmg::Object rmgNearObject(*nearby.obj);
 		std::set<int3> blockedArea = targetObject->getBlockedPos();
-		rmg::Area possibleArea(rmg::Area(rmg::Tileset(blockedArea.begin(), blockedArea.end())).getBorderOutside());
-		possibleArea.intersect(zone.areaPossible());
-		if(possibleArea.empty())
+		rmg::Area areaForObject(rmg::Area(rmg::Tileset(blockedArea.begin(), blockedArea.end())).getBorderOutside());
+		areaForObject.intersect(zone.areaPossible().get());
+		if(areaForObject.empty())
 		{
 			rmgNearObject.clear();
 			continue;
 		}
 
-		rmgNearObject.setPosition(*RandomGeneratorUtil::nextItem(possibleArea.getTiles(), zone.getRand()));
+		rmgNearObject.setPosition(*RandomGeneratorUtil::nextItem(areaForObject.getTiles(), zone.getRand()));
 		placeObject(rmgNearObject, false, false);
 		auto path = zone.searchPath(rmgNearObject.getVisitablePosition(), false);
 		if (path.valid())
@@ -515,8 +551,6 @@ bool ObjectManager::createRequiredObjects()
 
 void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateDistance, bool createRoad/* = false*/)
 {	
-	//object.finalize(map);
-
 	if (object.instances().size() == 1 && object.instances().front()->object().ID == Obj::MONSTER)
 	{
 		//Fix for HoTA offset - lonely guards
@@ -539,31 +573,33 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD
 	}
 	object.finalize(map, zone.getRand());
 
-	Zone::Lock lock(zone.areaMutex);
-	zone.areaPossible().subtract(object.getArea());
-	bool keepVisitable = zone.freePaths().contains(object.getVisitablePosition());
-	zone.freePaths().subtract(object.getArea()); //just to avoid areas overlapping
-	if(keepVisitable)
-		zone.freePaths().add(object.getVisitablePosition());
-	zone.areaUsed().unite(object.getArea());
-	zone.areaUsed().erase(object.getVisitablePosition());
-
-	if(guarded) //We assume the monster won't be guarded
-	{
-		auto guardedArea = object.instances().back()->getAccessibleArea();
-		guardedArea.add(object.instances().back()->getVisitablePosition());
-		auto areaToBlock = object.getAccessibleArea(true);
-		areaToBlock.subtract(guardedArea);
-		zone.areaPossible().subtract(areaToBlock);
-		for(const auto & i : areaToBlock.getTilesVector())
-			if(map.isOnMap(i) && map.isPossible(i))
-				map.setOccupied(i, ETileType::BLOCKED);
+	{
+		Zone::Lock lock(zone.areaMutex);
+
+		zone.areaPossible()->subtract(object.getArea());
+		bool keepVisitable = zone.freePaths()->contains(object.getVisitablePosition());
+		zone.freePaths()->subtract(object.getArea()); //just to avoid areas overlapping
+		if(keepVisitable)
+			zone.freePaths()->add(object.getVisitablePosition());
+		zone.areaUsed()->unite(object.getArea());
+		zone.areaUsed()->erase(object.getVisitablePosition());
+
+		if(guarded) //We assume the monster won't be guarded
+		{
+			auto guardedArea = object.instances().back()->getAccessibleArea();
+			guardedArea.add(object.instances().back()->getVisitablePosition());
+			auto areaToBlock = object.getAccessibleArea(true);
+			areaToBlock.subtract(guardedArea);
+			zone.areaPossible()->subtract(areaToBlock);
+			for(const auto & i : areaToBlock.getTilesVector())
+				if(map.isOnMap(i) && map.isPossible(i))
+					map.setOccupied(i, ETileType::BLOCKED);
+		}
 	}
-	lock.unlock();
-	
+
 	if (updateDistance)
 	{
-		//Update distances in every adjacent zone in case of wide connection
+		//Update distances in every adjacent zone (including this one) in case of wide connection
 
 		std::set<TRmgTemplateZoneId> adjacentZones;
 		auto objectArea = object.getArea();

+ 16 - 13
lib/rmg/modificators/ObstaclePlacer.cpp

@@ -37,22 +37,25 @@ void ObstaclePlacer::process()
 	collectPossibleObstacles(zone.getTerrainType());
 	
 	{
-		Zone::Lock lock(zone.areaMutex);
-		blockedArea = zone.area().getSubarea([this](const int3& t)
-			{
-				return map.shouldBeBlocked(t);
-			});
-		blockedArea.subtract(zone.areaUsed());
-		zone.areaPossible().subtract(blockedArea);
+		auto area = zone.area();
+		auto areaPossible = zone.areaPossible();
+		auto areaUsed = zone.areaUsed();
+
+		blockedArea = area->getSubarea([this](const int3& t)
+		{
+			return map.shouldBeBlocked(t);
+		});
+		blockedArea.subtract(*areaUsed);
+		areaPossible->subtract(blockedArea);
 
-		prohibitedArea = zone.freePaths() + zone.areaUsed() + manager->getVisitableArea();
+		prohibitedArea = zone.freePaths() + areaUsed + manager->getVisitableArea();
 
 		//Progressively block tiles, but make sure they don't seal any gap between blocks
 		rmg::Area toBlock;
 		do
 		{
 			toBlock.clear();
-			for (const auto& tile : zone.areaPossible().getTilesVector())
+			for (const auto& tile : areaPossible->getTilesVector())
 			{
 				rmg::Area neighbors;
 				rmg::Area t;
@@ -76,7 +79,7 @@ void ObstaclePlacer::process()
 					toBlock.add(tile);
 				}
 			}
-			zone.areaPossible().subtract(toBlock);
+			areaPossible->subtract(toBlock);
 			for (const auto& tile : toBlock.getTilesVector())
 			{
 				map.setOccupied(tile, ETileType::BLOCKED);
@@ -84,7 +87,7 @@ void ObstaclePlacer::process()
 
 		} while (!toBlock.empty());
 
-		prohibitedArea.unite(zone.areaPossible());
+		prohibitedArea.unite(areaPossible.get());
 	}
 
 	auto objs = createObstacles(zone.getRand(), map.mapInstance->cb);
@@ -119,7 +122,7 @@ void ObstaclePlacer::placeObject(rmg::Object & object, std::set<CGObjectInstance
 
 std::pair<bool, bool> ObstaclePlacer::verifyCoverage(const int3 & t) const
 {
-	return {map.shouldBeBlocked(t), zone.areaPossible().contains(t)};
+	return {map.shouldBeBlocked(t), zone.areaPossible()->contains(t)};
 }
 
 void ObstaclePlacer::postProcess(const rmg::Object & object)
@@ -141,7 +144,7 @@ bool ObstaclePlacer::isProhibited(const rmg::Area & objArea) const
 	if(prohibitedArea.overlap(objArea))
 		return true;
 	 
-	if(!zone.area().contains(objArea))
+	if(!zone.area()->contains(objArea))
 		return true;
 	
 	return false;

+ 14 - 9
lib/rmg/modificators/RiverPlacer.cpp

@@ -106,14 +106,14 @@ char RiverPlacer::dump(const int3 & t)
 		return '2';
 	if(source.contains(t))
 		return '1';
-	if(zone.area().contains(t))
+	if(zone.area()->contains(t))
 		return ' ';
 	return '?';
 }
 
 void RiverPlacer::addRiverNode(const int3 & node)
 {
-	assert(zone.area().contains(node));
+	assert(zone.area()->contains(node));
 	riverNodes.insert(node);
 }
 
@@ -140,14 +140,17 @@ void RiverPlacer::prepareHeightmap()
 		roads.unite(m->getRoads());
 	}
 
-	for(const auto & t : zone.area().getTilesVector())
+	auto area = zone.area();
+	auto areaUsed = zone.areaUsed();
+
+	for(const auto & t : area->getTilesVector())
 	{
 		heightMap[t] = zone.getRand().nextInt(5);
 		
 		if(roads.contains(t))
 			heightMap[t] += 30.f;
 		
-		if(zone.areaUsed().contains(t))
+		if(areaUsed->contains(t))
 			heightMap[t] += 1000.f;
 	}
 	
@@ -157,7 +160,7 @@ void RiverPlacer::prepareHeightmap()
 		for(int i = 0; i < map.width(); i += 2)
 		{
 			int3 t{i, j, zone.getPos().z};
-			if(zone.area().contains(t))
+			if(area->contains(t))
 				heightMap[t] += 10.f;
 		}
 	}
@@ -167,9 +170,10 @@ void RiverPlacer::preprocess()
 {
 	rmg::Area outOfMapTiles;
 	std::map<TRmgTemplateZoneId, rmg::Area> neighbourZonesTiles;
-	rmg::Area borderArea(zone.getArea().getBorder());
+
+	rmg::Area borderArea(zone.area()->getBorder());
 	TRmgTemplateZoneId connectedToWaterZoneId = -1;
-	for(const auto & t : zone.getArea().getBorderOutside())
+	for(const auto & t : zone.area()->getBorderOutside())
 	{
 		if(!map.isOnMap(t))
 		{
@@ -182,6 +186,7 @@ void RiverPlacer::preprocess()
 			neighbourZonesTiles[map.getZoneID(t)].add(t);
 		}
 	}
+
 	rmg::Area outOfMapInternal(outOfMapTiles.getBorderOutside());
 	outOfMapInternal.intersect(borderArea);
 	
@@ -297,7 +302,7 @@ void RiverPlacer::preprocess()
 	prepareHeightmap();
 	
 	//decorative river
-	if(!sink.empty() && !source.empty() && riverNodes.empty() && !zone.areaPossible().empty())
+	if(!sink.empty() && !source.empty() && riverNodes.empty() && !zone.areaPossible()->empty())
 	{
 		addRiverNode(*RandomGeneratorUtil::nextItem(source.getTilesVector(), zone.getRand()));
 	}
@@ -347,7 +352,7 @@ void RiverPlacer::connectRiver(const int3 & tile)
 		return cost;
 	};
 	
-	auto availableArea = zone.area() - prohibit;
+	auto availableArea = zone.area().get() - prohibit;
 	
 	rmg::Path pathToSource(availableArea);
 	pathToSource.connect(source);

+ 2 - 2
lib/rmg/modificators/RoadPlacer.cpp

@@ -144,8 +144,8 @@ void RoadPlacer::drawRoads(bool secondary)
 			return !terrain->isPassable() || !terrain->isLand();
 		});
 
-		zone.areaPossible().subtract(roads);
-		zone.freePaths().unite(roads);
+		zone.areaPossible()->subtract(roads);
+		zone.freePaths()->unite(roads);
 	}
 
 	if(!generator.getMapGenOptions().isRoadEnabled())

+ 1 - 1
lib/rmg/modificators/RockFiller.cpp

@@ -68,7 +68,7 @@ char RockFiller::dump(const int3 & t)
 {
 	if(!map.getTile(t).terType->isPassable())
 	{
-		return zone.area().contains(t) ? 'R' : 'E';
+		return zone.area()->contains(t) ? 'R' : 'E';
 	}
 	return Modificator::dump(t);
 }

+ 14 - 11
lib/rmg/modificators/RockPlacer.cpp

@@ -40,7 +40,7 @@ void RockPlacer::blockRock()
 		accessibleArea.unite(m->getVisitableArea());
 
 	//negative approach - create rock tiles first, then make sure all accessible tiles have no rock
-	rockArea = zone.area().getSubarea([this](const int3 & t)
+	rockArea = zone.area()->getSubarea([this](const int3 & t)
 	{
 		return map.shouldBeBlocked(t);
 	});
@@ -48,17 +48,20 @@ void RockPlacer::blockRock()
 
 void RockPlacer::postProcess()
 {
-	Zone::Lock lock(zone.areaMutex);
-	//Finally mark rock tiles as occupied, spawn no obstacles there
-	rockArea = zone.area().getSubarea([this](const int3 & t)
 	{
-		return !map.getTile(t).terType->isPassable();
-	});
-	
-	zone.areaUsed().unite(rockArea);
-	zone.areaPossible().subtract(rockArea);
+		Zone::Lock lock(zone.areaMutex);
+		//Finally mark rock tiles as occupied, spawn no obstacles there
+		rockArea = zone.area()->getSubarea([this](const int3 & t)
+		{
+			return !map.getTile(t).terType->isPassable();
+		});
+		
+		zone.areaUsed()->unite(rockArea);
+		zone.areaPossible()->subtract(rockArea);
+	}
+
+	//RecursiveLock lock(externalAccessMutex);
 
-	//TODO: Might need mutex here as well
 	if(auto * m = zone.getModificator<RiverPlacer>())
 		m->riverProhibit().unite(rockArea);
 	if(auto * m = zone.getModificator<RoadPlacer>())
@@ -84,7 +87,7 @@ char RockPlacer::dump(const int3 & t)
 {
 	if(!map.getTile(t).terType->isPassable())
 	{
-		return zone.area().contains(t) ? 'R' : 'E';
+		return zone.area()->contains(t) ? 'R' : 'E';
 	}
 	return Modificator::dump(t);
 }

+ 1 - 1
lib/rmg/modificators/TerrainPainter.cpp

@@ -28,7 +28,7 @@ void TerrainPainter::process()
 {
 	initTerrainType();
 
-	auto v = zone.getArea().getTilesVector();
+	auto v = zone.area()->getTilesVector();
 	mapProxy->drawTerrain(zone.getRand(), v, zone.getTerrainType());
 }
 

+ 3 - 3
lib/rmg/modificators/TownPlacer.cpp

@@ -146,7 +146,7 @@ int3 TownPlacer::placeMainTown(ObjectManager & manager, CGTownInstance & town)
 	int3 position(-1, -1, -1);
 	{
 		Zone::Lock lock(zone.areaMutex);
-		position = manager.findPlaceForObject(zone.areaPossible(), rmgObject, [this](const int3& t)
+		position = manager.findPlaceForObject(zone.areaPossible().get(), rmgObject, [this](const int3& t)
 			{
 				float distance = zone.getPos().dist2dSQ(t);
 				return 100000.f - distance; //some big number
@@ -169,8 +169,8 @@ void TownPlacer::cleanupBoundaries(const rmg::Object & rmgObject)
 			if (map.isOnMap(t))
 			{
 				map.setOccupied(t, ETileType::FREE);
-				zone.areaPossible().erase(t);
-				zone.freePaths().add(t);
+				zone.areaPossible()->erase(t);
+				zone.freePaths()->add(t);
 			}
 		}
 	}

+ 33 - 35
lib/rmg/modificators/TreasurePlacer.cpp

@@ -852,11 +852,7 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 		return oi1.value < oi2.value;
 	});
 
-	size_t size = 0;
-	{
-		Zone::Lock lock(zone.areaMutex);
-		size = zone.getArea().getTilesVector().size();
-	}
+	const size_t size = zone.area()->getTilesVector().size();
 
 	int totalDensity = 0;
 
@@ -920,42 +916,44 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 
 			auto path = rmg::Path::invalid();
 
-			Zone::Lock lock(zone.areaMutex); //We are going to subtract this area
-			auto possibleArea = zone.areaPossible();
-			possibleArea.erase_if([this, &minDistance](const int3& tile) -> bool
 			{
-				auto ti = map.getTileInfo(tile);
-				return (ti.getNearestObjectDistance() < minDistance);
-			});
+				Zone::Lock lock(zone.areaMutex); //We are going to subtract this area
 
-			if (guarded)
-			{
-				path = manager.placeAndConnectObject(possibleArea, rmgObject, [this, &rmgObject, &minDistance, &manager](const int3& tile)
-					{
-						float bestDistance = 10e9;
-						for (const auto& t : rmgObject.getArea().getTilesVector())
+				auto searchArea = zone.areaPossible().get();
+				searchArea.erase_if([this, &minDistance](const int3& tile) -> bool
+				{
+					auto ti = map.getTileInfo(tile);
+					return (ti.getNearestObjectDistance() < minDistance);
+				});
+
+				if (guarded)
+				{
+					path = manager.placeAndConnectObject(searchArea, rmgObject, [this, &rmgObject, &minDistance, &manager](const int3& tile)
 						{
-							float distance = map.getTileInfo(t).getNearestObjectDistance();
-							if (distance < minDistance)
+							float bestDistance = 10e9;
+							for (const auto& t : rmgObject.getArea().getTilesVector())
+							{
+								float distance = map.getTileInfo(t).getNearestObjectDistance();
+								if (distance < minDistance)
+									return -1.f;
+								else
+									vstd::amin(bestDistance, distance);
+							}
+
+							const auto & guardedArea = rmgObject.instances().back()->getAccessibleArea();
+							const auto areaToBlock = rmgObject.getAccessibleArea(true) - guardedArea;
+
+							if (zone.freePaths()->overlap(areaToBlock) || manager.getVisitableArea().overlap(areaToBlock))
 								return -1.f;
-							else
-								vstd::amin(bestDistance, distance);
-						}
 
-						const auto & guardedArea = rmgObject.instances().back()->getAccessibleArea();
-						const auto areaToBlock = rmgObject.getAccessibleArea(true) - guardedArea;
-
-						if (zone.freePaths().overlap(areaToBlock) || manager.getVisitableArea().overlap(areaToBlock))
-							return -1.f;
-
-						return bestDistance;
-					}, guarded, false, ObjectManager::OptimizeType::BOTH);
-			}
-			else
-			{
-				path = manager.placeAndConnectObject(possibleArea, rmgObject, minDistance, guarded, false, ObjectManager::OptimizeType::DISTANCE);
+							return bestDistance;
+						}, guarded, false, ObjectManager::OptimizeType::BOTH);
+				}
+				else
+				{
+					path = manager.placeAndConnectObject(searchArea, rmgObject, minDistance, guarded, false, ObjectManager::OptimizeType::DISTANCE);
+				}
 			}
-			lock.unlock();
 
 			if (path.valid())
 			{

+ 7 - 7
lib/rmg/modificators/WaterAdopter.cpp

@@ -43,13 +43,13 @@ void WaterAdopter::createWater(EWaterContent::EWaterContent waterContent)
 	if(waterContent == EWaterContent::NONE || zone.isUnderground() || zone.getType() == ETemplateZoneType::WATER)
 		return; //do nothing
 	
-	distanceMap = zone.area().computeDistanceMap(reverseDistanceMap);
+	distanceMap = zone.area()->computeDistanceMap(reverseDistanceMap);
 	
 	//add border tiles as water for ISLANDS
 	if(waterContent == EWaterContent::ISLANDS)
 	{
 		waterArea.unite(collectDistantTiles(zone, zone.getSize() + 1));
-		waterArea.unite(zone.area().getBorder());
+		waterArea.unite(zone.area()->getBorder());
 	}
 	
 	//protect some parts from water for NORMAL
@@ -199,7 +199,7 @@ void WaterAdopter::createWater(EWaterContent::EWaterContent waterContent)
 		std::vector<int3> groundCoast;
 		map.foreachDirectNeighbour(tile, [this, &groundCoast](const int3 & t)
 		{
-			if(!waterArea.contains(t) && zone.area().contains(t)) //for ground tiles of same zone
+			if(!waterArea.contains(t) && zone.area()->contains(t)) //for ground tiles of same zone
 			{
 				groundCoast.push_back(t);
 			}
@@ -223,12 +223,12 @@ void WaterAdopter::createWater(EWaterContent::EWaterContent waterContent)
 	
 	{
 		Zone::Lock waterLock(map.getZones()[waterZoneId]->areaMutex);
-		map.getZones()[waterZoneId]->area().unite(waterArea);
+		map.getZones()[waterZoneId]->area()->unite(waterArea);
 	}
 	Zone::Lock lock(zone.areaMutex);
-	zone.area().subtract(waterArea);
-	zone.areaPossible().subtract(waterArea);
-	distanceMap = zone.area().computeDistanceMap(reverseDistanceMap);
+	zone.area()->subtract(waterArea);
+	zone.areaPossible()->subtract(waterArea);
+	distanceMap = zone.area()->computeDistanceMap(reverseDistanceMap);
 }
 
 void WaterAdopter::setWaterZone(TRmgTemplateZoneId water)

+ 25 - 19
lib/rmg/modificators/WaterProxy.cpp

@@ -34,45 +34,51 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 void WaterProxy::process()
 {
-	for(const auto & t : zone.area().getTilesVector())
+	auto area = zone.area();
+
+	for(const auto & t : area->getTilesVector())
 	{
 		map.setZoneID(t, zone.getId());
 		map.setOccupied(t, ETileType::POSSIBLE);
 	}
 	
-	auto v = zone.getArea().getTilesVector();
+	auto v = area->getTilesVector();
 	mapProxy->drawTerrain(zone.getRand(), v, zone.getTerrainType());
 	
 	//check terrain type
-	for([[maybe_unused]] const auto & t : zone.area().getTilesVector())
+	for([[maybe_unused]] const auto & t : area->getTilesVector())
 	{
 		assert(map.isOnMap(t));
 		assert(map.getTile(t).terType->getId() == zone.getTerrainType());
 	}
 
+	// FIXME: Possible deadlock for 2 zones
+
+	auto areaPossible = zone.areaPossible();
 	for(const auto & z : map.getZones())
 	{
 		if(z.second->getId() == zone.getId())
 			continue;
 
-		Zone::Lock lock(z.second->areaMutex);
-		for(const auto & t : z.second->area().getTilesVector())
+		auto secondArea = z.second->area();
+		auto secondAreaPossible = z.second->areaPossible();
+		for(const auto & t : secondArea->getTilesVector())
 		{
 			if(map.getTile(t).terType->getId() == zone.getTerrainType())
 			{
-				z.second->areaPossible().erase(t);
-				z.second->area().erase(t);
-				zone.area().add(t);
-				zone.areaPossible().add(t);
+				secondArea->erase(t);
+				secondAreaPossible->erase(t);
+				area->add(t);
+				areaPossible->add(t);
 				map.setZoneID(t, zone.getId());
 				map.setOccupied(t, ETileType::POSSIBLE);
 			}
 		}
 	}
 	
-	if(!zone.area().contains(zone.getPos()))
+	if(!area->contains(zone.getPos()))
 	{
-		zone.setPos(zone.area().getTilesVector().front());
+		zone.setPos(area->getTilesVector().front());
 	}
 	
 	zone.initFreeTiles();
@@ -105,7 +111,7 @@ void WaterProxy::collectLakes()
 {
 	RecursiveLock lock(externalAccessMutex);
 	int lakeId = 0;
-	for(const auto & lake : connectedAreas(zone.getArea(), true))
+	for(const auto & lake : connectedAreas(zone.area().get(), true))
 	{
 		lakes.push_back(Lake{});
 		lakes.back().area = lake;
@@ -117,8 +123,8 @@ void WaterProxy::collectLakes()
 			lakeMap[t] = lakeId;
 		
 		//each lake must have at least one free tile
-		if(!lake.overlap(zone.freePaths()))
-			zone.freePaths().add(*lakes.back().reverseDistanceMap[lakes.back().reverseDistanceMap.size() - 1].begin());
+		if(!lake.overlap(zone.freePaths().get()))
+			zone.freePaths()->add(*lakes.back().reverseDistanceMap[lakes.back().reverseDistanceMap.size() - 1].begin());
 		
 		++lakeId;
 	}
@@ -151,7 +157,7 @@ RouteInfo WaterProxy::waterRoute(Zone & dst)
 				}
 
 				Zone::Lock lock(dst.areaMutex);
-				dst.areaPossible().subtract(lake.neighbourZones[dst.getId()]);
+				dst.areaPossible()->subtract(lake.neighbourZones[dst.getId()]);
 				continue;
 			}
 
@@ -349,7 +355,7 @@ bool WaterProxy::placeShipyard(Zone & land, const Lake & lake, si32 guard, bool
 		}
 		
 		//try to place shipyard close to boarding position and appropriate water access
-		auto path = manager->placeAndConnectObject(land.areaPossible(), rmgObject, [&rmgObject, &shipPositions, &boardingPosition](const int3 & tile)
+		auto path = manager->placeAndConnectObject(land.areaPossible().get(), rmgObject, [&rmgObject, &shipPositions, &boardingPosition](const int3 & tile)
 		{
 			//Must only check the border of shipyard and not the added guard
 			rmg::Area shipyardOut = rmgObject.instances().front()->getBlockedArea().getBorderOutside();
@@ -361,9 +367,9 @@ bool WaterProxy::placeShipyard(Zone & land, const Lake & lake, si32 guard, bool
 		}, guarded, true, ObjectManager::OptimizeType::NONE);
 		
 		//search path to boarding position
-		auto searchArea = land.areaPossible() - rmgObject.getArea();
+		auto searchArea = land.areaPossible().get() - rmgObject.getArea();
 		rmg::Path pathToBoarding(searchArea);
-		pathToBoarding.connect(land.freePaths());
+		pathToBoarding.connect(land.freePaths().get());
 		pathToBoarding.connect(path);
 		pathToBoarding = pathToBoarding.search(boardingPosition, false);
 		
@@ -391,7 +397,7 @@ bool WaterProxy::placeShipyard(Zone & land, const Lake & lake, si32 guard, bool
 		
 		manager->placeObject(rmgObject, guarded, true, createRoad);
 		
-		zone.areaPossible().subtract(shipyardOutToBlock);
+		zone.areaPossible()->subtract(shipyardOutToBlock);
 		for(const auto & i : shipyardOutToBlock.getTilesVector())
 			if(map.isOnMap(i) && map.isPossible(i))
 				map.setOccupied(i, ETileType::BLOCKED);

+ 17 - 11
lib/rmg/modificators/WaterRoutes.cpp

@@ -43,22 +43,25 @@ void WaterRoutes::process()
 			result.push_back(wproxy->waterRoute(*z.second));
 	}
 
-	Zone::Lock lock(zone.areaMutex);
+	auto area = zone.area();
+	auto freePaths = zone.freePaths();
+	auto areaPossible = zone.areaPossible();
+	auto areaUsed = zone.areaUsed();
 
 	//prohibit to place objects on sealed off lakes
 	for(const auto & lake : wproxy->getLakes())
 	{
-		if((lake.area * zone.freePaths()).getTilesVector().size() == 1)
+		if((lake.area * freePaths.get()).getTilesVector().size() == 1)
 		{
-			zone.freePaths().subtract(lake.area);
-			zone.areaPossible().subtract(lake.area);
+			freePaths->subtract(lake.area);
+			areaPossible->subtract(lake.area);
 		}
 	}
 	
 	//prohibit to place objects on the borders
-	for(const auto & t : zone.area().getBorder())
+	for(const auto & t : area->getBorder())
 	{
-		if(zone.areaPossible().contains(t))
+		if(areaPossible->contains(t))
 		{
 			std::vector<int3> landTiles;
 			map.foreachDirectNeighbour(t, [this, &landTiles, &t](const int3 & c)
@@ -74,8 +77,8 @@ void WaterRoutes::process()
 				int3 o = landTiles[0] + landTiles[1];
 				if(o.x * o.x * o.y * o.y == 1) 
 				{
-					zone.areaPossible().erase(t);
-					zone.areaUsed().add(t);
+					areaPossible->erase(t);
+					areaUsed->add(t);
 				}
 			}
 		}
@@ -96,6 +99,9 @@ void WaterRoutes::init()
 
 char WaterRoutes::dump(const int3 & t)
 {
+	auto area = zone.area();
+	auto freePaths = zone.freePaths();
+
 	for(auto & i : result)
 	{
 		if(t == i.boarding)
@@ -106,15 +112,15 @@ char WaterRoutes::dump(const int3 & t)
 			return '#';
 		if(i.water.contains(t))
 		{
-			if(zone.freePaths().contains(t))
+			if(freePaths->contains(t))
 				return '+';
 			else
 				return '-';
 		}
 	}
-	if(zone.freePaths().contains(t))
+	if(freePaths->contains(t))
 		return '.';
-	if(zone.area().contains(t))
+	if(area->contains(t))
 		return '~';
 	return ' ';
 }

+ 1 - 1
lib/serializer/BinaryDeserializer.cpp

@@ -18,7 +18,7 @@ BinaryDeserializer::BinaryDeserializer(IBinaryReader * r): CLoaderBase(r)
 	saving = false;
 	version = Version::NONE;
 	smartPointerSerialization = true;
-	reverseEndianess = false;
+	reverseEndianness = false;
 
 	registerTypes(*this);
 }

+ 4 - 4
lib/serializer/BinaryDeserializer.h

@@ -23,12 +23,12 @@ protected:
 public:
 	CLoaderBase(IBinaryReader * r): reader(r){};
 
-	inline void read(void * data, unsigned size, bool reverseEndianess)
+	inline void read(void * data, unsigned size, bool reverseEndianness)
 	{
 		auto bytePtr = reinterpret_cast<std::byte*>(data);
 
 		reader->read(bytePtr, size);
-		if(reverseEndianess)
+		if(reverseEndianness)
 			std::reverse(bytePtr, bytePtr + size);
 	};
 };
@@ -153,7 +153,7 @@ class DLL_LINKAGE BinaryDeserializer : public CLoaderBase
 public:
 	using Version = ESerializationVersion;
 
-	bool reverseEndianess; //if source has different endianness than us, we reverse bytes
+	bool reverseEndianness; //if source has different endianness than us, we reverse bytes
 	Version version;
 
 	std::map<ui32, void*> loadedPointers;
@@ -174,7 +174,7 @@ public:
 	template < class T, typename std::enable_if_t < std::is_fundamental_v<T> && !std::is_same_v<T, bool>, int  > = 0 >
 	void load(T &data)
 	{
-		this->read(static_cast<void *>(&data), sizeof(data), reverseEndianess);
+		this->read(static_cast<void *>(&data), sizeof(data), reverseEndianness);
 	}
 
 	template < typename T, typename std::enable_if_t < is_serializeable<BinaryDeserializer, T>::value, int  > = 0 >

+ 2 - 2
lib/serializer/CLoadFile.cpp

@@ -29,7 +29,7 @@ int CLoadFile::read(std::byte * data, unsigned size)
 
 void CLoadFile::openNextFile(const boost::filesystem::path & fname, ESerializationVersion minimalVersion)
 {
-	assert(!serializer.reverseEndianess);
+	assert(!serializer.reverseEndianness);
 	assert(minimalVersion <= ESerializationVersion::CURRENT);
 
 	try
@@ -62,7 +62,7 @@ void CLoadFile::openNextFile(const boost::filesystem::path & fname, ESerializati
 			if(serializer.version == ESerializationVersion::CURRENT)
 			{
 				logGlobal->warn("%s seems to have different endianness! Entering reversing mode.", fname.string());
-				serializer.reverseEndianess = true;
+				serializer.reverseEndianness = true;
 			}
 			else
 				THROW_FORMAT("Error: too new file format (%s)!", fName);

+ 9 - 1
lobby/EntryPoint.cpp

@@ -35,7 +35,15 @@ int main(int argc, const char * argv[])
 	LobbyServer server(databasePath);
 	logGlobal->info("Starting server on port %d", LISTENING_PORT);
 
-	server.start(LISTENING_PORT);
+	try
+	{
+		server.start(LISTENING_PORT);
+	}
+	catch (const boost::system::system_error & e)
+	{
+		logGlobal->error("Failed to start server! Another server already uses the same port? Reason: '%s'", e.what());
+		return 1;
+	}
 	server.run();
 
 	return 0;

+ 190 - 119
lobby/LobbyDatabase.cpp

@@ -18,7 +18,8 @@ void LobbyDatabase::createTables()
 		CREATE TABLE IF NOT EXISTS chatMessages (
 			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 			senderName TEXT,
-			roomType TEXT,
+			channelType TEXT,
+			channelName TEXT,
 			messageText TEXT,
 			creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
 		);
@@ -29,6 +30,7 @@ void LobbyDatabase::createTables()
 			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 			roomID TEXT,
 			hostAccountID TEXT,
+			description TEXT NOT NULL DEFAULT '',
 			status INTEGER NOT NULL DEFAULT 0,
 			playerLimit INTEGER NOT NULL,
 			creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
@@ -87,205 +89,207 @@ void LobbyDatabase::clearOldData()
 		WHERE online <> 0
 	)";
 
-	static const std::string removeActiveRooms = R"(
+	static const std::string removeActiveLobbyRooms = R"(
+		UPDATE gameRooms
+		SET status = 4
+		WHERE status IN (0,1,2)
+	)";
+	static const std::string removeActiveGameRooms = R"(
 		UPDATE gameRooms
 		SET status = 5
-		WHERE status <> 5
+		WHERE status = 3
 	)";
 
 	database->prepare(removeActiveAccounts)->execute();
-	database->prepare(removeActiveRooms)->execute();
+	database->prepare(removeActiveLobbyRooms)->execute();
+	database->prepare(removeActiveGameRooms)->execute();
 }
 
 void LobbyDatabase::prepareStatements()
 {
 	// INSERT INTO
 
-	static const std::string insertChatMessageText = R"(
-		INSERT INTO chatMessages(senderName, messageText) VALUES( ?, ?);
-	)";
+	insertChatMessageStatement = database->prepare(R"(
+		INSERT INTO chatMessages(senderName, messageText, channelType, channelName) VALUES( ?, ?, ?, ?);
+	)");
 
-	static const std::string insertAccountText = R"(
+	insertAccountStatement = database->prepare(R"(
 		INSERT INTO accounts(accountID, displayName, online) VALUES(?,?,0);
-	)";
+	)");
 
-	static const std::string insertAccessCookieText = R"(
+	insertAccessCookieStatement = database->prepare(R"(
 		INSERT INTO accountCookies(accountID, cookieUUID) VALUES(?,?);
-	)";
+	)");
 
-	static const std::string insertGameRoomText = R"(
+	insertGameRoomStatement = database->prepare(R"(
 		INSERT INTO gameRooms(roomID, hostAccountID, status, playerLimit) VALUES(?, ?, 0, 8);
-	)";
+	)");
 
-	static const std::string insertGameRoomPlayersText = R"(
+	insertGameRoomPlayersStatement = database->prepare(R"(
 		INSERT INTO gameRoomPlayers(roomID, accountID) VALUES(?,?);
-	)";
+	)");
 
-	static const std::string insertGameRoomInvitesText = R"(
+	insertGameRoomInvitesStatement = database->prepare(R"(
 		INSERT INTO gameRoomInvites(roomID, accountID) VALUES(?,?);
-	)";
+	)");
 
 	// DELETE FROM
 
-	static const std::string deleteGameRoomPlayersText = R"(
+	deleteGameRoomPlayersStatement = database->prepare(R"(
 		 DELETE FROM gameRoomPlayers WHERE roomID = ? AND accountID = ?
-	)";
-
-	static const std::string deleteGameRoomInvitesText = R"(
-		DELETE FROM gameRoomInvites WHERE roomID = ? AND accountID = ?
-	)";
+	)");
 
 	// UPDATE
 
-	static const std::string setAccountOnlineText = R"(
+	setAccountOnlineStatement = database->prepare(R"(
 		UPDATE accounts
 		SET online = ?
 		WHERE accountID = ?
-	)";
+	)");
 
-	static const std::string setGameRoomStatusText = R"(
+	setGameRoomStatusStatement = database->prepare(R"(
 		UPDATE gameRooms
 		SET status = ?
 		WHERE roomID = ?
-	)";
+	)");
 
-	static const std::string updateAccountLoginTimeText = R"(
+	updateAccountLoginTimeStatement = database->prepare(R"(
 		UPDATE accounts
 		SET lastLoginTime = CURRENT_TIMESTAMP
 		WHERE accountID = ?
-	)";
+	)");
+
+	updateRoomDescriptionStatement = database->prepare(R"(
+		UPDATE gameRooms
+		SET description = ?
+		WHERE roomID  = ?
+	)");
+
+	updateRoomPlayerLimitStatement = database->prepare(R"(
+		UPDATE gameRooms
+		SET playerLimit = ?
+		WHERE roomID  = ?
+	)");
 
 	// SELECT FROM
 
-	static const std::string getRecentMessageHistoryText = R"(
+	getRecentMessageHistoryStatement = database->prepare(R"(
 		SELECT senderName, displayName, messageText, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',cm.creationTime)  AS secondsElapsed
 		FROM chatMessages cm
 		LEFT JOIN accounts on accountID = senderName
-		WHERE secondsElapsed < 60*60*18
+		WHERE secondsElapsed < 60*60*18 AND channelType = ? AND channelName = ?
 		ORDER BY cm.creationTime DESC
 		LIMIT 100
-	)";
+	)");
+
+	getFullMessageHistoryStatement = database->prepare(R"(
+		SELECT senderName, displayName, messageText, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',cm.creationTime) AS secondsElapsed
+		FROM chatMessages cm
+		LEFT JOIN accounts on accountID = senderName
+		WHERE channelType = ? AND channelName = ?
+		ORDER BY cm.creationTime DESC
+	)");
 
-	static const std::string getIdleGameRoomText = R"(
+	getIdleGameRoomStatement = database->prepare(R"(
 		SELECT roomID
 		FROM gameRooms
 		WHERE hostAccountID = ? AND status = 0
 		LIMIT 1
-	)";
+	)");
 
-	static const std::string getGameRoomStatusText = R"(
+	getGameRoomStatusStatement = database->prepare(R"(
 		SELECT status
 		FROM gameRooms
 		WHERE roomID = ?
-	)";
+	)");
 
-	static const std::string getAccountGameRoomText = R"(
+	getAccountInviteStatusStatement = database->prepare(R"(
+		SELECT COUNT(accountID)
+		FROM gameRoomInvites
+		WHERE accountID = ? AND roomID = ?
+	)");
+
+	getAccountGameHistoryStatement = database->prepare(R"(
+		SELECT gr.roomID, hostAccountID, displayName, description, status, playerLimit, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',gr.creationTime)  AS secondsElapsed
+		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 = 5
+		ORDER BY secondsElapsed ASC
+	)");
+
+	getAccountGameRoomStatement = database->prepare(R"(
 		SELECT grp.roomID
 		FROM gameRoomPlayers grp
 		LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID
-		WHERE accountID = ? AND status IN (1, 2)
+		WHERE accountID = ? AND status IN (1, 2, 3)
 		LIMIT 1
-	)";
+	)");
 
-	static const std::string getActiveAccountsText = R"(
+	getActiveAccountsStatement = database->prepare(R"(
 		SELECT accountID, displayName
 		FROM accounts
 		WHERE online = 1
-	)";
-
-	static const std::string getActiveGameRoomsText = R"(
-		SELECT roomID, hostAccountID, displayName, status, playerLimit
-		FROM gameRooms
-		LEFT JOIN accounts ON hostAccountID = accountID
-		WHERE status = 1
-	)";
-
-	static const std::string countRoomUsedSlotsText = R"(
-		SELECT COUNT(accountID)
-		FROM gameRoomPlayers
+	)");
+
+	getActiveGameRoomsStatement = database->prepare(R"(
+		SELECT roomID, hostAccountID, displayName, description, status, playerLimit, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',gr.creationTime)  AS secondsElapsed
+		FROM gameRooms gr
+		LEFT JOIN accounts a ON gr.hostAccountID = a.accountID
+		WHERE status IN (1, 2, 3)
+		ORDER BY secondsElapsed ASC
+	)");
+
+	countRoomUsedSlotsStatement = database->prepare(R"(
+		SELECT a.accountID, a.displayName
+		FROM gameRoomPlayers grp
+		LEFT JOIN accounts a ON a.accountID = grp.accountID
 		WHERE roomID = ?
-	)";
+	)");
 
-	static const std::string countRoomTotalSlotsText = R"(
+	countRoomTotalSlotsStatement = database->prepare(R"(
 		SELECT playerLimit
 		FROM gameRooms
 		WHERE roomID = ?
-	)";
+	)");
 
-	static const std::string getAccountDisplayNameText = R"(
+	getAccountDisplayNameStatement = database->prepare(R"(
 		SELECT displayName
 		FROM accounts
 		WHERE accountID = ?
-	)";
+	)");
 
-	static const std::string isAccountCookieValidText = R"(
+	isAccountCookieValidStatement = database->prepare(R"(
 		SELECT COUNT(accountID)
 		FROM accountCookies
 		WHERE accountID = ? AND cookieUUID = ?
-	)";
-
-	static const std::string isGameRoomCookieValidText = R"(
-		SELECT COUNT(roomID)
-		FROM gameRooms
-		LEFT JOIN accountCookies ON accountCookies.accountID = gameRooms.hostAccountID
-		WHERE roomID = ? AND cookieUUID = ? AND strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',creationTime) < ?
-	)";
+	)");
 
-	static const std::string isPlayerInGameRoomText = R"(
+	isPlayerInGameRoomStatement = database->prepare(R"(
 		SELECT COUNT(accountID)
 		FROM gameRoomPlayers grp
 		LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID
-		WHERE accountID = ? AND grp.roomID = ? AND status IN (1, 2)
-	)";
+		WHERE accountID = ? AND grp.roomID = ?
+	)");
 
-	static const std::string isPlayerInAnyGameRoomText = R"(
+	isPlayerInAnyGameRoomStatement = database->prepare(R"(
 		SELECT COUNT(accountID)
 		FROM gameRoomPlayers grp
 		LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID
-		WHERE accountID = ? AND status IN (1, 2)
-	)";
+		WHERE accountID = ? AND status IN (1, 2, 3)
+	)");
 
-	static const std::string isAccountIDExistsText = R"(
+	isAccountIDExistsStatement = database->prepare(R"(
 		SELECT COUNT(accountID)
 		FROM accounts
 		WHERE accountID = ?
-	)";
+	)");
 
-	static const std::string isAccountNameExistsText = R"(
+	isAccountNameExistsStatement = database->prepare(R"(
 		SELECT COUNT(displayName)
 		FROM accounts
 		WHERE displayName = ?
-	)";
-
-	insertChatMessageStatement = database->prepare(insertChatMessageText);
-	insertAccountStatement = database->prepare(insertAccountText);
-	insertAccessCookieStatement = database->prepare(insertAccessCookieText);
-	insertGameRoomStatement = database->prepare(insertGameRoomText);
-	insertGameRoomPlayersStatement = database->prepare(insertGameRoomPlayersText);
-	insertGameRoomInvitesStatement = database->prepare(insertGameRoomInvitesText);
-
-	deleteGameRoomPlayersStatement = database->prepare(deleteGameRoomPlayersText);
-	deleteGameRoomInvitesStatement = database->prepare(deleteGameRoomInvitesText);
-
-	setAccountOnlineStatement = database->prepare(setAccountOnlineText);
-	setGameRoomStatusStatement = database->prepare(setGameRoomStatusText);
-	updateAccountLoginTimeStatement = database->prepare(updateAccountLoginTimeText);
-
-	getRecentMessageHistoryStatement = database->prepare(getRecentMessageHistoryText);
-	getIdleGameRoomStatement = database->prepare(getIdleGameRoomText);
-	getGameRoomStatusStatement = database->prepare(getGameRoomStatusText);
-	getAccountGameRoomStatement = database->prepare(getAccountGameRoomText);
-	getActiveAccountsStatement = database->prepare(getActiveAccountsText);
-	getActiveGameRoomsStatement = database->prepare(getActiveGameRoomsText);
-	getAccountDisplayNameStatement = database->prepare(getAccountDisplayNameText);
-	countRoomUsedSlotsStatement = database->prepare(countRoomUsedSlotsText);
-	countRoomTotalSlotsStatement = database->prepare(countRoomTotalSlotsText);
-
-	isAccountCookieValidStatement = database->prepare(isAccountCookieValidText);
-	isPlayerInGameRoomStatement = database->prepare(isPlayerInGameRoomText);
-	isPlayerInAnyGameRoomStatement = database->prepare(isPlayerInAnyGameRoomText);
-	isAccountIDExistsStatement = database->prepare(isAccountIDExistsText);
-	isAccountNameExistsStatement = database->prepare(isAccountNameExistsText);
+	)");
 }
 
 LobbyDatabase::~LobbyDatabase() = default;
@@ -298,9 +302,9 @@ LobbyDatabase::LobbyDatabase(const boost::filesystem::path & databasePath)
 	prepareStatements();
 }
 
-void LobbyDatabase::insertChatMessage(const std::string & sender, const std::string & roomType, const std::string & roomName, const std::string & messageText)
+void LobbyDatabase::insertChatMessage(const std::string & sender, const std::string & channelType, const std::string & channelName, const std::string & messageText)
 {
-	insertChatMessageStatement->executeOnce(sender, messageText);
+	insertChatMessageStatement->executeOnce(sender, messageText, channelType, channelName);
 }
 
 bool LobbyDatabase::isPlayerInGameRoom(const std::string & accountID)
@@ -327,10 +331,11 @@ bool LobbyDatabase::isPlayerInGameRoom(const std::string & accountID, const std:
 	return result;
 }
 
-std::vector<LobbyChatMessage> LobbyDatabase::getRecentMessageHistory()
+std::vector<LobbyChatMessage> LobbyDatabase::getRecentMessageHistory(const std::string & channelType, const std::string & channelName)
 {
 	std::vector<LobbyChatMessage> result;
 
+	getRecentMessageHistoryStatement->setBinds(channelType, channelName);
 	while(getRecentMessageHistoryStatement->execute())
 	{
 		LobbyChatMessage message;
@@ -342,6 +347,22 @@ std::vector<LobbyChatMessage> LobbyDatabase::getRecentMessageHistory()
 	return result;
 }
 
+std::vector<LobbyChatMessage> LobbyDatabase::getFullMessageHistory(const std::string & channelType, const std::string & channelName)
+{
+	std::vector<LobbyChatMessage> result;
+
+	getFullMessageHistoryStatement->setBinds(channelType, channelName);
+	while(getFullMessageHistoryStatement->execute())
+	{
+		LobbyChatMessage message;
+		getFullMessageHistoryStatement->getColumns(message.accountID, message.displayName, message.messageText, message.age);
+		result.push_back(message);
+	}
+	getFullMessageHistoryStatement->reset();
+
+	return result;
+}
+
 void LobbyDatabase::setAccountOnline(const std::string & accountID, bool isOnline)
 {
 	setAccountOnlineStatement->executeOnce(isOnline ? 1 : 0, accountID);
@@ -392,6 +413,16 @@ void LobbyDatabase::updateAccountLoginTime(const std::string & accountID)
 	updateAccountLoginTimeStatement->executeOnce(accountID);
 }
 
+void LobbyDatabase::updateRoomPlayerLimit(const std::string & gameRoomID, int playerLimit)
+{
+	updateRoomPlayerLimitStatement->executeOnce(playerLimit, gameRoomID);
+}
+
+void LobbyDatabase::updateRoomDescription(const std::string & gameRoomID, const std::string & description)
+{
+	updateRoomDescriptionStatement->executeOnce(description, gameRoomID);
+}
+
 std::string LobbyDatabase::getAccountDisplayName(const std::string & accountID)
 {
 	std::string result;
@@ -418,22 +449,31 @@ LobbyCookieStatus LobbyDatabase::getAccountCookieStatus(const std::string & acco
 
 LobbyInviteStatus LobbyDatabase::getAccountInviteStatus(const std::string & accountID, const std::string & roomID)
 {
-	assert(0);
-	return {};
+	int result = 0;
+
+	getAccountInviteStatusStatement->setBinds(accountID, roomID);
+	if(getAccountInviteStatusStatement->execute())
+		getAccountInviteStatusStatement->getColumns(result);
+	getAccountInviteStatusStatement->reset();
+
+	if (result > 0)
+		return LobbyInviteStatus::INVITED;
+	else
+		return LobbyInviteStatus::NOT_INVITED;
 }
 
 LobbyRoomState LobbyDatabase::getGameRoomStatus(const std::string & roomID)
 {
-	int result = -1;
+	LobbyRoomState result;
 
 	getGameRoomStatusStatement->setBinds(roomID);
 	if(getGameRoomStatusStatement->execute())
 		getGameRoomStatusStatement->getColumns(result);
-	getGameRoomStatusStatement->reset();
+	else
+		result = LobbyRoomState::CLOSED;
 
-	if (result != -1)
-		return static_cast<LobbyRoomState>(result);
-	return LobbyRoomState::CLOSED;
+	getGameRoomStatusStatement->reset();
+	return result;
 }
 
 uint32_t LobbyDatabase::getGameRoomFreeSlots(const std::string & roomID)
@@ -486,7 +526,7 @@ std::vector<LobbyGameRoom> LobbyDatabase::getActiveGameRooms()
 	while(getActiveGameRoomsStatement->execute())
 	{
 		LobbyGameRoom entry;
-		getActiveGameRoomsStatement->getColumns(entry.roomID, entry.hostAccountID, entry.hostAccountDisplayName, entry.roomStatus, entry.playersLimit);
+		getActiveGameRoomsStatement->getColumns(entry.roomID, entry.hostAccountID, entry.hostAccountDisplayName, entry.description, entry.roomState, entry.playerLimit, entry.age);
 		result.push_back(entry);
 	}
 	getActiveGameRoomsStatement->reset();
@@ -494,8 +534,39 @@ std::vector<LobbyGameRoom> LobbyDatabase::getActiveGameRooms()
 	for (auto & room : result)
 	{
 		countRoomUsedSlotsStatement->setBinds(room.roomID);
-		if(countRoomUsedSlotsStatement->execute())
-			countRoomUsedSlotsStatement->getColumns(room.playersCount);
+		while(countRoomUsedSlotsStatement->execute())
+		{
+			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, entry.age);
+		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();
 	}
 	return result;

+ 11 - 2
lobby/LobbyDatabase.h

@@ -34,12 +34,17 @@ class LobbyDatabase
 	SQLiteStatementPtr setAccountOnlineStatement;
 	SQLiteStatementPtr setGameRoomStatusStatement;
 	SQLiteStatementPtr updateAccountLoginTimeStatement;
+	SQLiteStatementPtr updateRoomDescriptionStatement;
+	SQLiteStatementPtr updateRoomPlayerLimitStatement;
 
 	SQLiteStatementPtr getRecentMessageHistoryStatement;
+	SQLiteStatementPtr getFullMessageHistoryStatement;
 	SQLiteStatementPtr getIdleGameRoomStatement;
 	SQLiteStatementPtr getGameRoomStatusStatement;
+	SQLiteStatementPtr getAccountGameHistoryStatement;
 	SQLiteStatementPtr getActiveGameRoomsStatement;
 	SQLiteStatementPtr getActiveAccountsStatement;
+	SQLiteStatementPtr getAccountInviteStatusStatement;
 	SQLiteStatementPtr getAccountGameRoomStatement;
 	SQLiteStatementPtr getAccountDisplayNameStatement;
 	SQLiteStatementPtr countRoomUsedSlotsStatement;
@@ -72,13 +77,17 @@ public:
 	void insertGameRoom(const std::string & roomID, const std::string & hostAccountID);
 	void insertAccount(const std::string & accountID, const std::string & displayName);
 	void insertAccessCookie(const std::string & accountID, const std::string & accessCookieUUID);
-	void insertChatMessage(const std::string & sender, const std::string & roomType, const std::string & roomID, const std::string & messageText);
+	void insertChatMessage(const std::string & sender, const std::string & channelType, const std::string & roomID, const std::string & messageText);
 
 	void updateAccountLoginTime(const std::string & accountID);
+	void updateRoomPlayerLimit(const std::string & gameRoomID, int playerLimit);
+	void updateRoomDescription(const std::string & gameRoomID, const std::string & description);
 
+	std::vector<LobbyGameRoom> getAccountGameHistory(const std::string & accountID);
 	std::vector<LobbyGameRoom> getActiveGameRooms();
 	std::vector<LobbyAccount> getActiveAccounts();
-	std::vector<LobbyChatMessage> getRecentMessageHistory();
+	std::vector<LobbyChatMessage> getRecentMessageHistory(const std::string & channelType, const std::string & channelName);
+	std::vector<LobbyChatMessage> getFullMessageHistory(const std::string & channelType, const std::string & channelName);
 
 	std::string getIdleGameRoom(const std::string & hostAccountID);
 	std::string getAccountGameRoom(const std::string & accountID);

+ 28 - 26
lobby/LobbyDefines.h

@@ -9,30 +9,6 @@
  */
 #pragma once
 
-struct LobbyAccount
-{
-	std::string accountID;
-	std::string displayName;
-};
-
-struct LobbyGameRoom
-{
-	std::string roomID;
-	std::string hostAccountID;
-	std::string hostAccountDisplayName;
-	std::string roomStatus;
-	uint32_t playersCount;
-	uint32_t playersLimit;
-};
-
-struct LobbyChatMessage
-{
-	std::string accountID;
-	std::string displayName;
-	std::string messageText;
-	std::chrono::seconds age;
-};
-
 enum class LobbyCookieStatus : int32_t
 {
 	INVALID,
@@ -51,7 +27,33 @@ enum class LobbyRoomState : int32_t
 	IDLE = 0, // server is ready but no players are in the room
 	PUBLIC = 1, // host has joined and allows anybody to join
 	PRIVATE = 2, // host has joined but only allows those he invited to join
-	//BUSY = 3, // match is ongoing
-	//CANCELLED = 4, // game room was cancelled without starting the game
+	BUSY = 3, // match is ongoing and no longer accepts players
+	CANCELLED = 4, // game room was cancelled without starting the game
 	CLOSED = 5, // game room was closed after playing for some time
 };
+
+struct LobbyAccount
+{
+	std::string accountID;
+	std::string displayName;
+};
+
+struct LobbyGameRoom
+{
+	std::string roomID;
+	std::string hostAccountID;
+	std::string hostAccountDisplayName;
+	std::string description;
+	std::vector<LobbyAccount> participants;
+	LobbyRoomState roomState;
+	uint32_t playerLimit;
+	std::chrono::seconds age;
+};
+
+struct LobbyChatMessage
+{
+	std::string accountID;
+	std::string displayName;
+	std::string messageText;
+	std::chrono::seconds age;
+};

+ 245 - 48
lobby/LobbyServer.cpp

@@ -12,6 +12,8 @@
 
 #include "LobbyDatabase.h"
 
+#include "../lib/Languages.h"
+#include "../lib/TextOperations.h"
 #include "../lib/json/JsonFormatException.h"
 #include "../lib/json/JsonNode.h"
 #include "../lib/json/JsonUtils.h"
@@ -21,12 +23,16 @@
 
 bool LobbyServer::isAccountNameValid(const std::string & accountName) const
 {
+	// Arbitrary limit on account name length.
+	// Can be extended if there are no issues with UI space
 	if(accountName.size() < 4)
 		return false;
 
 	if(accountName.size() > 20)
 		return false;
 
+	// For now permit only latin alphabet and numbers
+	// Can be extended, but makes sure that such symbols will be present in all H3 fonts
 	for(const auto & c : accountName)
 		if(!std::isalnum(c))
 			return false;
@@ -36,8 +42,23 @@ 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
-	return inputString;
+	static const std::string blacklist = "{}";
+	std::string sanitized;
+
+	for(const auto & ch : inputString)
+	{
+		// Remove all control characters
+		if (ch >= '\0' && ch < ' ')
+			continue;
+
+		// Remove blacklisted characters such as brackets that are used for text formatting
+		if (blacklist.find(ch) != std::string::npos)
+			continue;
+
+		sanitized += ch;
+	}
+
+	return boost::trim_copy(sanitized);
 }
 
 NetworkConnectionPtr LobbyServer::findAccount(const std::string & accountID) const
@@ -60,6 +81,8 @@ NetworkConnectionPtr LobbyServer::findGameRoom(const std::string & gameRoomID) c
 
 void LobbyServer::sendMessage(const NetworkConnectionPtr & target, const JsonNode & json)
 {
+	logGlobal->info("Sending message of type %s", json["type"].String());
+
 	assert(JsonUtils::validate(json, "vcmi:lobbyProtocol/" + json["type"].String(), json["type"].String() + " pack"));
 	target->sendPacket(json.toBytes());
 }
@@ -90,20 +113,39 @@ void LobbyServer::sendOperationFailed(const NetworkConnectionPtr & target, const
 	sendMessage(target, reply);
 }
 
-void LobbyServer::sendLoginSuccess(const NetworkConnectionPtr & target, const std::string & accountCookie, const std::string & displayName)
+void LobbyServer::sendClientLoginSuccess(const NetworkConnectionPtr & target, const std::string & accountCookie, const std::string & displayName)
+{
+	JsonNode reply;
+	reply["type"].String() = "clientLoginSuccess";
+	reply["accountCookie"].String() = accountCookie;
+	reply["displayName"].String() = displayName;
+	sendMessage(target, reply);
+}
+
+void LobbyServer::sendServerLoginSuccess(const NetworkConnectionPtr & target, const std::string & accountCookie)
 {
 	JsonNode reply;
-	reply["type"].String() = "loginSuccess";
+	reply["type"].String() = "serverLoginSuccess";
 	reply["accountCookie"].String() = accountCookie;
-	if(!displayName.empty())
-		reply["displayName"].String() = displayName;
 	sendMessage(target, reply);
 }
 
-void LobbyServer::sendChatHistory(const NetworkConnectionPtr & target, const std::vector<LobbyChatMessage> & history)
+void LobbyServer::sendFullChatHistory(const NetworkConnectionPtr & target, const std::string & channelType, const std::string & channelName, const std::string & channelNameForClient)
+{
+	sendChatHistory(target, channelType, channelNameForClient, database->getFullMessageHistory(channelType, channelName));
+}
+
+void LobbyServer::sendRecentChatHistory(const NetworkConnectionPtr & target, const std::string & channelType, const std::string & channelName)
+{
+	sendChatHistory(target, channelType, channelName, database->getRecentMessageHistory(channelType, channelName));
+}
+
+void LobbyServer::sendChatHistory(const NetworkConnectionPtr & target, const std::string & channelType, const std::string & channelName, const std::vector<LobbyChatMessage> & history)
 {
 	JsonNode reply;
 	reply["type"].String() = "chatHistory";
+	reply["channelType"].String() = channelType;
+	reply["channelName"].String() = channelName;
 	reply["messages"].Vector(); // force creation of empty vector
 
 	for(const auto & message : boost::adaptors::reverse(history))
@@ -142,6 +184,55 @@ void LobbyServer::broadcastActiveAccounts()
 		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 = {
+		"idle",
+		"public",
+		"private",
+		"busy",
+		"cancelled",
+		"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;
+	jsonEntry["ageSeconds"].Integer() = gameRoom.age.count();
+
+	for(const auto & 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()
 {
 	auto activeGameRoomStats = database->getActiveGameRooms();
@@ -150,16 +241,7 @@ JsonNode LobbyServer::prepareActiveGameRooms()
 	reply["gameRooms"].Vector(); // force creation of empty vector
 
 	for(const auto & gameRoom : activeGameRoomStats)
-	{
-		JsonNode jsonEntry;
-		jsonEntry["gameRoomID"].String() = gameRoom.roomID;
-		jsonEntry["hostAccountID"].String() = gameRoom.hostAccountID;
-		jsonEntry["hostAccountDisplayName"].String() = gameRoom.hostAccountDisplayName;
-		jsonEntry["description"].String() = "TODO: ROOM DESCRIPTION";
-		jsonEntry["playersCount"].Integer() = gameRoom.playersCount;
-		jsonEntry["playersLimit"].Integer() = gameRoom.playersLimit;
-		reply["gameRooms"].Vector().push_back(jsonEntry);
-	}
+		reply["gameRooms"].Vector().push_back(loadLobbyGameRoomToJson(gameRoom));
 
 	return reply;
 }
@@ -189,15 +271,15 @@ void LobbyServer::sendJoinRoomSuccess(const NetworkConnectionPtr & target, const
 	sendMessage(target, reply);
 }
 
-void LobbyServer::sendChatMessage(const NetworkConnectionPtr & target, const std::string & roomMode, const std::string & roomName, const std::string & accountID, const std::string & displayName, const std::string & messageText)
+void LobbyServer::sendChatMessage(const NetworkConnectionPtr & target, const std::string & channelType, const std::string & channelName, const std::string & accountID, const std::string & displayName, const std::string & messageText)
 {
 	JsonNode reply;
 	reply["type"].String() = "chatMessage";
 	reply["messageText"].String() = messageText;
 	reply["accountID"].String() = accountID;
 	reply["displayName"].String() = displayName;
-	reply["roomMode"].String() = roomMode;
-	reply["roomName"].String() = roomName;
+	reply["channelType"].String() = channelType;
+	reply["channelName"].String() = channelName;
 
 	sendMessage(target, reply);
 }
@@ -217,7 +299,18 @@ void LobbyServer::onDisconnected(const NetworkConnectionPtr & connection, const
 
 	if(activeGameRooms.count(connection))
 	{
-		database->setGameRoomStatus(activeGameRooms.at(connection), LobbyRoomState::CLOSED);
+		std::string gameRoomID = activeGameRooms.at(connection);
+
+		if (database->getGameRoomStatus(gameRoomID) == LobbyRoomState::BUSY)
+		{
+			database->setGameRoomStatus(gameRoomID, LobbyRoomState::CLOSED);
+			for(const auto & accountConnection : activeAccounts)
+				if (database->isPlayerInGameRoom(accountConnection.second, gameRoomID))
+					sendMatchesHistory(accountConnection.first);
+		}
+		else
+			database->setGameRoomStatus(gameRoomID, LobbyRoomState::CANCELLED);
+
 		activeGameRooms.erase(connection);
 	}
 
@@ -300,6 +393,9 @@ void LobbyServer::onPacketReceived(const NetworkConnectionPtr & connection, cons
 		if(messageType == "sendChatMessage")
 			return receiveSendChatMessage(connection, json);
 
+		if(messageType == "requestChatHistory")
+			return receiveRequestChatHistory(connection, json);
+
 		if(messageType == "activateGameRoom")
 			return receiveActivateGameRoom(connection, json);
 
@@ -309,9 +405,6 @@ void LobbyServer::onPacketReceived(const NetworkConnectionPtr & connection, cons
 		if(messageType == "sendInvite")
 			return receiveSendInvite(connection, json);
 
-		if(messageType == "declineInvite")
-			return receiveDeclineInvite(connection, json);
-
 		logGlobal->warn("%s: Unknown message type: %s", accountName, messageType);
 		return;
 	}
@@ -322,6 +415,12 @@ void LobbyServer::onPacketReceived(const NetworkConnectionPtr & connection, cons
 		std::string roomName = activeGameRooms.at(connection);
 		logGlobal->info("%s: Received message of type %s", roomName, messageType);
 
+		if(messageType == "changeRoomDescription")
+			return receiveChangeRoomDescription(connection, json);
+
+		if(messageType == "gameStarted")
+			return receiveGameStarted(connection, json);
+
 		if(messageType == "leaveGameRoom")
 			return receiveLeaveGameRoom(connection, json);
 
@@ -351,20 +450,107 @@ void LobbyServer::onPacketReceived(const NetworkConnectionPtr & connection, cons
 	logGlobal->info("(unauthorised): Unknown message type %s", messageType);
 }
 
-void LobbyServer::receiveSendChatMessage(const NetworkConnectionPtr & connection, const JsonNode & json)
+void LobbyServer::receiveRequestChatHistory(const NetworkConnectionPtr & connection, const JsonNode & json)
 {
 	std::string accountID = activeAccounts[connection];
+	std::string channelType = json["channelType"].String();
+	std::string channelName = json["channelName"].String();
+
+	if (channelType == "global")
+	{
+		// can only be sent on connection, initiated by server
+		sendOperationFailed(connection, "Operation not supported!");
+	}
+
+	if (channelType == "match")
+	{
+		if (!database->isPlayerInGameRoom(accountID, channelName))
+			return sendOperationFailed(connection, "Can not access room you are not part of!");
+
+		sendFullChatHistory(connection, channelType, channelName, channelName);
+	}
+
+	if (channelType == "player")
+	{
+		if (!database->isAccountIDExists(channelName))
+			return sendOperationFailed(connection, "Such player does not exists!");
+
+		// room ID for private messages is actually <player 1 ID>_<player 2 ID>, with player ID's sorted alphabetically (to generate unique room ID)
+		std::string roomID = std::min(accountID, channelName) + "_" + std::max(accountID, channelName);
+		sendFullChatHistory(connection, channelType, roomID, channelName);
+	}
+}
+
+void LobbyServer::receiveSendChatMessage(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string senderAccountID = activeAccounts[connection];
 	std::string messageText = json["messageText"].String();
-	std::string messageTextClean = sanitizeChatMessage(messageText);
-	std::string displayName = database->getAccountDisplayName(accountID);
+	std::string channelType = json["channelType"].String();
+	std::string channelName = json["channelName"].String();
+	std::string displayName = database->getAccountDisplayName(senderAccountID);
+
+	if(TextOperations::isValidUnicodeString(messageText))
+		return sendOperationFailed(connection, "String contains invalid characters!");
 
+	std::string messageTextClean = sanitizeChatMessage(messageText);
 	if(messageTextClean.empty())
 		return sendOperationFailed(connection, "No printable characters in sent message!");
 
-	database->insertChatMessage(accountID, "global", "english", messageText);
+	if (channelType == "global")
+	{
+		try
+		{
+			Languages::getLanguageOptions(channelName);
+		}
+		catch (const std::out_of_range &)
+		{
+			return sendOperationFailed(connection, "Unknown language!");
+		}
+		database->insertChatMessage(senderAccountID, channelType, channelName, messageText);
+
+		for(const auto & otherConnection : activeAccounts)
+			sendChatMessage(otherConnection.first, channelType, channelName, senderAccountID, displayName, messageText);
+	}
+
+	if (channelType == "match")
+	{
+		if (!database->isPlayerInGameRoom(senderAccountID, channelName))
+			return sendOperationFailed(connection, "Can not access room you are not part of!");
+
+		database->insertChatMessage(senderAccountID, channelType, channelName, messageText);
+
+		LobbyRoomState roomStatus = database->getGameRoomStatus(channelName);
 
-	for(const auto & otherConnection : activeAccounts)
-		sendChatMessage(otherConnection.first, "global", "english", accountID, displayName, messageText);
+		// Broadcast chat message only if it being sent to already closed match
+		// Othervice it will be handled by match server
+		if (roomStatus == LobbyRoomState::CLOSED)
+		{
+			for(const auto & otherConnection : activeAccounts)
+			{
+				if (database->isPlayerInGameRoom(otherConnection.second, channelName))
+					sendChatMessage(otherConnection.first, channelType, channelName, senderAccountID, displayName, messageText);
+			}
+		}
+	}
+
+	if (channelType == "player")
+	{
+		const std::string & receiverAccountID = channelName;
+		std::string roomID = std::min(senderAccountID, receiverAccountID) + "_" + std::max(senderAccountID, receiverAccountID);
+
+		if (!database->isAccountIDExists(receiverAccountID))
+			return sendOperationFailed(connection, "Such player does not exists!");
+
+		database->insertChatMessage(senderAccountID, channelType, roomID, messageText);
+
+		sendChatMessage(connection, channelType, receiverAccountID, senderAccountID, displayName, messageText);
+		if (senderAccountID != receiverAccountID)
+		{
+			for(const auto & otherConnection : activeAccounts)
+				if (otherConnection.second == receiverAccountID)
+					sendChatMessage(otherConnection.first, channelType, senderAccountID, senderAccountID, displayName, messageText);
+		}
+	}
 }
 
 void LobbyServer::receiveClientRegister(const NetworkConnectionPtr & connection, const JsonNode & json)
@@ -409,13 +595,16 @@ void LobbyServer::receiveClientLogin(const NetworkConnectionPtr & connection, co
 
 	activeAccounts[connection] = accountID;
 
-	sendLoginSuccess(connection, accountCookie, displayName);
-	sendChatHistory(connection, database->getRecentMessageHistory());
+	sendClientLoginSuccess(connection, accountCookie, displayName);
+	sendRecentChatHistory(connection, "global", "english");
+	if (language != "english")
+		sendRecentChatHistory(connection, "global", language);
 
 	// send active game rooms list to new account
 	// and update acount list to everybody else including new account
 	broadcastActiveAccounts();
 	sendMessage(connection, prepareActiveGameRooms());
+	sendMatchesHistory(connection);
 }
 
 void LobbyServer::receiveServerLogin(const NetworkConnectionPtr & connection, const JsonNode & json)
@@ -435,7 +624,7 @@ void LobbyServer::receiveServerLogin(const NetworkConnectionPtr & connection, co
 	{
 		database->insertGameRoom(gameRoomID, accountID);
 		activeGameRooms[connection] = gameRoomID;
-		sendLoginSuccess(connection, accountCookie, {});
+		sendServerLoginSuccess(connection, accountCookie);
 		broadcastActiveGameRooms();
 	}
 }
@@ -510,6 +699,7 @@ void LobbyServer::receiveActivateGameRoom(const NetworkConnectionPtr & connectio
 {
 	std::string hostAccountID = json["hostAccountID"].String();
 	std::string accountID = activeAccounts[connection];
+	int playerLimit = json["playerLimit"].Integer();
 
 	if(database->isPlayerInGameRoom(accountID))
 		return sendOperationFailed(connection, "Player already in the room!");
@@ -527,6 +717,7 @@ void LobbyServer::receiveActivateGameRoom(const NetworkConnectionPtr & connectio
 	if(roomType == "private")
 		database->setGameRoomStatus(gameRoomID, LobbyRoomState::PRIVATE);
 
+	database->updateRoomPlayerLimit(gameRoomID, playerLimit);
 	database->insertPlayerIntoGameRoom(accountID, gameRoomID);
 	broadcastActiveGameRooms();
 	sendJoinRoomSuccess(connection, gameRoomID, false);
@@ -566,6 +757,23 @@ void LobbyServer::receiveJoinGameRoom(const NetworkConnectionPtr & connection, c
 	broadcastActiveGameRooms();
 }
 
+void LobbyServer::receiveChangeRoomDescription(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string gameRoomID = activeGameRooms[connection];
+	std::string description = json["description"].String();
+
+	database->updateRoomDescription(gameRoomID, description);
+	broadcastActiveGameRooms();
+}
+
+void LobbyServer::receiveGameStarted(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string gameRoomID = activeGameRooms[connection];
+
+	database->setGameRoomStatus(gameRoomID, LobbyRoomState::BUSY);
+	broadcastActiveGameRooms();
+}
+
 void LobbyServer::receiveLeaveGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json)
 {
 	std::string accountID = json["accountID"].String();
@@ -585,10 +793,10 @@ void LobbyServer::receiveSendInvite(const NetworkConnectionPtr & connection, con
 	std::string accountID = json["accountID"].String();
 	std::string gameRoomID = database->getAccountGameRoom(senderName);
 
-	auto targetAccount = findAccount(accountID);
+	auto targetAccountConnection = findAccount(accountID);
 
-	if(!targetAccount)
-		return sendOperationFailed(connection, "Invalid account to invite!");
+	if(!targetAccountConnection)
+		return sendOperationFailed(connection, "Player is offline or does not exists!");
 
 	if(!database->isPlayerInGameRoom(senderName))
 		return sendOperationFailed(connection, "You are not in the room!");
@@ -600,18 +808,7 @@ void LobbyServer::receiveSendInvite(const NetworkConnectionPtr & connection, con
 		return sendOperationFailed(connection, "This player is already invited!");
 
 	database->insertGameRoomInvite(accountID, gameRoomID);
-	sendInviteReceived(targetAccount, senderName, gameRoomID);
-}
-
-void LobbyServer::receiveDeclineInvite(const NetworkConnectionPtr & connection, const JsonNode & json)
-{
-	std::string accountID = activeAccounts[connection];
-	std::string gameRoomID = json["gameRoomID"].String();
-
-	if(database->getAccountInviteStatus(accountID, gameRoomID) != LobbyInviteStatus::INVITED)
-		return sendOperationFailed(connection, "No active invite found!");
-
-	database->deleteGameRoomInvite(accountID, gameRoomID);
+	sendInviteReceived(targetAccountConnection, senderName, gameRoomID);
 }
 
 LobbyServer::~LobbyServer() = default;

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません