Преглед изворни кода

Extension of lobby server functionality

Support for:
- listing of active players
- listing of active rooms
- joining and leaving rooms
- placeholder support for multiple chat rooms
- proxy connections
- invites into private rooms

(only lobby server side for now, client and match server need work)
Ivan Savenko пре 1 година
родитељ
комит
4271fb3c95

+ 1 - 1
Mods/vcmi/config/vcmi/english.json

@@ -75,7 +75,7 @@
 	"vcmi.lobby.login.title" : "VCMI Lobby",
 	"vcmi.lobby.login.username" : "Username:",
 	"vcmi.lobby.login.connecting" : "Connecting...",
-//	"vcmi.lobby.login.connectionFailed" : "Connection failed: %s",
+	"vcmi.lobby.login.error" : "Connection error: %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.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.",

+ 9 - 3
lib/network/NetworkListener.h

@@ -15,6 +15,9 @@ class NetworkConnection;
 class NetworkServer;
 class NetworkClient;
 
+using NetworkConnectionPtr = std::shared_ptr<NetworkConnection>;
+using NetworkConnectionWeakPtr = std::weak_ptr<NetworkConnection>;
+
 class DLL_LINKAGE INetworkConnectionListener
 {
 	friend class NetworkConnection;
@@ -22,7 +25,8 @@ protected:
 	virtual void onDisconnected(const std::shared_ptr<NetworkConnection> & connection) = 0;
 	virtual void onPacketReceived(const std::shared_ptr<NetworkConnection> & connection, const std::vector<uint8_t> & message) = 0;
 
-	~INetworkConnectionListener() = default;
+public:
+	virtual ~INetworkConnectionListener() = default;
 };
 
 class DLL_LINKAGE INetworkServerListener : public INetworkConnectionListener
@@ -32,7 +36,8 @@ protected:
 	virtual void onNewConnection(const std::shared_ptr<NetworkConnection> &) = 0;
 	virtual void onTimer() = 0;
 
-	~INetworkServerListener() = default;
+public:
+	virtual ~INetworkServerListener() = default;
 };
 
 class DLL_LINKAGE INetworkClientListener : public INetworkConnectionListener
@@ -43,7 +48,8 @@ protected:
 	virtual void onConnectionEstablished(const std::shared_ptr<NetworkConnection> &) = 0;
 	virtual void onTimer() = 0;
 
-	~INetworkClientListener() = default;
+public:
+	virtual ~INetworkClientListener() = default;
 };
 
 

+ 1 - 0
lobby/CMakeLists.txt

@@ -11,6 +11,7 @@ set(lobby_HEADERS
 		StdInc.h
 
 		LobbyDatabase.h
+		LobbyDefines.h
 		LobbyServer.h
 		SQLiteConnection.h
 )

+ 5 - 4
lobby/EntryPoint.cpp

@@ -11,14 +11,15 @@
 
 #include "LobbyServer.h"
 
-static const std::string DATABASE_PATH = "/home/ivan/vcmi.db";
+#include "../lib/VCMIDirs.h"
+
 static const int LISTENING_PORT = 30303;
-//static const std::string SERVER_NAME = GameConstants::VCMI_VERSION + " (server)";
-//static const std::string SERVER_UUID = boost::uuids::to_string(boost::uuids::random_generator()());
 
 int main(int argc, const char * argv[])
 {
-	LobbyServer server(DATABASE_PATH);
+	auto databasePath = VCMIDirs::get().userDataPath() / "vcmiLobby.db";
+
+	LobbyServer server(databasePath);
 
 	server.start(LISTENING_PORT);
 	server.run();

+ 356 - 35
lobby/LobbyDatabase.cpp

@@ -12,76 +12,256 @@
 
 #include "SQLiteConnection.h"
 
+void LobbyDatabase::createTables()
+{
+	static const std::string createChatMessages = R"(
+		CREATE TABLE IF NOT EXISTS chatMessages (
+			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+			senderName TEXT,
+			roomType TEXT,
+			messageText TEXT,
+			creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
+		);
+	)";
+
+	static const std::string createTableGameRooms = R"(
+		CREATE TABLE IF NOT EXISTS gameRooms (
+			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+			roomID TEXT,
+			hostAccountID TEXT,
+			status INTEGER NOT NULL DEFAULT 0,
+			playerLimit INTEGER NOT NULL,
+			creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
+		);
+	)";
+
+	static const std::string createTableGameRoomPlayers = R"(
+		CREATE TABLE IF NOT EXISTS gameRoomPlayers (
+			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+			roomID TEXT,
+			accountID TEXT
+		);
+	)";
+
+	static const std::string createTableAccounts = R"(
+		CREATE TABLE IF NOT EXISTS accounts (
+			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+			accountID TEXT,
+			displayName TEXT,
+			online INTEGER NOT NULL,
+			lastLoginTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
+			creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
+		);
+	)";
+
+	static const std::string createTableAccountCookies = R"(
+		CREATE TABLE IF NOT EXISTS accountCookies (
+			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+			accountID TEXT,
+			cookieUUID TEXT,
+			creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
+		);
+	)";
+
+	static const std::string createTableGameRoomInvites = R"(
+		CREATE TABLE IF NOT EXISTS gameRoomInvites (
+			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+			roomID TEXT,
+			accountID TEXT
+		);
+	)";
+
+	database->prepare(createChatMessages)->execute();
+	database->prepare(createTableGameRoomPlayers)->execute();
+	database->prepare(createTableGameRooms)->execute();
+	database->prepare(createTableAccounts)->execute();
+	database->prepare(createTableAccountCookies)->execute();
+	database->prepare(createTableGameRoomInvites)->execute();
+}
+
 void LobbyDatabase::prepareStatements()
 {
+	// INSERT INTO
+
 	static const std::string insertChatMessageText = R"(
 		INSERT INTO chatMessages(senderName, messageText) VALUES( ?, ?);
 	)";
 
+	static const std::string insertAccountText = R"(
+		INSERT INTO accounts(accountID, displayName, online) VALUES(?,?,0);
+	)";
+
+	static const std::string insertAccessCookieText = R"(
+		INSERT INTO accountCookies(accountID, cookieUUID) VALUES(?,?);
+	)";
+
+	static const std::string insertGameRoomText = R"(
+		INSERT INTO gameRooms(roomID, hostAccountID, status, playerLimit) VALUES(?, ?, 'empty', 8);
+	)";
+
+	static const std::string insertGameRoomPlayersText = R"(
+		INSERT INTO gameRoomPlayers(roomID, accountID) VALUES(?,?);
+	)";
+
+	static const std::string insertGameRoomInvitesText = R"(
+		INSERT INTO gameRoomInvites(roomID, accountID) VALUES(?,?);
+	)";
+
+	// DELETE FROM
+
+	static const std::string deleteGameRoomPlayersText = R"(
+		 DELETE FROM gameRoomPlayers WHERE gameRoomID = ? AND accountID = ?
+	)";
+
+	static const std::string deleteGameRoomInvitesText = R"(
+		DELETE FROM gameRoomInvites WHERE gameRoomID = ? AND accountID = ?
+	)";
+
+	// UPDATE
+
+	static const std::string setGameRoomStatusText = R"(
+		UPDATE gameRooms
+		SET status = ?
+		WHERE roomID = ?
+	)";
+
+	static const std::string setGameRoomPlayerLimitText = R"(
+		UPDATE gameRooms
+		SET playerLimit = ?
+		WHERE roomID = ?
+	)";
+
+	// SELECT FROM
+
 	static const std::string getRecentMessageHistoryText = R"(
