Sfoglia il codice sorgente

Merge pull request #3776 from IvanSavenko/lobby_room_compatibility_check

Check for game version and mods compatibility in lobby
Ivan Savenko 1 anno fa
parent
commit
428c88d8c1

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


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

@@ -99,6 +99,20 @@
 	"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.preview.title" : "Join Game Room",
+	"vcmi.lobby.preview.subtitle" : "Game on %s, hosted by %s", //TL Note: 1) name of map or RMG template 2) nickname of game host
+	"vcmi.lobby.preview.version" : "Game version:",
+	"vcmi.lobby.preview.players" : "Players:",
+	"vcmi.lobby.preview.mods" : "Used mods:",
+	"vcmi.lobby.preview.title" : "Join Game Room",
+	"vcmi.lobby.preview.allowed" : "Join the game room?",
+	"vcmi.lobby.preview.error.header" : "Unable to join this room.",
+	"vcmi.lobby.preview.error.playing" : "You need to leave your current game first.",
+	"vcmi.lobby.preview.error.full" : "The room is already full.",
+	"vcmi.lobby.preview.error.busy" : "The room no longer accepts new players.",
+	"vcmi.lobby.preview.error.invite" : "You were not invited to this room.",
+	"vcmi.lobby.preview.error.mods" : "You are using different set of mods.",
+	"vcmi.lobby.preview.error.version" : "You are using different version of VCMI.",
 	"vcmi.lobby.room.new" : "New Game",
 	"vcmi.lobby.room.load" : "Load Game",
 	"vcmi.lobby.room.type" : "Room Type",
@@ -107,6 +121,11 @@
 	"vcmi.lobby.room.state.private" : "Private",
 	"vcmi.lobby.room.state.busy" : "In Game",
 	"vcmi.lobby.room.state.invited" : "Invited",
+	"vcmi.lobby.mod.state.compatible" : "Compatible",
+	"vcmi.lobby.mod.state.disabled" : "Must be enabled",
+	"vcmi.lobby.mod.state.version" : "Version mismatch",
+	"vcmi.lobby.mod.state.excessive" : "Must be disabled",
+	"vcmi.lobby.mod.state.missing" : "Not installed",
 
 	"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.",

+ 19 - 0
Mods/vcmi/config/vcmi/ukrainian.json

@@ -99,6 +99,20 @@
 	"vcmi.lobby.room.description.limit" : "До %d гравців можуть зайти у вашу кімнату, включаючи вас.",
 	"vcmi.lobby.invite.header" : "Запросити гравців",
 	"vcmi.lobby.invite.notification" : "Гравець запросив вас до своєї ігрової кімнати. Тепер ви можете приєднатися до його приватної кімнати.",
+	"vcmi.lobby.preview.title" : "Join Game Room",
+	"vcmi.lobby.preview.subtitle" : "Гра на %s, яку проводить %s", //TL Note: 1) name of map or RMG template 2) nickname of game host
+	"vcmi.lobby.preview.version" : "Версія гри:",
+	"vcmi.lobby.preview.players" : "Гравці:",
+	"vcmi.lobby.preview.mods" : "Активні модифікації:",
+	"vcmi.lobby.preview.title" : "Приєднатися до кімнати",
+	"vcmi.lobby.preview.allowed" : "Приєднатися до цієї ігрової кімнати?",
+	"vcmi.lobby.preview.error.header" : "Неможливо приєднатися до цієї кімнати.",
+	"vcmi.lobby.preview.error.playing" : "Ви повинні спочатку вийти з поточної гри.",
+	"vcmi.lobby.preview.error.full" : "Ця кімната вже повна.",
+	"vcmi.lobby.preview.error.busy" : "Кімната більше не приймає нових гравців.",
+	"vcmi.lobby.preview.error.invite" : "Ви не були запрошені до цієї кімнати.",
+	"vcmi.lobby.preview.error.mods" : "Ви використовуєте інший набір модифікацій.",
+	"vcmi.lobby.preview.error.version" : "Ви використовуєте іншу версію VCMI.",
 	"vcmi.lobby.room.new" : "Нова гра",
 	"vcmi.lobby.room.load" : "Завантажити гру",
 	"vcmi.lobby.room.type" : "Тип кімнати",
@@ -107,6 +121,11 @@
 	"vcmi.lobby.room.state.private" : "Приватна",
 	"vcmi.lobby.room.state.busy" : "У грі",
 	"vcmi.lobby.room.state.invited" : "Запрошено",
+	"vcmi.lobby.mod.state.compatible" : "Сумісна",
+	"vcmi.lobby.mod.state.disabled" : "Має бути увімкнена",
+	"vcmi.lobby.mod.state.version" : "Розбіжність версій",
+	"vcmi.lobby.mod.state.excessive" : "Має бути вимкнена",
+	"vcmi.lobby.mod.state.missing" : "Не встановлена",
 
 	"vcmi.client.errors.invalidMap" : "{Пошкоджена карта або кампанія}\n\nНе вдалося запустити гру! Вибрана карта або кампанія може бути невірною або пошкодженою. Причина:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",

+ 2 - 0
client/CMakeLists.txt

@@ -98,6 +98,7 @@ set(client_SRCS
 	globalLobby/GlobalLobbyClient.cpp
 	globalLobby/GlobalLobbyInviteWindow.cpp
 	globalLobby/GlobalLobbyLoginWindow.cpp
+	globalLobby/GlobalLobbyRoomWindow.cpp
 	globalLobby/GlobalLobbyServerSetup.cpp
 	globalLobby/GlobalLobbyWidget.cpp
 	globalLobby/GlobalLobbyWindow.cpp
@@ -288,6 +289,7 @@ set(client_HEADERS
 	globalLobby/GlobalLobbyDefines.h
 	globalLobby/GlobalLobbyInviteWindow.h
 	globalLobby/GlobalLobbyLoginWindow.h
+	globalLobby/GlobalLobbyRoomWindow.h
 	globalLobby/GlobalLobbyServerSetup.h
 	globalLobby/GlobalLobbyWidget.h
 	globalLobby/GlobalLobbyWindow.h

+ 14 - 1
client/globalLobby/GlobalLobbyClient.cpp

@@ -201,6 +201,8 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
 		room.hostAccountDisplayName = jsonEntry["hostAccountDisplayName"].String();
 		room.description = jsonEntry["description"].String();
 		room.statusID = jsonEntry["status"].String();
+		room.gameVersion = jsonEntry["version"].String();
+		room.modList = ModVerificationInfo::jsonDeserializeList(jsonEntry["mods"]);
 		std::chrono::seconds ageSeconds (jsonEntry["ageSeconds"].Integer());
 		room.startDateFormatted = TextOperations::getCurrentFormattedDateTimeLocal(-ageSeconds);
 
@@ -277,7 +279,7 @@ void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json)
 {
 	if (json["proxyMode"].Bool())
 	{
-		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, {});
+		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, { CSH->getGlobalLobby().getAccountDisplayName() });
 		CSH->loadMode = ELoadMode::MULTI;
 
 		std::string hostname = getServerHost();
@@ -430,6 +432,17 @@ const std::vector<GlobalLobbyRoom> & GlobalLobbyClient::getMatchesHistory() cons
 	return matchesHistory;
 }
 
