Browse Source

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

Dydzio 1 year ago
parent
commit
fe42fab2d6
100 changed files with 3045 additions and 769 deletions
  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.serverClosing" : "关闭中...",
 	"vcmi.mainMenu.hostTCP" : "创建TCP/IP游戏",
 	"vcmi.mainMenu.hostTCP" : "创建TCP/IP游戏",
 	"vcmi.mainMenu.joinTCP" : "加入TCP/IP游戏",
 	"vcmi.mainMenu.joinTCP" : "加入TCP/IP游戏",
-	"vcmi.mainMenu.playerName" : "Player",
 
 
 	"vcmi.lobby.filepath" : "文件路径",
 	"vcmi.lobby.filepath" : "文件路径",
 	"vcmi.lobby.creationDate" : "创建时间",
 	"vcmi.lobby.creationDate" : "创建时间",
@@ -73,12 +72,33 @@
 	"vcmi.lobby.noUnderground" : "无地下部分",
 	"vcmi.lobby.noUnderground" : "无地下部分",
 	"vcmi.lobby.sortDate" : "以修改时间排序地图",
 	"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.invalidMap" : "{非法地图或战役}\n\n启动游戏失败,选择的地图或者战役,无效或被污染。原因:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{找不到数据文件}\n\n没有找到战役数据文件!你可能使用了不完整或损坏的英雄无敌3数据文件,请重新安装数据文件。",
 	"vcmi.client.errors.missingCampaigns" : "{找不到数据文件}\n\n没有找到战役数据文件!你可能使用了不完整或损坏的英雄无敌3数据文件,请重新安装数据文件。",
+	"vcmi.server.errors.disconnected" : "{网络错误}\n\n与游戏服务器的连接已断开!",
 	"vcmi.server.errors.existingProcess"     : "一个VCMI进程已经在运行,启动新进程前请结束它。",
 	"vcmi.server.errors.existingProcess"     : "一个VCMI进程已经在运行,启动新进程前请结束它。",
 	"vcmi.server.errors.modsToEnable"    : "{需要启用的mod列表}",
 	"vcmi.server.errors.modsToEnable"    : "{需要启用的mod列表}",
 	"vcmi.server.errors.modsToDisable"   : "{需要禁用的mod列表}",
 	"vcmi.server.errors.modsToDisable"   : "{需要禁用的mod列表}",
-	"vcmi.server.confirmReconnect"           : "您想要重连上一个会话么?",
 	"vcmi.server.errors.modNoDependency" : "读取mod包 {'%s'}失败!\n 需要的mod {'%s'} 没有安装或无效!\n",
 	"vcmi.server.errors.modNoDependency" : "读取mod包 {'%s'}失败!\n 需要的mod {'%s'} 没有安装或无效!\n",
 	"vcmi.server.errors.modConflict" : "读取的mod包 {'%s'}无法运行!\n 与另一个mod {'%s'}冲突!\n",
 	"vcmi.server.errors.modConflict" : "读取的mod包 {'%s'}无法运行!\n 与另一个mod {'%s'}冲突!\n",
 	"vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!",
 	"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.noPreview" : "no preview",
 	"vcmi.lobby.noUnderground" : "no underground",
 	"vcmi.lobby.noUnderground" : "no underground",
 	"vcmi.lobby.sortDate" : "Sorts maps by change date",
 	"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.username" : "Username:",
 	"vcmi.lobby.login.connecting" : "Connecting...",
 	"vcmi.lobby.login.connecting" : "Connecting...",
 	"vcmi.lobby.login.error" : "Connection error: %s",
 	"vcmi.lobby.login.error" : "Connection error: %s",
 	"vcmi.lobby.login.create" : "New Account",
 	"vcmi.lobby.login.create" : "New Account",
 	"vcmi.lobby.login.login" : "Login",
 	"vcmi.lobby.login.login" : "Login",
-
-	"vcmi.lobby.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.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.public" : "Any player can join public room.",
 	"vcmi.lobby.room.description.private" : "Only invited players can join private 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.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.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.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.new" : "New Game",
 	"vcmi.lobby.room.load" : "Load Game",
 	"vcmi.lobby.room.load" : "Load Game",
 	"vcmi.lobby.room.type" : "Room Type",
 	"vcmi.lobby.room.type" : "Room Type",
 	"vcmi.lobby.room.mode" : "Game Mode",
 	"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.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.",
 	"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.filepath" : "Назва файлу",
 	"vcmi.lobby.creationDate" : "Дата створення",
 	"vcmi.lobby.creationDate" : "Дата створення",
-	"vcmi.lobby.scenarioName" : "Scenario name",
+	"vcmi.lobby.scenarioName" : "Назва сценарію",
 	"vcmi.lobby.mapPreview" : "Огляд мапи",
 	"vcmi.lobby.mapPreview" : "Огляд мапи",
 	"vcmi.lobby.noPreview" : "огляд недоступний",
 	"vcmi.lobby.noPreview" : "огляд недоступний",
 	"vcmi.lobby.noUnderground" : "немає підземелля",
 	"vcmi.lobby.noUnderground" : "немає підземелля",
 	"vcmi.lobby.sortDate" : "Сортувати мапи за датою зміни",
 	"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.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
 	"vcmi.server.errors.modsToEnable"    : "{Потрібні модифікації для завантаження гри}",
 	"vcmi.server.errors.modsToEnable"    : "{Потрібні модифікації для завантаження гри}",

+ 4 - 0
client/CMakeLists.txt

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

+ 11 - 4
client/CServerHandler.cpp

@@ -13,6 +13,7 @@
 #include "Client.h"
 #include "Client.h"
 #include "CGameInfo.h"
 #include "CGameInfo.h"
 #include "ServerRunner.h"
 #include "ServerRunner.h"
+#include "GameChatHandler.h"
 #include "CPlayerInterface.h"
 #include "CPlayerInterface.h"
 #include "gui/CGuiHandler.h"
 #include "gui/CGuiHandler.h"
 #include "gui/WindowHandler.h"
 #include "gui/WindowHandler.h"
@@ -128,6 +129,7 @@ CServerHandler::~CServerHandler()
 CServerHandler::CServerHandler()
 CServerHandler::CServerHandler()
 	: networkHandler(INetworkHandler::createHandler())
 	: networkHandler(INetworkHandler::createHandler())
 	, lobbyClient(std::make_unique<GlobalLobbyClient>())
 	, lobbyClient(std::make_unique<GlobalLobbyClient>())
+	, gameChat(std::make_unique<GameChatHandler>())
 	, applier(std::make_unique<CApplier<CBaseForLobbyApply>>())
 	, applier(std::make_unique<CApplier<CBaseForLobbyApply>>())
 	, threadNetwork(&CServerHandler::threadRunNetwork, this)
 	, threadNetwork(&CServerHandler::threadRunNetwork, this)
 	, state(EClientState::NONE)
 	, state(EClientState::NONE)
@@ -168,6 +170,14 @@ void CServerHandler::resetStateForLobby(EStartMode mode, ESelectionScreen screen
 		localPlayerNames = playerNames;
 		localPlayerNames = playerNames;
 	else
 	else
 		localPlayerNames.push_back(settings["general"]["playerName"].String());
 		localPlayerNames.push_back(settings["general"]["playerName"].String());
+
+	gameChat->resetMatchState();
+	lobbyClient->resetMatchState();
+}
+
+GameChatHandler & CServerHandler::getGameChat()
+{
+	return *gameChat;
 }
 }
 
 
 GlobalLobbyClient & CServerHandler::getGlobalLobby()
 GlobalLobbyClient & CServerHandler::getGlobalLobby()
@@ -532,10 +542,7 @@ void CServerHandler::sendMessage(const std::string & txt) const
 	}
 	}
 	else
 	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 CClient;
 class CBaseForLobbyApply;
 class CBaseForLobbyApply;
 class GlobalLobbyClient;
 class GlobalLobbyClient;
+class GameChatHandler;
 class IServerRunner;
 class IServerRunner;
 
 
 class HighScoreCalculation;
 class HighScoreCalculation;
@@ -100,6 +101,7 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
 	std::unique_ptr<INetworkHandler> networkHandler;
 	std::unique_ptr<INetworkHandler> networkHandler;
 	std::shared_ptr<INetworkConnection> networkConnection;
 	std::shared_ptr<INetworkConnection> networkConnection;
 	std::unique_ptr<GlobalLobbyClient> lobbyClient;
 	std::unique_ptr<GlobalLobbyClient> lobbyClient;
+	std::unique_ptr<GameChatHandler> gameChat;
 	std::unique_ptr<CApplier<CBaseForLobbyApply>> applier;
 	std::unique_ptr<CApplier<CBaseForLobbyApply>> applier;
 	std::unique_ptr<IServerRunner> serverRunner;
 	std::unique_ptr<IServerRunner> serverRunner;
 	std::shared_ptr<CMapInfo> mapToStart;
 	std::shared_ptr<CMapInfo> mapToStart;
@@ -113,8 +115,6 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
 	void threadRunNetwork();
 	void threadRunNetwork();
 	void waitForServerShutdown();
 	void waitForServerShutdown();
 
 
-	void sendLobbyPack(const CPackForLobby & pack) const override;
-
 	void onPacketReceived(const NetworkConnectionPtr &, const std::vector<std::byte> & message) override;
 	void onPacketReceived(const NetworkConnectionPtr &, const std::vector<std::byte> & message) override;
 	void onConnectionFailed(const std::string & errorMessage) override;
 	void onConnectionFailed(const std::string & errorMessage) override;
 	void onConnectionEstablished(const NetworkConnectionPtr &) override;
 	void onConnectionEstablished(const NetworkConnectionPtr &) override;
@@ -153,6 +153,7 @@ public:
 	void startLocalServerAndConnect(bool connectToLobby);
 	void startLocalServerAndConnect(bool connectToLobby);
 	void connectToServer(const std::string & addr, const ui16 port);
 	void connectToServer(const std::string & addr, const ui16 port);
 
 
+	GameChatHandler & getGameChat();
 	GlobalLobbyClient & getGlobalLobby();
 	GlobalLobbyClient & getGlobalLobby();
 	INetworkHandler & getNetworkHandler();
 	INetworkHandler & getNetworkHandler();
 
 
@@ -179,6 +180,8 @@ public:
 	ui16 getRemotePort() const;
 	ui16 getRemotePort() const;
 
 
 	// Lobby server API for UI
 	// Lobby server API for UI
+	void sendLobbyPack(const CPackForLobby & pack) const override;
+
 	void sendClientConnecting() const override;
 	void sendClientConnecting() const override;
 	void sendClientDisconnecting() override;
 	void sendClientDisconnecting() override;
 	void setCampaignState(std::shared_ptr<CampaignState> newCampaign) 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);
 		boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
 		if(LOCPLINT && LOCPLINT->cingconsole)
 		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 "gui/WindowHandler.h"
 #include "widgets/MiscWidgets.h"
 #include "widgets/MiscWidgets.h"
 #include "CMT.h"
 #include "CMT.h"
+#include "GameChatHandler.h"
 #include "CServerHandler.h"
 #include "CServerHandler.h"
 
 
 #include "../CCallback.h"
 #include "../CCallback.h"
@@ -228,21 +229,31 @@ void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack)
 void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack)
 void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack)
 {
 {
 	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
 	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)
 void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack)
 {
 {
 	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
 	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)
 void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack)
 {
 {
 	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
 	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)
 void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack)
 {
 {
 	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
 	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)
 void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack)
@@ -253,6 +264,9 @@ void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & p
 			? ObjectInstanceID()
 			? ObjectInstanceID()
 			: pack.moves[0].dstArmy;
 			: pack.moves[0].dstArmy;
 		dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy);
 		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)
 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)
 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);
 	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)
 void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack)

+ 5 - 9
client/NetPacksLobbyClient.cpp

@@ -24,6 +24,7 @@
 #include "globalLobby/GlobalLobbyClient.h"
 #include "globalLobby/GlobalLobbyClient.h"
 
 
 #include "CServerHandler.h"
 #include "CServerHandler.h"
+#include "GameChatHandler.h"
 #include "CGameInfo.h"
 #include "CGameInfo.h"
 #include "Client.h"
 #include "Client.h"
 #include "gui/CGuiHandler.h"
 #include "gui/CGuiHandler.h"
@@ -58,11 +59,12 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientCon
 				// announce opened game room
 				// announce opened game room
 				// TODO: find better approach?
 				// TODO: find better approach?
 				int roomType = settings["lobby"]["roomType"].Integer();
 				int roomType = settings["lobby"]["roomType"].Integer();
+				int roomPlayerLimit = settings["lobby"]["roomPlayerLimit"].Integer();
 
 
 				if (roomType != 0)
 				if (roomType != 0)
-					handler.getGlobalLobby().sendOpenPrivateRoom();
+					handler.getGlobalLobby().sendOpenRoom("private", roomPlayerLimit);
 				else
 				else