-		SELECT senderName, messageText, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',sendTime)  AS secondsElapsed
+		SELECT senderName, messageText, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',creationTime)  AS secondsElapsed
 		FROM chatMessages
-		WHERE secondsElapsed < 60*60*24
-		ORDER BY sendTime DESC
+		WHERE secondsElapsed < 60*60*18
+		ORDER BY creationTime DESC
 		LIMIT 100
 	)";
 
-	insertChatMessageStatement = database->prepare(insertChatMessageText);
-	getRecentMessageHistoryStatement = database->prepare(getRecentMessageHistoryText);
-}
+	static const std::string getIdleGameRoomText = R"(
+		SELECT roomID
+		FROM gameRooms
+		WHERE hostAccountID = ? AND status = 'idle'
+		LIMIT 1
+	)";
 
-void LobbyDatabase::createTableChatMessages()
-{
-	static const std::string statementText = R"(
-		CREATE TABLE IF NOT EXISTS chatMessages (
-			id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-			senderName TEXT,
-			messageText TEXT,
-			sendTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
-		);
+	static const std::string getAccountGameRoomText = R"(
+		SELECT roomID
+		FROM gameRoomPlayers grp
+		LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID
+		WHERE accountID = ? AND status IN ('public', 'private', 'busy')
+		LIMIT 1
 	)";
 
-	auto statement = database->prepare(statementText);
-	statement->execute();
-}
+	static const std::string getActiveAccountsText = R"(
+		SELECT accountID, displayName
+		FROM accounts
+		WHERE online <> 1
+	)";
 
-void LobbyDatabase::initializeDatabase()
-{
-	createTableChatMessages();
+	static const std::string isAccountCookieValidText = R"(
+		SELECT COUNT(accountID)
+		FROM accountCookies
+		WHERE accountID = ? AND cookieUUID = ? AND strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',creationTime) < ?
+	)";
+
+	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"(
+		SELECT COUNT(accountID)
+		FROM gameRoomPlayers
+		WHERE accountID = ? AND roomID = ?
+	)";
+
+	static const std::string isPlayerInAnyGameRoomText = R"(
+		SELECT COUNT(accountID)
+		FROM gameRoomPlayers
+		WHERE accountID = ?
+	)";
+
+	static const std::string isAccountExistsText = R"(
+		SELECT COUNT(accountID)
+		FROM accounts
+		WHERE accountID = ?
+	)";
+
+	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);
+
+	setGameRoomStatusStatement = database->prepare(setGameRoomStatusText);
+	setGameRoomPlayerLimitStatement = database->prepare(setGameRoomPlayerLimitText);
+
+	getRecentMessageHistoryStatement = database->prepare(getRecentMessageHistoryText);
+	getIdleGameRoomStatement = database->prepare(getIdleGameRoomText);
+	getAccountGameRoomStatement = database->prepare(getAccountGameRoomText);
+	getActiveAccountsStatement = database->prepare(getActiveAccountsText);
+
+	isAccountCookieValidStatement = database->prepare(isAccountCookieValidText);
+	isPlayerInGameRoomStatement = database->prepare(isPlayerInGameRoomText);
+	isPlayerInAnyGameRoomStatement = database->prepare(isPlayerInAnyGameRoomText);
+	isAccountExistsStatement = database->prepare(isAccountExistsText);
 }
 
 LobbyDatabase::~LobbyDatabase() = default;
 
-LobbyDatabase::LobbyDatabase(const std::string & databasePath)
+LobbyDatabase::LobbyDatabase(const boost::filesystem::path & databasePath)
 {
 	database = SQLiteInstance::open(databasePath, true);
-
-	if(!database)
-		throw std::runtime_error("Failed to open SQLite database!");
-
-	initializeDatabase();
+	createTables();
 	prepareStatements();
 }
 
 void LobbyDatabase::insertChatMessage(const std::string & sender, const std::string & roomType, const std::string & roomName, const std::string & messageText)
 {
-	insertChatMessageStatement->setBinds(sender, messageText);
-	insertChatMessageStatement->execute();
-	insertChatMessageStatement->reset();
+	insertChatMessageStatement->executeOnce(sender, messageText);
 }
 
-bool LobbyDatabase::isPlayerInGameRoom(const std::string & accountName)
+bool LobbyDatabase::isPlayerInGameRoom(const std::string & accountID)
 {
-	return false; //TODO
+	bool result = false;
+
+	isPlayerInAnyGameRoomStatement->setBinds(accountID);
+	if (isPlayerInAnyGameRoomStatement->execute())
+		isPlayerInAnyGameRoomStatement->getColumns(result);
+	isPlayerInAnyGameRoomStatement->reset();
+
+	return result;
 }
 
-std::vector<LobbyDatabase::ChatMessage> LobbyDatabase::getRecentMessageHistory()
+bool LobbyDatabase::isPlayerInGameRoom(const std::string & accountID, const std::string & roomID)
 {
-	std::vector<LobbyDatabase::ChatMessage> result;
+	bool result = false;
+
+	isPlayerInGameRoomStatement->setBinds(accountID, roomID);
+	if (isPlayerInGameRoomStatement->execute())
+		isPlayerInGameRoomStatement->getColumns(result);
+	isPlayerInGameRoomStatement->reset();
+
+	return result;
+}
+
+std::vector<LobbyChatMessage> LobbyDatabase::getRecentMessageHistory()
+{
+	std::vector<LobbyChatMessage> result;
 
 	while(getRecentMessageHistoryStatement->execute())
 	{
-		LobbyDatabase::ChatMessage message;
+		LobbyChatMessage message;
 		getRecentMessageHistoryStatement->getColumns(message.sender, message.messageText, message.age);
 		result.push_back(message);
 	}
@@ -89,3 +269,144 @@ std::vector<LobbyDatabase::ChatMessage> LobbyDatabase::getRecentMessageHistory()
 
 	return result;
 }