+const GlobalLobbyRoom & GlobalLobbyClient::getActiveRoomByName(const std::string & roomUUID) const
+{
+	for (auto const & room : activeRooms)
+	{
+		if (room.gameRoomID == roomUUID)
+			return room;
+	}
+
+	throw std::out_of_range("Failed to find room with UUID of " + roomUUID);
+}
+
 const std::vector<GlobalLobbyChannelMessage> & GlobalLobbyClient::getChannelHistory(const std::string & channelType, const std::string & channelName) const
 {
 	static const std::vector<GlobalLobbyChannelMessage> emptyVector;

+ 3 - 0
client/globalLobby/GlobalLobbyClient.h

@@ -73,6 +73,9 @@ public:
 	const std::vector<GlobalLobbyRoom> & getMatchesHistory() const;
 	const std::vector<GlobalLobbyChannelMessage> & getChannelHistory(const std::string & channelType, const std::string & channelName) const;
 
+	/// Returns active room by ID. Throws out-of-range on failure
+	const GlobalLobbyRoom & getActiveRoomByName(const std::string & roomUUID) const;
+
 	const std::string & getAccountID() const;
 	const std::string & getAccountCookie() const;
 	const std::string & getAccountDisplayName() const;

+ 4 - 0
client/globalLobby/GlobalLobbyDefines.h

@@ -9,6 +9,8 @@
  */
 #pragma once
 
+#include "../../lib/modding/ModVerificationInfo.h"
+
 struct GlobalLobbyAccount
 {
 	std::string accountID;
@@ -22,8 +24,10 @@ struct GlobalLobbyRoom
 	std::string hostAccountID;
 	std::string hostAccountDisplayName;
 	std::string description;
+	std::string gameVersion;
 	std::string statusID;
 	std::string startDateFormatted;
+	ModCompatibilityInfo modList;
 	std::vector<GlobalLobbyAccount> participants;
 	int playerLimit;
 };

+ 200 - 0
client/globalLobby/GlobalLobbyRoomWindow.cpp

@@ -0,0 +1,200 @@
+/*
+ * GlobalLobbyRoomWindow.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 "GlobalLobbyRoomWindow.h"
+
+#include "GlobalLobbyClient.h"
+#include "GlobalLobbyDefines.h"
+#include "GlobalLobbyWindow.h"
+
+#include "../CGameInfo.h"
+#include "../CServerHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../mainmenu/CMainMenu.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/ObjectLists.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/MetaString.h"
+#include "../../lib/VCMI_Lib.h"
+#include "../../lib/modding/CModHandler.h"
+#include "../../lib/modding/CModInfo.h"
+
+GlobalLobbyRoomAccountCard::GlobalLobbyRoomAccountCard(const GlobalLobbyAccount & accountDescription)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos.w = 130;
+	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), 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);
+}
+
+GlobalLobbyRoomModCard::GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & modInfo)
+{
+	const std::map<ModVerificationStatus, std::string> statusToString = {
+		{ ModVerificationStatus::NOT_INSTALLED, "missing" },
+		{ ModVerificationStatus::DISABLED, "disabled" },
+		{ ModVerificationStatus::EXCESSIVE, "excessive" },
+		{ ModVerificationStatus::VERSION_MISMATCH, "version" },
+		{ ModVerificationStatus::FULL_MATCH, "compatible" }
+	};
+
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos.w = 200;
+	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), 1);
+
+	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName);
+	labelVersion = std::make_shared<CLabel>(195, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, modInfo.version);
+	labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status)));
+}
+
+static const std::string getJoinRoomErrorMessage(const GlobalLobbyRoom & roomDescription, const std::vector<GlobalLobbyRoomModInfo> & modVerificationList)
+{
+	bool publicRoom = roomDescription.statusID == "public";
+	bool privateRoom = roomDescription.statusID == "private";
+	bool gameStarted = !publicRoom && !privateRoom;
+	bool hasInvite = CSH->getGlobalLobby().isInvitedToRoom(roomDescription.gameRoomID);
+	bool alreadyInRoom = CSH->inGame();
+
+	if (alreadyInRoom)
+		return "vcmi.lobby.preview.error.playing";
+
+	if (gameStarted)
+		return "vcmi.lobby.preview.error.busy";
+
+	if (VCMI_VERSION_STRING != roomDescription.gameVersion)
+		return "vcmi.lobby.preview.error.version";
+
+	if (roomDescription.playerLimit == roomDescription.participants.size())
+		return "vcmi.lobby.preview.error.full";
+
+	if (privateRoom && !hasInvite)
+		return "vcmi.lobby.preview.error.invite";
+
+	for(const auto & mod : modVerificationList)
+	{
+		switch (mod.status)
+		{
+			case ModVerificationStatus::NOT_INSTALLED:
+			case ModVerificationStatus::DISABLED:
+			case ModVerificationStatus::EXCESSIVE:
+				return "vcmi.lobby.preview.error.mods";
+				break;
+			case ModVerificationStatus::VERSION_MISMATCH:
+			case ModVerificationStatus::FULL_MATCH:
+				break;
+			default:
+				assert(0);
+		}
+	}
+	return "";
+}
+
+GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const std::string & roomUUID)
+	: CWindowObject(BORDERED)
+	, roomUUID(roomUUID)
+	, window(window)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	pos.w = 400;
+	pos.h = 400;
+
+	GlobalLobbyRoom roomDescription = CSH->getGlobalLobby().getActiveRoomByName(roomUUID);
+	for(const auto & modEntry : ModVerificationInfo::verifyListAgainstLocalMods(roomDescription.modList))
+	{
+		GlobalLobbyRoomModInfo modInfo;
+		modInfo.status = modEntry.second;
+		if (modEntry.second == ModVerificationStatus::EXCESSIVE)
+			modInfo.version = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().version.toString();
+		else
+			modInfo.version = roomDescription.modList.at(modEntry.first).version.toString();
+
+		if (modEntry.second == ModVerificationStatus::NOT_INSTALLED)
+			modInfo.modName = roomDescription.modList.at(modEntry.first).name;
+		else
+			modInfo.modName = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().name;
+
+		modVerificationList.push_back(modInfo);
+	}
+
+	MetaString subtitleText;
+	subtitleText.appendTextID("vcmi.lobby.preview.subtitle");
+	subtitleText.replaceRawString(roomDescription.description);
+	subtitleText.replaceRawString(roomDescription.hostAccountDisplayName);
+
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.title").toString());
+	labelSubtitle = std::make_shared<CLabel>( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString());
+
+	labelVersionTitle = std::make_shared<CLabel>( 10, 60, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.version").toString());
+	labelVersionValue = std::make_shared<CLabel>( 10, 80, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.gameVersion);
+
+	buttonJoin = std::make_shared<CButton>(Point(10, 360), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onJoin(); });
+	buttonClose = std::make_shared<CButton>(Point(100, 360), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
+
+	MetaString joinStatusText;
+	std::string errorMessage = getJoinRoomErrorMessage(roomDescription, modVerificationList);
+	if (!errorMessage.empty())
+	{
+		joinStatusText.appendTextID("vcmi.lobby.preview.error.header");
+		joinStatusText.appendRawString("\n");
+		joinStatusText.appendTextID(errorMessage);
+	}
+	else
+		joinStatusText.appendTextID("vcmi.lobby.preview.allowed");
+
+	labelJoinStatus = std::make_shared<CTextBox>(joinStatusText.toString(), Rect(10, 280, 150, 70), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
+
+	const auto & createAccountCardCallback = [participants = roomDescription.participants](size_t index) -> std::shared_ptr<CIntObject>
+	{
+		if(index < participants.size())
+			return std::make_shared<GlobalLobbyRoomAccountCard>(participants[index]);
+		return std::make_shared<CIntObject>();
+	};
+
+	accountListBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 98, 150, 180), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
+	accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 116), Point(0, 40), 4, roomDescription.participants.size(), 0, 1 | 4, Rect(130, 0, 160, 160));
+	accountList->setRedrawParent(true);
+	accountListTitle = std::make_shared<CLabel>( 12, 109, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.players").toString());
+
+	const auto & createModCardCallback = [this](size_t index) -> std::shared_ptr<CIntObject>
+	{
+		if(index < modVerificationList.size())
+			return std::make_shared<GlobalLobbyRoomModCard>(modVerificationList[index]);
+		return std::make_shared<CIntObject>();
+	};
+
+	modListBackground = std::make_shared<TransparentFilledRectangle>(Rect(178, 48, 220, 340), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
+	modList = std::make_shared<CListBox>(createModCardCallback, Point(180, 66), Point(0, 40), 8, modVerificationList.size(), 0, 1 | 4, Rect(200, 0, 320, 320));
+	modList->setRedrawParent(true);
+	modListTitle = std::make_shared<CLabel>( 182, 59, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.mods").toString());
+
+	buttonJoin->block(!errorMessage.empty());
+	filledBackground->playerColored(PlayerColor(1));
+	center();
+}
+
+void GlobalLobbyRoomWindow::onJoin()
+{
+	window->doJoinRoom(roomUUID);
+}
+
+void GlobalLobbyRoomWindow::onClose()
+{
+	close();
+}

+ 89 - 0
client/globalLobby/GlobalLobbyRoomWindow.h

@@ -0,0 +1,89 @@
+/*
+ * GlobalLobbyRoomWindow.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"
+#include "../../lib/modding/ModVerificationInfo.h"
+
+class CLabel;
+class CTextBox;
+class FilledTexturePlayerColored;
+class CButton;
+class CToggleGroup;
+class GlobalLobbyWindow;
+class TransparentFilledRectangle;
+class CListBox;
+
+struct GlobalLobbyAccount;
+struct GlobalLobbyRoom;
+
+VCMI_LIB_NAMESPACE_BEGIN
+struct ModVerificationInfo;
+VCMI_LIB_NAMESPACE_END
+
+struct GlobalLobbyRoomModInfo
+{
+	std::string modName;
+	std::string version;
+	ModVerificationStatus status;
+};
+
+class GlobalLobbyRoomAccountCard : public CIntObject
+{
+	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
+	std::shared_ptr<CLabel> labelName;
+	std::shared_ptr<CLabel> labelStatus;
+
+public:
+	GlobalLobbyRoomAccountCard(const GlobalLobbyAccount & accountDescription);
+};
+
+class GlobalLobbyRoomModCard : public CIntObject
+{
+	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
+	std::shared_ptr<CLabel> labelName;
+	std::shared_ptr<CLabel> labelStatus;
+	std::shared_ptr<CLabel> labelVersion;
+
+public:
+	GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & modInfo);
+};
+
+class GlobalLobbyRoomWindow : public CWindowObject
+{
+	std::vector<GlobalLobbyRoomModInfo> modVerificationList;
+	GlobalLobbyWindow * window;
+	std::string roomUUID;
+
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+	std::shared_ptr<CLabel> labelTitle;
+	std::shared_ptr<CLabel> labelSubtitle;
+	std::shared_ptr<CLabel> labelVersionTitle;
+	std::shared_ptr<CLabel> labelVersionValue;
+	std::shared_ptr<CTextBox> labelJoinStatus;
+
+	std::shared_ptr<CLabel> accountListTitle;
+	std::shared_ptr<CLabel> modListTitle;
+
+	std::shared_ptr<TransparentFilledRectangle> accountListBackground;
+	std::shared_ptr<TransparentFilledRectangle> modListBackground;
+
+	std::shared_ptr<CListBox> accountList;
+	std::shared_ptr<CListBox> modList;
+
+	std::shared_ptr<CButton> buttonJoin;
+	std::shared_ptr<CButton> buttonClose;
+
+	void onJoin();
+	void onClose();
+
+public:
+	GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const std::string & roomUUID);
+};

+ 4 - 2
client/globalLobby/GlobalLobbyServerSetup.cpp

@@ -11,6 +11,8 @@
 #include "StdInc.h"
 #include "GlobalLobbyServerSetup.h"
 
+#include "GlobalLobbyClient.h"
+
 #include "../CGameInfo.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
@@ -125,9 +127,9 @@ void GlobalLobbyServerSetup::onGameModeChanged(int value)
 void GlobalLobbyServerSetup::onCreate()
 {
 	if(toggleGameMode->getSelected() == 0)
-		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_HOST, {});
+		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_HOST, { CSH->getGlobalLobby().getAccountDisplayName() });
 	else
-		CSH->resetStateForLobby(EStartMode::LOAD_GAME, ESelectionScreen::loadGame, EServerMode::LOBBY_HOST, {});
+		CSH->resetStateForLobby(EStartMode::LOAD_GAME, ESelectionScreen::loadGame, EServerMode::LOBBY_HOST, { CSH->getGlobalLobby().getAccountDisplayName() });
 
 	CSH->loadMode = ELoadMode::MULTI;
 	CSH->startLocalServerAndConnect(true);

+ 11 - 15
client/globalLobby/GlobalLobbyWidget.cpp

@@ -13,6 +13,7 @@
 
 #include "GlobalLobbyClient.h"
 #include "GlobalLobbyWindow.h"
+#include "GlobalLobbyRoomWindow.h"
 
 #include "../CGameInfo.h"
 #include "../CMusicHandler.h"
@@ -209,17 +210,13 @@ GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const
 }
 
 GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription)
+	: roomUUID(roomDescription.gameRoomID)
+	, window(window)
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	addUsedEvents(LCLICK);
 
-	const auto & onJoinClicked = [window, roomID = roomDescription.gameRoomID]()
-	{
-		window->doJoinRoom(roomID);
-	};
-
-	bool publicRoom = roomDescription.statusID == "public";
 	bool hasInvite = CSH->getGlobalLobby().isInvitedToRoom(roomDescription.gameRoomID);
-	bool canJoin = publicRoom || hasInvite;
 
 	auto roomSizeText = MetaString::createFromRawString("%d/%d");
 	roomSizeText.replaceNumber(roomDescription.participants.size());
@@ -241,15 +238,14 @@ GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const Globa
 
 	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));
+	labelRoomSize = std::make_shared<CLabel>(212, 10, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomSizeText.toString());
+	labelRoomStatus = std::make_shared<CLabel>(225, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomStatusText.toString());
+	iconRoomSize = std::make_shared<CPicture>(ImagePath::builtin("lobby/iconPlayer"), Point(214, 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")));
-	}
+void GlobalLobbyRoomCard::clickPressed(const Point & cursorPosition)
+{
+	GH.windows().createAndPushWindow<GlobalLobbyRoomWindow>(window, roomUUID);
 }
 
 GlobalLobbyChannelCard::GlobalLobbyChannelCard(GlobalLobbyWindow * window, const std::string & channelName)

+ 4 - 0
client/globalLobby/GlobalLobbyWidget.h

@@ -69,6 +69,9 @@ public:
 
 class GlobalLobbyRoomCard : public CIntObject
 {
+	GlobalLobbyWindow * window;
+	std::string roomUUID;
+
 	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
 	std::shared_ptr<CLabel> labelName;
 	std::shared_ptr<CLabel> labelRoomSize;
@@ -77,6 +80,7 @@ class GlobalLobbyRoomCard : public CIntObject
 	std::shared_ptr<CButton> buttonJoin;
 	std::shared_ptr<CPicture> iconRoomSize;
 
+	void clickPressed(const Point & cursorPosition) override;
 public:
 	GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription);
 };

+ 38 - 1
config/schemas/lobbyProtocol/activeGameRooms.json

@@ -20,7 +20,7 @@
 			{
 				"type" : "object",
 				"additionalProperties" : false,
-				"required" : [ "gameRoomID", "hostAccountID", "hostAccountDisplayName", "description", "participants", "playerLimit", "status", "ageSeconds" ],
+				"required" : [ "gameRoomID", "hostAccountID", "hostAccountDisplayName", "description", "participants", "playerLimit", "status", "ageSeconds", "mods", "version" ],
 				"properties" : {
 					"gameRoomID" :
 					{
@@ -65,6 +65,38 @@
 							}
 						}
 					},
+					"mods" : 
+					{
+						"type" : "array",
+						"description" : "List of gameplay-affecting mods active on server",
+						"items" : {
+							"type" : "object",
+							"additionalProperties" : false,
+							"required" : [ "modId", "name", "version" ],
+							"properties" : {
+								"modId" :
+								{
+									"type" : "string",
+									"description" : "Unique identifier of the mod"
+								},
+								"name" :
+								{
+									"type" : "string",
+									"description" : "Human-readable name of the mod"
+								},
+								"parent" :
+								{
+									"type" : "string",
+									"description" : "Unique ID of parent mod, only for submods"
+								},
+								"version" :
+								{
+									"type" : "string",
+									"description" : "Version of mod, as specified in mod config"
+								}
+							}
+						}
+					},
 					"status" :
 					{
 						"type" : "string",
@@ -78,6 +110,11 @@
 						"maximum" : 8,
 						"description" : "Maximum number of players that can join this room, including host"
 					},
+					"version" :
+					{
+						"type" : "string",
+						"description" : "Version of match server, e.g. 1.5.0"
+					},
 					"ageSeconds" :
 					{
 						"type" : "number",

+ 32 - 0
config/schemas/lobbyProtocol/serverLogin.json

@@ -32,6 +32,38 @@
 		{
 			"type" : "string",
 			"description" : "Version of match server, e.g. 1.5.0"
+		},
+		"mods" : 
+		{
+			"type" : "array",
+			"description" : "List of gameplay-affecting mods active on server",
+			"items" : {
+				"type" : "object",
+				"additionalProperties" : false,
+				"required" : [ "modId", "name", "version" ],
+				"properties" : {
+					"modId" :
+					{
+						"type" : "string",
+						"description" : "Unique identifier of the mod"
+					},
+					"name" :
+					{
+						"type" : "string",
+						"description" : "Human-readable name of the mod"
+					},
+					"parent" :
+					{
+						"type" : "string",
+						"description" : "Unique ID of parent mod, only for submods"
+					},
+					"version" :
+					{
+						"type" : "string",
+						"description" : "Version of mod, as specified in mod config"
+					}
+				}
+			}
 		}
 	}
 }

+ 1 - 0
lib/CMakeLists.txt

@@ -145,6 +145,7 @@ set(lib_MAIN_SRCS
 	modding/ContentTypeHandler.cpp
 	modding/IdentifierStorage.cpp
 	modding/ModUtility.cpp
+	modding/ModVerificationInfo.cpp
 
 	networkPacks/NetPacksLib.cpp
 

+ 2 - 2
lib/StartInfo.cpp

@@ -109,9 +109,9 @@ void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const
 		throw std::domain_error(VLC->generaltexth->translate("core.genrltxt.529"));
 	
 	auto missingMods = CMapService::verifyMapHeaderMods(*mi->mapHeader);
-	ModIncompatibility::ModListWithVersion modList;
+	ModIncompatibility::ModList modList;
 	for(const auto & m : missingMods)
-		modList.push_back({m.second.name, m.second.version.toString()});
+		modList.push_back(m.second.name);
 	
 	if(!modList.empty())
 		throw ModIncompatibility(modList);

+ 0 - 1
lib/mapping/CMapHeader.h

@@ -23,7 +23,6 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 class CGObjectInstance;
 enum class EMapFormat : uint8_t;
-using ModCompatibilityInfo = std::map<std::string, ModVerificationInfo>;
 
 /// The hero name struct consists of the hero id and the hero name.
 struct DLL_LINKAGE SHeroName

+ 0 - 2
lib/mapping/CMapService.h

@@ -24,8 +24,6 @@ class IMapLoader;
 class IMapPatcher;
 class IGameCallback;
 
-using ModCompatibilityInfo = std::map<std::string, ModVerificationInfo>;
-
 /**
  * The map service provides loading of VCMI/H3 map files. It can
  * be extended to save maps later as well.

+ 2 - 28
lib/mapping/MapFormatJson.cpp

@@ -850,23 +850,7 @@ void CMapLoaderJson::readHeader(const bool complete)
 	mapHeader->version = EMapFormat::VCMI;//todo: new version field
 	
 	//loading mods
-	if(!header["mods"].isNull())
-	{
-		for(auto & mod : header["mods"].Vector())
-		{
-			ModVerificationInfo info;
-			info.version = CModVersion::fromString(mod["version"].String());
-			info.checksum = mod["checksum"].Integer();
-			info.name = mod["name"].String();
-			info.parent = mod["parent"].String();
-			info.impactsGameplay = true;
-			
-			if(!mod["modId"].isNull())
-				mapHeader->mods[mod["modId"].String()] = info;
-			else
-				mapHeader->mods[mod["name"].String()] = info;
-		}
-	}
+	mapHeader->mods = ModVerificationInfo::jsonDeserializeList(header["mods"]);
 
 	//todo: multilevel map load support
 	{
@@ -1231,17 +1215,7 @@ void CMapSaverJson::writeHeader()
 	header["versionMinor"].Float() = VERSION_MINOR;
 	
 	//write mods
-	JsonNode & mods = header["mods"];
-	for(const auto & mod : mapHeader->mods)
-	{
-		JsonNode modWriter;
-		modWriter["modId"].String() = mod.first;
-		modWriter["name"].String() = mod.second.name;
-		modWriter["parent"].String() = mod.second.parent;
-		modWriter["version"].String() = mod.second.version.toString();
-		modWriter["checksum"].Integer() = mod.second.checksum;
-		mods.Vector().push_back(modWriter);
-	}
+	header["mods"] = ModVerificationInfo::jsonSerializeList(mapHeader->mods);
 
 	//todo: multilevel map save support
 	JsonNode & levels = header["mapLevels"];

+ 13 - 72
lib/modding/ActiveModsInSaveList.cpp

@@ -32,85 +32,26 @@ const ModVerificationInfo & ActiveModsInSaveList::getVerificationInfo(TModID mod
 	return VLC->modh->getModInfo(mod).getVerificationInfo();
 }
 
-void ActiveModsInSaveList::verifyActiveMods(const std::vector<std::pair<TModID, ModVerificationInfo>> & modList)
+void ActiveModsInSaveList::verifyActiveMods(const std::map<TModID, ModVerificationInfo> & modList)
 {
-	auto searchVerificationInfo = [&modList](const TModID & m) -> const ModVerificationInfo*
-	{
-		for(auto & i : modList)
-			if(i.first == m)
-				return &i.second;
-		return nullptr;
-	};
-
-	std::vector<TModID> missingMods, excessiveMods;
-	ModIncompatibility::ModListWithVersion missingModsResult;
-	ModIncompatibility::ModList excessiveModsResult;
-
-	for(const auto & m : VLC->modh->getActiveMods())
-	{
-		if(searchVerificationInfo(m))
-			continue;
+	auto comparison = ModVerificationInfo::verifyListAgainstLocalMods(modList);
+	std::vector<TModID> missingMods;
+	std::vector<TModID> excessiveMods;
 
-		//TODO: support actual disabling of these mods
-		if(VLC->modh->getModInfo(m).checkModGameplayAffecting())
-			excessiveMods.push_back(m);
-	}
-
-	for(const auto & infoPair : modList)
+	for (auto const & compared : comparison)
 	{
-		auto & remoteModId = infoPair.first;
-		auto & remoteModInfo = infoPair.second;
-
-		bool modAffectsGameplay = remoteModInfo.impactsGameplay;
-		//parent mod affects gameplay if child affects too
-		for(const auto & subInfoPair : modList)
-			modAffectsGameplay |= (subInfoPair.second.impactsGameplay && subInfoPair.second.parent == remoteModId);
-
-		if(!vstd::contains(VLC->modh->getAllMods(), remoteModId))
-		{
-			if(modAffectsGameplay)
-				missingMods.push_back(remoteModId); //mod is not installed
-			continue;
-		}
+		if (compared.second == ModVerificationStatus::NOT_INSTALLED)
+			missingMods.push_back(modList.at(compared.first).name);
 
-		auto & localModInfo = VLC->modh->getModInfo(remoteModId).getVerificationInfo();
-		modAffectsGameplay |= VLC->modh->getModInfo(remoteModId).checkModGameplayAffecting();
-		bool modVersionCompatible = localModInfo.version.isNull()
-			|| remoteModInfo.version.isNull()
-			|| localModInfo.version.compatible(remoteModInfo.version);
-		bool modLocalyEnabled = vstd::contains(VLC->modh->getActiveMods(), remoteModId);
+		if (compared.second == ModVerificationStatus::DISABLED)
+			missingMods.push_back(VLC->modh->getModInfo(compared.first).getVerificationInfo().name);
 
-		if(modVersionCompatible && modAffectsGameplay && modLocalyEnabled)
-			continue;
-
-		if(modAffectsGameplay)
-			missingMods.push_back(remoteModId); //incompatible mod impacts gameplay
-	}
-
-	//filter mods
-	for(auto & m : missingMods)
-	{
-		if(auto * vInfo = searchVerificationInfo(m))
-		{
-			assert(vInfo->parent != m);
-			if(!vInfo->parent.empty() && vstd::contains(missingMods, vInfo->parent))
-				continue;
-			missingModsResult.push_back({vInfo->name, vInfo->version.toString()});
-		}
+		if (compared.second == ModVerificationStatus::EXCESSIVE)
+			excessiveMods.push_back(modList.at(compared.first).name);
 	}
-	for(auto & m : excessiveMods)
-	{
-		auto & vInfo = VLC->modh->getModInfo(m).getVerificationInfo();
-		assert(vInfo.parent != m);
-		if(!vInfo.parent.empty() && vstd::contains(excessiveMods, vInfo.parent))
-			continue;
-		excessiveModsResult.push_back(vInfo.name);
-	}
-
-	if(!missingModsResult.empty() || !excessiveModsResult.empty())
-		throw ModIncompatibility(missingModsResult, excessiveModsResult);
 
-	//TODO: support actual enabling of required mods
+	if(!missingMods.empty() || !excessiveMods.empty())
+		throw ModIncompatibility(missingMods, excessiveMods);
 }
 
 

+ 4 - 4
lib/modding/ActiveModsInSaveList.h

@@ -20,7 +20,7 @@ class ActiveModsInSaveList
 	const ModVerificationInfo & getVerificationInfo(TModID mod);
 
 	/// Checks whether provided mod list is compatible with current VLC and throws on failure
-	void verifyActiveMods(const std::vector<std::pair<TModID, ModVerificationInfo>> & modList);
+	void verifyActiveMods(const std::map<TModID, ModVerificationInfo> & modList);
 public:
 	template <typename Handler> void serialize(Handler &h)
 	{
@@ -36,11 +36,11 @@ public:
 			std::vector<TModID> saveActiveMods;
 			h & saveActiveMods;
 
-			std::vector<std::pair<TModID, ModVerificationInfo>> saveModInfos(saveActiveMods.size());
+			std::map<TModID, ModVerificationInfo> saveModInfos;
 			for(int i = 0; i < saveActiveMods.size(); ++i)
 			{
-				saveModInfos[i].first = saveActiveMods[i];
-				h & saveModInfos[i].second;
+				ModVerificationInfo data;
+				h & saveModInfos[saveActiveMods[i]];
 			}
 
 			verifyActiveMods(saveModInfos);

+ 10 - 0
lib/modding/CModVersion.cpp

@@ -59,6 +59,16 @@ std::string CModVersion::toString() const
 	return res;
 }
 
+bool CModVersion::operator ==(const CModVersion & other) const
+{
+	return major == other.major && minor == other.minor && patch == other.patch;
+}
+
+bool CModVersion::operator !=(const CModVersion & other) const
+{
+	return major != other.major || minor != other.minor || patch != other.patch;
+}
+
 bool CModVersion::compatible(const CModVersion & other, bool checkMinor, bool checkPatch) const
 {
 	bool doCheckMinor = checkMinor && minor != Any && other.minor != Any;

+ 2 - 0
lib/modding/CModVersion.h

@@ -35,6 +35,8 @@ struct DLL_LINKAGE CModVersion
 	static CModVersion fromString(std::string from);
 	std::string toString() const;
 
+	bool operator !=(const CModVersion & other) const;
+	bool operator ==(const CModVersion & other) const;
 	bool compatible(const CModVersion & other, bool checkMinor = false, bool checkPatch = false) const;
 	bool isNull() const;
 

+ 3 - 4
lib/modding/ModIncompatibility.h

@@ -14,18 +14,17 @@ VCMI_LIB_NAMESPACE_BEGIN
 class DLL_LINKAGE ModIncompatibility: public std::exception
 {
 public:
-	using ModListWithVersion = std::vector<std::pair<const std::string, const std::string>>;
 	using ModList = std::vector<std::string>;
 
-	ModIncompatibility(const ModListWithVersion & _missingMods)
+	ModIncompatibility(const ModList & _missingMods)
 	{
 		std::ostringstream _ss;
 		for(const auto & m : _missingMods)
-			_ss << m.first << ' ' << m.second << std::endl;
+			_ss << m << std::endl;
 		messageMissingMods = _ss.str();
 	}
 	
-	ModIncompatibility(const ModListWithVersion & _missingMods, ModList & _excessiveMods)
+	ModIncompatibility(const ModList & _missingMods, ModList & _excessiveMods)
 		: ModIncompatibility(_missingMods)
 	{
 		std::ostringstream _ss;

+ 116 - 0
lib/modding/ModVerificationInfo.cpp

@@ -0,0 +1,116 @@
+/*
+ * ModVerificationInfo.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 "ModVerificationInfo.h"
+
+#include "CModInfo.h"
+#include "CModHandler.h"
+#include "ModIncompatibility.h"
+
+#include "../json/JsonNode.h"
+#include "../VCMI_Lib.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+JsonNode ModVerificationInfo::jsonSerializeList(const ModCompatibilityInfo & input)
+{
+	JsonNode output;
+
+	for(const auto & mod : input)
+	{
+		JsonNode modWriter;
+		modWriter["modId"].String() = mod.first;
+		modWriter["name"].String() = mod.second.name;
+		if (!mod.second.parent.empty())
+			modWriter["parent"].String() = mod.second.parent;
+		modWriter["version"].String() = mod.second.version.toString();
+		output.Vector().push_back(modWriter);
+	}
+
+	return output;
+}
+
+ModCompatibilityInfo ModVerificationInfo::jsonDeserializeList(const JsonNode & input)
+{
+	ModCompatibilityInfo output;
+
+	for(const auto & mod : input.Vector())
+	{
+		ModVerificationInfo info;
+		info.version = CModVersion::fromString(mod["version"].String());
+		info.name = mod["name"].String();
+		info.parent = mod["parent"].String();
+		info.checksum = 0;
+		info.impactsGameplay = true;
+
+		if(!mod["modId"].isNull())
+			output[mod["modId"].String()] = info;
+		else
+			output[mod["name"].String()] = info;
+	}
+
+	return output;
+}
+
+ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const ModCompatibilityInfo & modList)
+{
+	ModListVerificationStatus result;
+
+	for(const auto & m : VLC->modh->getActiveMods())
+	{
+		if(modList.count(m))
+			continue;
+
+		if(VLC->modh->getModInfo(m).checkModGameplayAffecting())
+			result[m] = ModVerificationStatus::EXCESSIVE;
+	}
+
+	for(const auto & infoPair : modList)
+	{
+		auto & remoteModId = infoPair.first;
+		auto & remoteModInfo = infoPair.second;
+
+		bool modAffectsGameplay = remoteModInfo.impactsGameplay;
+		//parent mod affects gameplay if child affects too
+		for(const auto & subInfoPair : modList)
+			modAffectsGameplay |= (subInfoPair.second.impactsGameplay && subInfoPair.second.parent == remoteModId);
+
+		if(!vstd::contains(VLC->modh->getAllMods(), remoteModId))
+		{
+			result[remoteModId] = ModVerificationStatus::NOT_INSTALLED;
+			continue;
+		}
+
+		auto & localModInfo = VLC->modh->getModInfo(remoteModId).getVerificationInfo();
+		modAffectsGameplay |= VLC->modh->getModInfo(remoteModId).checkModGameplayAffecting();
+
+		assert(modAffectsGameplay); // such mods should not be in the list to begin with
+		if (!modAffectsGameplay)
+			continue; // skip it
+
+		if (!vstd::contains(VLC->modh->getActiveMods(), remoteModId))
+		{
+			result[remoteModId] = ModVerificationStatus::DISABLED;
+			continue;
+		}
+
+		if(remoteModInfo.version != localModInfo.version)
+		{
+			result[remoteModId] = ModVerificationStatus::VERSION_MISMATCH;
+			continue;
+		}
+
+		result[remoteModId] = ModVerificationStatus::FULL_MATCH;
+	}
+
+	return result;
+}
+
+VCMI_LIB_NAMESPACE_END

+ 20 - 1
lib/modding/ModVerificationInfo.h

@@ -13,7 +13,22 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-struct ModVerificationInfo
+class JsonNode;
+struct ModVerificationInfo;
+using ModCompatibilityInfo = std::map<std::string, ModVerificationInfo>;
+
+enum class ModVerificationStatus
+{
+	NOT_INSTALLED, /// Mod is not installed locally
+	DISABLED, /// Mod is installed locally but not enabled
+	EXCESSIVE, /// Mod is enabled locally but must be disabled
+	VERSION_MISMATCH, /// Mod is present on both sides, but has different version
+	FULL_MATCH, /// No issues detected, everything matches
+};
+
+using ModListVerificationStatus = std::map<std::string, ModVerificationStatus>;
+
+struct DLL_LINKAGE ModVerificationInfo
 {
 	/// human-readable mod name
 	std::string name;
@@ -30,6 +45,10 @@ struct ModVerificationInfo
 	/// for serialization purposes
 	bool impactsGameplay = true;
 
+	static JsonNode jsonSerializeList(const ModCompatibilityInfo & input);
+	static ModCompatibilityInfo jsonDeserializeList(const JsonNode & input);
+	static ModListVerificationStatus verifyListAgainstLocalMods(const ModCompatibilityInfo & input);
+
 	template <typename Handler>
 	void serialize(Handler & h)
 	{

+ 1 - 0
lib/network/NetworkConnection.cpp

@@ -17,6 +17,7 @@ NetworkConnection::NetworkConnection(INetworkConnectionListener & listener, cons
 	, listener(listener)
 {
 	socket->set_option(boost::asio::ip::tcp::no_delay(true));
+	socket->set_option(boost::asio::socket_base::keep_alive(true));
 
 	// iOS throws exception on attempt to set buffer size
 	constexpr auto bufferSize = 4 * 1024 * 1024;

+ 53 - 14
lobby/LobbyDatabase.cpp

@@ -81,6 +81,38 @@ void LobbyDatabase::createTables()
 	database->prepare(createTableGameRoomInvites)->execute();
 }
 
+void LobbyDatabase::upgradeDatabase()
+{
+	auto getDatabaseVersionStatement = database->prepare(R"(
+		PRAGMA user_version
+	)");
+
+	auto upgradeDatabaseVersionStatement = database->prepare(R"(
+		PRAGMA user_version = 10501
+	)");
+
+	int databaseVersion;
+	getDatabaseVersionStatement->execute();
+	getDatabaseVersionStatement->getColumns(databaseVersion);
+	getDatabaseVersionStatement->reset();
+
+	if (databaseVersion < 10501)
+	{
+		database->prepare(R"(
+			ALTER TABLE gameRooms
+			ADD COLUMN mods TEXT NOT NULL DEFAULT '{}'
+		)")->execute();
+
+		database->prepare(R"(
+			ALTER TABLE gameRooms
+			ADD COLUMN version TEXT NOT NULL DEFAULT ''
+		)")->execute();
+
+		upgradeDatabaseVersionStatement->execute();
+		upgradeDatabaseVersionStatement->reset();
+	}
+}
+
 void LobbyDatabase::clearOldData()
 {
 	static const std::string removeActiveAccounts = R"(
@@ -122,7 +154,7 @@ void LobbyDatabase::prepareStatements()
 	)");
 
 	insertGameRoomStatement = database->prepare(R"(
-		INSERT INTO gameRooms(roomID, hostAccountID, status, playerLimit) VALUES(?, ?, 0, 8);
+		INSERT INTO gameRooms(roomID, hostAccountID, status, playerLimit, version, mods) VALUES(?, ?, 0, 8, ?, ?);
 	)");
 
 	insertGameRoomPlayersStatement = database->prepare(R"(
@@ -233,20 +265,26 @@ void LobbyDatabase::prepareStatements()
 	)");
 
 	getActiveGameRoomsStatement = database->prepare(R"(
-		SELECT roomID, hostAccountID, displayName, description, status, playerLimit, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',gr.creationTime)  AS secondsElapsed
+		SELECT roomID, hostAccountID, displayName, description, status, playerLimit, version, mods, 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"(
+	getGameRoomPlayersStatement = database->prepare(R"(
 		SELECT a.accountID, a.displayName
 		FROM gameRoomPlayers grp
 		LEFT JOIN accounts a ON a.accountID = grp.accountID
 		WHERE roomID = ?
 	)");
 
+	countRoomUsedSlotsStatement = database->prepare(R"(
+		SELECT COUNT(grp.accountID)
+		FROM gameRoomPlayers grp
+		WHERE roomID = ?
+	)");
+
 	countRoomTotalSlotsStatement = database->prepare(R"(
 		SELECT playerLimit
 		FROM gameRooms
@@ -298,6 +336,7 @@ LobbyDatabase::LobbyDatabase(const boost::filesystem::path & databasePath)
 {
 	database = SQLiteInstance::open(databasePath, true);
 	createTables();
+	upgradeDatabase();
 	clearOldData();
 	prepareStatements();
 }
@@ -393,9 +432,9 @@ void LobbyDatabase::insertGameRoomInvite(const std::string & targetAccountID, co
 	insertGameRoomInvitesStatement->executeOnce(roomID, targetAccountID);
 }
 
-void LobbyDatabase::insertGameRoom(const std::string & roomID, const std::string & hostAccountID)
+void LobbyDatabase::insertGameRoom(const std::string & roomID, const std::string & hostAccountID, const std::string & serverVersion, const std::string & modListJson)
 {
-	insertGameRoomStatement->executeOnce(roomID, hostAccountID);
+	insertGameRoomStatement->executeOnce(roomID, hostAccountID, serverVersion, modListJson);
 }
 
 void LobbyDatabase::insertAccount(const std::string & accountID, const std::string & displayName)
@@ -526,21 +565,21 @@ std::vector<LobbyGameRoom> LobbyDatabase::getActiveGameRooms()
 	while(getActiveGameRoomsStatement->execute())
 	{
 		LobbyGameRoom entry;
-		getActiveGameRoomsStatement->getColumns(entry.roomID, entry.hostAccountID, entry.hostAccountDisplayName, entry.description, entry.roomState, entry.playerLimit, entry.age);
+		getActiveGameRoomsStatement->getColumns(entry.roomID, entry.hostAccountID, entry.hostAccountDisplayName, entry.description, entry.roomState, entry.playerLimit, entry.version, entry.modsJson, entry.age);
 		result.push_back(entry);
 	}
 	getActiveGameRoomsStatement->reset();
 
 	for (auto & room : result)
 	{
-		countRoomUsedSlotsStatement->setBinds(room.roomID);
-		while(countRoomUsedSlotsStatement->execute())
+		getGameRoomPlayersStatement->setBinds(room.roomID);
+		while(getGameRoomPlayersStatement->execute())
 		{
 			LobbyAccount account;
-			countRoomUsedSlotsStatement->getColumns(account.accountID, account.displayName);
+			getGameRoomPlayersStatement->getColumns(account.accountID, account.displayName);
 			room.participants.push_back(account);
 		}
-		countRoomUsedSlotsStatement->reset();
+		getGameRoomPlayersStatement->reset();
 	}
 	return result;
 }
@@ -560,14 +599,14 @@ std::vector<LobbyGameRoom> LobbyDatabase::getAccountGameHistory(const std::strin
 
 	for (auto & room : result)
 	{
-		countRoomUsedSlotsStatement->setBinds(room.roomID);
-		while(countRoomUsedSlotsStatement->execute())
+		getGameRoomPlayersStatement->setBinds(room.roomID);
+		while(getGameRoomPlayersStatement->execute())
 		{
 			LobbyAccount account;
-			countRoomUsedSlotsStatement->getColumns(account.accountID, account.displayName);
+			getGameRoomPlayersStatement->getColumns(account.accountID, account.displayName);
 			room.participants.push_back(account);
 		}
-		countRoomUsedSlotsStatement->reset();
+		getGameRoomPlayersStatement->reset();
 	}
 	return result;
 }

+ 3 - 1
lobby/LobbyDatabase.h

@@ -47,6 +47,7 @@ class LobbyDatabase
 	SQLiteStatementPtr getAccountInviteStatusStatement;
 	SQLiteStatementPtr getAccountGameRoomStatement;
 	SQLiteStatementPtr getAccountDisplayNameStatement;
+	SQLiteStatementPtr getGameRoomPlayersStatement;
 	SQLiteStatementPtr countRoomUsedSlotsStatement;
 	SQLiteStatementPtr countRoomTotalSlotsStatement;
 
@@ -59,6 +60,7 @@ class LobbyDatabase
 
 	void prepareStatements();
 	void createTables();
+	void upgradeDatabase();
 	void clearOldData();
 
 public:
@@ -74,7 +76,7 @@ public:
 	void deleteGameRoomInvite(const std::string & targetAccountID, const std::string & roomID);
 	void insertGameRoomInvite(const std::string & targetAccountID, const std::string & roomID);
 
-	void insertGameRoom(const std::string & roomID, const std::string & hostAccountID);
+	void insertGameRoom(const std::string & roomID, const std::string & hostAccountID, const std::string & serverVersion, const std::string & modListJson);
 	void insertAccount(const std::string & accountID, const std::string & displayName);
 	void insertAccessCookie(const std::string & accountID, const std::string & accessCookieUUID);
 	void insertChatMessage(const std::string & sender, const std::string & channelType, const std::string & roomID, const std::string & messageText);

+ 2 - 0
lobby/LobbyDefines.h

@@ -44,6 +44,8 @@ struct LobbyGameRoom
 	std::string hostAccountID;
 	std::string hostAccountDisplayName;
 	std::string description;
+	std::string version;
+	std::string modsJson;
 	std::vector<LobbyAccount> participants;
 	LobbyRoomState roomState;
 	uint32_t playerLimit;

+ 4 - 1
lobby/LobbyServer.cpp

@@ -208,9 +208,11 @@ static JsonNode loadLobbyGameRoomToJson(const LobbyGameRoom & gameRoom)
 	jsonEntry["hostAccountID"].String() = gameRoom.hostAccountID;
 	jsonEntry["hostAccountDisplayName"].String() = gameRoom.hostAccountDisplayName;
 	jsonEntry["description"].String() = gameRoom.description;
+	jsonEntry["version"].String() = gameRoom.version;
 	jsonEntry["status"].String() = LOBBY_ROOM_STATE_NAMES[vstd::to_underlying(gameRoom.roomState)];
 	jsonEntry["playerLimit"].Integer() = gameRoom.playerLimit;
 	jsonEntry["ageSeconds"].Integer() = gameRoom.age.count();
+	jsonEntry["mods"] = JsonNode(reinterpret_cast<const std::byte *>(gameRoom.modsJson.data()), gameRoom.modsJson.size());
 
 	for(const auto & account : gameRoom.participants)
 		jsonEntry["participants"].Vector().push_back(loadLobbyAccountToJson(account));
@@ -625,7 +627,8 @@ void LobbyServer::receiveServerLogin(const NetworkConnectionPtr & connection, co
 	}
 	else
 	{
-		database->insertGameRoom(gameRoomID, accountID);
+		std::string modListString = json["mods"].isNull() ? "[]" : json["mods"].toCompactString();
+		database->insertGameRoom(gameRoomID, accountID, version, modListString);
 		activeGameRooms[connection] = gameRoomID;
 		sendServerLoginSuccess(connection, accountCookie);
 		broadcastActiveGameRooms();

+ 2 - 2
mapeditor/mainwindow.cpp

@@ -340,9 +340,9 @@ std::unique_ptr<CMap> MainWindow::openMapInternal(const QString & filenameSelect
 	if(auto header = mapService.loadMapHeader(resId))
 	{
 		auto missingMods = CMapService::verifyMapHeaderMods(*header);
-		ModIncompatibility::ModListWithVersion modList;
+		ModIncompatibility::ModList modList;
 		for(const auto & m : missingMods)
-			modList.push_back({m.second.name, m.second.version.toString()});
+			modList.push_back(m.second.name);
 		
 		if(!modList.empty())
 			throw ModIncompatibility(modList);

+ 17 - 0
server/GlobalLobbyProcessor.cpp

@@ -13,6 +13,9 @@
 #include "CVCMIServer.h"
 #include "../lib/CConfigHandler.h"
 #include "../lib/json/JsonUtils.h"
+#include "../lib/VCMI_Lib.h"
+#include "../lib/modding/CModHandler.h"
+#include "../lib/modding/CModInfo.h"
 
 GlobalLobbyProcessor::GlobalLobbyProcessor(CVCMIServer & owner)
 	: owner(owner)
@@ -125,6 +128,7 @@ void GlobalLobbyProcessor::onConnectionEstablished(const std::shared_ptr<INetwor
 		toSend["accountID"].String() = getHostAccountID();
 		toSend["accountCookie"].String() = getHostAccountCookie();
 		toSend["version"].String() = VCMI_VERSION_STRING;
+		toSend["mods"] = getHostModList();
 
 		sendMessage(connection, toSend);
 	}
@@ -149,6 +153,19 @@ void GlobalLobbyProcessor::onConnectionEstablished(const std::shared_ptr<INetwor
 	}
 }
 
+JsonNode GlobalLobbyProcessor::getHostModList() const
+{
+	ModCompatibilityInfo info;
+
+	for (auto const & modName : VLC->modh->getActiveMods())
+	{
+		if(VLC->modh->getModInfo(modName).checkModGameplayAffecting())
+			info[modName] = VLC->modh->getModInfo(modName).getVerificationInfo();
+	}
+
+	return ModVerificationInfo::jsonSerializeList(info);
+}
+
 void GlobalLobbyProcessor::sendGameStarted()
 {
 	JsonNode toSend;

+ 1 - 0
server/GlobalLobbyProcessor.h

@@ -36,6 +36,7 @@ class GlobalLobbyProcessor : public INetworkClientListener
 	void establishNewConnection();
 	void sendMessage(const NetworkConnectionPtr & targetConnection, const JsonNode & payload);
 
+	JsonNode getHostModList() const;
 	const std::string & getHostAccountID() const;
 	const std::string & getHostAccountCookie() const;
 	const std::string & getHostAccountDisplayName() const;