-					handler.getGlobalLobby().sendOpenPublicRoom();
+					handler.getGlobalLobby().sendOpenRoom("public", roomPlayerLimit);
 			}
 			}
 
 
 			while (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
 			while (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
@@ -97,13 +99,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyClientDisconnected(LobbyClientD
 
 
 void ApplyOnLobbyScreenNetPackVisitor::visitLobbyChatMessage(LobbyChatMessage & pack)
 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)
 void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack)

+ 61 - 33
client/adventureMap/CInGameConsole.cpp

@@ -14,7 +14,8 @@
 #include "../CGameInfo.h"
 #include "../CGameInfo.h"
 #include "../CMusicHandler.h"
 #include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
 #include "../CPlayerInterface.h"
-#include "../PlayerLocalState.h"
+#include "../CServerHandler.h"
+#include "../GameChatHandler.h"
 #include "../ClientCommandManager.h"
 #include "../ClientCommandManager.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../gui/WindowHandler.h"
@@ -31,6 +32,7 @@
 #include "../../lib/CThreadHelper.h"
 #include "../../lib/CThreadHelper.h"
 #include "../../lib/TextOperations.h"
 #include "../../lib/TextOperations.h"
 #include "../../lib/mapObjects/CArmedInstance.h"
 #include "../../lib/mapObjects/CArmedInstance.h"
+#include "../../lib/MetaString.h"
 
 
 CInGameConsole::CInGameConsole()
 CInGameConsole::CInGameConsole()
 	: CIntObject(KEYBOARD | TIME | TEXTINPUT)
 	: CIntObject(KEYBOARD | TIME | TEXTINPUT)
@@ -51,7 +53,6 @@ void CInGameConsole::show(Canvas & to)
 
 
 	int number = 0;
 	int number = 0;
 
 
-	boost::unique_lock<boost::mutex> lock(texts_mx);
 	for(auto & text : texts)
 	for(auto & text : texts)
 	{
 	{
 		Point leftBottomCorner(0, pos.h);
 		Point leftBottomCorner(0, pos.h);
@@ -64,46 +65,52 @@ void CInGameConsole::show(Canvas & to)
 
 
 void CInGameConsole::tick(uint32_t msPassed)
 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();
 	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())
 	if(sizeBefore != texts.size())
 		GH.windows().totalRedraw(); // FIXME: ingame console has no parent widget set
 		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
 	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)
 bool CInGameConsole::captureThisKey(EShortcut key)
 {
 {
-	if (enteredText.empty())
+	if (!isEnteringText())
 		return false;
 		return false;
 
 
 	switch (key)
 	switch (key)
@@ -140,7 +147,7 @@ void CInGameConsole::keyPressed (EShortcut key)
 	if (LOCPLINT->cingconsole != this)
 	if (LOCPLINT->cingconsole != this)
 		return;
 		return;
 
 
-	if(enteredText.empty() && key != EShortcut::GAME_ACTIVATE_CONSOLE)
+	if(!isEnteringText() && key != EShortcut::GAME_ACTIVATE_CONSOLE)
 		return; //because user is not entering any text
 		return; //because user is not entering any text
 
 
 	switch(key)
 	switch(key)
@@ -222,7 +229,7 @@ void CInGameConsole::textInputed(const std::string & inputtedText)
 	if (LOCPLINT->cingconsole != this)
 	if (LOCPLINT->cingconsole != this)
 		return;
 		return;
 
 
-	if(enteredText.empty())
+	if(!isEnteringText())
 		return;
 		return;
 
 
 	enteredText.resize(enteredText.size()-1);
 	enteredText.resize(enteredText.size()-1);
@@ -238,12 +245,27 @@ void CInGameConsole::textEdited(const std::string & inputtedText)
  //do nothing here
  //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()
 void CInGameConsole::startEnteringText()
 {
 {
 	if (!isActive())
 	if (!isActive())
 		return;
 		return;
 
 
-	if(enteredText != "")
+	if(isEnteringText())
 		return;
 		return;
 		
 		
 	assert(currentStatusBar.expired());//effectively, nullptr check
 	assert(currentStatusBar.expired());//effectively, nullptr check
@@ -254,6 +276,8 @@ void CInGameConsole::startEnteringText()
 
 
 	GH.statusbar()->setEnteringMode(true);
 	GH.statusbar()->setEnteringMode(true);
 	GH.statusbar()->setEnteredText(enteredText);
 	GH.statusbar()->setEnteredText(enteredText);
+
+	showRecentChatHistory();
 }
 }
 
 
 void CInGameConsole::endEnteringText(bool processEnteredText)
 void CInGameConsole::endEnteringText(bool processEnteredText)
@@ -278,7 +302,7 @@ void CInGameConsole::endEnteringText(bool processEnteredText)
 			clientCommandThread.detach();
 			clientCommandThread.detach();
 		}
 		}
 		else
 		else
-			LOCPLINT->cb->sendMessage(txt, LOCPLINT->localState->getCurrentArmy());
+			CSH->getGameChat().sendMessageGameplay(txt);
 	}
 	}
 	enteredText.clear();
 	enteredText.clear();
 
 
@@ -300,3 +324,7 @@ void CInGameConsole::refreshEnteredText()
 		statusbar->setEnteredText(enteredText);
 		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
 	/// Currently visible texts in the overlay
 	std::vector<TextState> texts;
 	std::vector<TextState> texts;
 
 
-	/// protects texts
-	boost::mutex texts_mx;
-
 	/// previously entered texts, for up/down arrows to work
 	/// previously entered texts, for up/down arrows to work
 	std::vector<std::string> previouslyEntered;
 	std::vector<std::string> previouslyEntered;
 
 
@@ -41,8 +38,13 @@ private:
 	std::weak_ptr<IStatusBar> currentStatusBar;
 	std::weak_ptr<IStatusBar> currentStatusBar;
 	std::string enteredText;
 	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:
 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 tick(uint32_t msPassed) override;
 	void show(Canvas & to) override;
 	void show(Canvas & to) override;

+ 6 - 0
client/eventsSDL/InputSourceTouch.cpp

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

+ 191 - 47
client/globalLobby/GlobalLobbyClient.cpp

@@ -11,15 +11,17 @@
 #include "StdInc.h"
 #include "StdInc.h"
 #include "GlobalLobbyClient.h"
 #include "GlobalLobbyClient.h"
 
 
+#include "GlobalLobbyInviteWindow.h"
 #include "GlobalLobbyLoginWindow.h"
 #include "GlobalLobbyLoginWindow.h"
 #include "GlobalLobbyWindow.h"
 #include "GlobalLobbyWindow.h"
 
 
+#include "../CGameInfo.h"
+#include "../CMusicHandler.h"
+#include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../gui/WindowHandler.h"
-#include "../windows/InfoWindows.h"
-#include "../CServerHandler.h"
 #include "../mainmenu/CMainMenu.h"
 #include "../mainmenu/CMainMenu.h"
-#include "../CGameInfo.h"
+#include "../windows/InfoWindows.h"
 
 
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/MetaString.h"
 #include "../../lib/MetaString.h"
@@ -27,18 +29,15 @@
 #include "../../lib/TextOperations.h"
 #include "../../lib/TextOperations.h"
 #include "../../lib/CGeneralTextHandler.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)
 void GlobalLobbyClient::onPacketReceived(const std::shared_ptr<INetworkConnection> &, const std::vector<std::byte> & message)
 {
 {
 	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
 	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
@@ -51,8 +50,8 @@ void GlobalLobbyClient::onPacketReceived(const std::shared_ptr<INetworkConnectio
 	if(json["type"].String() == "operationFailed")
 	if(json["type"].String() == "operationFailed")
 		return receiveOperationFailed(json);
 		return receiveOperationFailed(json);
 
 
-	if(json["type"].String() == "loginSuccess")
-		return receiveLoginSuccess(json);
+	if(json["type"].String() == "clientLoginSuccess")
+		return receiveClientLoginSuccess(json);
 
 
 	if(json["type"].String() == "chatHistory")
 	if(json["type"].String() == "chatHistory")
 		return receiveChatHistory(json);
 		return receiveChatHistory(json);
@@ -72,6 +71,9 @@ void GlobalLobbyClient::onPacketReceived(const std::shared_ptr<INetworkConnectio
 	if(json["type"].String() == "inviteReceived")
 	if(json["type"].String() == "inviteReceived")
 		return receiveInviteReceived(json);
 		return receiveInviteReceived(json);
 
 
+	if(json["type"].String() == "matchesHistory")
+		return receiveMatchesHistory(json);
+
 	logGlobal->error("Received unexpected message from lobby server: %s", json["type"].String());
 	logGlobal->error("Received unexpected message from lobby server: %s", json["type"].String());
 }
 }
 
 
@@ -106,7 +108,7 @@ void GlobalLobbyClient::receiveOperationFailed(const JsonNode & json)
 	// TODO: handle errors in lobby menu
 	// TODO: handle errors in lobby menu
 }
 }
 
 