+
+void LobbyDatabase::setGameRoomStatus(const std::string & roomID, LobbyRoomState roomStatus)
+{
+	setGameRoomStatusStatement->executeOnce(vstd::to_underlying(roomStatus), roomID);
+}
+
+void LobbyDatabase::setGameRoomPlayerLimit(const std::string & roomID, uint32_t playerLimit)
+{
+	setGameRoomPlayerLimitStatement->executeOnce(playerLimit, roomID);
+}
+
+void LobbyDatabase::insertPlayerIntoGameRoom(const std::string & accountID, const std::string & roomID)
+{
+	insertGameRoomPlayersStatement->executeOnce(roomID, accountID);
+}
+
+void LobbyDatabase::deletePlayerFromGameRoom(const std::string & accountID, const std::string & roomID)
+{
+	deleteGameRoomPlayersStatement->executeOnce(roomID, accountID);
+}
+
+void LobbyDatabase::deleteGameRoomInvite(const std::string & targetAccountID, const std::string & roomID)
+{
+	deleteGameRoomInvitesStatement->executeOnce(roomID, targetAccountID);
+}
+
+void LobbyDatabase::insertGameRoomInvite(const std::string & targetAccountID, const std::string & roomID)
+{
+	insertGameRoomInvitesStatement->executeOnce(roomID, targetAccountID);
+}
+
+void LobbyDatabase::insertGameRoom(const std::string & roomID, const std::string & hostAccountID)
+{
+	insertGameRoomStatement->executeOnce(roomID, hostAccountID);
+}
+
+void LobbyDatabase::insertAccount(const std::string & accountID, const std::string & displayName)
+{
+	insertAccountStatement->executeOnce(accountID, displayName);
+}
+
+void LobbyDatabase::insertAccessCookie(const std::string & accountID, const std::string & accessCookieUUID)
+{
+	insertAccessCookieStatement->executeOnce(accountID, accessCookieUUID);
+}
+
+void LobbyDatabase::updateAccessCookie(const std::string & accountID, const std::string & accessCookieUUID)
+{
+
+}
+
+void LobbyDatabase::updateAccountLoginTime(const std::string & accountID)
+{
+
+}
+
+void LobbyDatabase::updateActiveAccount(const std::string & accountID, bool isActive)
+{
+
+}
+
+std::string LobbyDatabase::getAccountDisplayName(const std::string & accountID)
+{
+	return {};
+}
+
+LobbyCookieStatus LobbyDatabase::getGameRoomCookieStatus(const std::string & accountID, const std::string & accessCookieUUID, std::chrono::seconds cookieLifetime)
+{
+	return {};
+}
+
+LobbyCookieStatus LobbyDatabase::getAccountCookieStatus(const std::string & accountID, const std::string & accessCookieUUID, std::chrono::seconds cookieLifetime)
+{
+	return {};
+}
+
+LobbyCookieStatus LobbyDatabase::getAccountCookieStatus(const std::string & accountID, std::chrono::seconds cookieLifetime)
+{
+	return {};
+}
+
+LobbyInviteStatus LobbyDatabase::getAccountInviteStatus(const std::string & accountID, const std::string & roomID)
+{
+	return {};
+}
+
+LobbyRoomState LobbyDatabase::getGameRoomStatus(const std::string & roomID)
+{
+	return {};
+}
+
+uint32_t LobbyDatabase::getGameRoomFreeSlots(const std::string & roomID)
+{
+	return 0;
+}
+
+bool LobbyDatabase::isAccountExists(const std::string & accountID)
+{
+	return false;
+}
+
+std::vector<LobbyGameRoom> LobbyDatabase::getActiveGameRooms()
+{
+	return {};
+}
+
+std::vector<LobbyAccount> LobbyDatabase::getActiveAccounts()
+{
+	std::vector<LobbyAccount> result;
+
+	while(getActiveAccountsStatement->execute())
+	{
+		LobbyAccount entry;
+		getActiveAccountsStatement->getColumns(entry.accountID, entry.displayName);
+		result.push_back(entry);
+	}
+	getActiveAccountsStatement->reset();
+	return result;
+}
+
+std::string LobbyDatabase::getIdleGameRoom(const std::string & hostAccountID)
+{
+	std::string result;
+
+	if (getIdleGameRoomStatement->execute())
+		getIdleGameRoomStatement->getColumns(result);
+
+	getIdleGameRoomStatement->reset();
+	return result;
+}
+
+std::string LobbyDatabase::getAccountGameRoom(const std::string & accountID)
+{
+	std::string result;
+
+	if (getAccountGameRoomStatement->execute())
+		getAccountGameRoomStatement->getColumns(result);
+
+	getAccountGameRoomStatement->reset();
+	return result;
+}

+ 52 - 43
lobby/LobbyDatabase.h

@@ -9,6 +9,8 @@
  */
 #pragma once
 
+#include "LobbyDefines.h"
+
 class SQLiteInstance;
 class SQLiteStatement;
 
@@ -23,61 +25,68 @@ class LobbyDatabase
 	SQLiteStatementPtr insertAccountStatement;
 	SQLiteStatementPtr insertAccessCookieStatement;
 	SQLiteStatementPtr insertGameRoomStatement;
+	SQLiteStatementPtr insertGameRoomPlayersStatement;
+	SQLiteStatementPtr insertGameRoomInvitesStatement;
 
-	SQLiteStatementPtr checkAccessCookieStatement;
-	SQLiteStatementPtr isPlayerInGameRoomStatement;
-	SQLiteStatementPtr isAccountNameAvailableStatement;
+	SQLiteStatementPtr deleteGameRoomPlayersStatement;
+	SQLiteStatementPtr deleteGameRoomInvitesStatement;
 
-	SQLiteStatementPtr getRecentMessageHistoryStatement;
 	SQLiteStatementPtr setGameRoomStatusStatement;
 	SQLiteStatementPtr setGameRoomPlayerLimitStatement;
-	SQLiteStatementPtr insertPlayerIntoGameRoomStatement;
-	SQLiteStatementPtr removePlayerFromGameRoomStatement;
 
-	void initializeDatabase();
-	void prepareStatements();
+	SQLiteStatementPtr getRecentMessageHistoryStatement;
+	SQLiteStatementPtr getIdleGameRoomStatement;
+	SQLiteStatementPtr getAccountGameRoomStatement;
+	SQLiteStatementPtr getActiveAccountsStatement;
+
+	SQLiteStatementPtr isAccountCookieValidStatement;
+	SQLiteStatementPtr isGameRoomCookieValidStatement;
+	SQLiteStatementPtr isPlayerInGameRoomStatement;
+	SQLiteStatementPtr isPlayerInAnyGameRoomStatement;
+	SQLiteStatementPtr isAccountExistsStatement;
 
-	void createTableChatMessages();
-	void createTableGameRoomPlayers();
-	void createTableGameRooms();
-	void createTableAccounts();
-	void createTableAccountCookies();
+	void prepareStatements();
+	void createTables();
 
 public:
