Переглянути джерело

Implemented basic version of login window and persistent connection on
client

Ivan Savenko 1 рік тому
батько
коміт
55b504792e

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

@@ -71,6 +71,11 @@
 	"vcmi.lobby.noPreview" : "no preview",
 	"vcmi.lobby.noUnderground" : "no underground",
 	"vcmi.lobby.sortDate" : "Sorts maps by change date",
+	
+	"vcmi.lobby.login.title" : "VCMI Lobby",
+	"vcmi.lobby.login.username" : "Username:",
+	"vcmi.lobby.login.connecting" : "Connecting...",
+//	"vcmi.lobby.login.connectionFailed" : "Connection failed: %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.",

+ 2 - 0
client/CMakeLists.txt

@@ -96,6 +96,7 @@ set(client_SRCS
 	renderSDL/SDL_Extensions.cpp
 
 	globalLobby/GlobalLobbyClient.cpp
+	globalLobby/GlobalLobbyLoginWindow.cpp
 	globalLobby/GlobalLobbyWidget.cpp
 	globalLobby/GlobalLobbyWindow.cpp
 
@@ -275,6 +276,7 @@ set(client_HEADERS
 	renderSDL/SDL_PixelAccess.h
 
 	globalLobby/GlobalLobbyClient.h
+	globalLobby/GlobalLobbyLoginWindow.h
 	globalLobby/GlobalLobbyWidget.h
 	globalLobby/GlobalLobbyWindow.h
 

+ 7 - 0
client/CServerHandler.cpp

@@ -16,6 +16,7 @@
 #include "gui/CGuiHandler.h"
 #include "gui/WindowHandler.h"
 
+#include "globalLobby/GlobalLobbyClient.h"
 #include "lobby/CSelectionBase.h"
 #include "lobby/CLobbyScreen.h"
 #include "windows/InfoWindows.h"
@@ -139,6 +140,7 @@ CServerHandler::CServerHandler()
 	: state(EClientState::NONE)
 	, networkClient(std::make_unique<NetworkClient>(*this))
 	, applier(std::make_unique<CApplier<CBaseForLobbyApply>>())
+	, lobbyClient(std::make_unique<GlobalLobbyClient>())
 	, client(nullptr)
 	, loadMode(0)
 	, campaignStateToSend(nullptr)
@@ -175,6 +177,11 @@ void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std::
 		myNames.push_back(settings["general"]["playerName"].String());
 }
 
+GlobalLobbyClient & CServerHandler::getGlobalLobby()
+{
+	return *lobbyClient;
+}
+
 void CServerHandler::startLocalServerAndConnect()
 {
 	if(threadRunLocalServer)

+ 4 - 0
client/CServerHandler.h

@@ -35,6 +35,7 @@ VCMI_LIB_NAMESPACE_END
 
 class CClient;
 class CBaseForLobbyApply;
+class GlobalLobbyClient;
 
 class HighScoreCalculation;
 class HighScoreParameter;
@@ -86,6 +87,7 @@ class CServerHandler : public IServerAPI, public LobbyInfo, public INetworkClien
 	friend class ApplyOnLobbyHandlerNetPackVisitor;
 
 	std::unique_ptr<NetworkClient> networkClient;
+	std::unique_ptr<GlobalLobbyClient> lobbyClient;
 	std::unique_ptr<CApplier<CBaseForLobbyApply>> applier;
 	std::shared_ptr<CMapInfo> mapToStart;
 	std::vector<std::string> myNames;
@@ -138,6 +140,8 @@ public:
 	void startLocalServerAndConnect();
 	void connectToServer(const std::string & addr, const ui16 port);
 
+	GlobalLobbyClient & getGlobalLobby();
+
 	// Helpers for lobby state access
 	std::set<PlayerColor> getHumanColors();
 	PlayerColor myFirstColor() const;

+ 64 - 14
client/globalLobby/GlobalLobbyClient.cpp

@@ -12,6 +12,7 @@
 #include "GlobalLobbyClient.h"
 
 #include "GlobalLobbyWindow.h"
+#include "GlobalLobbyLoginWindow.h"
 
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
@@ -21,12 +22,19 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/network/NetworkClient.h"
 
-GlobalLobbyClient::~GlobalLobbyClient() = default;
+GlobalLobbyClient::~GlobalLobbyClient()
+{
+	networkClient->stop();
+	networkThread->join();
+}
 
-GlobalLobbyClient::GlobalLobbyClient(GlobalLobbyWindow * window)
+GlobalLobbyClient::GlobalLobbyClient()
 	: networkClient(std::make_unique<NetworkClient>(*this))
-	, window(window)
-{}
+{
+	networkThread = std::make_unique<boost::thread>([this](){
+		networkClient->run();
+	});
+}
 
 static std::string getCurrentTimeFormatted(int timeOffsetSeconds = 0)
 {
@@ -47,10 +55,22 @@ static std::string getCurrentTimeFormatted(int timeOffsetSeconds = 0)
 
 void GlobalLobbyClient::onPacketReceived(const std::shared_ptr<NetworkConnection> &, const std::vector<uint8_t> & message)
 {
+	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+
 	// FIXME: find better approach
 	const char * payloadBegin = reinterpret_cast<const char*>(message.data());
 	JsonNode json(payloadBegin, message.size());
 
+	if (json["type"].String() == "authentication")
+	{
+		auto loginWindowPtr = loginWindow.lock();
+
+		if (!loginWindowPtr || !GH.windows().topWindow<GlobalLobbyLoginWindow>())
+			throw std::runtime_error("lobby connection finished without active login window!");
+
+		loginWindowPtr->onConnectionSuccess();
+	}
+
 	if (json["type"].String() == "chatHistory")
 	{
 		for (auto const & entry : json["messages"].Vector())
@@ -59,7 +79,10 @@ void GlobalLobbyClient::onPacketReceived(const std::shared_ptr<NetworkConnection
 			std::string messageText = entry["messageText"].String();
 			int ageSeconds = entry["ageSeconds"].Integer();
 			std::string timeFormatted = getCurrentTimeFormatted(-ageSeconds);
-			window->onGameChatMessage(senderName, messageText, timeFormatted);
+
+			auto lobbyWindowPtr = lobbyWindow.lock();
+			if(lobbyWindowPtr)
+				lobbyWindowPtr->onGameChatMessage(senderName, messageText, timeFormatted);
 		}
 	}
 
@@ -68,7 +91,9 @@ void GlobalLobbyClient::onPacketReceived(const std::shared_ptr<NetworkConnection
 		std::string senderName = json["senderName"].String();
 		std::string messageText = json["messageText"].String();
 		std::string timeFormatted = getCurrentTimeFormatted();
-		window->onGameChatMessage(senderName, messageText, timeFormatted);
+		auto lobbyWindowPtr = lobbyWindow.lock();
+		if(lobbyWindowPtr)
+			lobbyWindowPtr->onGameChatMessage(senderName, messageText, timeFormatted);
 	}
 }
 
@@ -83,8 +108,13 @@ void GlobalLobbyClient::onConnectionEstablished(const std::shared_ptr<NetworkCon
 
 void GlobalLobbyClient::onConnectionFailed(const std::string & errorMessage)
 {
-	GH.windows().popWindows(1);
-	CInfoWindow::showInfoDialog("Failed to connect to game lobby!\n" + errorMessage, {});
+	auto loginWindowPtr = loginWindow.lock();
+
+	if (!loginWindowPtr || !GH.windows().topWindow<GlobalLobbyLoginWindow>())
+		throw std::runtime_error("lobby connection failed without active login window!");
+
+	logGlobal->warn("Connection to game lobby failed! Reason: %s", errorMessage);
+	loginWindowPtr->onConnectionFailed(errorMessage);
 }
 
 void GlobalLobbyClient::onDisconnected(const std::shared_ptr<NetworkConnection> &)
@@ -111,17 +141,37 @@ void GlobalLobbyClient::sendMessage(const JsonNode & data)
 	networkClient->sendPacket(payloadBuffer);
 }
 
-void GlobalLobbyClient::start(const std::string & host, uint16_t port)
+void GlobalLobbyClient::connect()
+{
+	networkClient->start("127.0.0.1", 30303);
+}
+
+bool GlobalLobbyClient::isConnected()
 {
-	networkClient->start(host, port);
+	return networkClient->isConnected();
 }
 
-void GlobalLobbyClient::run()
+std::shared_ptr<GlobalLobbyLoginWindow> GlobalLobbyClient::createLoginWindow()
 {
-	networkClient->run();
+	auto loginWindowPtr = loginWindow.lock();
+	if (loginWindowPtr)
+		return loginWindowPtr;
+
+	auto loginWindowNew = std::make_shared<GlobalLobbyLoginWindow>();
+	loginWindow = loginWindowNew;
+
+	return loginWindowNew;
 }
 
-void GlobalLobbyClient::poll()
+std::shared_ptr<GlobalLobbyWindow> GlobalLobbyClient::createLobbyWindow()
 {
-	networkClient->poll();
+	auto lobbyWindowPtr = lobbyWindow.lock();
+	if (lobbyWindowPtr)
+		return lobbyWindowPtr;
+
+	lobbyWindowPtr = std::make_shared<GlobalLobbyWindow>();
+	lobbyWindow = lobbyWindowPtr;
+	lobbyWindowLock = lobbyWindowPtr;
+	return lobbyWindowPtr;
 }
+

+ 12 - 7
client/globalLobby/GlobalLobbyClient.h

@@ -15,12 +15,17 @@ VCMI_LIB_NAMESPACE_BEGIN
 class JsonNode;
 VCMI_LIB_NAMESPACE_END
 
+class GlobalLobbyLoginWindow;
 class GlobalLobbyWindow;
 
-class GlobalLobbyClient : public INetworkClientListener
+class GlobalLobbyClient : public INetworkClientListener, boost::noncopyable
 {
+	std::unique_ptr<boost::thread> networkThread;
 	std::unique_ptr<NetworkClient> networkClient;
-	GlobalLobbyWindow * window;
+
+	std::weak_ptr<GlobalLobbyLoginWindow> loginWindow;
+	std::weak_ptr<GlobalLobbyWindow> lobbyWindow;
+	std::shared_ptr<GlobalLobbyWindow> lobbyWindowLock; // helper strong reference to prevent window destruction on closing
 
 	void onPacketReceived(const std::shared_ptr<NetworkConnection> &, const std::vector<uint8_t> & message) override;
 	void onConnectionFailed(const std::string & errorMessage) override;
@@ -29,12 +34,12 @@ class GlobalLobbyClient : public INetworkClientListener
 	void onTimer() override;
 
 public:
-	explicit GlobalLobbyClient(GlobalLobbyWindow * window);
+	explicit GlobalLobbyClient();
 	~GlobalLobbyClient();
 
 	void sendMessage(const JsonNode & data);
-	void start(const std::string & host, uint16_t port);
-	void run();
-	void poll();
-
+	void connect();
+	bool isConnected();
+	std::shared_ptr<GlobalLobbyLoginWindow> createLoginWindow();
+	std::shared_ptr<GlobalLobbyWindow> createLobbyWindow();
 };

+ 76 - 0
client/globalLobby/GlobalLobbyLoginWindow.cpp

@@ -0,0 +1,76 @@
+/*
+ * GlobalLobbyLoginWindow.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 "GlobalLobbyLoginWindow.h"
+
+#include "GlobalLobbyClient.h"
+#include "GlobalLobbyWindow.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../widgets/TextControls.h"
+#include "../widgets/Images.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/MiscWidgets.h"
+#include "../CGameInfo.h"
+#include "../CServerHandler.h"
+
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/MetaString.h"
+
+GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
+	: CWindowObject(BORDERED)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	pos.w = 200;
+	pos.h = 200;
+
+	background = 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, CGI->generaltexth->translate("vcmi.lobby.login.title"));
+	labelUsername = std::make_shared<CLabel>( 10, 45, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username"));
+	backgroundUsername = std::make_shared<TransparentFilledRectangle>(Rect(10, 70, 180, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
+	inputUsername = std::make_shared<CTextInput>(Rect(15, 73, 176, 16), FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true);
+	buttonLogin = std::make_shared<CButton>(Point(10, 160), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); });
+	buttonClose = std::make_shared<CButton>(Point(126, 160), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
+	labelStatus = std::make_shared<CTextBox>( "", Rect(15, 95, 175, 60), 1, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE);
+
+	background->playerColored(PlayerColor(1));
+
+	center();
+}
+
+void GlobalLobbyLoginWindow::onClose()
+{
+	close();
+	// TODO: abort ongoing connection attempt, if any
+}
+
+void GlobalLobbyLoginWindow::onLogin()
+{
+	labelStatus->setText(CGI->generaltexth->translate("vcmi.lobby.login.connecting"));
+	CSH->getGlobalLobby().connect();
+}
+
+void GlobalLobbyLoginWindow::onConnectionSuccess()
+{
+	close();
+	GH.windows().pushWindow(CSH->getGlobalLobby().createLobbyWindow());
+}
+
+void GlobalLobbyLoginWindow::onConnectionFailed(const std::string & reason)
+{
+	MetaString formatter;
+	formatter.appendTextID("vcmi.lobby.login.error");
+	formatter.replaceRawString(reason);
+
+	labelStatus->setText(formatter.toString());
+}

+ 41 - 0
client/globalLobby/GlobalLobbyLoginWindow.h

@@ -0,0 +1,41 @@
+/*
+ * GlobalLobbyLoginWindow.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../windows/CWindowObject.h"
+
+class CLabel;
+class CTextBox;
+class CTextInput;
+class FilledTexturePlayerColored;
+class TransparentFilledRectangle;
+class CButton;
+
+class GlobalLobbyLoginWindow : public CWindowObject
+{
+	std::shared_ptr<FilledTexturePlayerColored> background;
+	std::shared_ptr<CLabel> labelTitle;
+	std::shared_ptr<CLabel> labelUsername;
+	std::shared_ptr<CTextBox> labelStatus;
+	std::shared_ptr<TransparentFilledRectangle> backgroundUsername;
+	std::shared_ptr<CTextInput> inputUsername;
+
+	std::shared_ptr<CButton> buttonLogin;
+	std::shared_ptr<CButton> buttonClose;
+
+	void onClose();
+	void onLogin();
+
+public:
+	GlobalLobbyLoginWindow();
+
+	void onConnectionSuccess();
+	void onConnectionFailed(const std::string & reason);
+};

+ 1 - 0
client/globalLobby/GlobalLobbyWidget.cpp

@@ -21,6 +21,7 @@ GlobalLobbyWidget::GlobalLobbyWidget(GlobalLobbyWindow * window)
 {
 	addCallback("closeWindow", [](int) { GH.windows().popWindows(1); });
 	addCallback("sendMessage", [this](int) { this->window->doSendChatMessage(); });
+	addCallback("createGameRoom", [this](int) { this->window->doCreateGameRoom(); });
 
 	const JsonNode config(JsonPath::builtin("config/widgets/lobbyWindow.json"));
 	build(config);

+ 13 - 10
client/globalLobby/GlobalLobbyWindow.cpp

@@ -16,6 +16,7 @@
 
 #include "../gui/CGuiHandler.h"
 #include "../widgets/TextControls.h"
+#include "../CServerHandler.h"
 
 #include "../../lib/MetaString.h"
 #include "../../lib/CConfigHandler.h"
@@ -27,17 +28,8 @@ GlobalLobbyWindow::GlobalLobbyWindow():
 	widget = std::make_shared<GlobalLobbyWidget>(this);
 	pos = widget->pos;
 	center();
-	connection = std::make_shared<GlobalLobbyClient>(this);
 
-	connection->start("127.0.0.1", 30303);
 	widget->getAccountNameLabel()->setText(settings["general"]["playerName"].String());
-
-	addUsedEvents(TIME);
-}
-
-void GlobalLobbyWindow::tick(uint32_t msPassed)
-{
-	connection->poll();
 }
 
 void GlobalLobbyWindow::doSendChatMessage()
@@ -48,11 +40,22 @@ void GlobalLobbyWindow::doSendChatMessage()
 	toSend["type"].String() = "sendChatMessage";
 	toSend["messageText"].String() = messageText;
 
-	connection->sendMessage(toSend);
+	CSH->getGlobalLobby().sendMessage(toSend);
 
 	widget->getMessageInput()->setText("");
 }
 
+void GlobalLobbyWindow::doCreateGameRoom()
+{
+	// TODO:
+	// start local server and supply our UUID / client credentials to it
+	// server logs into lobby ( uuid = client, mode = server ). This creates 'room' in mode 'empty'
+	// server starts accepting connections from players (including host)
+	// client connects to local server
+	// client sends createGameRoom query to lobby with own / server UUID and mode 'direct' (non-proxy)
+	// client requests to change room status to private or public
+}
+
 void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when)
 {
 	MetaString chatMessageFormatted;

+ 2 - 4
client/globalLobby/GlobalLobbyWindow.h

@@ -12,21 +12,19 @@
 #include "../windows/CWindowObject.h"
 
 class GlobalLobbyWidget;
-class GlobalLobbyClient;
 
 class GlobalLobbyWindow : public CWindowObject
 {
 	std::string chatHistory;
 
 	std::shared_ptr<GlobalLobbyWidget> widget;
-	std::shared_ptr<GlobalLobbyClient> connection;
-
-	void tick(uint32_t msPassed);
 
 public:
 	GlobalLobbyWindow();
 
 	void doSendChatMessage();
+	void doCreateGameRoom();
 
 	void onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when);
+
 };

+ 1 - 1
client/lobby/CSelectionBase.cpp

@@ -335,7 +335,7 @@ CChatBox::CChatBox(const Rect & rect)
 	Rect textInputArea(1, rect.h - height, rect.w - 1, height);
 	Rect chatHistoryArea(3, 1, rect.w - 3, rect.h - height - 1);
 	inputBackground = std::make_shared<TransparentFilledRectangle>(textInputArea, ColorRGBA(0,0,0,192));
-	inputBox = std::make_shared<CTextInput>(textInputArea, EFonts::FONT_SMALL, 0);
+	inputBox = std::make_shared<CTextInput>(textInputArea, EFonts::FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true);
 	inputBox->removeUsedEvents(KEYBOARD);
 	chatHistory = std::make_shared<CTextBox>("", chatHistoryArea, 1);
 

+ 1 - 1
client/lobby/OptionsTab.cpp

@@ -892,7 +892,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con
 		labelPlayerName = std::make_shared<CLabel>(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, name, 95);
 	else
 	{
-		labelPlayerNameEdit = std::make_shared<CTextInput>(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, nullptr, false);
+		labelPlayerNameEdit = std::make_shared<CTextInput>(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, nullptr, ETextAlignment::CENTER, false);
 		labelPlayerNameEdit->setText(name);
 	}
 	labelWhoCanPlay = std::make_shared<CMultiLineLabel>(Rect(6, 23, 45, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]);

+ 2 - 2
client/mainmenu/CHighScoreScreen.cpp

@@ -372,7 +372,7 @@ CHighScoreInput::CHighScoreInput(std::string playerName, std::function<void(std:
 	buttonOk = std::make_shared<CButton>(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT);
 	buttonCancel = std::make_shared<CButton>(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL);
 	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));
-	textInput = std::make_shared<CTextInput>(Rect(18, 104, 200, 25), FONT_SMALL, 0);
+	textInput = std::make_shared<CTextInput>(Rect(18, 104, 200, 25), FONT_SMALL, nullptr, ETextAlignment::CENTER, true);
 	textInput->setText(playerName);
 }
 
@@ -384,4 +384,4 @@ void CHighScoreInput::okay()
 void CHighScoreInput::abort()
 {
 	ready("");
-}
+}

+ 6 - 1
client/mainmenu/CMainMenu.cpp

@@ -24,6 +24,8 @@
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
 #include "../render/Canvas.h"
+#include "../globalLobby/GlobalLobbyLoginWindow.h"
+#include "../globalLobby/GlobalLobbyClient.h"
 #include "../globalLobby/GlobalLobbyWindow.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/Buttons.h"
@@ -467,7 +469,10 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType)
 void CMultiMode::openLobby()
 {
 	close();
-	GH.windows().createAndPushWindow<GlobalLobbyWindow>();
+	if (CSH->getGlobalLobby().isConnected())
+		GH.windows().pushWindow(CSH->getGlobalLobby().createLobbyWindow());
+	else
+		GH.windows().pushWindow(CSH->getGlobalLobby().createLoginWindow());
 }
 
 void CMultiMode::hostTCP()

+ 2 - 2
client/widgets/TextControls.cpp

@@ -551,8 +551,8 @@ Point CGStatusBar::getBorderSize()
 	return Point();
 }
 
-CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList<void(const std::string &)> & CB, bool giveFocusToInput)
-	: CLabel(Pos.x, Pos.y, font, ETextAlignment::CENTER),
+CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList<void(const std::string &)> & CB, ETextAlignment alignment, bool giveFocusToInput)
+	: CLabel(Pos.x, Pos.y, font, alignment),
 	cb(CB),
 	CFocusable(std::make_shared<CKeyboardFocusListener>(this))
 {

+ 1 - 1
client/widgets/TextControls.h

@@ -228,7 +228,7 @@ public:
 	void setText(const std::string & nText, bool callCb);
 	void setHelpText(const std::string &);
 
-	CTextInput(const Rect & Pos, EFonts font, const CFunctionList<void(const std::string &)> & CB, bool giveFocusToInput = true);
+	CTextInput(const Rect & Pos, EFonts font, const CFunctionList<void(const std::string &)> & CB, ETextAlignment alignment, bool giveFocusToInput);
 	CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName, const CFunctionList<void(const std::string &)> & CB);
 	CTextInput(const Rect & Pos, std::shared_ptr<IImage> srf);
 

+ 1 - 1
client/windows/CSpellWindow.cpp

@@ -136,7 +136,7 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 		searchBoxRectangle = std::make_shared<TransparentFilledRectangle>(r.resize(1), rectangleColor, borderColor);
 		searchBoxDescription = std::make_shared<CLabel>(r.center().x, r.center().y, FONT_SMALL, ETextAlignment::CENTER, grayedColor, CGI->generaltexth->translate("vcmi.spellBook.search"));
 
-		searchBox = std::make_shared<CTextInput>(r, FONT_SMALL, std::bind(&CSpellWindow::searchInput, this));
+		searchBox = std::make_shared<CTextInput>(r, FONT_SMALL, std::bind(&CSpellWindow::searchInput, this), ETextAlignment::CENTER, true);
 	}
 
 	processSpells();

+ 2 - 2
client/windows/GUIClasses.cpp

@@ -324,8 +324,8 @@ CSplitWindow::CSplitWindow(const CCreature * creature, std::function<void(int, i
 
 	int sliderPosition = total - leftMin - rightMin;
 
-	leftInput = std::make_shared<CTextInput>(Rect(20, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, true));
-	rightInput = std::make_shared<CTextInput>(Rect(176, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, false));
+	leftInput = std::make_shared<CTextInput>(Rect(20, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, true), ETextAlignment::CENTER, true);
+	rightInput = std::make_shared<CTextInput>(Rect(176, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, false), ETextAlignment::CENTER, true);
 
 	//add filters to allow only number input
 	leftInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, leftMin, leftMax);

+ 2 - 0
config/widgets/lobbyWindow.json

@@ -158,6 +158,7 @@
 			"position": {"x": 10, "y": 520},
 			"image": "settingsWindow/button190",
 			"help": "core.help.288",
+			"callback": "createGameRoom",
 			"items":
 			[
 				{
@@ -175,6 +176,7 @@
 			"position": {"x": 10, "y": 555},
 			"image": "settingsWindow/button190",
 			"help": "core.help.288",
+			"callback": "createGameRoom",
 			"items":
 			[
 				{

+ 9 - 0
lib/network/NetworkClient.cpp

@@ -22,6 +22,9 @@ NetworkClient::NetworkClient(INetworkClientListener & listener)
 
 void NetworkClient::start(const std::string & host, uint16_t port)
 {
+	if (isConnected())
+		throw std::runtime_error("Attempting to connect while already connected!");
+
 	boost::asio::ip::tcp::resolver resolver(*io);
 	auto endpoints = resolver.resolve(host, std::to_string(port));
 
@@ -58,6 +61,11 @@ void NetworkClient::stop()
 	io->stop();
 }
 
+bool NetworkClient::isConnected() const
+{
+	return connection != nullptr;
+}
+
 void NetworkClient::setTimer(std::chrono::milliseconds duration)
 {
 	auto timer = std::make_shared<NetworkTimer>(*io, duration);
@@ -74,6 +82,7 @@ void NetworkClient::sendPacket(const std::vector<uint8_t> & message)
 
 void NetworkClient::onDisconnected(const std::shared_ptr<NetworkConnection> & connection)
 {
+	this->connection.reset();
 	listener.onDisconnected(connection);
 }
 

+ 2 - 0
lib/network/NetworkClient.h

@@ -33,6 +33,8 @@ public:
 	NetworkClient(INetworkClientListener & listener);
 	virtual ~NetworkClient() = default;
 
+	bool isConnected() const;
+
 	void setTimer(std::chrono::milliseconds duration);
 	void sendPacket(const std::vector<uint8_t> & message);
 

+ 20 - 11
lobby/LobbyServer.cpp

@@ -173,23 +173,32 @@ void LobbyServer::receiveAuthentication(const std::shared_ptr<NetworkConnection>
 
 	activeAccounts[connection].accountName = accountName;
 
-	auto history = database->getRecentMessageHistory();
+	{
+		JsonNode reply;
+		reply["type"].String() = "authentication";
 
-	JsonNode reply;
-	reply["type"].String() = "chatHistory";
+		sendMessage(connection, reply);
+	}
+
+	auto history = database->getRecentMessageHistory();
 
-	for (auto const & message : boost::adaptors::reverse(history))
 	{
-		JsonNode jsonEntry;
+		JsonNode reply;
+		reply["type"].String() = "chatHistory";
 
-		jsonEntry["messageText"].String() = message.messageText;
-		jsonEntry["senderName"].String() = message.sender;
-		jsonEntry["ageSeconds"].Integer() = message.messageAgeSeconds;
+		for (auto const & message : boost::adaptors::reverse(history))
+		{
+			JsonNode jsonEntry;
 
-		reply["messages"].Vector().push_back(jsonEntry);
-	}
+			jsonEntry["messageText"].String() = message.messageText;
+			jsonEntry["senderName"].String() = message.sender;
+			jsonEntry["ageSeconds"].Integer() = message.messageAgeSeconds;
 
-	sendMessage(connection, reply);
+			reply["messages"].Vector().push_back(jsonEntry);
+		}
+
+		sendMessage(connection, reply);
+	}
 }
 
 void LobbyServer::receiveJoinGameRoom(const std::shared_ptr<NetworkConnection> & connection, const JsonNode & json)