-void GlobalLobbyClient::receiveLoginSuccess(const JsonNode & json)
+void GlobalLobbyClient::receiveClientLoginSuccess(const JsonNode & json)
 {
 {
 	{
 	{
 		Settings configCookie = settings.write["lobby"]["accountCookie"];
 		Settings configCookie = settings.write["lobby"]["accountCookie"];
@@ -126,36 +128,59 @@ void GlobalLobbyClient::receiveLoginSuccess(const JsonNode & json)
 
 
 void GlobalLobbyClient::receiveChatHistory(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())
 	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)
 		if(lobbyWindowPtr)
-			lobbyWindowPtr->onGameChatMessage(displayName, messageText, timeFormatted);
+			lobbyWindowPtr->onGameChatMessage(message.displayName, message.messageText, message.timeFormatted, channelType, channelName);
 	}
 	}
 }
 }
 
 
 void GlobalLobbyClient::receiveChatMessage(const JsonNode & json)
 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();
 	auto lobbyWindowPtr = lobbyWindow.lock();
 	if(lobbyWindowPtr)
 	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)
 void GlobalLobbyClient::receiveActiveAccounts(const JsonNode & json)
 {
 {
 	activeAccounts.clear();
 	activeAccounts.clear();
 
 
-	for (auto const & jsonEntry : json["accounts"].Vector())
+	for(const auto & jsonEntry : json["accounts"].Vector())
 	{
 	{
 		GlobalLobbyAccount account;
 		GlobalLobbyAccount account;
 
 
@@ -175,7 +200,7 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
 {
 {
 	activeRooms.clear();
 	activeRooms.clear();
 
 
-	for (auto const & jsonEntry : json["gameRooms"].Vector())
+	for(const auto & jsonEntry : json["gameRooms"].Vector())
 	{
 	{
 		GlobalLobbyRoom room;
 		GlobalLobbyRoom room;
 
 
@@ -183,8 +208,18 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
 		room.hostAccountID = jsonEntry["hostAccountID"].String();
 		room.hostAccountID = jsonEntry["hostAccountID"].String();
 		room.hostAccountDisplayName = jsonEntry["hostAccountDisplayName"].String();
 		room.hostAccountDisplayName = jsonEntry["hostAccountDisplayName"].String();
 		room.description = jsonEntry["description"].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);
 		activeRooms.push_back(room);
 	}
 	}
@@ -194,16 +229,60 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
 		lobbyWindowPtr->onActiveRooms(activeRooms);
 		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)
 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)
 void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json)
 {
 {
-	Settings configRoom = settings.write["lobby"]["roomID"];
-	configRoom->String() = json["gameRoomID"].String();
-
 	if (json["proxyMode"].Bool())
 	if (json["proxyMode"].Bool())
 	{
 	{
 		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, {});
 		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();
 		int16_t port = settings["lobby"]["port"].Integer();
 		CSH->connectToServer(hostname, port);
 		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)
 void GlobalLobbyClient::onConnectionEstablished(const std::shared_ptr<INetworkConnection> & connection)
@@ -284,21 +366,13 @@ void GlobalLobbyClient::sendMessage(const JsonNode & data)
 	networkConnection->sendPacket(data.toBytes());
 	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;
 	JsonNode toSend;
 	toSend["type"].String() = "activateGameRoom";
 	toSend["type"].String() = "activateGameRoom";
 	toSend["hostAccountID"] = settings["lobby"]["accountID"];
 	toSend["hostAccountID"] = settings["lobby"]["accountID"];
-	toSend["roomType"].String() = "private";
+	toSend["roomType"].String() = mode;
+	toSend["playerLimit"].Integer() = playerLimit;
 	sendMessage(toSend);
 	sendMessage(toSend);
 }
 }
 
 
@@ -348,8 +422,46 @@ const std::vector<GlobalLobbyRoom> & GlobalLobbyClient::getActiveRooms() const
 	return activeRooms;
 	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()
 void GlobalLobbyClient::activateInterface()
 {
 {
+	if (GH.windows().topWindow<GlobalLobbyWindow>() != nullptr)
+	{
+		GH.windows().popWindows(1);
+		return;
+	}
+
 	if (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
 	if (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
 		return;
 		return;
 
 
@@ -362,14 +474,46 @@ void GlobalLobbyClient::activateInterface()
 		GH.windows().pushWindow(createLoginWindow());
 		GH.windows().pushWindow(createLoginWindow());
 }
 }
 
 
+void GlobalLobbyClient::activateRoomInviteInterface()
+{
+	GH.windows().createAndPushWindow<GlobalLobbyInviteWindow>();
+}
+
 void GlobalLobbyClient::sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection)
 void GlobalLobbyClient::sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection)
 {
 {
 	JsonNode toSend;
 	JsonNode toSend;
 	toSend["type"].String() = "clientProxyLogin";
 	toSend["type"].String() = "clientProxyLogin";
 	toSend["accountID"] = settings["lobby"]["accountID"];
 	toSend["accountID"] = settings["lobby"]["accountID"];
 	toSend["accountCookie"] = settings["lobby"]["accountCookie"];
 	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"));
 	assert(JsonUtils::validate(toSend, "vcmi:lobbyProtocol/" + toSend["type"].String(), toSend["type"].String() + " pack"));
 	netConnection->sendPacket(toSend.toBytes());
 	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<GlobalLobbyAccount> activeAccounts;
 	std::vector<GlobalLobbyRoom> activeRooms;
 	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::shared_ptr<INetworkConnection> networkConnection;
+	std::string currentGameRoomUUID;
 
 
 	std::weak_ptr<GlobalLobbyLoginWindow> loginWindow;
 	std::weak_ptr<GlobalLobbyLoginWindow> loginWindow;
 	std::weak_ptr<GlobalLobbyWindow> lobbyWindow;
 	std::weak_ptr<GlobalLobbyWindow> lobbyWindow;
@@ -37,11 +46,12 @@ class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyabl
 
 
 	void receiveAccountCreated(const JsonNode & json);
 	void receiveAccountCreated(const JsonNode & json);
 	void receiveOperationFailed(const JsonNode & json);
 	void receiveOperationFailed(const JsonNode & json);
-	void receiveLoginSuccess(const JsonNode & json);
+	void receiveClientLoginSuccess(const JsonNode & json);
 	void receiveChatHistory(const JsonNode & json);
 	void receiveChatHistory(const JsonNode & json);
 	void receiveChatMessage(const JsonNode & json);
 	void receiveChatMessage(const JsonNode & json);
 	void receiveActiveAccounts(const JsonNode & json);
 	void receiveActiveAccounts(const JsonNode & json);
 	void receiveActiveGameRooms(const JsonNode & json);
 	void receiveActiveGameRooms(const JsonNode & json);
+	void receiveMatchesHistory(const JsonNode & json);
 	void receiveJoinRoomSuccess(const JsonNode & json);
 	void receiveJoinRoomSuccess(const JsonNode & json);
 	void receiveInviteReceived(const JsonNode & json);
 	void receiveInviteReceived(const JsonNode & json);
 
 
@@ -54,17 +64,24 @@ public:
 
 
 	const std::vector<GlobalLobbyAccount> & getActiveAccounts() const;
 	const std::vector<GlobalLobbyAccount> & getActiveAccounts() const;
 	const std::vector<GlobalLobbyRoom> & getActiveRooms() const;
 	const std::vector<GlobalLobbyRoom> & getActiveRooms() const;
+	const std::vector<std::string> & getActiveChannels() const;
+	const std::vector<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
 	/// Activate interface and pushes lobby UI as top window
 	void activateInterface();
 	void activateInterface();
+	void activateRoomInviteInterface();
+
+	void sendMatchChatMessage(const std::string & messageText);
 	void sendMessage(const JsonNode & data);
 	void sendMessage(const JsonNode & data);
 	void sendClientRegister(const std::string & accountName);
 	void sendClientRegister(const std::string & accountName);
 	void sendClientLogin();
 	void sendClientLogin();
-	void sendOpenPublicRoom();
-	void sendOpenPrivateRoom();
+	void sendOpenRoom(const std::string & mode, int playerLimit);
 
 
 	void sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection);
 	void sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection);
+	void resetMatchState();
 
 
 	void connect();
 	void connect();
 	bool isConnected() const;
 	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
  * Authors: listed in file AUTHORS in main folder
  *
  *
@@ -22,6 +22,16 @@ struct GlobalLobbyRoom
 	std::string hostAccountID;
 	std::string hostAccountID;
 	std::string hostAccountDisplayName;
 	std::string hostAccountDisplayName;
 	std::string description;
 	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 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);
 	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 = std::make_shared<CToggleGroup>(nullptr);
 	toggleRoomType->addToggle(0, buttonPublic);
 	toggleRoomType->addToggle(0, buttonPublic);

+ 183 - 46
client/globalLobby/GlobalLobbyWidget.cpp

@@ -14,33 +14,38 @@
 #include "GlobalLobbyClient.h"
 #include "GlobalLobbyClient.h"
 #include "GlobalLobbyWindow.h"
 #include "GlobalLobbyWindow.h"
 
 
+#include "../CGameInfo.h"
+#include "../CMusicHandler.h"
 #include "../CServerHandler.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/Images.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
 #include "../widgets/ObjectLists.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/TextControls.h"
 
 
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/Languages.h"
 #include "../../lib/MetaString.h"
 #include "../../lib/MetaString.h"
+
 GlobalLobbyWidget::GlobalLobbyWidget(GlobalLobbyWindow * window)
 GlobalLobbyWidget::GlobalLobbyWidget(GlobalLobbyWindow * window)
 	: window(window)
 	: window(window)
 {
 {
 	addCallback("closeWindow", [](int) { GH.windows().popWindows(1); });
 	addCallback("closeWindow", [](int) { GH.windows().popWindows(1); });
 	addCallback("sendMessage", [this](int) { this->window->doSendChatMessage(); });
 	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"));
 	const JsonNode config(JsonPath::builtin("config/widgets/lobbyWindow.json"));
 	build(config);
 	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();
 		const auto & accounts = CSH->getGlobalLobby().getActiveAccounts();
 
 
@@ -49,21 +54,7 @@ std::shared_ptr<CIntObject> GlobalLobbyWidget::buildAccountList(const JsonNode &
 		return std::make_shared<CIntObject>();
 		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();
 		const auto & rooms = CSH->getGlobalLobby().getActiveRooms();
 
 
@@ -72,16 +63,55 @@ std::shared_ptr<CIntObject> GlobalLobbyWidget::buildRoomList(const JsonNode & co
 		return std::make_shared<CIntObject>();
 		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 position = readPosition(config["position"]);
 	auto itemOffset = readPosition(config["itemOffset"]);
 	auto itemOffset = readPosition(config["itemOffset"]);
 	auto sliderPosition = readPosition(config["sliderPosition"]);
 	auto sliderPosition = readPosition(config["sliderPosition"]);
 	auto sliderSize = readPosition(config["sliderSize"]);
 	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;
 	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()
 std::shared_ptr<CLabel> GlobalLobbyWidget::getAccountNameLabel()
@@ -109,47 +139,154 @@ std::shared_ptr<CListBox> GlobalLobbyWidget::getRoomList()
 	return widget<CListBox>("roomList");
 	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)
 GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription)
 {
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 
 
-	const auto & onJoinClicked = [window, roomID=roomDescription.gameRoomID]()
+	const auto & onJoinClicked = [window, roomID = roomDescription.gameRoomID]()
 	{
 	{
 		window->doJoinRoom(roomID);
 		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");
 	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.w = 230;
 	pos.h = 40;
 	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;
 	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:
 public:
 	explicit GlobalLobbyWidget(GlobalLobbyWindow * window);
 	explicit GlobalLobbyWidget(GlobalLobbyWindow * window);
@@ -31,27 +33,67 @@ public:
 	std::shared_ptr<CTextBox> getGameChat();
 	std::shared_ptr<CTextBox> getGameChat();
 	std::shared_ptr<CListBox> getAccountList();
 	std::shared_ptr<CListBox> getAccountList();
 	std::shared_ptr<CListBox> getRoomList();
 	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:
 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<TransparentFilledRectangle> backgroundOverlay;
 	std::shared_ptr<CLabel> labelName;
 	std::shared_ptr<CLabel> labelName;
 	std::shared_ptr<CLabel> labelStatus;
 	std::shared_ptr<CLabel> labelStatus;
-	std::shared_ptr<CButton> buttonInvite;
+
+public:
+	GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription);
 };
 };
 
 
 class GlobalLobbyRoomCard : public CIntObject
 class GlobalLobbyRoomCard : public CIntObject
 {
 {
-public:
-	GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription);
-
 	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
 	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
 	std::shared_ptr<CLabel> labelName;
 	std::shared_ptr<CLabel> labelName;
 	std::shared_ptr<CLabel> labelRoomSize;
 	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<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 "../widgets/ObjectLists.h"
 
 
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CConfigHandler.h"
+#include "../../lib/Languages.h"
 #include "../../lib/MetaString.h"
 #include "../../lib/MetaString.h"
 
 
 GlobalLobbyWindow::GlobalLobbyWindow()
 GlobalLobbyWindow::GlobalLobbyWindow()
@@ -33,6 +34,40 @@ GlobalLobbyWindow::GlobalLobbyWindow()
 	center();
 	center();
 
 
 	widget->getAccountNameLabel()->setText(settings["lobby"]["displayName"].String());
 	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()
 void GlobalLobbyWindow::doSendChatMessage()
@@ -41,6 +76,8 @@ void GlobalLobbyWindow::doSendChatMessage()
 
 
 	JsonNode toSend;
 	JsonNode toSend;
 	toSend["type"].String() = "sendChatMessage";
 	toSend["type"].String() = "sendChatMessage";
+	toSend["channelType"].String() = currentChannelType;
+	toSend["channelName"].String() = currentChannelName;
 	toSend["messageText"].String() = messageText;
 	toSend["messageText"].String() = messageText;
 
 
 	CSH->getGlobalLobby().sendMessage(toSend);
 	CSH->getGlobalLobby().sendMessage(toSend);
@@ -64,6 +101,8 @@ void GlobalLobbyWindow::doInviteAccount(const std::string & accountID)
 
 
 void GlobalLobbyWindow::doJoinRoom(const std::string & roomID)
 void GlobalLobbyWindow::doJoinRoom(const std::string & roomID)
 {
 {
+	unreadInvites.erase(roomID);
+
 	JsonNode toSend;
 	JsonNode toSend;
 	toSend["type"].String() = "joinGameRoom";
 	toSend["type"].String() = "joinGameRoom";
 	toSend["gameRoomID"].String() = roomID;
 	toSend["gameRoomID"].String() = roomID;
@@ -71,8 +110,18 @@ void GlobalLobbyWindow::doJoinRoom(const std::string & roomID)
 	CSH->getGlobalLobby().sendMessage(toSend);
 	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;
 	MetaString chatMessageFormatted;
 	chatMessageFormatted.appendRawString("[%s] {%s}: %s\n");
 	chatMessageFormatted.appendRawString("[%s] {%s}: %s\n");
 	chatMessageFormatted.replaceRawString(when);
 	chatMessageFormatted.replaceRawString(when);
@@ -84,16 +133,58 @@ void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std:
 	widget->getGameChat()->setText(chatHistory);
 	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)
 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)
 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();
 	widget->getRoomList()->reset();
 }
 }
 
 
+bool GlobalLobbyWindow::isInviteUnread(const std::string & gameRoomID) const
+{
+	return unreadInvites.count(gameRoomID) > 0;
+}
+
 void GlobalLobbyWindow::onJoinedRoom()
 void GlobalLobbyWindow::onJoinedRoom()
 {
 {
 	widget->getAccountList()->reset();
 	widget->getAccountList()->reset();

+ 17 - 2
client/globalLobby/GlobalLobbyWindow.h

@@ -18,21 +18,36 @@ struct GlobalLobbyRoom;
 class GlobalLobbyWindow : public CWindowObject
 class GlobalLobbyWindow : public CWindowObject
 {
 {
 	std::string chatHistory;
 	std::string chatHistory;
+	std::string currentChannelType;
+	std::string currentChannelName;
 
 
 	std::shared_ptr<GlobalLobbyWidget> widget;
 	std::shared_ptr<GlobalLobbyWidget> widget;
+	std::set<std::string> unreadChannels;
+	std::set<std::string> unreadInvites;
 
 
 public:
 public:
 	GlobalLobbyWindow();
 	GlobalLobbyWindow();
 
 
+	// Callbacks for UI
+
 	void doSendChatMessage();
 	void doSendChatMessage();
 	void doCreateGameRoom();
 	void doCreateGameRoom();
-
+	void doOpenChannel(const std::string & channelType, const std::string & channelName, const std::string & roomDescription);
 	void doInviteAccount(const std::string & accountID);
 	void doInviteAccount(const std::string & accountID);
 	void doJoinRoom(const std::string & roomID);
 	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 onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts);
 	void onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms);
 	void onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms);
+	void onMatchesHistory(const std::vector<GlobalLobbyRoom> & history);
+	void onInviteReceived(const std::string & invitedRoomID);
 	void onJoinedRoom();
 	void onJoinedRoom();
 	void onLeftRoom();
 	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 alignment = readTextAlignment(config["alignment"]);
 	auto color = readColor(config["color"]);
 	auto color = readColor(config["color"]);
 	auto text = readText(config["text"]);
 	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
 std::shared_ptr<CIntObject> InterfaceObjectConfigurable::buildWidget(JsonNode config) const

+ 14 - 1
client/gui/TextAlignment.h

@@ -9,4 +9,17 @@
  */
  */
 #pragma once
 #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 "../widgets/Buttons.h"
 #include "../windows/InfoWindows.h"
 #include "../windows/InfoWindows.h"
 #include "../render/Colors.h"
 #include "../render/Colors.h"
+#include "../globalLobby/GlobalLobbyClient.h"
 
 
 #include "../../CCallback.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], [&]()
 	buttonBack = std::make_shared<CButton>(Point(581, 535), AnimationPath::builtin("SCNRBACK.DEF"), CGI->generaltexth->zelp[105], [&]()
 	{
 	{
+		bool wasInLobbyRoom = CSH->inLobbyRoom();
 		CSH->sendClientDisconnecting();
 		CSH->sendClientDisconnecting();
 		close();
 		close();
+
+		if (wasInLobbyRoom)
+			CSH->getGlobalLobby().activateInterface();
 	}, EShortcut::GLOBAL_CANCEL);
 	}, EShortcut::GLOBAL_CANCEL);
 }
 }
 
 

+ 33 - 26
client/lobby/CSelectionBase.cpp

@@ -26,6 +26,7 @@
 #include "../gui/CGuiHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
 #include "../gui/WindowHandler.h"
+#include "../globalLobby/GlobalLobbyClient.h"
 #include "../mainmenu/CMainMenu.h"
 #include "../mainmenu/CMainMenu.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/CComponent.h"
@@ -44,10 +45,10 @@
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/CRandomGenerator.h"
 #include "../../lib/CRandomGenerator.h"
 #include "../../lib/CThreadHelper.h"
 #include "../../lib/CThreadHelper.h"
+#include "../../lib/MetaString.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/filesystem/Filesystem.h"
-#include "../../lib/mapping/CMapInfo.h"
 #include "../../lib/mapping/CMapHeader.h"
 #include "../../lib/mapping/CMapHeader.h"
-#include "../../lib/CRandomGenerator.h"
+#include "../../lib/mapping/CMapInfo.h"
 
 
 ISelectionScreenInfo::ISelectionScreenInfo(ESelectionScreen ScreenType)
 ISelectionScreenInfo::ISelectionScreenInfo(ESelectionScreen ScreenType)
 	: screenType(ScreenType)
 	: screenType(ScreenType)
@@ -137,6 +138,12 @@ InfoCard::InfoCard()
 	playerListBg = std::make_shared<CPicture>(ImagePath::builtin("CHATPLUG.bmp"), 16, 276);
 	playerListBg = std::make_shared<CPicture>(ImagePath::builtin("CHATPLUG.bmp"), 16, 276);
 	chat = std::make_shared<CChatBox>(Rect(18, 126, 335, 143));
 	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)
 	if(SEL->screenType == ESelectionScreen::campaignList)
 	{
 	{
 		labelCampaignDescription = std::make_shared<CLabel>(26, 132, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]);
 		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);
 		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);
 		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();
 		disableLabelRedraws();
 	}
 	}
 	setChat(false);
 	setChat(false);
+	if (CSH->inLobbyRoom())
+		setChat(true); // FIXME: less ugly version?
 }
 }
 
 
 void InfoCard::disableLabelRedraws()
 void InfoCard::disableLabelRedraws()
@@ -240,27 +248,21 @@ void InfoCard::changeSelection()
 
 
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	// FIXME: We recreate them each time because CLabelGroup don't use smart pointers
 	// 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)
 	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
 		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();
 			labelVictoryConditionText->disable();
 			iconsLossCondition->disable();
 			iconsLossCondition->disable();
 			labelLossConditionText->disable();
 			labelLossConditionText->disable();
-			labelGroupPlayersAssigned->enable();
-			labelGroupPlayersUnassigned->enable();
+			labelGroupPlayers->enable();
+		}
+		if (CSH->inLobbyRoom())
+		{
+			buttonInvitePlayers->enable();
+			buttonOpenGlobalLobby->enable();
 		}
 		}
 		mapDescription->disable();
 		mapDescription->disable();
 		chat->enable();
 		chat->enable();