-	struct GameRoom
-	{
-		std::string roomUUID;
-		std::string roomStatus;
-		std::chrono::seconds age;
-		uint32_t playersCount;
-		uint32_t playersLimit;
-	};
-
-	struct ChatMessage
-	{
-		std::string sender;
-		std::string messageText;
-		std::chrono::seconds age;
-	};
-
-	explicit LobbyDatabase(const std::string & databasePath);
+	explicit LobbyDatabase(const boost::filesystem::path & databasePath);
 	~LobbyDatabase();
 
-	void setGameRoomStatus(const std::string & roomUUID, const std::string & roomStatus);
-	void setGameRoomPlayerLimit(const std::string & roomUUID, uint32_t playerLimit);
+	void setGameRoomStatus(const std::string & roomID, LobbyRoomState roomStatus);
+	void setGameRoomPlayerLimit(const std::string & roomID, uint32_t playerLimit);
+
+	void insertPlayerIntoGameRoom(const std::string & accountID, const std::string & roomID);
+	void deletePlayerFromGameRoom(const std::string & accountID, const std::string & roomID);
+
+	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 insertAccount(const std::string & accountID, const std::string & displayName);
+	void insertAccessCookie(const std::string & accountID, const std::string & accessCookieUUID);
+	void insertChatMessage(const std::string & sender, const std::string & roomType, const std::string & roomID, const std::string & messageText);
+
+	void updateAccessCookie(const std::string & accountID, const std::string & accessCookieUUID);
+	void updateAccountLoginTime(const std::string & accountID);
+	void updateActiveAccount(const std::string & accountID, bool isActive);
 
-	void insertPlayerIntoGameRoom(const std::string & accountName, const std::string & roomUUID);
-	void removePlayerFromGameRoom(const std::string & accountName, const std::string & roomUUID);
+	std::vector<LobbyGameRoom> getActiveGameRooms();
+	std::vector<LobbyAccount> getActiveAccounts();
+	std::vector<LobbyAccount> getAccountsInRoom(const std::string & roomID);
+	std::vector<LobbyChatMessage> getRecentMessageHistory();
 
-	void insertGameRoom(const std::string & roomUUID, const std::string & hostAccountName);
-	void insertAccount(const std::string & accountName);
-	void insertAccessCookie(const std::string & accountName, const std::string & accessCookieUUID, std::chrono::seconds cookieLifetime);
-	void insertChatMessage(const std::string & sender, const std::string & roomType, const std::string & roomName, const std::string & messageText);
+	std::string getIdleGameRoom(const std::string & hostAccountID);
+	std::string getAccountGameRoom(const std::string & accountID);
+	std::string getAccountDisplayName(const std::string & accountID);
 
-	std::vector<GameRoom> getActiveGameRooms();
-	std::vector<ChatMessage> getRecentMessageHistory();
+	LobbyCookieStatus getGameRoomCookieStatus(const std::string & accountID, const std::string & accessCookieUUID, std::chrono::seconds cookieLifetime);
+	LobbyCookieStatus getAccountCookieStatus(const std::string & accountID, const std::string & accessCookieUUID, std::chrono::seconds cookieLifetime);
+	LobbyCookieStatus getAccountCookieStatus(const std::string & accountID, std::chrono::seconds cookieLifetime);
+	LobbyInviteStatus getAccountInviteStatus(const std::string & accountID, const std::string & roomID);
+	LobbyRoomState getGameRoomStatus(const std::string & roomID);
+	uint32_t getGameRoomFreeSlots(const std::string & roomID);
 
-	bool checkAccessCookie(const std::string & accountName, const std::string & accessCookieUUID);
-	bool isPlayerInGameRoom(const std::string & accountName);
-	bool isAccountNameAvailable(const std::string & accountName);
+	bool isPlayerInGameRoom(const std::string & accountID);
+	bool isPlayerInGameRoom(const std::string & accountID, const std::string & roomID);
+	bool isAccountExists(const std::string & accountID);
 };

+ 56 - 0
lobby/LobbyDefines.h

@@ -0,0 +1,56 @@
+/*
+ * LobbyDefines.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
+
+struct LobbyAccount
+{
+	std::string accountID;
+	std::string displayName;
+	//std::string status;
+};
+
+struct LobbyGameRoom
+{
+	std::string roomUUID;
+	std::string roomStatus;
+	uint32_t playersCount;
+	uint32_t playersLimit;
+};
+
+struct LobbyChatMessage
+{
+	std::string sender;
+	std::string messageText;
+	std::chrono::seconds age;
+};
+
+enum class LobbyCookieStatus : int32_t
+{
+	INVALID,
+	EXPIRED,
+	VALID
+};
+
+enum class LobbyInviteStatus : int32_t
+{
+	NOT_INVITED,
+	INVITED,
+	DECLINED
+};
+
+enum class LobbyRoomState : int32_t
+{
+	IDLE,        // server is ready but no players are in the room
+	PUBLIC,      // host has joined and allows anybody to join
+	PRIVATE,     // host has joined but only allows those he invited to join
+	//BUSY,        // match is ongoing
+	//CANCELLED,   // game room was cancelled without starting the game
+	//CLOSED,      // game room was closed after playing for some time
+};

+ 462 - 58
lobby/LobbyServer.cpp

@@ -14,126 +14,530 @@
 
 #include "../lib/JsonNode.h"
 #include "../lib/network/NetworkServer.h"
+#include "../lib/network/NetworkConnection.h"
 
-void LobbyServer::sendMessage(const std::shared_ptr<NetworkConnection> & target, const JsonNode & json)
+#include <boost/uuid/uuid_io.hpp>
+#include <boost/uuid/uuid_generators.hpp>
+
+static const auto accountCookieLifetime = std::chrono::hours(24*7);
+
+bool LobbyServer::isAccountNameValid(const std::string & accountName)
+{
+	if (accountName.size() < 4)
+		return false;
+
+	if (accountName.size() < 20)
+		return false;
+
+	for (auto const & c : accountName)
+		if (!std::isalnum(c))
+			return false;
+
+	return true;
+}
+
+std::string LobbyServer::sanitizeChatMessage(const std::string & inputString) const
+{
+	// TODO: sanitize message and remove any "weird" symbols from it
+	return inputString;
+}
+
+NetworkConnectionPtr LobbyServer::findAccount(const std::string & accountID)
 {
-	//FIXME: copy-paste from LobbyClient::sendMessage
+	for (auto const & account : activeAccounts)
+		if (account.second.accountID == accountID)
+			return account.first;
+
+	return nullptr;
+}
+
+NetworkConnectionPtr LobbyServer::findGameRoom(const std::string & gameRoomID)
+{
+	for (auto const & account : activeGameRooms)
+		if (account.second.roomID == gameRoomID)
+			return account.first;
+
+	return nullptr;
+}
+
+void LobbyServer::sendMessage(const NetworkConnectionPtr & target, const JsonNode & json)
+{
+	//NOTE: copy-paste from LobbyClient::sendMessage
 	std::string payloadString = json.toJson(true);
 
-	// FIXME: find better approach
-	uint8_t * payloadBegin = reinterpret_cast<uint8_t *>(payloadString.data());
-	uint8_t * payloadEnd = payloadBegin + payloadString.size();
+	// TODO: find better approach
+	const uint8_t * payloadBegin = reinterpret_cast<uint8_t *>(payloadString.data());
+	const uint8_t * payloadEnd = payloadBegin + payloadString.size();
 
 	std::vector<uint8_t> payloadBuffer(payloadBegin, payloadEnd);
 
 	networkServer->sendPacket(target, payloadBuffer);
 }
 
-void LobbyServer::onTimer()
+void LobbyServer::sendAccountCreated(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & accountCookie)
 {
-	// no-op
+	JsonNode reply;
+	reply["type"].String() = "accountCreated";
+	reply["accountID"].String() = accountID;
+	reply["accountCookie"].String() = accountCookie;
+	sendMessage(target, reply);
 }
 
-void LobbyServer::onNewConnection(const std::shared_ptr<NetworkConnection> & connection)
-{}
+void LobbyServer::sendInviteReceived(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & gameRoomID)
+{
+	JsonNode reply;
+	reply["type"].String() = "inviteReceived";
+	reply["accountID"].String() = accountID;
+	reply["gameRoomID"].String() = gameRoomID;
+	sendMessage(target, reply);
+}
 
-void LobbyServer::onDisconnected(const std::shared_ptr<NetworkConnection> & connection)
+void LobbyServer::sendLoginFailed(const NetworkConnectionPtr & target, const std::string & reason)
 {
-	activeAccounts.erase(connection);
+	JsonNode reply;
+	reply["type"].String() = "loginFailed";
+	reply["reason"].String() = reason;
+	sendMessage(target, reply);
 }
 
-void LobbyServer::onPacketReceived(const std::shared_ptr<NetworkConnection> & connection, const std::vector<uint8_t> & message)
+void LobbyServer::sendLoginSuccess(const NetworkConnectionPtr & target, const std::string & accountCookie)
 {
-	// FIXME: find better approach
-	JsonNode json(message.data(), message.size());
+	JsonNode reply;
+	reply["type"].String() = "loginSuccess";
+	reply["accountCookie"].String() = accountCookie;
+	sendMessage(target, reply);
+}
+
+void LobbyServer::sendChatHistory(const NetworkConnectionPtr & target, const std::vector<LobbyChatMessage> & history)
+{
+	JsonNode reply;
+	reply["type"].String() = "chatHistory";
+
+	for(const auto & message : boost::adaptors::reverse(history))
+	{
+		JsonNode jsonEntry;
 
-	if(json["type"].String() == "sendChatMessage")
-		return receiveSendChatMessage(connection, json);
+		jsonEntry["messageText"].String() = message.messageText;
+		jsonEntry["senderName"].String() = message.sender;
+		jsonEntry["ageSeconds"].Integer() = message.age.count();
 
-	if(json["type"].String() == "authentication")
-		return receiveAuthentication(connection, json);
+		reply["messages"].Vector().push_back(jsonEntry);
+	}
 
-	if(json["type"].String() == "joinGameRoom")
-		return receiveJoinGameRoom(connection, json);
+	sendMessage(target, reply);
 }
 
-void LobbyServer::receiveSendChatMessage(const std::shared_ptr<NetworkConnection> & connection, const JsonNode & json)
+void LobbyServer::broadcastActiveAccounts()
 {
-	if(activeAccounts.count(connection) == 0)
-		return; // unauthenticated
+	auto activeAccountsStats = database->getActiveAccounts();
 
-	std::string senderName = activeAccounts[connection].accountName;
-	std::string messageText = json["messageText"].String();
+	JsonNode reply;
+	reply["type"].String() = "activeAccounts";
+
+	for(const auto & account : activeAccountsStats)
+	{
+		JsonNode jsonEntry;
+		jsonEntry["accountID"].String() = account.accountID;
+		jsonEntry["displayName"].String() = account.displayName;
+//		jsonEntry["status"].String() = account.status;
+		reply["accounts"].Vector().push_back(jsonEntry);
+	}
+
+	for(const auto & connection : activeAccounts)
+		sendMessage(connection.first, reply);
+}
 
-	database->insertChatMessage(senderName, "general", "english", messageText);
+void LobbyServer::broadcastActiveGameRooms()
+{
+	auto activeGameRoomStats = database->getActiveGameRooms();
+	JsonNode reply;
+	reply["type"].String() = "activeGameRooms";
+
+	for(const auto & gameRoom : activeGameRoomStats)
+	{
+		JsonNode jsonEntry;
+		jsonEntry["gameRoomID"].String() = gameRoom.roomUUID;
+		jsonEntry["status"].String() = gameRoom.roomStatus;
+		jsonEntry["status"].Integer() = gameRoom.playersCount;
+		jsonEntry["status"].Integer() = gameRoom.playersLimit;
+		reply["gameRooms"].Vector().push_back(jsonEntry);
+	}
+
+	for(const auto & connection : activeAccounts)
+		sendMessage(connection.first, reply);
+}
 
+void LobbyServer::sendAccountJoinsRoom(const NetworkConnectionPtr & target, const std::string & accountID)
+{
+	JsonNode reply;
+	reply["type"].String() = "accountJoinsRoom";
+	reply["accountID"].String() = accountID;
+	sendMessage(target, reply);
+}
+
+void LobbyServer::sendJoinRoomSuccess(const NetworkConnectionPtr & target, const std::string & gameRoomID)
+{
+	JsonNode reply;
+	reply["type"].String() = "joinRoomSuccess";
+	reply["gameRoomID"].String() = gameRoomID;
+	sendMessage(target, reply);
+}
+
+void LobbyServer::sendChatMessage(const NetworkConnectionPtr & target, const std::string & roomMode, const std::string & roomName, const std::string & senderName, const std::string & messageText)
+{
 	JsonNode reply;
 	reply["type"].String() = "chatMessage";
 	reply["messageText"].String() = messageText;
 	reply["senderName"].String() = senderName;
+	reply["roomMode"].String() = roomMode;
+	reply["roomName"].String() = roomName;
 
-	for(const auto & connection : activeAccounts)
-		sendMessage(connection.first, reply);
+	sendMessage(target, reply);
 }
 
-void LobbyServer::receiveAuthentication(const std::shared_ptr<NetworkConnection> & connection, const JsonNode & json)
+void LobbyServer::onTimer()
 {
-	std::string accountName = json["accountName"].String();
+	// no-op
+}
 
-	// TODO: account cookie check
-	// TODO: account password check
-	// TODO: protocol version number
-	// TODO: client/server mode flag
-	// TODO: client language
+void LobbyServer::onNewConnection(const NetworkConnectionPtr & connection)
+{
+	// no-op - waiting for incoming data
+}
+
+void LobbyServer::onDisconnected(const NetworkConnectionPtr & connection)
+{
+	// NOTE: lost connection can be in only one of these lists (or in none of them)
+	// calling on all possible containers since calling std::map::erase() with non-existing key is legal
+	activeAccounts.erase(connection);
+	activeProxies.erase(connection);
+	activeGameRooms.erase(connection);
+
+	broadcastActiveAccounts();
+	broadcastActiveGameRooms();
+}
+
+void LobbyServer::onPacketReceived(const NetworkConnectionPtr & connection, const std::vector<uint8_t> & message)
+{
+	// proxy connection - no processing, only redirect
+	if (activeProxies.count(connection))
+	{
+		auto lockedPtr = activeProxies.at(connection).lock();
+		if (lockedPtr)
+			lockedPtr->sendPacket(message);
+		return;
+	}
 
-	activeAccounts[connection].accountName = accountName;
+	JsonNode json(message.data(), message.size());
+
+	// TODO: check for json parsing errors
+	// TODO: validate json based on received message type
 
+	// communication messages from vcmiclient
+	if (activeAccounts.count(connection))
 	{
-		JsonNode reply;
-		reply["type"].String() = "authentication";
+		if(json["type"].String() == "sendChatMessage")
+			return receiveSendChatMessage(connection, json);
+
+		if(json["type"].String() == "openGameRoom")
+			return receiveOpenGameRoom(connection, json);
+
+		if(json["type"].String() == "joinGameRoom")
+			return receiveJoinGameRoom(connection, json);
+
+		if(json["type"].String() == "sendInvite")
+			return receiveSendInvite(connection, json);
+
+		if(json["type"].String() == "declineInvite")
+			return receiveDeclineInvite(connection, json);
 
-		sendMessage(connection, reply);
+		return;
 	}
 
-	auto history = database->getRecentMessageHistory();
+	// communication messages from vcmiserver
+	if (activeGameRooms.count(connection))
+	{
+		if(json["type"].String() == "leaveGameRoom")
+			return receiveLeaveGameRoom(connection, json);
+
+		return;
+	}
+
+	// unauthorized connections - permit only login or register attempts
+	if(json["type"].String() == "clientLogin")
+		return receiveClientLogin(connection, json);
+
+	if(json["type"].String() == "clientRegister")
+		return receiveClientRegister(connection, json);
 
+	if(json["type"].String() == "serverLogin")
+		return receiveServerLogin(connection, json);
+
+	if(json["type"].String() == "clientProxyLogin")
+		return receiveClientProxyLogin(connection, json);
+
+	if(json["type"].String() == "serverProxyLogin")
+		return receiveServerProxyLogin(connection, json);
+
+	// TODO: add logging of suspicious connections.
+	networkServer->closeConnection(connection);
+}
+
+void LobbyServer::receiveSendChatMessage(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string senderName = activeAccounts[connection].accountID;
+	std::string messageText = json["messageText"].String();
+	std::string messageTextClean = sanitizeChatMessage(messageText);
+
+	if (messageTextClean.empty())
+		return;
+
+	database->insertChatMessage(senderName, "global", "english", messageText);
+
+	for(const auto & connection : activeAccounts)
+		sendChatMessage(connection.first, "global", "english", senderName, messageText);
+}
+
+void LobbyServer::receiveClientRegister(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string accountID = json["accountID"].String();
+	std::string displayName = json["displayName"].String();
+	std::string language = json["language"].String();
+
+	if (database->isAccountExists(accountID))
+		return sendLoginFailed(connection, "Account name already in use");
+
+	if (isAccountNameValid(accountID))
+		return sendLoginFailed(connection, "Illegal account name");
+
+	std::string accountCookie = boost::uuids::to_string(boost::uuids::random_generator()());
+
+	database->insertAccount(accountID, displayName);
+	database->insertAccessCookie(accountID, accountCookie);
+
+	sendAccountCreated(connection, accountID, accountCookie);
+}
+
+void LobbyServer::receiveClientLogin(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string accountID = json["accountID"].String();
+	std::string accountCookie = json["accountCookie"].String();
+	std::string language = json["language"].String();
+	std::string version = json["version"].String();
+
+	if (!database->isAccountExists(accountID))
+		return sendLoginFailed(connection, "Account not found");
+
+	auto clientCookieStatus = database->getAccountCookieStatus(accountID, accountCookie, accountCookieLifetime);
+
+	if (clientCookieStatus == LobbyCookieStatus::INVALID)
+		return sendLoginFailed(connection, "Authentification failure");
+
+	// prolong existing cookie
+	database->updateAccessCookie(accountID, accountCookie);
+	database->updateAccountLoginTime(accountID);
+
+	std::string displayName = database->getAccountDisplayName(accountID);
+
+	activeAccounts[connection].accountID = accountID;
+	activeAccounts[connection].displayName = displayName;
+	activeAccounts[connection].version = version;
+	activeAccounts[connection].language = language;
+
+	sendLoginSuccess(connection, accountCookie);
+	sendChatHistory(connection, database->getRecentMessageHistory());
+
+	// send active accounts list to new account
+	// and update acount list to everybody else
+	broadcastActiveAccounts();
+}
+
+void LobbyServer::receiveServerLogin(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string gameRoomID = json["gameRoomID"].String();
+	std::string accountID = json["accountID"].String();
+	std::string accountCookie = json["accountCookie"].String();
+	std::string version = json["version"].String();
+
+	auto clientCookieStatus = database->getAccountCookieStatus(accountID, accountCookie, accountCookieLifetime);
+
+	if (clientCookieStatus == LobbyCookieStatus::INVALID)
+	{
+		sendLoginFailed(connection, "Invalid credentials");
+	}
+	else
 	{
-		JsonNode reply;
-		reply["type"].String() = "chatHistory";
+		database->insertGameRoom(gameRoomID, accountID);
+		activeGameRooms[connection].roomID = gameRoomID;
+		sendLoginSuccess(connection, accountCookie);
+	}
+}
 
-		for(const auto & message : boost::adaptors::reverse(history))
+void LobbyServer::receiveClientProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string gameRoomID = json["gameRoomID"].String();
+	std::string accountID = json["accountID"].String();
+	std::string accountCookie = json["accountCookie"].String();
+
+	auto clientCookieStatus = database->getAccountCookieStatus(accountID, accountCookie, accountCookieLifetime);
+
+	if (clientCookieStatus != LobbyCookieStatus::INVALID)
+	{
+		for (auto & proxyEntry : awaitingProxies)
 		{
-			JsonNode jsonEntry;
+			if (proxyEntry.accountID != accountID)
+				continue;
+			if (proxyEntry.roomID != gameRoomID)
+				continue;
 
-			jsonEntry["messageText"].String() = message.messageText;
-			jsonEntry["senderName"].String() = message.sender;
-			jsonEntry["ageSeconds"].Integer() = message.age.count();
+			proxyEntry.accountConnection = connection;
 
-			reply["messages"].Vector().push_back(jsonEntry);
+			auto gameRoomConnection = proxyEntry.roomConnection.lock();
+
+			if (gameRoomConnection)
+			{
+				activeProxies[gameRoomConnection] = connection;
+				activeProxies[connection] = gameRoomConnection;
+			}
+			return;
 		}
+	}
 
-		sendMessage(connection, reply);
+	networkServer->closeConnection(connection);
+}
+
+void LobbyServer::receiveServerProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string gameRoomID = json["gameRoomID"].String();
+	std::string guestAccountID = json["guestAccountID"].String();
+	std::string hostCookie = json["hostCookie"].String();
+
+	auto clientCookieStatus = database->getGameRoomCookieStatus(gameRoomID, hostCookie, accountCookieLifetime);
+
+	if (clientCookieStatus != LobbyCookieStatus::INVALID)
+	{
+		NetworkConnectionPtr targetAccount = findAccount(guestAccountID);
+
+		if (targetAccount == nullptr)
+			return; // unknown / disconnected account
+
+		sendJoinRoomSuccess(targetAccount, gameRoomID);
+
+		AwaitingProxyState proxy;
+		proxy.accountID = guestAccountID;
+		proxy.roomID = gameRoomID;
+		proxy.roomConnection = connection;
+		awaitingProxies.push_back(proxy);
+		return;
 	}
+
+	networkServer->closeConnection(connection);
 }
 
-void LobbyServer::receiveJoinGameRoom(const std::shared_ptr<NetworkConnection> & connection, const JsonNode & json)
+void LobbyServer::receiveOpenGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json)
 {
-	if(activeAccounts.count(connection) == 0)
-		return; // unauthenticated
+	std::string hostAccountID = json["hostAccountID"].String();
+	std::string accountID = activeAccounts[connection].accountID;
+
+	if(database->isPlayerInGameRoom(accountID))
+		return; // only 1 room per player allowed
 
-	std::string senderName = activeAccounts[connection].accountName;
+	std::string gameRoomID = database->getIdleGameRoom(hostAccountID);
+	if (gameRoomID.empty())
+		return;
 
-	if(database->isPlayerInGameRoom(senderName))
+	std::string roomType = json["roomType"].String();
+	if (roomType == "public")
+		database->setGameRoomStatus(gameRoomID, LobbyRoomState::PUBLIC);
+	if (roomType == "private")
+		database->setGameRoomStatus(gameRoomID, LobbyRoomState::PRIVATE);
+
+	// TODO: additional flags / initial settings, e.g. allowCheats
+	// TODO: connection mode: direct or proxy. For now direct is assumed
+
+	broadcastActiveGameRooms();
+	sendJoinRoomSuccess(connection, gameRoomID);
+}
+
+void LobbyServer::receiveJoinGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string gameRoomID = json["gameRoomID"].String();
+	std::string accountID = activeAccounts[connection].accountID;
+
+	if(database->isPlayerInGameRoom(accountID))
 		return; // only 1 room per player allowed
 
-	// TODO: roomType: private, public
-	// TODO: additional flags, e.g. allowCheats
-	// TODO: connection mode: direct or proxy
+	NetworkConnectionPtr targetRoom = findGameRoom(gameRoomID);
+
+	if (targetRoom == nullptr)
+		return; // unknown / disconnected room
+
+	auto roomStatus = database->getGameRoomStatus(gameRoomID);
+
+	if (roomStatus == LobbyRoomState::PRIVATE)
+	{
+		if (database->getAccountInviteStatus(accountID, gameRoomID) != LobbyInviteStatus::INVITED)
+			return;
+	}
+
+	if (database->getGameRoomFreeSlots(gameRoomID) == 0)
+		return;
+
+	sendAccountJoinsRoom(targetRoom, accountID);
+	//No reply to client - will be sent once match server establishes proxy connection with lobby
+
+	broadcastActiveGameRooms();
+}
+
+void LobbyServer::receiveLeaveGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string gameRoomID = json["gameRoomID"].String();
+	std::string senderName = activeAccounts[connection].accountID;
+
+	if(!database->isPlayerInGameRoom(senderName, gameRoomID))
+		return;
+
+	broadcastActiveGameRooms();
+}
+
+void LobbyServer::receiveSendInvite(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string senderName = activeAccounts[connection].accountID;
+	std::string accountID = json["accountID"].String();
+	std::string gameRoomID = database->getAccountGameRoom(senderName);
+
+	auto targetAccount = findAccount(accountID);
+
+	if (!targetAccount)
+		return; // target player does not exists or offline
+
+	if(!database->isPlayerInGameRoom(senderName))
+		return; // current player is not in room
+
+	if(database->isPlayerInGameRoom(accountID))
+		return; // target player is busy
+
+	if (database->getAccountInviteStatus(accountID, gameRoomID) != LobbyInviteStatus::NOT_INVITED)
+		return; // already has invite
+
+	database->insertGameRoomInvite(accountID, gameRoomID);
+	sendInviteReceived(targetAccount, senderName, gameRoomID);
+}
+
+void LobbyServer::receiveDeclineInvite(const NetworkConnectionPtr & connection, const JsonNode & json)
+{
+	std::string accountID = activeAccounts[connection].accountID;
+	std::string gameRoomID = json["gameRoomID"].String();
+
+	if (database->getAccountInviteStatus(accountID, gameRoomID) != LobbyInviteStatus::INVITED)
+		return; // already has invite
+
+	database->deleteGameRoomInvite(accountID, gameRoomID);
 }
 
 LobbyServer::~LobbyServer() = default;
 
-LobbyServer::LobbyServer(const std::string & databasePath)
+LobbyServer::LobbyServer(const boost::filesystem::path & databasePath)
 	: database(new LobbyDatabase(databasePath))
 	, networkServer(new NetworkServer(*this))
 {

+ 60 - 10
lobby/LobbyServer.h

@@ -9,6 +9,7 @@
  */
 #pragma once
 