@@ -298,6 +304,8 @@ void InfoCard::setChat(bool activateChat)
 	}
 	}
 	else
 	else
 	{
 	{
+		buttonInvitePlayers->disable();
+		buttonOpenGlobalLobby->disable();
 		mapDescription->enable();
 		mapDescription->enable();
 		chat->disable();
 		chat->disable();
 		playerListBg->disable();
 		playerListBg->disable();
@@ -315,8 +323,7 @@ void InfoCard::setChat(bool activateChat)
 			iconsLossCondition->enable();
 			iconsLossCondition->enable();
 			labelVictoryConditionText->enable();
 			labelVictoryConditionText->enable();
 			labelLossConditionText->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> labelVictoryConditionText;
 	std::shared_ptr<CLabel> labelLossConditionText;
 	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:
 public:
 
 
 	bool showChat;
 	bool showChat;

+ 7 - 1
client/widgets/ObjectLists.cpp

@@ -66,6 +66,7 @@ size_t CTabbedInt::getActive() const
 
 
 void CTabbedInt::reset()
 void CTabbedInt::reset()
 {
 {
+
 	deleteItem(activeTab);
 	deleteItem(activeTab);
 	activeTab = createItem(activeID);
 	activeTab = createItem(activeID);
 	activeTab->moveTo(pos.topLeft());
 	activeTab->moveTo(pos.topLeft());
@@ -127,6 +128,11 @@ void CListBox::updatePositions()
 
 
 void CListBox::reset()
 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;
 	size_t current = first;
 	for (auto & elem : items)
 	for (auto & elem : items)
 	{
 	{
@@ -279,4 +285,4 @@ void CListBoxWithCallback::moveToPrev()
 	CListBox::moveToPrev();
 	CListBox::moveToPrev();
 	if(movedPosCallback)
 	if(movedPosCallback)
 		movedPosCallback(getPos());
 		movedPosCallback(getPos());
-}
+}

+ 15 - 18
client/widgets/TextControls.cpp

@@ -82,6 +82,8 @@ void CLabel::setAutoRedraw(bool value)
 
 
 void CLabel::setText(const std::string & Txt)
 void CLabel::setText(const std::string & Txt)
 {
 {
+	assert(TextOperations::isValidUnicodeString(Txt));
+
 	text = Txt;
 	text = Txt;
 	
 	
 	trimText();
 	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
 	// input is rect in which given text should be placed
 	// calculate proper position for top-left corner of the text
 	// 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.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;
 		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 begin = 0;
 	size_t currDelimeter = 0;
 	size_t currDelimeter = 0;

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

@@ -22,6 +22,13 @@
 			"type" : "string",
 			"type" : "string",
 			"enum" : [ "public", "private" ],
 			"enum" : [ "public", "private" ],
 			"description" : "Room type to use for activation"
 			"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",
 				"type" : "object",
 				"additionalProperties" : false,
 				"additionalProperties" : false,
-				"required" : [ "gameRoomID", "hostAccountID", "hostAccountDisplayName", "description", "playersCount", "playersLimit" ],
+				"required" : [ "gameRoomID", "hostAccountID", "hostAccountDisplayName", "description", "participants", "playerLimit", "status", "ageSeconds" ],
 				"properties" : {
 				"properties" : {
 					"gameRoomID" :
 					"gameRoomID" :
 					{
 					{
@@ -42,15 +42,46 @@
 						"type" : "string",
 						"type" : "string",
 						"description" : "Auto-generated description of this room"
 						"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",
 						"type" : "number",
+						"minimum" : 1,
+						"maximum" : 8,
 						"description" : "Maximum number of players that can join this room, including host"
 						"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",
 	"$schema" : "http://json-schema.org/draft-06/schema",
 	"title" : "Lobby protocol: chatHistory",
 	"title" : "Lobby protocol: chatHistory",
 	"description" : "Sent by server immediately after login to fill initial chat history",
 	"description" : "Sent by server immediately after login to fill initial chat history",
-	"required" : [ "type", "messages" ],
+	"required" : [ "type", "messages", "channelType", "channelName" ],
 	"additionalProperties" : false,
 	"additionalProperties" : false,
 
 
 	"properties" : {
 	"properties" : {
@@ -12,6 +12,19 @@
 			"type" : "string",
 			"type" : "string",
 			"const" : "chatHistory"
 			"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" :
 		"messages" :
 		{
 		{
 			"type" : "array",
 			"type" : "array",

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

@@ -3,7 +3,7 @@
 	"$schema" : "http://json-schema.org/draft-06/schema",
 	"$schema" : "http://json-schema.org/draft-06/schema",
 	"title" : "Lobby protocol: chatMessage",
 	"title" : "Lobby protocol: chatMessage",
 	"description" : "Sent by server to all players in lobby whenever new message is sent to game chat",
 	"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,
 	"additionalProperties" : false,
 
 
 	"properties" : {
 	"properties" : {
@@ -27,17 +27,16 @@
 			"type" : "string",
 			"type" : "string",
 			"description" : "Display name of account that have sent this message"
 			"description" : "Display name of account that have sent this message"
 		},
 		},
-		"roomMode" :
+		"channelType" :
 		{
 		{
 			"type" : "string",
 			"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",
 			"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",
 	"type" : "object",
 	"$schema" : "http://json-schema.org/draft-06/schema",
 	"$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",
 	"description" : "Sent by server once player sucesfully logs into the lobby",
 	"required" : [ "type", "accountCookie", "displayName" ],
 	"required" : [ "type", "accountCookie", "displayName" ],
 	"additionalProperties" : false,
 	"additionalProperties" : false,
@@ -10,7 +10,7 @@
 		"type" :
 		"type" :
 		{
 		{
 			"type" : "string",
 			"type" : "string",
-			"const" : "loginSuccess"
+			"const" : "clientLoginSuccess"
 		},
 		},
 		"accountCookie" :
 		"accountCookie" :
 		{
 		{

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

@@ -10,7 +10,7 @@
 		"type" :
 		"type" :
 		{
 		{
 			"type" : "string",
 			"type" : "string",
-			"const" : "clientLogin"
+			"const" : "clientProxyLogin"
 		},
 		},
 		"accountID" :
 		"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",
 	"type" : "object",
 	"$schema" : "http://json-schema.org/draft-06/schema",
 	"$schema" : "http://json-schema.org/draft-06/schema",
 	"title" : "Lobby protocol: sendChatMessage",
 	"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,
 	"additionalProperties" : false,
 
 
 	"properties" : {
 	"properties" : {
@@ -16,6 +16,17 @@
 		{
 		{
 			"type" : "string",
 			"type" : "string",
 			"description" : "Text of sent message"
 			"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",
 			"type" : "object",
 			"additionalProperties" : false,
 			"additionalProperties" : false,
 			"default" : {},
 			"default" : {},
-			"required" : [ "mapPreview", "accountID", "accountCookie", "displayName", "hostname", "port", "roomPlayerLimit", "roomType", "roomMode", "roomID" ],
+			"required" : [ "mapPreview", "accountID", "accountCookie", "displayName", "hostname", "port", "roomPlayerLimit", "roomType", "roomMode" ],
 			"properties" : {
 			"properties" : {
 				"mapPreview" : {
 				"mapPreview" : {
 					"type" : "boolean",
 					"type" : "boolean",
@@ -607,10 +607,6 @@
 				"roomMode" : {
 				"roomMode" : {
 					"type" : "number",
 					"type" : "number",
 					"default" : 0
 					"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}
 			"rect": {"x": 5, "y": 50, "w": 250, "h": 500}
 		},
 		},
 		{
 		{
+			"name" : "headerRoomList",
 			"type": "labelTitle",
 			"type": "labelTitle",
-			"position": {"x": 15, "y": 53},
-			"text" : "Room List"
+			"position": {"x": 15, "y": 53}
 		},
 		},
 		{
 		{
-			"type" : "roomList",
+			"type" : "lobbyItemList",
 			"name" : "roomList",
 			"name" : "roomList",
-			"position" : { "x" : 7, "y" : 69 },
+			"itemType" : "room",
+			"position" : { "x" : 7, "y" : 68 },
 			"itemOffset" : { "x" : 0, "y" : 40 },
 			"itemOffset" : { "x" : 0, "y" : 40 },
 			"sliderPosition" : { "x" : 230, "y" : 0 },
 			"sliderPosition" : { "x" : 230, "y" : 0 },
-			"sliderSize" : { "x" : 450, "y" : 450 }
+			"sliderSize" : { "x" : 480, "y" : 480 },
+			"visibleAmount" : 12
 		},
 		},
 		
 		
 		{
 		{
 			"type": "areaFilled",
 			"type": "areaFilled",
-			"rect": {"x": 270, "y": 50, "w": 150, "h": 540}
+			"rect": {"x": 270, "y": 50, "w": 150, "h": 140}
 		},
 		},
 		{
 		{
+			"name" : "headerChannelList",
 			"type": "labelTitle",
 			"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",
 			"type": "areaFilled",
 			"rect": {"x": 430, "y": 50, "w": 430, "h": 515}
 			"rect": {"x": 430, "y": 50, "w": 430, "h": 515}
 		},
 		},
 		{
 		{
+			"name" : "headerGameChat",
 			"type": "labelTitle",
 			"type": "labelTitle",
-			"position": {"x": 440, "y": 53},
-			"text" : "Game Chat"
+			"position": {"x": 440, "y": 53}
 		},
 		},
 		{
 		{
 			"type": "textBox",
 			"type": "textBox",
@@ -89,18 +119,19 @@
 			"font": "small",
 			"font": "small",
 			"alignment": "left",
 			"alignment": "left",
 			"color": "white",
 			"color": "white",
-			"rect": {"x": 440, "y": 70, "w": 430, "h": 495}
+			"blueTheme" : true,
+			"rect": {"x": 440, "y": 68, "w": 418, "h": 495}
 		},
 		},
 		
 		
 		{
 		{
 			"type": "areaFilled",
 			"type": "areaFilled",
-			"rect": {"x": 430, "y": 565, "w": 395, "h": 25}
+			"rect": {"x": 430, "y": 565, "w": 397, "h": 25}
 		},
 		},
 		{
 		{
 			"name" : "messageInput",
 			"name" : "messageInput",
 			"type": "textInput",
 			"type": "textInput",
 			"alignment" : "left",
 			"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}
 			"rect": {"x": 870, "y": 50, "w": 150, "h": 540}
 		},
 		},
 		{
 		{
+			"name": "headerAccountList",
 			"type": "labelTitle",
 			"type": "labelTitle",
-			"position": {"x": 880, "y": 53},
-			"text" : "Account List"
+			"position": {"x": 880, "y": 53}
 		},
 		},
 		{
 		{
-			"type" : "accountList",
+			"type" : "lobbyItemList",
 			"name" : "accountList",
 			"name" : "accountList",
-			"position" : { "x" : 872, "y" : 69 },
+			"itemType" : "account",
+			"position" : { "x" : 872, "y" : 68 },
 			"itemOffset" : { "x" : 0, "y" : 40 },
 			"itemOffset" : { "x" : 0, "y" : 40 },
 			"sliderPosition" : { "x" : 130, "y" : 0 },
 			"sliderPosition" : { "x" : 130, "y" : 0 },
-			"sliderSize" : { "x" : 520, "y" : 520 }
+			"sliderSize" : { "x" : 520, "y" : 520 },
+			"visibleAmount" : 13
 		},
 		},
 
 
 		{
 		{
 			"type": "button",
 			"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",
 			"help": "core.help.288",
 			"callback": "closeWindow",
 			"callback": "closeWindow",
 			"hotkey": "globalCancel",
 			"hotkey": "globalCancel",
@@ -153,7 +168,7 @@
 					"font": "medium",
 					"font": "medium",
 					"alignment": "center",
 					"alignment": "center",
 					"color": "yellow",
 					"color": "yellow",
-					"text": "Close"
+					"text": "core.help.561.hover" // Back
 				}
 				}
 			]
 			]
 		},
 		},
@@ -161,18 +176,15 @@
 		{
 		{
 			"type": "button",
 			"type": "button",
 			"position": {"x": 828, "y": 565},
 			"position": {"x": 828, "y": 565},
-			"image": "settingsWindow/button32",
+			"image": "lobbySendMessage",
 			"help": "core.help.288",
 			"help": "core.help.288",
 			"callback": "sendMessage",
 			"callback": "sendMessage",
 			"hotkey": "globalAccept",
 			"hotkey": "globalAccept",
 			"items":
 			"items":
 			[
 			[
 				{
 				{
-					"type": "label",
-					"font": "medium",
-					"alignment": "center",
-					"color": "yellow",
-					"text": ">"
+					"type": "picture",
+					"image": "lobby/iconSend"
 				}
 				}
 			]
 			]
 		},
 		},
@@ -180,7 +192,7 @@
 		{
 		{
 			"type": "button",
 			"type": "button",
 			"position": {"x": 10, "y": 555},
 			"position": {"x": 10, "y": 555},
-			"image": "settingsWindow/button190",
+			"image": "lobbyCreateRoom",
 			"help": "core.help.288",
 			"help": "core.help.288",
 			"callback": "createGameRoom",
 			"callback": "createGameRoom",
 			"items":
 			"items":
@@ -190,7 +202,7 @@
 					"font": "medium",
 					"font": "medium",
 					"alignment": "center",
 					"alignment": "center",
 					"color": "yellow",
 					"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:
 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)
 - 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.
 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
 ### 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
 ### Serialization to file
 
 

+ 2 - 5
lib/Languages.h

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

+ 8 - 0
lib/TextOperations.h

@@ -60,8 +60,16 @@ namespace TextOperations
 	/// get formatted DateTime depending on the language selected
 	/// get formatted DateTime depending on the language selected
 	DLL_LINKAGE std::string getFormattedDateTimeLocal(std::time_t dt);
 	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)
 	/// get formatted time (without date)
 	DLL_LINKAGE std::string getFormattedTimeLocal(std::time_t dt);
 	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)
 	std::string stacking; // bonuses with the same stacking value don't stack (e.g. Angel/Archangel morale bonus)
 
 
 	CAddInfo additionalInfo;
 	CAddInfo additionalInfo;
-	BonusLimitEffect effectRange = BonusLimitEffect::NO_LIMIT; //if not NO_LIMIT, bonus will be omitted by default
+	BonusLimitEffect effectRange = BonusLimitEffect::NO_LIMIT;
 
 
 	TLimiterPtr limiter;
 	TLimiterPtr limiter;
 	TPropagatorPtr propagator;
 	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());
 	out.reserve(bonuses.size());
 	for(const auto & b : bonuses)
 	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);
 			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)
 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);
 	return resolveIdentifier("spell", identifier);
 }
 }
 
 
@@ -369,6 +373,10 @@ std::string SpellID::encode(const si32 index)
 {
 {
 	if (index == -1)
 	if (index == -1)
 		return "";
 		return "";
+	if (index == SpellID::PRESET)
+		return "preset";
+	if (index == SpellID::SPELLBOOK_PRESET)
+		return "spellbook_preset";
 	return VLC->spells()->getByIndex(index)->getJsonKey();
 	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
 			//ask if player agrees to pay gold
 			BlockingDialog ynd(true,false);
 			BlockingDialog ynd(true,false);
 			ynd.player = h->tempOwner;
 			ynd.player = h->tempOwner;
+			ynd.components.emplace_back(ComponentType::RESOURCE, GameResID(GameResID::GOLD), action);
 			std::string tmp = VLC->generaltexth->advobtxt[90];
 			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(getStackCount(SlotID(0))));
 			boost::algorithm::replace_first(tmp, "%d", std::to_string(action));
 			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'))) &&
                    ((argv[i][1]>='0') || (argv[i][1]<='9'))) &&
                   (strlen(argv[i]) == 2)))
                   (strlen(argv[i]) == 2)))
             {
             {
-                FILE * fin;
+                FILE * fin = NULL;
                 int size_read;
                 int size_read;
                 const char* filenameinzip = argv[i];
                 const char* filenameinzip = argv[i];
                 const char *savefilenameinzip;
                 const char *savefilenameinzip;

+ 7 - 0
lib/minizip/mztools.c

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

+ 2 - 0
lib/modding/IdentifierStorage.cpp

@@ -83,6 +83,8 @@ CIdentifierStorage::CIdentifierStorage()
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel5", 5);
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel5", 5);
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel6", 6);
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel6", 6);
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel7", 7);
 	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)
 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);
 	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");
 	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
 	auto moveZoneToCenterOfMass = [width, height](const std::shared_ptr<Zone> & zone) -> void
 	{
 	{
 		int3 total(0, 0, 0);
 		int3 total(0, 0, 0);
-		auto tiles = zone->area().getTiles();
+		auto tiles = zone->area()->getTiles();
 		for(const auto & tile : tiles)
 		for(const auto & tile : tiles)
 		{
 		{
 			total += tile;
 			total += tile;
@@ -892,14 +892,14 @@ void CZonePlacer::assignZones(CRandomGenerator * rand)
 				{
 				{
 					distances.emplace_back(zone.second, static_cast<float>(pos.dist2dSQ(zone.second->getPos())));
 					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)
 	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");
 			throw rmgException("Empty zone is generated, probably RMG template is inappropriate for map size");
 		
 		
 		moveZoneToCenterOfMass(zone.second);
 		moveZoneToCenterOfMass(zone.second);
@@ -948,14 +948,14 @@ void CZonePlacer::assignZones(CRandomGenerator * rand)
 
 
 				//Tile closest to vertex belongs to zone
 				//Tile closest to vertex belongs to zone
 				auto closestZone = boost::min_element(distances, simpleCompareByDistance)->first;
 				auto closestZone = boost::min_element(distances, simpleCompareByDistance)->first;
-				closestZone->area().add(pos);
+				closestZone->area()->add(pos);
 				map.setZoneID(pos, closestZone->getId());
 				map.setZoneID(pos, closestZone->getId());
 			}
 			}
 		}
 		}
 
 
 		for(const auto & zone : zonesOnLevel[level])
 		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
 				// 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());
 				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);
 				auto discardTiles = collectDistantTiles(*zone.second, zone.second->getSize() + 1.f);
 				for(const auto & t : discardTiles)
 				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
 			//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);
 			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)
 rmg::Tileset collectDistantTiles(const Zone& zone, int distance)
 {
 {
 	uint32_t distanceSq = distance * 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;
 		return t.dist2dSQ(zone.getPos()) > distanceSq;
 	});
 	});

+ 38 - 23
lib/rmg/Zone.cpp

@@ -75,30 +75,49 @@ void Zone::setPos(const int3 &Pos)
 	pos = 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()
 void Zone::clearTiles()
 {
 {
-	//Lock lock(mx);
+	Lock lock(areaMutex);
 	dArea.clear();
 	dArea.clear();
 	dAreaPossible.clear();
 	dAreaPossible.clear();
 	dAreaFree.clear();
 	dAreaFree.clear();
@@ -107,7 +126,6 @@ void Zone::clearTiles()
 void Zone::initFreeTiles()
 void Zone::initFreeTiles()
 {
 {
 	rmg::Tileset possibleTiles;
 	rmg::Tileset possibleTiles;
-	//Lock lock(mx);
 	vstd::copy_if(dArea.getTiles(), vstd::set_inserter(possibleTiles), [this](const int3 &tile) -> bool
 	vstd::copy_if(dArea.getTiles(), vstd::set_inserter(possibleTiles), [this](const int3 &tile) -> bool
 	{
 	{
 		return map.isPossible(tile);
 		return map.isPossible(tile);
@@ -122,11 +140,6 @@ void Zone::initFreeTiles()
 	}
 	}
 }
 }
 
 
-rmg::Area & Zone::freePaths()
-{
-	return dAreaFree;
-}
-
 FactionID Zone::getTownType() const
 FactionID Zone::getTownType() const
 {
 {
 	return townType;
 	return townType;
@@ -225,18 +238,14 @@ TModificators Zone::getModificators()
 void Zone::connectPath(const rmg::Path & path)
 void Zone::connectPath(const rmg::Path & path)
 ///connect current tile to any other free tile within zone
 ///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())
 	for(const auto & t : path.getPathArea().getTilesVector())
 		map.setOccupied(t, ETileType::FREE);
 		map.setOccupied(t, ETileType::FREE);
 }
 }
 
 
 void Zone::fractalize()
 void Zone::fractalize()
 {
 {
-	rmg::Area clearedTiles(dAreaFree);
-	rmg::Area possibleTiles(dAreaPossible);
-	rmg::Area tilesToIgnore; //will be erased in this iteration
-
 	//Squared
 	//Squared
 	float minDistance = 9 * 9;
 	float minDistance = 9 * 9;
 	float freeDistance = pos.z ? (10 * 10) : (9 * 9);
 	float freeDistance = pos.z ? (10 * 10) : (9 * 9);
@@ -282,6 +291,13 @@ void Zone::fractalize()
 	freeDistance = freeDistance * marginFactor;
 	freeDistance = freeDistance * marginFactor;
 	vstd::amax(freeDistance, 4 * 4);
 	vstd::amax(freeDistance, 4 * 4);
 	logGlobal->trace("Zone %d: treasureValue %d blockDistance: %2.f, freeDistance: %2.f", getId(), treasureValue, blockDistance, freeDistance);
 	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)
 	if(type != ETemplateZoneType::JUNCTION)
 	{
 	{
@@ -336,7 +352,6 @@ void Zone::fractalize()
 		}
 		}
 	}
 	}
 
 
-	Lock lock(areaMutex);
 	//Connect with free areas
 	//Connect with free areas
 	auto areas = connectedAreas(clearedTiles, true);
 	auto areas = connectedAreas(clearedTiles, true);
 	for(auto & area : areas)
 	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;
 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
 class Zone : public rmg::ZoneOptions
 {
 {
 public:
 public:
@@ -48,11 +85,14 @@ public:
 	int3 getPos() const;
 	int3 getPos() const;
 	void setPos(const int3 &pos);
 	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 initFreeTiles();
 	void clearTiles();
 	void clearTiles();
@@ -89,7 +129,7 @@ public:
 	
 	
 	CRandomGenerator & getRand();
 	CRandomGenerator & getRand();
 public:
 public:
-	boost::recursive_mutex areaMutex;
+	mutable boost::recursive_mutex areaMutex;
 	using Lock = boost::unique_lock<boost::recursive_mutex>;
 	using Lock = boost::unique_lock<boost::recursive_mutex>;
 	
 	
 protected:
 protected:

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

@@ -28,6 +28,23 @@
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 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()
 void ConnectionsPlacer::process()
 {
 {
 	collectNeighbourZones();
 	collectNeighbourZones();
@@ -36,16 +53,13 @@ void ConnectionsPlacer::process()
 	{
 	{
 		for (auto& c : dConnections)
 		for (auto& c : dConnections)
 		{
 		{
-			if (c.getZoneA() != zone.getId() && c.getZoneB() != zone.getId())
-				continue;
-
 			auto otherZone = map.getZones().at(c.getZoneB());
 			auto otherZone = map.getZones().at(c.getZoneB());
 			auto* cp = otherZone->getModificator<ConnectionsPlacer>();
 			auto* cp = otherZone->getModificator<ConnectionsPlacer>();
 
 
 			while (cp)
 			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 (lock1.owns_lock() && lock2.owns_lock())
 				{
 				{
 					if (!vstd::contains(dCompleted, c))
 					if (!vstd::contains(dCompleted, c))
@@ -78,8 +92,15 @@ void ConnectionsPlacer::init()
 	POSTFUNCTION(RoadPlacer);
 	POSTFUNCTION(RoadPlacer);
 	POSTFUNCTION(ObjectManager);
 	POSTFUNCTION(ObjectManager);
 	
 	
+	auto id = zone.getId();
 	for(auto c : map.getMapGenOptions().getMapTemplate()->getConnectedZoneIds())
 	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)
 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())
 	bool directProhibited = vstd::contains(ourTerrain->prohibitTransitions, otherZone->getTerrainType())
 						 || vstd::contains(otherTerrain->prohibitTransitions, zone.getTerrainType());
 						 || vstd::contains(otherTerrain->prohibitTransitions, zone.getTerrainType());
+
+	auto lock = lockZones(otherZone);
+
 	auto directConnectionIterator = dNeighbourZones.find(otherZoneId);
 	auto directConnectionIterator = dNeighbourZones.find(otherZoneId);
 
 
 	if (directConnectionIterator != dNeighbourZones.end())
 	if (directConnectionIterator != dNeighbourZones.end())
@@ -115,19 +139,19 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 			for (auto borderPos : directConnectionIterator->second)
 			for (auto borderPos : directConnectionIterator->second)
 			{
 			{
 				//TODO: Refactor common code with direct connection
 				//TODO: Refactor common code with direct connection
-				int3 potentialPos = zone.areaPossible().nearest(borderPos);
+				int3 potentialPos = zone.areaPossible()->nearest(borderPos);
 				assert(borderPos != potentialPos);
 				assert(borderPos != potentialPos);
 
 
 				auto safetyGap = rmg::Area({ potentialPos });
 				auto safetyGap = rmg::Area({ potentialPos });
 				safetyGap.unite(safetyGap.getBorderOutside());
 				safetyGap.unite(safetyGap.getBorderOutside());
-				safetyGap.intersect(zone.areaPossible());
+				safetyGap.intersect(zone.areaPossible().get());
 				if (!safetyGap.empty())
 				if (!safetyGap.empty())
 				{
 				{
-					safetyGap.intersect(otherZone->areaPossible());
+					safetyGap.intersect(otherZone->areaPossible().get());
 					if (safetyGap.empty())
 					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)
 						auto costFunction = [&border](const int3& s, const int3& d)
 						{
 						{
@@ -139,9 +163,9 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 						theirArea.add(potentialPos);
 						theirArea.add(potentialPos);
 						rmg::Path ourPath(ourArea);
 						rmg::Path ourPath(ourArea);
 						rmg::Path theirPath(theirArea);
 						rmg::Path theirPath(theirArea);
-						ourPath.connect(zone.freePaths());
+						ourPath.connect(zone.freePaths().get());
 						ourPath = ourPath.search(potentialPos, true, costFunction);
 						ourPath = ourPath.search(potentialPos, true, costFunction);
-						theirPath.connect(otherZone->freePaths());
+						theirPath.connect(otherZone->freePaths().get());
 						theirPath = theirPath.search(potentialPos, true, costFunction);
 						theirPath = theirPath.search(potentialPos, true, costFunction);
 
 
 						if (ourPath.valid() && theirPath.valid())
 						if (ourPath.valid() && theirPath.valid())
@@ -174,7 +198,7 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 		int3 roadNode;
 		int3 roadNode;
 		for (auto borderPos : directConnectionIterator->second)
 		for (auto borderPos : directConnectionIterator->second)
 		{
 		{
-			int3 potentialPos = zone.areaPossible().nearest(borderPos);
+			int3 potentialPos = zone.areaPossible()->nearest(borderPos);
 			assert(borderPos != potentialPos);
 			assert(borderPos != potentialPos);
 
 
 			//Check if guard pos doesn't touch any 3rd zone. This would create unwanted passage to 3rd zone
 			//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 });
 				auto safetyGap = rmg::Area({ potentialPos });
 				safetyGap.unite(safetyGap.getBorderOutside());
 				safetyGap.unite(safetyGap.getBorderOutside());
-				safetyGap.intersect(zone.areaPossible());
+				safetyGap.intersect(zone.areaPossible().get());
 				if (!safetyGap.empty())
 				if (!safetyGap.empty())
 				{
 				{
-					safetyGap.intersect(otherZone->areaPossible());
+					safetyGap.intersect(otherZone->areaPossible().get());
 					if (safetyGap.empty())
 					if (safetyGap.empty())
 					{
 					{
 						float distanceToCenter = zone.getPos().dist2d(potentialPos) * otherZone->getPos().dist2d(potentialPos);
 						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 & manager = *zone.getModificator<ObjectManager>();
 			auto * monsterType = manager.chooseGuard(connection.getGuardStrength(), true);
 			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)
 			auto costFunction = [&border](const int3 & s, const int3 & d)
 			{
 			{
@@ -241,9 +265,9 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 			theirArea.add(guardPos);
 			theirArea.add(guardPos);
 			rmg::Path ourPath(ourArea);
 			rmg::Path ourPath(ourArea);
 			rmg::Path theirPath(theirArea);
 			rmg::Path theirPath(theirArea);
-			ourPath.connect(zone.freePaths());
+			ourPath.connect(zone.freePaths().get());
 			ourPath = ourPath.search(guardPos, true, costFunction);
 			ourPath = ourPath.search(guardPos, true, costFunction);
-			theirPath.connect(otherZone->freePaths());
+			theirPath.connect(otherZone->freePaths().get());
 			theirPath = theirPath.search(guardPos, true, costFunction);
 			theirPath = theirPath.search(guardPos, true, costFunction);
 			
 			
 			if(ourPath.valid() && theirPath.valid())
 			if(ourPath.valid() && theirPath.valid())
@@ -262,8 +286,8 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 				else
 				else
 				{
 				{
 					//Update distances from empty passage, too
 					//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);
 					map.setOccupied(guardPos, ETileType::FREE);
 					manager.updateDistances(guardPos);
 					manager.updateDistances(guardPos);
 					otherZone->getModificator<ObjectManager>()->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);
 		int3 zShift(0, 0, zone.getPos().z - otherZone->getPos().z);
 
 
+		auto lock = lockZones(otherZone);
+
 		std::scoped_lock doubleLock(zone.areaMutex, otherZone->areaMutex);
 		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())
 		if(!commonArea.empty())
 		{
 		{
 			assert(zone.getModificator<ObjectManager>());
 			assert(zone.getModificator<ObjectManager>());
@@ -339,7 +365,7 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 			bool guarded2 = managerOther.addGuard(rmgGate2, connection.getGuardStrength(), true);
 			bool guarded2 = managerOther.addGuard(rmgGate2, connection.getGuardStrength(), true);
 			int minDist = 3;
 			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)
 			rmg::Path path1 = manager.placeAndConnectObject(commonArea, rmgGate1, [this, minDist, &path2, &rmgGate1, &zShift, guarded2, &managerOther, &rmgGate2	](const int3 & tile)
 			{
 			{
 				auto ti = map.getTileInfo(tile);
 				auto ti = map.getTileInfo(tile);
@@ -403,7 +429,7 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 
 
 void ConnectionsPlacer::collectNeighbourZones()
 void ConnectionsPlacer::collectNeighbourZones()
 {
 {
-	auto border = zone.area().getBorderOutside();
+	auto border = zone.area()->getBorderOutside();
 	for(const auto & i : border)
 	for(const auto & i : border)
 	{
 	{
 		if(!map.isOnMap(i))
 		if(!map.isOnMap(i))
@@ -423,8 +449,8 @@ bool ConnectionsPlacer::shouldGenerateRoad(const rmg::ZoneConnection& connection
 
 
 void ConnectionsPlacer::createBorder()
 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 blockBorder = borderArea.getSubarea([this, &borderOutsideArea](const int3 & t)
 	{
 	{
 		auto tile = borderOutsideArea.nearest(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())
 	for(const auto & tile : blockBorder.getTilesVector())
 	{
 	{
 		if(map.isPossible(tile))
 		if(map.isPossible(tile))
 		{
 		{
 			map.setOccupied(tile, ETileType::BLOCKED);
 			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())
 			if(map.isPossible(nearbyPos) && map.getZoneID(nearbyPos) == zone.getId())
 			{
 			{
 				map.setOccupied(nearbyPos, ETileType::BLOCKED);
 				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:
 protected:
 	void collectNeighbourZones();
 	void collectNeighbourZones();
+	std::pair<Zone::Lock, Zone::Lock> lockZones(std::shared_ptr<Zone> otherZone);
 
 
 protected:
 protected:
 	std::vector<rmg::ZoneConnection> dConnections;
 	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()
 bool Modificator::isReady()
 {
 {
-	Lock lock(mx, boost::try_to_lock_t{});
+	Lock lock(mx, boost::try_to_lock);
 	if (!lock.owns_lock())
 	if (!lock.owns_lock())
 	{
 	{
 		return false;
 		return false;
@@ -63,7 +63,7 @@ bool Modificator::isReady()
 
 
 bool Modificator::isFinished()
 bool Modificator::isFinished()
 {
 {
-	Lock lock(mx, boost::try_to_lock_t{});
+	Lock lock(mx, boost::try_to_lock);
 	if (!lock.owns_lock())
 	if (!lock.owns_lock())
 	{
 	{
 		return false;
 		return false;
@@ -119,6 +119,8 @@ void Modificator::postfunction(Modificator * modificator)
 
 
 void Modificator::dump()
 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()));
 	std::ofstream out(boost::str(boost::format("seed_%d_modzone_%d_%s.txt") % generator.getRandomSeed() % zone.getId() % getName()));
 	int levels = map.levels();
 	int levels = map.levels();
 	int width =  map.width();
 	int width =  map.width();
@@ -140,13 +142,13 @@ void Modificator::dump()
 
 
 char Modificator::dump(const int3 & t)
 char Modificator::dump(const int3 & t)
 {
 {
-	if(zone.freePaths().contains(t))
+	if(zone.freePaths()->contains(t))
 		return '.'; //free path
 		return '.'; //free path
-	if(zone.areaPossible().contains(t))
+	if(zone.areaPossible()->contains(t))
 		return ' '; //possible
 		return ' '; //possible
-	if(zone.areaUsed().contains(t))
+	if(zone.areaUsed()->contains(t))
 		return 'U'; //used
 		return 'U'; //used
-	if(zone.area().contains(t))
+	if(zone.area()->contains(t))
 	{
 	{
 		if(map.shouldBeBlocked(t))
 		if(map.shouldBeBlocked(t))
 			return '#'; //obstacle
 			return '#'; //obstacle

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

@@ -40,7 +40,30 @@ void ObjectManager::process()
 void ObjectManager::init()
 void ObjectManager::init()
 {
 {
 	DEPENDENCY(WaterAdopter);
 	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(TownPlacer); //Only secondary towns
 	DEPENDENCY(MinePlacer);
 	DEPENDENCY(MinePlacer);
 	POSTFUNCTION(RoadPlacer);
 	POSTFUNCTION(RoadPlacer);
@@ -49,9 +72,11 @@ void ObjectManager::init()
 
 
 void ObjectManager::createDistancesPriorityQueue()
 void ObjectManager::createDistancesPriorityQueue()
 {
 {
+	const auto tiles = zone.areaPossible()->getTilesVector();
+
 	RecursiveLock lock(externalAccessMutex);
 	RecursiveLock lock(externalAccessMutex);
 	tilesByDistance.clear();
 	tilesByDistance.clear();
-	for(const auto & tile : zone.areaPossible().getTilesVector())
+	for(const auto & tile : tiles)
 	{
 	{
 		tilesByDistance.push(std::make_pair(tile, map.getNearestObjectDistance(tile)));
 		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)
 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();
 	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);
 		ui32 d = distanceFunction(tile);
 		map.setNearestObjectDistance(tile, std::min(static_cast<float>(d), map.getNearestObjectDistance(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)
 	if(optimizer & OptimizeType::DISTANCE)
 	{
 	{
+		// Do not add or remove tiles while we iterate on them
+		//RecursiveLock lock(externalAccessMutex);
 		auto open = tilesByDistance;
 		auto open = tilesByDistance;
+
 		while(!open.empty())
 		while(!open.empty())
 		{
 		{
 			auto node = open.top();
 			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
 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)
 	return placeAndConnectObject(searchArea, obj, [this, min_dist, &obj](const int3 & tile)
 	{
 	{
 		float bestDistance = 10e9;
 		float bestDistance = 10e9;
@@ -372,7 +409,7 @@ bool ObjectManager::createMonoliths()
 		bool guarded = addGuard(rmgObject, objInfo.guardStrength, true);
 		bool guarded = addGuard(rmgObject, objInfo.guardStrength, true);
 
 
 		Zone::Lock lock(zone.areaMutex);
 		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())
 		if(!path.valid())
 		{
 		{
@@ -395,7 +432,7 @@ bool ObjectManager::createRequiredObjects()
 {
 {
 	logGlobal->trace("Creating required objects");
 	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)
 	for(const auto & objInfo : requiredObjects)
 	{
 	{
 		rmg::Object rmgObject(*objInfo.obj);
 		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));
 		bool guarded = addGuard(rmgObject, objInfo.guardStrength, (objInfo.obj->ID == Obj::MONOLITH_TWO_WAY));
 
 
 		Zone::Lock lock(zone.areaMutex);
 		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())
 		if(!path.valid())
 		{
 		{
@@ -421,7 +458,7 @@ bool ObjectManager::createRequiredObjects()
 			
 			
 			rmg::Object rmgNearObject(*nearby.obj);
 			rmg::Object rmgNearObject(*nearby.obj);
 			rmg::Area possibleArea(rmgObject.instances().front()->getBlockedArea().getBorderOutside());
 			rmg::Area possibleArea(rmgObject.instances().front()->getBlockedArea().getBorderOutside());
-			possibleArea.intersect(zone.areaPossible());
+			possibleArea.intersect(zone.areaPossible().get());
 			if(possibleArea.empty())
 			if(possibleArea.empty())
 			{
 			{
 				rmgNearObject.clear();
 				rmgNearObject.clear();
@@ -436,12 +473,11 @@ bool ObjectManager::createRequiredObjects()
 	for(const auto & objInfo : closeObjects)
 	for(const auto & objInfo : closeObjects)
 	{
 	{
 		Zone::Lock lock(zone.areaMutex);
 		Zone::Lock lock(zone.areaMutex);
-		auto possibleArea = zone.areaPossible();
 
 
 		rmg::Object rmgObject(*objInfo.obj);
 		rmg::Object rmgObject(*objInfo.obj);
 		rmgObject.setTemplate(zone.getTerrainType(), zone.getRand());
 		rmgObject.setTemplate(zone.getTerrainType(), zone.getRand());
 		bool guarded = addGuard(rmgObject, objInfo.guardStrength, (objInfo.obj->ID == Obj::MONOLITH_TWO_WAY));
 		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)
 										  [this, &rmgObject](const int3 & tile)
 		{
 		{
 			float dist = rmgObject.getArea().distanceSqr(zone.getPos());
 			float dist = rmgObject.getArea().distanceSqr(zone.getPos());
@@ -470,15 +506,15 @@ bool ObjectManager::createRequiredObjects()
 
 
 		rmg::Object rmgNearObject(*nearby.obj);
 		rmg::Object rmgNearObject(*nearby.obj);
 		std::set<int3> blockedArea = targetObject->getBlockedPos();
 		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();
 			rmgNearObject.clear();
 			continue;
 			continue;
 		}
 		}
 
 
-		rmgNearObject.setPosition(*RandomGeneratorUtil::nextItem(possibleArea.getTiles(), zone.getRand()));
+		rmgNearObject.setPosition(*RandomGeneratorUtil::nextItem(areaForObject.getTiles(), zone.getRand()));
 		placeObject(rmgNearObject, false, false);
 		placeObject(rmgNearObject, false, false);
 		auto path = zone.searchPath(rmgNearObject.getVisitablePosition(), false);
 		auto path = zone.searchPath(rmgNearObject.getVisitablePosition(), false);
 		if (path.valid())
 		if (path.valid())
@@ -515,8 +551,6 @@ bool ObjectManager::createRequiredObjects()
 
 
 void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateDistance, bool createRoad/* = false*/)
 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)
 	if (object.instances().size() == 1 && object.instances().front()->object().ID == Obj::MONSTER)
 	{
 	{
 		//Fix for HoTA offset - lonely guards
 		//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());
 	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)
 	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;
 		std::set<TRmgTemplateZoneId> adjacentZones;
 		auto objectArea = object.getArea();
 		auto objectArea = object.getArea();

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

@@ -37,22 +37,25 @@ void ObstaclePlacer::process()
 	collectPossibleObstacles(zone.getTerrainType());
 	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
 		//Progressively block tiles, but make sure they don't seal any gap between blocks
 		rmg::Area toBlock;
 		rmg::Area toBlock;
 		do
 		do
 		{
 		{
 			toBlock.clear();
 			toBlock.clear();
-			for (const auto& tile : zone.areaPossible().getTilesVector())
+			for (const auto& tile : areaPossible->getTilesVector())
 			{
 			{
 				rmg::Area neighbors;
 				rmg::Area neighbors;
 				rmg::Area t;
 				rmg::Area t;
@@ -76,7 +79,7 @@ void ObstaclePlacer::process()
 					toBlock.add(tile);
 					toBlock.add(tile);
 				}
 				}
 			}
 			}
-			zone.areaPossible().subtract(toBlock);
+			areaPossible->subtract(toBlock);
 			for (const auto& tile : toBlock.getTilesVector())
 			for (const auto& tile : toBlock.getTilesVector())
 			{
 			{
 				map.setOccupied(tile, ETileType::BLOCKED);
 				map.setOccupied(tile, ETileType::BLOCKED);
@@ -84,7 +87,7 @@ void ObstaclePlacer::process()
 
 
 		} while (!toBlock.empty());
 		} while (!toBlock.empty());
 
 
-		prohibitedArea.unite(zone.areaPossible());
+		prohibitedArea.unite(areaPossible.get());
 	}
 	}
 
 
 	auto objs = createObstacles(zone.getRand(), map.mapInstance->cb);
 	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
 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)
 void ObstaclePlacer::postProcess(const rmg::Object & object)
@@ -141,7 +144,7 @@ bool ObstaclePlacer::isProhibited(const rmg::Area & objArea) const
 	if(prohibitedArea.overlap(objArea))
 	if(prohibitedArea.overlap(objArea))
 		return true;
 		return true;
 	 
 	 
-	if(!zone.area().contains(objArea))
+	if(!zone.area()->contains(objArea))
 		return true;
 		return true;
 	
 	
 	return false;
 	return false;

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

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

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

@@ -144,8 +144,8 @@ void RoadPlacer::drawRoads(bool secondary)
 			return !terrain->isPassable() || !terrain->isLand();
 			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())
 	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())
 	if(!map.getTile(t).terType->isPassable())
 	{
 	{
-		return zone.area().contains(t) ? 'R' : 'E';
+		return zone.area()->contains(t) ? 'R' : 'E';
 	}
 	}
 	return Modificator::dump(t);
 	return Modificator::dump(t);
 }
 }

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

@@ -40,7 +40,7 @@ void RockPlacer::blockRock()
 		accessibleArea.unite(m->getVisitableArea());
 		accessibleArea.unite(m->getVisitableArea());
 
 
 	//negative approach - create rock tiles first, then make sure all accessible tiles have no rock
 	//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);
 		return map.shouldBeBlocked(t);
 	});
 	});
@@ -48,17 +48,20 @@ void RockPlacer::blockRock()
 
 
 void RockPlacer::postProcess()
 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>())
 	if(auto * m = zone.getModificator<RiverPlacer>())
 		m->riverProhibit().unite(rockArea);
 		m->riverProhibit().unite(rockArea);
 	if(auto * m = zone.getModificator<RoadPlacer>())
 	if(auto * m = zone.getModificator<RoadPlacer>())
@@ -84,7 +87,7 @@ char RockPlacer::dump(const int3 & t)
 {
 {
 	if(!map.getTile(t).terType->isPassable())
 	if(!map.getTile(t).terType->isPassable())
 	{
 	{
-		return zone.area().contains(t) ? 'R' : 'E';
+		return zone.area()->contains(t) ? 'R' : 'E';
 	}
 	}
 	return Modificator::dump(t);
 	return Modificator::dump(t);
 }
 }

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

@@ -28,7 +28,7 @@ void TerrainPainter::process()
 {
 {
 	initTerrainType();
 	initTerrainType();
 
 
-	auto v = zone.getArea().getTilesVector();
+	auto v = zone.area()->getTilesVector();
 	mapProxy->drawTerrain(zone.getRand(), v, zone.getTerrainType());
 	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);
 	int3 position(-1, -1, -1);
 	{
 	{
 		Zone::Lock lock(zone.areaMutex);
 		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);
 				float distance = zone.getPos().dist2dSQ(t);
 				return 100000.f - distance; //some big number
 				return 100000.f - distance; //some big number
@@ -169,8 +169,8 @@ void TownPlacer::cleanupBoundaries(const rmg::Object & rmgObject)
 			if (map.isOnMap(t))
 			if (map.isOnMap(t))
 			{
 			{
 				map.setOccupied(t, ETileType::FREE);
 				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;
 		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;
 	int totalDensity = 0;
 
 
@@ -920,42 +916,44 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 
 
 			auto path = rmg::Path::invalid();
 			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;
 								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())
 			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)
 	if(waterContent == EWaterContent::NONE || zone.isUnderground() || zone.getType() == ETemplateZoneType::WATER)
 		return; //do nothing
 		return; //do nothing
 	
 	
-	distanceMap = zone.area().computeDistanceMap(reverseDistanceMap);
+	distanceMap = zone.area()->computeDistanceMap(reverseDistanceMap);
 	
 	
 	//add border tiles as water for ISLANDS
 	//add border tiles as water for ISLANDS
 	if(waterContent == EWaterContent::ISLANDS)
 	if(waterContent == EWaterContent::ISLANDS)
 	{
 	{
 		waterArea.unite(collectDistantTiles(zone, zone.getSize() + 1));
 		waterArea.unite(collectDistantTiles(zone, zone.getSize() + 1));
-		waterArea.unite(zone.area().getBorder());
+		waterArea.unite(zone.area()->getBorder());
 	}
 	}
 	
 	
 	//protect some parts from water for NORMAL
 	//protect some parts from water for NORMAL
@@ -199,7 +199,7 @@ void WaterAdopter::createWater(EWaterContent::EWaterContent waterContent)
 		std::vector<int3> groundCoast;
 		std::vector<int3> groundCoast;
 		map.foreachDirectNeighbour(tile, [this, &groundCoast](const int3 & t)
 		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);
 				groundCoast.push_back(t);
 			}
 			}
@@ -223,12 +223,12 @@ void WaterAdopter::createWater(EWaterContent::EWaterContent waterContent)
 	
 	
 	{
 	{
 		Zone::Lock waterLock(map.getZones()[waterZoneId]->areaMutex);
 		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::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)
 void WaterAdopter::setWaterZone(TRmgTemplateZoneId water)

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

@@ -34,45 +34,51 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 
 void WaterProxy::process()
 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.setZoneID(t, zone.getId());
 		map.setOccupied(t, ETileType::POSSIBLE);
 		map.setOccupied(t, ETileType::POSSIBLE);
 	}
 	}
 	
 	
-	auto v = zone.getArea().getTilesVector();
+	auto v = area->getTilesVector();
 	mapProxy->drawTerrain(zone.getRand(), v, zone.getTerrainType());
 	mapProxy->drawTerrain(zone.getRand(), v, zone.getTerrainType());
 	
 	
 	//check terrain type
 	//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.isOnMap(t));
 		assert(map.getTile(t).terType->getId() == zone.getTerrainType());
 		assert(map.getTile(t).terType->getId() == zone.getTerrainType());
 	}
 	}
 
 
+	// FIXME: Possible deadlock for 2 zones
+
+	auto areaPossible = zone.areaPossible();
 	for(const auto & z : map.getZones())
 	for(const auto & z : map.getZones())
 	{
 	{
 		if(z.second->getId() == zone.getId())
 		if(z.second->getId() == zone.getId())
 			continue;
 			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())
 			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.setZoneID(t, zone.getId());
 				map.setOccupied(t, ETileType::POSSIBLE);
 				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();
 	zone.initFreeTiles();
@@ -105,7 +111,7 @@ void WaterProxy::collectLakes()
 {
 {
 	RecursiveLock lock(externalAccessMutex);
 	RecursiveLock lock(externalAccessMutex);
 	int lakeId = 0;
 	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.push_back(Lake{});
 		lakes.back().area = lake;
 		lakes.back().area = lake;
@@ -117,8 +123,8 @@ void WaterProxy::collectLakes()
 			lakeMap[t] = lakeId;
 			lakeMap[t] = lakeId;
 		
 		
 		//each lake must have at least one free tile
 		//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;
 		++lakeId;
 	}
 	}
@@ -151,7 +157,7 @@ RouteInfo WaterProxy::waterRoute(Zone & dst)
 				}
 				}
 
 
 				Zone::Lock lock(dst.areaMutex);
 				Zone::Lock lock(dst.areaMutex);
-				dst.areaPossible().subtract(lake.neighbourZones[dst.getId()]);
+				dst.areaPossible()->subtract(lake.neighbourZones[dst.getId()]);
 				continue;
 				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
 		//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
 			//Must only check the border of shipyard and not the added guard
 			rmg::Area shipyardOut = rmgObject.instances().front()->getBlockedArea().getBorderOutside();
 			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);
 		}, guarded, true, ObjectManager::OptimizeType::NONE);
 		
 		
 		//search path to boarding position
 		//search path to boarding position
-		auto searchArea = land.areaPossible() - rmgObject.getArea();
+		auto searchArea = land.areaPossible().get() - rmgObject.getArea();
 		rmg::Path pathToBoarding(searchArea);
 		rmg::Path pathToBoarding(searchArea);
-		pathToBoarding.connect(land.freePaths());
+		pathToBoarding.connect(land.freePaths().get());
 		pathToBoarding.connect(path);
 		pathToBoarding.connect(path);
 		pathToBoarding = pathToBoarding.search(boardingPosition, false);
 		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);
 		manager->placeObject(rmgObject, guarded, true, createRoad);
 		
 		
-		zone.areaPossible().subtract(shipyardOutToBlock);
+		zone.areaPossible()->subtract(shipyardOutToBlock);
 		for(const auto & i : shipyardOutToBlock.getTilesVector())
 		for(const auto & i : shipyardOutToBlock.getTilesVector())
 			if(map.isOnMap(i) && map.isPossible(i))
 			if(map.isOnMap(i) && map.isPossible(i))
 				map.setOccupied(i, ETileType::BLOCKED);
 				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));
 			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
 	//prohibit to place objects on sealed off lakes
 	for(const auto & lake : wproxy->getLakes())
 	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
 	//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;
 			std::vector<int3> landTiles;
 			map.foreachDirectNeighbour(t, [this, &landTiles, &t](const int3 & c)
 			map.foreachDirectNeighbour(t, [this, &landTiles, &t](const int3 & c)
@@ -74,8 +77,8 @@ void WaterRoutes::process()
 				int3 o = landTiles[0] + landTiles[1];
 				int3 o = landTiles[0] + landTiles[1];
 				if(o.x * o.x * o.y * o.y == 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)
 char WaterRoutes::dump(const int3 & t)
 {
 {
+	auto area = zone.area();
+	auto freePaths = zone.freePaths();
+
 	for(auto & i : result)
 	for(auto & i : result)
 	{
 	{
 		if(t == i.boarding)
 		if(t == i.boarding)
@@ -106,15 +112,15 @@ char WaterRoutes::dump(const int3 & t)
 			return '#';
 			return '#';
 		if(i.water.contains(t))
 		if(i.water.contains(t))
 		{
 		{
-			if(zone.freePaths().contains(t))
+			if(freePaths->contains(t))
 				return '+';
 				return '+';
 			else
 			else
 				return '-';
 				return '-';
 		}
 		}
 	}
 	}
-	if(zone.freePaths().contains(t))
+	if(freePaths->contains(t))
 		return '.';
 		return '.';
-	if(zone.area().contains(t))
+	if(area->contains(t))
 		return '~';
 		return '~';
 	return ' ';
 	return ' ';
 }
 }

+ 1 - 1
lib/serializer/BinaryDeserializer.cpp

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

+ 4 - 4
lib/serializer/BinaryDeserializer.h

@@ -23,12 +23,12 @@ protected:
 public:
 public:
 	CLoaderBase(IBinaryReader * r): reader(r){};
 	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);
 		auto bytePtr = reinterpret_cast<std::byte*>(data);
 
 
 		reader->read(bytePtr, size);
 		reader->read(bytePtr, size);
-		if(reverseEndianess)
+		if(reverseEndianness)
 			std::reverse(bytePtr, bytePtr + size);
 			std::reverse(bytePtr, bytePtr + size);
 	};
 	};
 };
 };
@@ -153,7 +153,7 @@ class DLL_LINKAGE BinaryDeserializer : public CLoaderBase
 public:
 public:
 	using Version = ESerializationVersion;
 	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;
 	Version version;
 
 
 	std::map<ui32, void*> loadedPointers;
 	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 >
 	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)
 	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 >
 	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)
 void CLoadFile::openNextFile(const boost::filesystem::path & fname, ESerializationVersion minimalVersion)
 {
 {
-	assert(!serializer.reverseEndianess);
+	assert(!serializer.reverseEndianness);
 	assert(minimalVersion <= ESerializationVersion::CURRENT);
 	assert(minimalVersion <= ESerializationVersion::CURRENT);
 
 
 	try
 	try
@@ -62,7 +62,7 @@ void CLoadFile::openNextFile(const boost::filesystem::path & fname, ESerializati
 			if(serializer.version == ESerializationVersion::CURRENT)
 			if(serializer.version == ESerializationVersion::CURRENT)
 			{
 			{
 				logGlobal->warn("%s seems to have different endianness! Entering reversing mode.", fname.string());
 				logGlobal->warn("%s seems to have different endianness! Entering reversing mode.", fname.string());
-				serializer.reverseEndianess = true;
+				serializer.reverseEndianness = true;
 			}
 			}
 			else
 			else
 				THROW_FORMAT("Error: too new file format (%s)!", fName);
 				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);
 	LobbyServer server(databasePath);
 	logGlobal->info("Starting server on port %d", LISTENING_PORT);
 	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();
 	server.run();
 
 
 	return 0;
 	return 0;

+ 190 - 119
lobby/LobbyDatabase.cpp

@@ -18,7 +18,8 @@ void LobbyDatabase::createTables()
 		CREATE TABLE IF NOT EXISTS chatMessages (
 		CREATE TABLE IF NOT EXISTS chatMessages (
 			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 			senderName TEXT,
 			senderName TEXT,
-			roomType TEXT,
+			channelType TEXT,
+			channelName TEXT,
 			messageText TEXT,
 			messageText TEXT,
 			creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
 			creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
 		);
 		);
@@ -29,6 +30,7 @@ void LobbyDatabase::createTables()
 			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 			roomID TEXT,
 			roomID TEXT,
 			hostAccountID TEXT,
 			hostAccountID TEXT,
+			description TEXT NOT NULL DEFAULT '',
 			status INTEGER NOT NULL DEFAULT 0,
 			status INTEGER NOT NULL DEFAULT 0,
 			playerLimit INTEGER NOT NULL,
 			playerLimit INTEGER NOT NULL,
 			creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
 			creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
@@ -87,205 +89,207 @@ void LobbyDatabase::clearOldData()
 		WHERE online <> 0
 		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
 		UPDATE gameRooms
 		SET status = 5
 		SET status = 5
-		WHERE status <> 5
+		WHERE status = 3
 	)";
 	)";
 
 
 	database->prepare(removeActiveAccounts)->execute();
 	database->prepare(removeActiveAccounts)->execute();
-	database->prepare(removeActiveRooms)->execute();
+	database->prepare(removeActiveLobbyRooms)->execute();
+	database->prepare(removeActiveGameRooms)->execute();
 }
 }
 
 
 void LobbyDatabase::prepareStatements()
 void LobbyDatabase::prepareStatements()
 {
 {
 	// INSERT INTO
 	// 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);
 		INSERT INTO accounts(accountID, displayName, online) VALUES(?,?,0);
-	)";
+	)");
 
 
-	static const std::string insertAccessCookieText = R"(
+	insertAccessCookieStatement = database->prepare(R"(
 		INSERT INTO accountCookies(accountID, cookieUUID) VALUES(?,?);
 		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);
 		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(?,?);
 		INSERT INTO gameRoomPlayers(roomID, accountID) VALUES(?,?);
-	)";
+	)");
 
 
-	static const std::string insertGameRoomInvitesText = R"(
+	insertGameRoomInvitesStatement = database->prepare(R"(
 		INSERT INTO gameRoomInvites(roomID, accountID) VALUES(?,?);
 		INSERT INTO gameRoomInvites(roomID, accountID) VALUES(?,?);
-	)";
+	)");
 
 
 	// DELETE FROM
 	// DELETE FROM
 
 
-	static const std::string deleteGameRoomPlayersText = R"(
+	deleteGameRoomPlayersStatement = database->prepare(R"(
 		 DELETE FROM gameRoomPlayers WHERE roomID = ? AND accountID = ?
 		 DELETE FROM gameRoomPlayers WHERE roomID = ? AND accountID = ?
-	)";
-
-	static const std::string deleteGameRoomInvitesText = R"(
-		DELETE FROM gameRoomInvites WHERE roomID = ? AND accountID = ?
-	)";
+	)");
 
 
 	// UPDATE
 	// UPDATE
 
 
-	static const std::string setAccountOnlineText = R"(
+	setAccountOnlineStatement = database->prepare(R"(
 		UPDATE accounts
 		UPDATE accounts
 		SET online = ?
 		SET online = ?
 		WHERE accountID = ?
 		WHERE accountID = ?
-	)";
+	)");
 
 
-	static const std::string setGameRoomStatusText = R"(
+	setGameRoomStatusStatement = database->prepare(R"(
 		UPDATE gameRooms
 		UPDATE gameRooms
 		SET status = ?
 		SET status = ?
 		WHERE roomID = ?
 		WHERE roomID = ?
-	)";
+	)");
 
 
-	static const std::string updateAccountLoginTimeText = R"(
+	updateAccountLoginTimeStatement = database->prepare(R"(
 		UPDATE accounts
 		UPDATE accounts
 		SET lastLoginTime = CURRENT_TIMESTAMP
 		SET lastLoginTime = CURRENT_TIMESTAMP
 		WHERE accountID = ?
 		WHERE accountID = ?
-	)";
+	)");
+
+	updateRoomDescriptionStatement = database->prepare(R"(
+		UPDATE gameRooms
+		SET description = ?
+		WHERE roomID  = ?
+	)");
+
+	updateRoomPlayerLimitStatement = database->prepare(R"(
+		UPDATE gameRooms
+		SET playerLimit = ?
+		WHERE roomID  = ?
+	)");
 
 
 	// SELECT FROM
 	// 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
 		SELECT senderName, displayName, messageText, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',cm.creationTime)  AS secondsElapsed
 		FROM chatMessages cm
 		FROM chatMessages cm
 		LEFT JOIN accounts on accountID = senderName
 		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
 		ORDER BY cm.creationTime DESC
 		LIMIT 100
 		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
 		SELECT roomID
 		FROM gameRooms
 		FROM gameRooms
 		WHERE hostAccountID = ? AND status = 0
 		WHERE hostAccountID = ? AND status = 0
 		LIMIT 1
 		LIMIT 1
-	)";
+	)");
 
 
-	static const std::string getGameRoomStatusText = R"(
+	getGameRoomStatusStatement = database->prepare(R"(
 		SELECT status
 		SELECT status
 		FROM gameRooms
 		FROM gameRooms
 		WHERE roomID = ?
 		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
 		SELECT grp.roomID
 		FROM gameRoomPlayers grp
 		FROM gameRoomPlayers grp
 		LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID
 		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
 		LIMIT 1
-	)";
+	)");
 
 
-	static const std::string getActiveAccountsText = R"(
+	getActiveAccountsStatement = database->prepare(R"(
 		SELECT accountID, displayName
 		SELECT accountID, displayName
 		FROM accounts
 		FROM accounts
 		WHERE online = 1
 		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 = ?
 		WHERE roomID = ?
-	)";
+	)");
 
 
-	static const std::string countRoomTotalSlotsText = R"(
+	countRoomTotalSlotsStatement = database->prepare(R"(
 		SELECT playerLimit
 		SELECT playerLimit
 		FROM gameRooms
 		FROM gameRooms
 		WHERE roomID = ?
 		WHERE roomID = ?
-	)";
+	)");
 
 
-	static const std::string getAccountDisplayNameText = R"(
+	getAccountDisplayNameStatement = database->prepare(R"(
 		SELECT displayName
 		SELECT displayName
 		FROM accounts
 		FROM accounts
 		WHERE accountID = ?
 		WHERE accountID = ?
-	)";
+	)");
 
 
-	static const std::string isAccountCookieValidText = R"(
+	isAccountCookieValidStatement = database->prepare(R"(
 		SELECT COUNT(accountID)
 		SELECT COUNT(accountID)
 		FROM accountCookies
 		FROM accountCookies
 		WHERE accountID = ? AND cookieUUID = ?
 		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)
 		SELECT COUNT(accountID)
 		FROM gameRoomPlayers grp
 		FROM gameRoomPlayers grp
 		LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID
 		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)
 		SELECT COUNT(accountID)
 		FROM gameRoomPlayers grp
 		FROM gameRoomPlayers grp
 		LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID
 		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)
 		SELECT COUNT(accountID)
 		FROM accounts
 		FROM accounts
 		WHERE accountID = ?
 		WHERE accountID = ?
-	)";
+	)");
 
 
-	static const std::string isAccountNameExistsText = R"(
+	isAccountNameExistsStatement = database->prepare(R"(
 		SELECT COUNT(displayName)
 		SELECT COUNT(displayName)
 		FROM accounts
 		FROM accounts
 		WHERE displayName = ?
 		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;
 LobbyDatabase::~LobbyDatabase() = default;
@@ -298,9 +302,9 @@ LobbyDatabase::LobbyDatabase(const boost::filesystem::path & databasePath)
 	prepareStatements();
 	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)
 bool LobbyDatabase::isPlayerInGameRoom(const std::string & accountID)
@@ -327,10 +331,11 @@ bool LobbyDatabase::isPlayerInGameRoom(const std::string & accountID, const std:
 	return result;
 	return result;
 }
 }
 
 
-std::vector<LobbyChatMessage> LobbyDatabase::getRecentMessageHistory()
+std::vector<LobbyChatMessage> LobbyDatabase::getRecentMessageHistory(const std::string & channelType, const std::string & channelName)
 {
 {
 	std::vector<LobbyChatMessage> result;
 	std::vector<LobbyChatMessage> result;
 
 
+	getRecentMessageHistoryStatement->setBinds(channelType, channelName);
 	while(getRecentMessageHistoryStatement->execute())
 	while(getRecentMessageHistoryStatement->execute())
 	{
 	{
 		LobbyChatMessage message;
 		LobbyChatMessage message;
@@ -342,6 +347,22 @@ std::vector<LobbyChatMessage> LobbyDatabase::getRecentMessageHistory()
 	return result;
 	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)
 void LobbyDatabase::setAccountOnline(const std::string & accountID, bool isOnline)
 {
 {
 	setAccountOnlineStatement->executeOnce(isOnline ? 1 : 0, accountID);
 	setAccountOnlineStatement->executeOnce(isOnline ? 1 : 0, accountID);
@@ -392,6 +413,16 @@ void LobbyDatabase::updateAccountLoginTime(const std::string & accountID)
 	updateAccountLoginTimeStatement->executeOnce(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 LobbyDatabase::getAccountDisplayName(const std::string & accountID)
 {
 {
 	std::string result;
 	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)
 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)
 LobbyRoomState LobbyDatabase::getGameRoomStatus(const std::string & roomID)
 {
 {
-	int result = -1;
+	LobbyRoomState result;
 
 
 	getGameRoomStatusStatement->setBinds(roomID);
 	getGameRoomStatusStatement->setBinds(roomID);
 	if(getGameRoomStatusStatement->execute())
 	if(getGameRoomStatusStatement->execute())
 		getGameRoomStatusStatement->getColumns(result);
 		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)
 uint32_t LobbyDatabase::getGameRoomFreeSlots(const std::string & roomID)
@@ -486,7 +526,7 @@ std::vector<LobbyGameRoom> LobbyDatabase::getActiveGameRooms()
 	while(getActiveGameRoomsStatement->execute())
 	while(getActiveGameRoomsStatement->execute())
 	{
 	{
 		LobbyGameRoom entry;
 		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);
 		result.push_back(entry);
 	}
 	}
 	getActiveGameRoomsStatement->reset();
 	getActiveGameRoomsStatement->reset();
@@ -494,8 +534,39 @@ std::vector<LobbyGameRoom> LobbyDatabase::getActiveGameRooms()
 	for (auto & room : result)
 	for (auto & room : result)
 	{
 	{
 		countRoomUsedSlotsStatement->setBinds(room.roomID);
 		countRoomUsedSlotsStatement->setBinds(room.roomID);
-		if(countRoomUsedSlotsStatement->execute())
-			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();
 		countRoomUsedSlotsStatement->reset();
 	}
 	}
 	return result;
 	return result;

+ 11 - 2
lobby/LobbyDatabase.h

@@ -34,12 +34,17 @@ class LobbyDatabase
 	SQLiteStatementPtr setAccountOnlineStatement;
 	SQLiteStatementPtr setAccountOnlineStatement;
 	SQLiteStatementPtr setGameRoomStatusStatement;
 	SQLiteStatementPtr setGameRoomStatusStatement;
 	SQLiteStatementPtr updateAccountLoginTimeStatement;
 	SQLiteStatementPtr updateAccountLoginTimeStatement;
+	SQLiteStatementPtr updateRoomDescriptionStatement;
+	SQLiteStatementPtr updateRoomPlayerLimitStatement;
 
 
 	SQLiteStatementPtr getRecentMessageHistoryStatement;
 	SQLiteStatementPtr getRecentMessageHistoryStatement;
+	SQLiteStatementPtr getFullMessageHistoryStatement;
 	SQLiteStatementPtr getIdleGameRoomStatement;
 	SQLiteStatementPtr getIdleGameRoomStatement;
 	SQLiteStatementPtr getGameRoomStatusStatement;
 	SQLiteStatementPtr getGameRoomStatusStatement;
+	SQLiteStatementPtr getAccountGameHistoryStatement;
 	SQLiteStatementPtr getActiveGameRoomsStatement;
 	SQLiteStatementPtr getActiveGameRoomsStatement;
 	SQLiteStatementPtr getActiveAccountsStatement;
 	SQLiteStatementPtr getActiveAccountsStatement;
+	SQLiteStatementPtr getAccountInviteStatusStatement;
 	SQLiteStatementPtr getAccountGameRoomStatement;
 	SQLiteStatementPtr getAccountGameRoomStatement;
 	SQLiteStatementPtr getAccountDisplayNameStatement;
 	SQLiteStatementPtr getAccountDisplayNameStatement;
 	SQLiteStatementPtr countRoomUsedSlotsStatement;
 	SQLiteStatementPtr countRoomUsedSlotsStatement;
@@ -72,13 +77,17 @@ public:
 	void insertGameRoom(const std::string & roomID, const std::string & hostAccountID);
 	void insertGameRoom(const std::string & roomID, const std::string & hostAccountID);
 	void insertAccount(const std::string & accountID, const std::string & displayName);
 	void insertAccount(const std::string & accountID, const std::string & displayName);
 	void insertAccessCookie(const std::string & accountID, const std::string & accessCookieUUID);
 	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 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<LobbyGameRoom> getActiveGameRooms();
 	std::vector<LobbyAccount> getActiveAccounts();
 	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 getIdleGameRoom(const std::string & hostAccountID);
 	std::string getAccountGameRoom(const std::string & accountID);
 	std::string getAccountGameRoom(const std::string & accountID);

+ 28 - 26
lobby/LobbyDefines.h

@@ -9,30 +9,6 @@
  */
  */
 #pragma once
 #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
 enum class LobbyCookieStatus : int32_t
 {
 {
 	INVALID,
 	INVALID,
@@ -51,7 +27,33 @@ enum class LobbyRoomState : int32_t
 	IDLE = 0, // server is ready but no players are in the room
 	IDLE = 0, // server is ready but no players are in the room
 	PUBLIC = 1, // host has joined and allows anybody to join
 	PUBLIC = 1, // host has joined and allows anybody to join
 	PRIVATE = 2, // host has joined but only allows those he invited 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
 	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 "LobbyDatabase.h"
 
 
+#include "../lib/Languages.h"
+#include "../lib/TextOperations.h"
 #include "../lib/json/JsonFormatException.h"
 #include "../lib/json/JsonFormatException.h"
 #include "../lib/json/JsonNode.h"
 #include "../lib/json/JsonNode.h"
 #include "../lib/json/JsonUtils.h"
 #include "../lib/json/JsonUtils.h"
@@ -21,12 +23,16 @@
 
 
 bool LobbyServer::isAccountNameValid(const std::string & accountName) const
 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)
 	if(accountName.size() < 4)
 		return false;
 		return false;
 
 
 	if(accountName.size() > 20)
 	if(accountName.size() > 20)
 		return false;
 		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)
 	for(const auto & c : accountName)
 		if(!std::isalnum(c))
 		if(!std::isalnum(c))
 			return false;
 			return false;
@@ -36,8 +42,23 @@ bool LobbyServer::isAccountNameValid(const std::string & accountName) const
 
 
 std::string LobbyServer::sanitizeChatMessage(const std::string & inputString) 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
 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)
 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"));
 	assert(JsonUtils::validate(json, "vcmi:lobbyProtocol/" + json["type"].String(), json["type"].String() + " pack"));
 	target->sendPacket(json.toBytes());
 	target->sendPacket(json.toBytes());
 }
 }
@@ -90,20 +113,39 @@ void LobbyServer::sendOperationFailed(const NetworkConnectionPtr & target, const
 	sendMessage(target, reply);
 	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;
 	JsonNode reply;
-	reply["type"].String() = "loginSuccess";
+	reply["type"].String() = "serverLoginSuccess";
 	reply["accountCookie"].String() = accountCookie;
 	reply["accountCookie"].String() = accountCookie;
-	if(!displayName.empty())
-		reply["displayName"].String() = displayName;
 	sendMessage(target, reply);
 	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;
 	JsonNode reply;
 	reply["type"].String() = "chatHistory";
 	reply["type"].String() = "chatHistory";
+	reply["channelType"].String() = channelType;
+	reply["channelName"].String() = channelName;
 	reply["messages"].Vector(); // force creation of empty vector
 	reply["messages"].Vector(); // force creation of empty vector
 
 
 	for(const auto & message : boost::adaptors::reverse(history))
 	for(const auto & message : boost::adaptors::reverse(history))
@@ -142,6 +184,55 @@ void LobbyServer::broadcastActiveAccounts()
 		sendMessage(connection.first, reply);
 		sendMessage(connection.first, reply);
 }
 }
 
 
+static JsonNode loadLobbyAccountToJson(const LobbyAccount & account)
+{
+	JsonNode jsonEntry;
+	jsonEntry["accountID"].String() = account.accountID;
+	jsonEntry["displayName"].String() = account.displayName;
+	return jsonEntry;
+}
+
+static JsonNode loadLobbyGameRoomToJson(const LobbyGameRoom & gameRoom)
+{
+	static constexpr std::array LOBBY_ROOM_STATE_NAMES = {
+		"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()
 JsonNode LobbyServer::prepareActiveGameRooms()
 {
 {
 	auto activeGameRoomStats = database->getActiveGameRooms();
 	auto activeGameRoomStats = database->getActiveGameRooms();
@@ -150,16 +241,7 @@ JsonNode LobbyServer::prepareActiveGameRooms()
 	reply["gameRooms"].Vector(); // force creation of empty vector
 	reply["gameRooms"].Vector(); // force creation of empty vector
 
 
 	for(const auto & gameRoom : activeGameRoomStats)
 	for(const auto & gameRoom : activeGameRoomStats)
-	{
-		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;
 	return reply;
 }
 }
@@ -189,15 +271,15 @@ void LobbyServer::sendJoinRoomSuccess(const NetworkConnectionPtr & target, const
 	sendMessage(target, reply);
 	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;
 	JsonNode reply;
 	reply["type"].String() = "chatMessage";
 	reply["type"].String() = "chatMessage";
 	reply["messageText"].String() = messageText;
 	reply["messageText"].String() = messageText;
 	reply["accountID"].String() = accountID;
 	reply["accountID"].String() = accountID;
 	reply["displayName"].String() = displayName;
 	reply["displayName"].String() = displayName;
-	reply["roomMode"].String() = roomMode;
-	reply["roomName"].String() = roomName;
+	reply["channelType"].String() = channelType;
+	reply["channelName"].String() = channelName;
 
 
 	sendMessage(target, reply);
 	sendMessage(target, reply);
 }
 }
@@ -217,7 +299,18 @@ void LobbyServer::onDisconnected(const NetworkConnectionPtr & connection, const
 
 
 	if(activeGameRooms.count(connection))
 	if(activeGameRooms.count(connection))
 	{
 	{
-		database->setGameRoomStatus(activeGameRooms.at(connection), LobbyRoomState::CLOSED);
+		std::string gameRoomID = activeGameRooms.at(connection);
+
+		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);
 		activeGameRooms.erase(connection);
 	}
 	}
 
 
@@ -300,6 +393,9 @@ void LobbyServer::onPacketReceived(const NetworkConnectionPtr & connection, cons
 		if(messageType == "sendChatMessage")
 		if(messageType == "sendChatMessage")
 			return receiveSendChatMessage(connection, json);
 			return receiveSendChatMessage(connection, json);
 
 
+		if(messageType == "requestChatHistory")
+			return receiveRequestChatHistory(connection, json);
+
 		if(messageType == "activateGameRoom")
 		if(messageType == "activateGameRoom")
 			return receiveActivateGameRoom(connection, json);
 			return receiveActivateGameRoom(connection, json);
 
 
@@ -309,9 +405,6 @@ void LobbyServer::onPacketReceived(const NetworkConnectionPtr & connection, cons
 		if(messageType == "sendInvite")
 		if(messageType == "sendInvite")
 			return receiveSendInvite(connection, json);
 			return receiveSendInvite(connection, json);
 
 
-		if(messageType == "declineInvite")
-			return receiveDeclineInvite(connection, json);
-
 		logGlobal->warn("%s: Unknown message type: %s", accountName, messageType);
 		logGlobal->warn("%s: Unknown message type: %s", accountName, messageType);
 		return;
 		return;
 	}
 	}
@@ -322,6 +415,12 @@ void LobbyServer::onPacketReceived(const NetworkConnectionPtr & connection, cons
 		std::string roomName = activeGameRooms.at(connection);
 		std::string roomName = activeGameRooms.at(connection);
 		logGlobal->info("%s: Received message of type %s", roomName, messageType);
 		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")
 		if(messageType == "leaveGameRoom")
 			return receiveLeaveGameRoom(connection, json);
 			return receiveLeaveGameRoom(connection, json);
 
 
@@ -351,20 +450,107 @@ void LobbyServer::onPacketReceived(const NetworkConnectionPtr & connection, cons
 	logGlobal->info("(unauthorised): Unknown message type %s", messageType);
 	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 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 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())
 	if(messageTextClean.empty())
 		return sendOperationFailed(connection, "No printable characters in sent message!");
 		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)
 void LobbyServer::receiveClientRegister(const NetworkConnectionPtr & connection, const JsonNode & json)
@@ -409,13 +595,16 @@ void LobbyServer::receiveClientLogin(const NetworkConnectionPtr & connection, co
 
 
 	activeAccounts[connection] = accountID;
 	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
 	// send active game rooms list to new account
 	// and update acount list to everybody else including new account
 	// and update acount list to everybody else including new account
 	broadcastActiveAccounts();
 	broadcastActiveAccounts();
 	sendMessage(connection, prepareActiveGameRooms());
 	sendMessage(connection, prepareActiveGameRooms());
+	sendMatchesHistory(connection);
 }
 }
 
 
 void LobbyServer::receiveServerLogin(const NetworkConnectionPtr & connection, const JsonNode & json)
 void LobbyServer::receiveServerLogin(const NetworkConnectionPtr & connection, const JsonNode & json)
@@ -435,7 +624,7 @@ void LobbyServer::receiveServerLogin(const NetworkConnectionPtr & connection, co
 	{
 	{
 		database->insertGameRoom(gameRoomID, accountID);
 		database->insertGameRoom(gameRoomID, accountID);
 		activeGameRooms[connection] = gameRoomID;
 		activeGameRooms[connection] = gameRoomID;
-		sendLoginSuccess(connection, accountCookie, {});
+		sendServerLoginSuccess(connection, accountCookie);
 		broadcastActiveGameRooms();
 		broadcastActiveGameRooms();
 	}
 	}
 }
 }
@@ -510,6 +699,7 @@ void LobbyServer::receiveActivateGameRoom(const NetworkConnectionPtr & connectio
 {
 {
 	std::string hostAccountID = json["hostAccountID"].String();
 	std::string hostAccountID = json["hostAccountID"].String();
 	std::string accountID = activeAccounts[connection];
 	std::string accountID = activeAccounts[connection];
+	int playerLimit = json["playerLimit"].Integer();
 
 
 	if(database->isPlayerInGameRoom(accountID))
 	if(database->isPlayerInGameRoom(accountID))
 		return sendOperationFailed(connection, "Player already in the room!");
 		return sendOperationFailed(connection, "Player already in the room!");
@@ -527,6 +717,7 @@ void LobbyServer::receiveActivateGameRoom(const NetworkConnectionPtr & connectio
 	if(roomType == "private")
 	if(roomType == "private")
 		database->setGameRoomStatus(gameRoomID, LobbyRoomState::PRIVATE);
 		database->setGameRoomStatus(gameRoomID, LobbyRoomState::PRIVATE);
 
 
+	database->updateRoomPlayerLimit(gameRoomID, playerLimit);
 	database->insertPlayerIntoGameRoom(accountID, gameRoomID);
 	database->insertPlayerIntoGameRoom(accountID, gameRoomID);
 	broadcastActiveGameRooms();
 	broadcastActiveGameRooms();
 	sendJoinRoomSuccess(connection, gameRoomID, false);
 	sendJoinRoomSuccess(connection, gameRoomID, false);
@@ -566,6 +757,23 @@ void LobbyServer::receiveJoinGameRoom(const NetworkConnectionPtr & connection, c
 	broadcastActiveGameRooms();
 	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)
 void LobbyServer::receiveLeaveGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json)
 {
 {
 	std::string accountID = json["accountID"].String();
 	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 accountID = json["accountID"].String();
 	std::string gameRoomID = database->getAccountGameRoom(senderName);
 	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))
 	if(!database->isPlayerInGameRoom(senderName))
 		return sendOperationFailed(connection, "You are not in the room!");
 		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!");
 		return sendOperationFailed(connection, "This player is already invited!");
 
 
 	database->insertGameRoomInvite(accountID, gameRoomID);
 	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;
 LobbyServer::~LobbyServer() = default;

Some files were not shown because too many files changed in this diff