+#include "LobbyDefines.h"
 #include "../lib/network/NetworkListener.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -21,29 +22,78 @@ class LobbyServer : public INetworkServerListener
 {
 	struct AccountState
 	{
-		std::string accountName;
+		std::string accountID;
+		std::string displayName;
+		std::string version;
+		std::string language;
 	};
+	struct GameRoomState
+	{
+		std::string roomID;
+	};
+	struct AwaitingProxyState
+	{
+		std::string accountID;
+		std::string roomID;
+		std::weak_ptr<NetworkConnection> accountConnection;
+		std::weak_ptr<NetworkConnection> roomConnection;
+	};
+
+	/// list of connected proxies. All messages received from (key) will be redirected to (value) connection
+	std::map<NetworkConnectionPtr, std::weak_ptr<NetworkConnection>> activeProxies;
+
+	/// list of half-established proxies from server that are still waiting for client to connect
+	std::vector<AwaitingProxyState> awaitingProxies;
 
-	std::map<std::shared_ptr<NetworkConnection>, AccountState> activeAccounts;
+	/// list of logged in accounts (vcmiclient's)
+	std::map<NetworkConnectionPtr, AccountState> activeAccounts;
+
+	/// list of currently logged in game rooms (vcmiserver's)
+	std::map<NetworkConnectionPtr, GameRoomState> activeGameRooms;
 
 	std::unique_ptr<LobbyDatabase> database;
 	std::unique_ptr<NetworkServer> networkServer;
 
+	std::string sanitizeChatMessage(const std::string & inputString) const;
 	bool isAccountNameValid(const std::string & accountName);
 
-	void onNewConnection(const std::shared_ptr<NetworkConnection> &) override;
-	void onDisconnected(const std::shared_ptr<NetworkConnection> &) override;
-	void onPacketReceived(const std::shared_ptr<NetworkConnection> &, const std::vector<uint8_t> & message) override;
+	NetworkConnectionPtr findAccount(const std::string & accountID);
+	NetworkConnectionPtr findGameRoom(const std::string & gameRoomID);
+
+	void onNewConnection(const NetworkConnectionPtr & connection) override;
+	void onDisconnected(const NetworkConnectionPtr & connection) override;
+	void onPacketReceived(const NetworkConnectionPtr & connection, const std::vector<uint8_t> & message) override;
 	void onTimer() override;
 
-	void sendMessage(const std::shared_ptr<NetworkConnection> & target, const JsonNode & json);
+	void sendMessage(const NetworkConnectionPtr & target, const JsonNode & json);
+
+	void broadcastActiveAccounts();
+	void broadcastActiveGameRooms();
+
+	void sendChatMessage(const NetworkConnectionPtr & target, const std::string & roomMode, const std::string & roomName, const std::string & senderName, const std::string & messageText);
+	void sendAccountCreated(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & accountCookie);
+	void sendLoginFailed(const NetworkConnectionPtr & target, const std::string & reason);
+	void sendLoginSuccess(const NetworkConnectionPtr & target, const std::string & accountCookie);
+	void sendChatHistory(const NetworkConnectionPtr & target, const std::vector<LobbyChatMessage> &);
+	void sendAccountJoinsRoom(const NetworkConnectionPtr & target, const std::string & accountID);
+	void sendJoinRoomSuccess(const NetworkConnectionPtr & target, const std::string & gameRoomID);
+	void sendInviteReceived(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & gameRoomID);
+
+	void receiveClientRegister(const NetworkConnectionPtr & connection, const JsonNode & json);
+	void receiveClientLogin(const NetworkConnectionPtr & connection, const JsonNode & json);
+	void receiveServerLogin(const NetworkConnectionPtr & connection, const JsonNode & json);
+	void receiveClientProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json);
+	void receiveServerProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json);
 
-	void receiveSendChatMessage(const std::shared_ptr<NetworkConnection> & connection, const JsonNode & json);
-	void receiveAuthentication(const std::shared_ptr<NetworkConnection> & connection, const JsonNode & json);
-	void receiveJoinGameRoom(const std::shared_ptr<NetworkConnection> & connection, const JsonNode & json);
+	void receiveSendChatMessage(const NetworkConnectionPtr & connection, const JsonNode & json);
+	void receiveOpenGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json);
+	void receiveJoinGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json);
+	void receiveLeaveGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json);
+	void receiveSendInvite(const NetworkConnectionPtr & connection, const JsonNode & json);
+	void receiveDeclineInvite(const NetworkConnectionPtr & connection, const JsonNode & json);
 
 public:
-	LobbyServer(const std::string & databasePath);
+	explicit LobbyServer(const boost::filesystem::path & databasePath);
 	~LobbyServer();
 
 	void start(uint16_t port);

+ 12 - 1
lobby/SQLiteConnection.cpp

@@ -70,6 +70,12 @@ void SQLiteStatement::setBindSingle(size_t index, const double & value)
 	checkSQLiteError(m_instance.m_connection, result);
 }
 
+void SQLiteStatement::setBindSingle(size_t index, const bool & value)
+{
+	int result = sqlite3_bind_int(m_statement, static_cast<int>(value), value);
+	checkSQLiteError(m_instance.m_connection, result);
+}
+
 void SQLiteStatement::setBindSingle(size_t index, const uint8_t & value)
 {
 	int result = sqlite3_bind_int(m_statement, static_cast<int>(index), value);
@@ -116,6 +122,11 @@ void SQLiteStatement::getColumnSingle(size_t index, double & value)
 	value = sqlite3_column_double(m_statement, static_cast<int>(index));
 }
 
+void SQLiteStatement::getColumnSingle(size_t index, bool & value)
+{
+	value = sqlite3_column_int(m_statement, static_cast<int>(index)) != 0;
+}
+
 void SQLiteStatement::getColumnSingle(size_t index, uint8_t & value)
 {
 	value = static_cast<uint8_t>(sqlite3_column_int(m_statement, static_cast<int>(index)));
@@ -147,7 +158,7 @@ void SQLiteStatement::getColumnSingle(size_t index, std::string & value)
 	value = reinterpret_cast<const char *>(value_raw);
 }
 
-SQLiteInstancePtr SQLiteInstance::open(const std::string & db_path, bool allow_write)
+SQLiteInstancePtr SQLiteInstance::open(const boost::filesystem::path & db_path, bool allow_write)
 {
 	int flags = allow_write ? (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) : SQLITE_OPEN_READONLY;
 

+ 11 - 1
lobby/SQLiteConnection.h

@@ -29,6 +29,14 @@ public:
 
 	~SQLiteStatement();
 
+	template<typename... Args>
+	void executeOnce(const Args &... args)
+	{
+		setBinds(args...);
+		execute();
+		reset();
+	}
+
 	template<typename... Args>
 	void setBinds(const Args &... args)
 	{
@@ -43,6 +51,7 @@ public:
 
 private:
 	void setBindSingle(size_t index, const double & value);
+	void setBindSingle(size_t index, const bool & value);
 	void setBindSingle(size_t index, const uint8_t & value);
 	void setBindSingle(size_t index, const uint16_t & value);
 	void setBindSingle(size_t index, const uint32_t & value);
@@ -52,6 +61,7 @@ private:
 	void setBindSingle(size_t index, const char * value);
 
 	void getColumnSingle(size_t index, double & value);
+	void getColumnSingle(size_t index, bool & value);
 	void getColumnSingle(size_t index, uint8_t & value);
 	void getColumnSingle(size_t index, uint16_t & value);
 	void getColumnSingle(size_t index, uint32_t & value);
@@ -92,7 +102,7 @@ class SQLiteInstance : boost::noncopyable
 public:
 	friend class SQLiteStatement;
 
-	static SQLiteInstancePtr open(const std::string & db_path, bool allow_write);
+	static SQLiteInstancePtr open(const boost::filesystem::path & db_path, bool allow_write);
 
 	~SQLiteInstance();