2
0
Эх сурвалжийг харах

Merge pull request #306 from vcmi/feature/debugOptions

Debug features: auto tests, headless and spectator modes
ArseniyShestakov 8 жил өмнө
parent
commit
1f9c154ec2

+ 5 - 0
ChangeLog

@@ -1,3 +1,8 @@
+0.99 -> 1.00
+
+GENERAL:
+* Spectator mode was implemented through command-line options
+
 0.98 -> 0.99
 0.98 -> 0.99
 
 
 GENERAL:
 GENERAL:

+ 4 - 0
Global.h

@@ -143,6 +143,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #include <unordered_map>
 #include <unordered_map>
 #include <utility>
 #include <utility>
 #include <vector>
 #include <vector>
+#include <atomic>
 
 
 //The only available version is 3, as of Boost 1.50
 //The only available version is 3, as of Boost 1.50
 #include <boost/version.hpp>
 #include <boost/version.hpp>
@@ -184,6 +185,9 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #include <boost/variant.hpp>
 #include <boost/variant.hpp>
 #include <boost/math/special_functions/round.hpp>
 #include <boost/math/special_functions/round.hpp>
 #include <boost/multi_array.hpp>
 #include <boost/multi_array.hpp>
+#include <boost/uuid/uuid.hpp>
+#include <boost/uuid/uuid_io.hpp>
+#include <boost/uuid/uuid_generators.hpp>
 
 
 #ifndef M_PI
 #ifndef M_PI
 #  define M_PI 3.14159265358979323846
 #  define M_PI 3.14159265358979323846

+ 92 - 40
client/CMT.cpp

@@ -91,7 +91,6 @@ SDL_Surface *screen = nullptr, //main screen surface
 std::queue<SDL_Event> events;
 std::queue<SDL_Event> events;
 boost::mutex eventsM;
 boost::mutex eventsM;
 
 
-bool gNoGUI = false;
 CondSh<bool> serverAlive(false);
 CondSh<bool> serverAlive(false);
 static po::variables_map vm;
 static po::variables_map vm;
 
 
@@ -114,6 +113,29 @@ void endGame();
 #include <getopt.h>
 #include <getopt.h>
 #endif
 #endif
 
 
+void startTestMap(const std::string &mapname)
+{
+	StartInfo si;
+	si.mapname = mapname;
+	si.mode = StartInfo::NEW_GAME;
+	for (int i = 0; i < 8; i++)
+	{
+		PlayerSettings &pset = si.playerInfos[PlayerColor(i)];
+		pset.color = PlayerColor(i);
+		pset.name = CGI->generaltexth->allTexts[468];//Computer
+		pset.playerID = PlayerSettings::PLAYER_AI;
+		pset.compOnly = true;
+		pset.castle = 0;
+		pset.hero = -1;
+		pset.heroPortrait = -1;
+		pset.handicap = PlayerSettings::NO_HANDICAP;
+	}
+
+	while(GH.topInt())
+		GH.popIntTotally(GH.topInt());
+	startGame(&si);
+}
+
 void startGameFromFile(const bfs::path &fname)
 void startGameFromFile(const bfs::path &fname)
 {
 {
 	StartInfo si;
 	StartInfo si;
@@ -150,7 +172,7 @@ void init()
 	logGlobal->infoStream()<<"Initializing VCMI_Lib: "<<tmh.getDiff();
 	logGlobal->infoStream()<<"Initializing VCMI_Lib: "<<tmh.getDiff();
 
 
 
 
-	if(!gNoGUI)
+	if(!settings["session"]["headless"].Bool())
 	{
 	{
 		pomtime.getDiff();
 		pomtime.getDiff();
 		CCS->curh = new CCursorHandler;
 		CCS->curh = new CCursorHandler;
@@ -238,10 +260,19 @@ int main(int argc, char** argv)
 	opts.add_options()
 	opts.add_options()
 		("help,h", "display help and exit")
 		("help,h", "display help and exit")
 		("version,v", "display version information and exit")
 		("version,v", "display version information and exit")
+		("disable-shm", "force disable shared memory usage")
+		("enable-shm-uuid", "use UUID for shared memory identifier")
 		("battle,b", po::value<std::string>(), "runs game in duel mode (battle-only")
 		("battle,b", po::value<std::string>(), "runs game in duel mode (battle-only")
 		("start", po::value<bfs::path>(), "starts game from saved StartInfo file")
 		("start", po::value<bfs::path>(), "starts game from saved StartInfo file")
+		("testmap", po::value<std::string>(), "")
+		("spectate,s", "enable spectator interface for AI-only games")
+		("spectate-ignore-hero", "wont follow heroes on adventure map")
+		("spectate-hero-speed", po::value<int>(), "hero movement speed on adventure map")
+		("spectate-battle-speed", po::value<int>(), "battle animation speed for spectator")
+		("spectate-skip-battle", "skip battles in spectator view")
+		("spectate-skip-battle-result", "skip battle result window")
 		("onlyAI", "runs without human player, all players will be default AI")
 		("onlyAI", "runs without human player, all players will be default AI")
-		("noGUI", "runs without GUI, implies --onlyAI")
+		("headless", "runs without GUI, implies --onlyAI")
 		("ai", po::value<std::vector<std::string>>(), "AI to be used for the player, can be specified several times for the consecutive players")
 		("ai", po::value<std::vector<std::string>>(), "AI to be used for the player, can be specified several times for the consecutive players")
 		("oneGoodAI", "puts one default AI and the rest will be EmptyAI")
 		("oneGoodAI", "puts one default AI and the rest will be EmptyAI")
 		("autoSkip", "automatically skip turns in GUI")
 		("autoSkip", "automatically skip turns in GUI")
@@ -281,11 +312,6 @@ int main(int argc, char** argv)
 		prog_version();
 		prog_version();
 		return 0;
 		return 0;
 	}
 	}
-	if(vm.count("noGUI"))
-	{
-		gNoGUI = true;
-		vm.insert(std::pair<std::string, po::variable_value>("onlyAI", po::variable_value()));
-	}
 	if(vm.count("donotstartserver"))
 	if(vm.count("donotstartserver"))
 	{
 	{
 		CServerHandler::DO_NOT_START_SERVER = true;
 		CServerHandler::DO_NOT_START_SERVER = true;
@@ -314,9 +340,18 @@ int main(int argc, char** argv)
 	// Init filesystem and settings
 	// Init filesystem and settings
 	preinitDLL(::console);
 	preinitDLL(::console);
 	settings.init();
 	settings.init();
+	Settings session = settings.write["session"];
+	session["onlyai"].Bool() = vm.count("onlyAI");
+	if(vm.count("headless"))
+	{
+		session["headless"].Bool() = true;
+		session["onlyai"].Bool() = true;
+	}
+	// Shared memory options
+	session["disable-shm"].Bool() = vm.count("disable-shm");
+	session["enable-shm-uuid"].Bool() = vm.count("enable-shm-uuid");
 
 
 	// Init special testing settings
 	// Init special testing settings
-	Settings session = settings.write["session"];
 	session["serverport"].Integer() = vm.count("serverport") ? vm["serverport"].as<si64>() : 0;
 	session["serverport"].Integer() = vm.count("serverport") ? vm["serverport"].as<si64>() : 0;
 	session["saveprefix"].String() = vm.count("saveprefix") ? vm["saveprefix"].as<std::string>() : "";
 	session["saveprefix"].String() = vm.count("saveprefix") ? vm["saveprefix"].as<std::string>() : "";
 	session["savefrequency"].Integer() = vm.count("savefrequency") ? vm["savefrequency"].as<si64>() : 1;
 	session["savefrequency"].Integer() = vm.count("savefrequency") ? vm["savefrequency"].as<si64>() : 1;
@@ -364,7 +399,7 @@ int main(int argc, char** argv)
 		exit(EXIT_FAILURE);
 		exit(EXIT_FAILURE);
 	}
 	}
 
 
-	if(!gNoGUI)
+	if(!settings["session"]["headless"].Bool())
 	{
 	{
 		if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_AUDIO|SDL_INIT_NOPARACHUTE))
 		if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_AUDIO|SDL_INIT_NOPARACHUTE))
 		{
 		{
@@ -427,7 +462,7 @@ int main(int argc, char** argv)
 #ifdef DISABLE_VIDEO
 #ifdef DISABLE_VIDEO
 	CCS->videoh = new CEmptyVideoPlayer;
 	CCS->videoh = new CEmptyVideoPlayer;
 #else
 #else
-	if (!gNoGUI && !vm.count("disable-video"))
+	if (!settings["session"]["headless"].Bool() && !vm.count("disable-video"))
 		CCS->videoh = new CVideoPlayer;
 		CCS->videoh = new CVideoPlayer;
 	else
 	else
 		CCS->videoh = new CEmptyVideoPlayer;
 		CCS->videoh = new CEmptyVideoPlayer;
@@ -456,7 +491,7 @@ int main(int argc, char** argv)
 	init();
 	init();
 #endif
 #endif
 
 
-	if(!gNoGUI )
+	if(!settings["session"]["headless"].Bool())
 	{
 	{
 		if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool())
 		if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool())
 			playIntro();
 			playIntro();
@@ -480,7 +515,6 @@ int main(int argc, char** argv)
 
 
 	if(!vm.count("battle"))
 	if(!vm.count("battle"))
 	{
 	{
-		Settings session = settings.write["session"];
 		session["autoSkip"].Bool()  = vm.count("autoSkip");
 		session["autoSkip"].Bool()  = vm.count("autoSkip");
 		session["oneGoodAI"].Bool() = vm.count("oneGoodAI");
 		session["oneGoodAI"].Bool() = vm.count("oneGoodAI");
 		session["aiSolo"].Bool() = false;
 		session["aiSolo"].Bool() = false;
@@ -488,17 +522,39 @@ int main(int argc, char** argv)
 		bfs::path fileToStartFrom; //none by default
 		bfs::path fileToStartFrom; //none by default
 		if(vm.count("start"))
 		if(vm.count("start"))
 			fileToStartFrom = vm["start"].as<bfs::path>();
 			fileToStartFrom = vm["start"].as<bfs::path>();
+		if(vm.count("testmap"))
+		{
+			session["testmap"].String() = vm["testmap"].as<std::string>();
+		}
 
 
-		if(!fileToStartFrom.empty() && bfs::exists(fileToStartFrom))
-			startGameFromFile(fileToStartFrom); //ommit pregame and start the game using settings from file
+		session["spectate"].Bool() = vm.count("spectate");
+		if(session["spectate"].Bool())
+		{
+			session["spectate-ignore-hero"].Bool() = vm.count("spectate-ignore-hero");
+			session["spectate-skip-battle"].Bool() = vm.count("spectate-skip-battle");
+			session["spectate-skip-battle-result"].Bool() = vm.count("spectate-skip-battle-result");
+			if(vm.count("spectate-hero-speed"))
+				session["spectate-hero-speed"].Integer() = vm["spectate-hero-speed"].as<int>();
+			if(vm.count("spectate-battle-speed"))
+				session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as<int>();
+		}
+		if(!session["testmap"].isNull())
+		{
+			startTestMap(session["testmap"].String());
+		}
 		else
 		else
 		{
 		{
-			if(!fileToStartFrom.empty())
+			if(!fileToStartFrom.empty() && bfs::exists(fileToStartFrom))
+				startGameFromFile(fileToStartFrom); //ommit pregame and start the game using settings from file
+			else
 			{
 			{
-				logGlobal->warnStream() << "Warning: cannot find given file to start from (" << fileToStartFrom
-					<< "). Falling back to main menu.";
+				if(!fileToStartFrom.empty())
+				{
+					logGlobal->warnStream() << "Warning: cannot find given file to start from (" << fileToStartFrom
+						<< "). Falling back to main menu.";
+				}
+				GH.curInt = CGPreGame::create(); //will set CGP pointer to itself
 			}
 			}
-			GH.curInt = CGPreGame::create(); //will set CGP pointer to itself
 		}
 		}
 	}
 	}
 	else
 	else
@@ -511,7 +567,7 @@ int main(int argc, char** argv)
 		startGame(si);
 		startGame(si);
 	}
 	}
 
 
-	if(!gNoGUI)
+	if(!settings["session"]["headless"].Bool())
 	{
 	{
 		mainLoop();
 		mainLoop();
 	}
 	}
@@ -554,6 +610,20 @@ void printInfoAboutIntObject(const CIntObject *obj, int level)
 		printInfoAboutIntObject(child, level+1);
 		printInfoAboutIntObject(child, level+1);
 }
 }
 
 
+void removeGUI()
+{
+	// CClient::endGame
+	GH.curInt = nullptr;
+	if(GH.topInt())
+		GH.topInt()->deactivate();
+	GH.listInt.clear();
+	GH.objsToBlit.clear();
+	GH.statusbar = nullptr;
+	logGlobal->infoStream() << "Removed GUI.";
+
+	LOCPLINT = nullptr;
+};
+
 void processCommand(const std::string &message)
 void processCommand(const std::string &message)
 {
 {
 	std::istringstream readed;
 	std::istringstream readed;
@@ -674,10 +744,6 @@ void processCommand(const std::string &message)
 		*ptr = 666;
 		*ptr = 666;
 		//disaster!
 		//disaster!
 	}
 	}
-	else if(cn == "onlyai")
-	{
-		vm.insert(std::pair<std::string, po::variable_value>("onlyAI", po::variable_value()));
-	}
 	else if(cn == "mp" && adventureInt)
 	else if(cn == "mp" && adventureInt)
 	{
 	{
 		if(const CGHeroInstance *h = dynamic_cast<const CGHeroInstance *>(adventureInt->selection))
 		if(const CGHeroInstance *h = dynamic_cast<const CGHeroInstance *>(adventureInt->selection))
@@ -813,20 +879,6 @@ void processCommand(const std::string &message)
 		}
 		}
 	}
 	}
 
 
-	auto removeGUI = [&]()
-	{
-		// CClient::endGame
-		GH.curInt = nullptr;
-		if(GH.topInt())
-			GH.topInt()->deactivate();
-		GH.listInt.clear();
-		GH.objsToBlit.clear();
-		GH.statusbar = nullptr;
-		logNetwork->infoStream() << "Removed GUI.";
-
-		LOCPLINT = nullptr;
-
-	};
 	auto giveTurn = [&](PlayerColor player)
 	auto giveTurn = [&](PlayerColor player)
 	{
 	{
 		YourTurn yt;
 		YourTurn yt;
@@ -1275,7 +1327,7 @@ void startGame(StartInfo * options, CConnection *serv/* = nullptr*/)
 		serverAlive.setn(true);
 		serverAlive.setn(true);
 	}
 	}
 
 
-	if(vm.count("onlyAI"))
+	if(settings["session"]["onlyai"].Bool())
 	{
 	{
 		auto ais = vm.count("ai") ? vm["ai"].as<std::vector<std::string>>() : std::vector<std::string>();
 		auto ais = vm.count("ai") ? vm["ai"].as<std::vector<std::string>>() : std::vector<std::string>();
 
 
@@ -1327,7 +1379,7 @@ void handleQuit(bool ask/* = true*/)
 		dispose();
 		dispose();
 		vstd::clear_pointer(console);
 		vstd::clear_pointer(console);
 		boost::this_thread::sleep(boost::posix_time::milliseconds(750));
 		boost::this_thread::sleep(boost::posix_time::milliseconds(750));
-		if(!gNoGUI)
+		if(!settings["session"]["headless"].Bool())
 		{
 		{
 			cleanupRenderer();
 			cleanupRenderer();
 			SDL_Quit();
 			SDL_Quit();

+ 1 - 3
client/CMT.h

@@ -11,9 +11,7 @@ extern SDL_Surface *screen;      // main screen surface
 extern SDL_Surface *screen2;     // and hlp surface (used to store not-active interfaces layer)
 extern SDL_Surface *screen2;     // and hlp surface (used to store not-active interfaces layer)
 extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed
 extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed
 
 
-
-extern bool gNoGUI; //if true there is no client window and game is silently played between AIs
-
 extern CondSh<bool> serverAlive; //used to prevent game start from executing if server is already running
 extern CondSh<bool> serverAlive; //used to prevent game start from executing if server is already running
 
 
+void removeGUI();
 void handleQuit(bool ask = true);
 void handleQuit(bool ask = true);

+ 2 - 0
client/CMessage.cpp

@@ -296,6 +296,8 @@ void CMessage::drawIWindow(CInfoWindow * ret, std::string text, PlayerColor play
 
 
 void CMessage::drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x, int y)
 void CMessage::drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x, int y)
 {
 {
+	if(playerColor.isSpectator())
+		playerColor = PlayerColor(1);
 	std::vector<const IImage*> &box = piecesOfBox.at(playerColor.getNum());
 	std::vector<const IImage*> &box = piecesOfBox.at(playerColor.getNum());
 
 
 	// Note: this code assumes that the corner dimensions are all the same.
 	// Note: this code assumes that the corner dimensions are all the same.

+ 21 - 5
client/CPlayerInterface.cpp

@@ -245,6 +245,9 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details)
 	if (LOCPLINT != this)
 	if (LOCPLINT != this)
 		return;
 		return;
 
 
+	if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-ignore-hero"].Bool())
+		return;
+
 	const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero
 	const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero
 	int3 hp = details.start;
 	int3 hp = details.start;
 
 
@@ -321,7 +324,12 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details)
 	}
 	}
 
 
 	ui32 speed;
 	ui32 speed;
-	if (makingTurn) // our turn, our hero moves
+	if(settings["session"]["spectate"].Bool())
+	{
+		if(!settings["session"]["spectate-hero-speed"].isNull())
+			speed = settings["session"]["spectate-hero-speed"].Integer();
+	}
+	else if (makingTurn) // our turn, our hero moves
 		speed = settings["adventure"]["heroSpeed"].Float();
 		speed = settings["adventure"]["heroSpeed"].Float();
 	else
 	else
 		speed = settings["adventure"]["enemySpeed"].Float();
 		speed = settings["adventure"]["enemySpeed"].Float();
@@ -334,7 +342,6 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details)
 		return; // no animation
 		return; // no animation
 	}
 	}
 
 
-
 	adventureInt->centerOn(hero); //actualizing screen pos
 	adventureInt->centerOn(hero); //actualizing screen pos
 	adventureInt->minimap.redraw();
 	adventureInt->minimap.redraw();
 	adventureInt->heroList.redraw();
 	adventureInt->heroList.redraw();
@@ -471,6 +478,14 @@ int3 CPlayerInterface::repairScreenPos(int3 pos)
 		pos.y = CGI->mh->sizes.y - adventureInt->terrain.tilesh + CGI->mh->frameH;
 		pos.y = CGI->mh->sizes.y - adventureInt->terrain.tilesh + CGI->mh->frameH;
 	return pos;
 	return pos;
 }
 }
+
+void CPlayerInterface::activateForSpectator()
+{
+	adventureInt->state = CAdvMapInt::INGAME;
+	adventureInt->activate();
+	adventureInt->minimap.activate();
+}
+
 void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val)
 void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val)
 {
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	EVENT_HANDLER_CALLED_BY_CLIENT;
@@ -1637,7 +1652,8 @@ void CPlayerInterface::update()
 	}
 	}
 
 
 	//in some conditions we may receive calls before selection is initialized - we must ignore them
 	//in some conditions we may receive calls before selection is initialized - we must ignore them
-	if (adventureInt && !adventureInt->selection && GH.topInt() == adventureInt)
+	if(adventureInt && GH.topInt() == adventureInt
+		&& (!adventureInt->selection && !settings["session"]["spectate"].Bool()))
 	{
 	{
 		return;
 		return;
 	}
 	}
@@ -2131,7 +2147,7 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul
 
 
 		--howManyPeople;
 		--howManyPeople;
 
 
-		if (howManyPeople == 0) //all human players eliminated
+		if(howManyPeople == 0 && !settings["session"]["spectate"].Bool()) //all human players eliminated
 		{
 		{
 			if (adventureInt)
 			if (adventureInt)
 			{
 			{
@@ -2152,7 +2168,7 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul
 		}
 		}
 		else
 		else
 		{
 		{
-			if (howManyPeople == 0) //all human players eliminated
+			if(howManyPeople == 0 && !settings["session"]["spectate"].Bool()) //all human players eliminated
 			{
 			{
 				requestReturningToMainMenu();
 				requestReturningToMainMenu();
 			}
 			}

+ 1 - 0
client/CPlayerInterface.h

@@ -236,6 +236,7 @@ public:
 	void updateInfo(const CGObjectInstance * specific);
 	void updateInfo(const CGObjectInstance * specific);
 	void init(std::shared_ptr<CCallback> CB) override;
 	void init(std::shared_ptr<CCallback> CB) override;
 	int3 repairScreenPos(int3 pos); //returns position closest to pos we can center screen on
 	int3 repairScreenPos(int3 pos); //returns position closest to pos we can center screen on
+	void activateForSpectator(); // TODO: spectator probably need own player interface class
 
 
 	// show dialogs
 	// show dialogs
 	void showInfoDialog(const std::string &text, CComponent * component);
 	void showInfoDialog(const std::string &text, CComponent * component);

+ 105 - 51
client/Client.cpp

@@ -41,9 +41,7 @@
 #include "CMT.h"
 #include "CMT.h"
 
 
 extern std::string NAME;
 extern std::string NAME;
-#ifndef VCMI_ANDROID
-namespace intpr = boost::interprocess;
-#else
+#ifdef VCMI_ANDROID
 #include "lib/CAndroidVMHelper.h"
 #include "lib/CAndroidVMHelper.h"
 #endif
 #endif
 
 
@@ -432,10 +430,13 @@ void CClient::newGame( CConnection *con, StartInfo *si )
 	// Init map handler
 	// Init map handler
 	if(gs->map)
 	if(gs->map)
 	{
 	{
-		const_cast<CGameInfo*>(CGI)->mh = new CMapHandler();
-		CGI->mh->map = gs->map;
-		logNetwork->infoStream() << "Creating mapHandler: " << tmh.getDiff();
-		CGI->mh->init();
+		if(!settings["session"]["headless"].Bool())
+		{
+			const_cast<CGameInfo*>(CGI)->mh = new CMapHandler();
+			CGI->mh->map = gs->map;
+			logNetwork->infoStream() << "Creating mapHandler: " << tmh.getDiff();
+			CGI->mh->init();
+		}
 		pathInfo = make_unique<CPathsInfo>(getMapSize());
 		pathInfo = make_unique<CPathsInfo>(getMapSize());
 		logNetwork->infoStream() << "Initializing mapHandler (together): " << tmh.getDiff();
 		logNetwork->infoStream() << "Initializing mapHandler (together): " << tmh.getDiff();
 	}
 	}
@@ -472,7 +473,7 @@ void CClient::newGame( CConnection *con, StartInfo *si )
 
 
 	if(si->mode == StartInfo::DUEL)
 	if(si->mode == StartInfo::DUEL)
 	{
 	{
-		if(!gNoGUI)
+		if(!settings["session"]["headless"].Bool())
 		{
 		{
 			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
 			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
 			auto p = std::make_shared<CPlayerInterface>(PlayerColor::NEUTRAL);
 			auto p = std::make_shared<CPlayerInterface>(PlayerColor::NEUTRAL);
@@ -484,6 +485,10 @@ void CClient::newGame( CConnection *con, StartInfo *si )
 	}
 	}
 	else
 	else
 	{
 	{
+		if(settings["session"]["spectate"].Bool())
+		{
+			installNewPlayerInterface(std::make_shared<CPlayerInterface>(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true);
+		}
 		loadNeutralBattleAI();
 		loadNeutralBattleAI();
 	}
 	}
 
 
@@ -635,10 +640,33 @@ void CClient::serialize(BinaryDeserializer & h, const int version, const std::se
 			nInt->human = isHuman;
 			nInt->human = isHuman;
 			nInt->playerID = pid;
 			nInt->playerID = pid;
 
 
-			if(playerIDs.count(pid))
-				installNewPlayerInterface(nInt, pid);
-
 			nInt->loadGame(h, version);
 			nInt->loadGame(h, version);
+			if(settings["session"]["onlyai"].Bool() && isHuman)
+			{
+				removeGUI();
+				nInt.reset();
+				dllname = aiNameForPlayer(false);
+				nInt = CDynLibHandler::getNewAI(dllname);
+				nInt->dllName = dllname;
+				nInt->human = false;
+				nInt->playerID = pid;
+				installNewPlayerInterface(nInt, pid);
+				GH.totalRedraw();
+			}
+			else
+			{
+				if(playerIDs.count(pid))
+					installNewPlayerInterface(nInt, pid);
+			}
+		}
+		if(settings["session"]["spectate"].Bool())
+		{
+			removeGUI();
+			auto p = std::make_shared<CPlayerInterface>(PlayerColor::SPECTATOR);
+			installNewPlayerInterface(p, PlayerColor::SPECTATOR, true);
+			GH.curInt = p.get();
+			LOCPLINT->activateForSpectator();
+			GH.totalRedraw();
 		}
 		}
 
 
 		if(playerIDs.count(PlayerColor::NEUTRAL))
 		if(playerIDs.count(PlayerColor::NEUTRAL))
@@ -684,19 +712,22 @@ void CClient::stopConnection()
 {
 {
 	terminate = true;
 	terminate = true;
 
 
-	if (serv && serv->isHost()) //request closing connection
+	if(serv)
 	{
 	{
-		logNetwork->infoStream() << "Connection has been requested to be closed.";
 		boost::unique_lock<boost::mutex>(*serv->wmx);
 		boost::unique_lock<boost::mutex>(*serv->wmx);
-		CloseServer close_server;
-		sendRequest(&close_server, PlayerColor::NEUTRAL);
-		logNetwork->infoStream() << "Sent closing signal to the server";
-	}
-	else
-	{
-		LeaveGame leave_Game;
-		sendRequest(&leave_Game, PlayerColor::NEUTRAL);
-		logNetwork->infoStream() << "Sent leaving signal to the server";
+		if(serv->isHost()) //request closing connection
+		{
+			logNetwork->infoStream() << "Connection has been requested to be closed.";
+			CloseServer close_server;
+			sendRequest(&close_server, PlayerColor::NEUTRAL);
+			logNetwork->infoStream() << "Sent closing signal to the server";
+		}
+		else
+		{
+			LeaveGame leave_Game;
+			sendRequest(&leave_Game, PlayerColor::NEUTRAL);
+			logNetwork->infoStream() << "Sent leaving signal to the server";
+		}
 	}
 	}
 
 
 	if(connectionHandler)//end connection handler
 	if(connectionHandler)//end connection handler
@@ -744,14 +775,29 @@ void CClient::battleStarted(const BattleInfo * info)
 			def = std::dynamic_pointer_cast<CPlayerInterface>( playerint[rightSide.color] );
 			def = std::dynamic_pointer_cast<CPlayerInterface>( playerint[rightSide.color] );
 	}
 	}
 
 
-	if(!gNoGUI && (!!att || !!def || gs->scenarioOps->mode == StartInfo::DUEL))
+	if(!settings["session"]["headless"].Bool())
 	{
 	{
-		boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
-		auto bi = new CBattleInterface(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero,
-			Rect((screen->w - 800)/2,
-			     (screen->h - 600)/2, 800, 600), att, def);
+		if(!!att || !!def || gs->scenarioOps->mode == StartInfo::DUEL)
+		{
+			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
+			auto bi = new CBattleInterface(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero,
+				Rect((screen->w - 800)/2,
+					 (screen->h - 600)/2, 800, 600), att, def);
+
+			GH.pushInt(bi);
+		}
+		else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
+		{
+			//TODO: This certainly need improvement
+			auto spectratorInt = std::dynamic_pointer_cast<CPlayerInterface>(playerint[PlayerColor::SPECTATOR]);
+			spectratorInt->cb->setBattle(info);
+			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
+			auto bi = new CBattleInterface(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero,
+				Rect((screen->w - 800)/2,
+					 (screen->h - 600)/2, 800, 600), att, def, spectratorInt);
 
 
-		GH.pushInt(bi);
+			GH.pushInt(bi);
+		}
 	}
 	}
 
 
 	auto callBattleStart = [&](PlayerColor color, ui8 side){
 	auto callBattleStart = [&](PlayerColor color, ui8 side){
@@ -762,6 +808,8 @@ void CClient::battleStarted(const BattleInfo * info)
 	callBattleStart(leftSide.color, 0);
 	callBattleStart(leftSide.color, 0);
 	callBattleStart(rightSide.color, 1);
 	callBattleStart(rightSide.color, 1);
 	callBattleStart(PlayerColor::UNFLAGGABLE, 1);
 	callBattleStart(PlayerColor::UNFLAGGABLE, 1);
+	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
+		callBattleStart(PlayerColor::SPECTATOR, 1);
 
 
 	if(info->tacticDistance && vstd::contains(battleints,info->sides[info->tacticsSide].color))
 	if(info->tacticDistance && vstd::contains(battleints,info->sides[info->tacticsSide].color))
 	{
 	{
@@ -774,6 +822,9 @@ void CClient::battleFinished()
 	for(auto & side : gs->curB->sides)
 	for(auto & side : gs->curB->sides)
 		if(battleCallbacks.count(side.color))
 		if(battleCallbacks.count(side.color))
 			battleCallbacks[side.color]->setBattle(nullptr);
 			battleCallbacks[side.color]->setBattle(nullptr);
+
+	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
+		battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr);
 }
 }
 
 
 void CClient::loadNeutralBattleAI()
 void CClient::loadNeutralBattleAI()
@@ -871,7 +922,7 @@ void CClient::campaignMapFinished( std::shared_ptr<CCampaignState> camp )
 	}
 	}
 }
 }
 
 
-void CClient::installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, boost::optional<PlayerColor> color)
+void CClient::installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, boost::optional<PlayerColor> color, bool battlecb)
 {
 {
 	boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
 	boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
 	PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE);
 	PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE);
@@ -887,7 +938,7 @@ void CClient::installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInte
 	battleCallbacks[colorUsed] = cb;
 	battleCallbacks[colorUsed] = cb;
 	gameInterface->init(cb);
 	gameInterface->init(cb);
 
 
-	installNewBattleInterface(gameInterface, color, false);
+	installNewBattleInterface(gameInterface, color, battlecb);
 }
 }
 
 
 void CClient::installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, boost::optional<PlayerColor> color, bool needCallback /*= true*/)
 void CClient::installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, boost::optional<PlayerColor> color, bool needCallback /*= true*/)
@@ -918,6 +969,11 @@ std::string CClient::aiNameForPlayer(const PlayerSettings &ps, bool battleAI)
 			return ps.name;
 			return ps.name;
 	}
 	}
 
 
+	return aiNameForPlayer(battleAI);
+}
+
+std::string CClient::aiNameForPlayer(bool battleAI)
+{
 	const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I;
 	const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I;
 	std::string goodAI = battleAI ? settings["server"]["neutralAI"].String() : settings["server"]["playerAI"].String();
 	std::string goodAI = battleAI ? settings["server"]["neutralAI"].String() : settings["server"]["playerAI"].String();
 	std::string badAI = battleAI ? "StupidAI" : "EmptyAI";
 	std::string badAI = battleAI ? "StupidAI" : "EmptyAI";
@@ -961,13 +1017,7 @@ void CServerHandler::waitForServer()
 
 
 #ifndef VCMI_ANDROID
 #ifndef VCMI_ANDROID
 	if(shared)
 	if(shared)
-	{
-		intpr::scoped_lock<intpr::interprocess_mutex> slock(shared->sr->mutex);
-		while(!shared->sr->ready)
-		{
-			shared->sr->cond.wait(slock);
-		}
-	}
+		shared->sr->waitTillReady();
 #else
 #else
 	logNetwork->infoStream() << "waiting for server";
 	logNetwork->infoStream() << "waiting for server";
 	while (!androidTestServerReadyFlag.load())
 	while (!androidTestServerReadyFlag.load())
@@ -984,15 +1034,7 @@ void CServerHandler::waitForServer()
 
 
 CConnection * CServerHandler::connectToServer()
 CConnection * CServerHandler::connectToServer()
 {
 {
-#ifndef VCMI_ANDROID
-	if(shared)
-	{
-		if(!shared->sr->ready)
-			waitForServer();
-	}
-#else
 	waitForServer();
 	waitForServer();
-#endif
 
 
 	th.update(); //put breakpoint here to attach to server before it does something stupid
 	th.update(); //put breakpoint here to attach to server before it does something stupid
 
 
@@ -1026,15 +1068,21 @@ CServerHandler::CServerHandler(bool runServer /*= false*/)
 	serverThread = nullptr;
 	serverThread = nullptr;
 	shared = nullptr;
 	shared = nullptr;
 	verbose = true;
 	verbose = true;
+	uuid = boost::uuids::to_string(boost::uuids::random_generator()());
 
 
 #ifndef VCMI_ANDROID
 #ifndef VCMI_ANDROID
-	if(DO_NOT_START_SERVER)
+	if(DO_NOT_START_SERVER || settings["session"]["disable-shm"].Bool())
 		return;
 		return;
 
 
-	boost::interprocess::shared_memory_object::remove("vcmi_memory"); //if the application has previously crashed, the memory may not have been removed. to avoid problems - try to destroy it
+	std::string sharedMemoryName = "vcmi_memory";
+	if(settings["session"]["enable-shm-uuid"].Bool())
+	{
+		//used or automated testing when multiple clients start simultaneously
+		sharedMemoryName += "_" + uuid;
+	}
 	try
 	try
 	{
 	{
-		shared = new SharedMem();
+		shared = new SharedMemory(sharedMemoryName, true);
 	}
 	}
 	catch(...)
 	catch(...)
 	{
 	{
@@ -1057,11 +1105,17 @@ void CServerHandler::callServer()
 #ifndef VCMI_ANDROID
 #ifndef VCMI_ANDROID
 	setThreadName("CServerHandler::callServer");
 	setThreadName("CServerHandler::callServer");
 	const std::string logName = (VCMIDirs::get().userCachePath() / "server_log.txt").string();
 	const std::string logName = (VCMIDirs::get().userCachePath() / "server_log.txt").string();
-	const std::string comm = VCMIDirs::get().serverPath().string()
+	std::string comm = VCMIDirs::get().serverPath().string()
 		+ " --port=" + getDefaultPortStr()
 		+ " --port=" + getDefaultPortStr()
 		+ " --run-by-client"
 		+ " --run-by-client"
-		+ (shared ? " --use-shm" : "")
-		+ " > \"" + logName + '\"';
+		+ " --uuid=" + uuid;
+	if(shared)
+	{
+		comm += " --enable-shm";
+		if(settings["session"]["enable-shm-uuid"].Bool())
+			comm += " --enable-shm-uuid";
+	}
+	comm += " > \"" + logName + '\"';
 
 
 	int result = std::system(comm.c_str());
 	int result = std::system(comm.c_str());
 	if (result == 0)
 	if (result == 0)

+ 5 - 3
client/Client.h

@@ -28,7 +28,7 @@ class CGameInterface;
 class CConnection;
 class CConnection;
 class CCallback;
 class CCallback;
 struct BattleAction;
 struct BattleAction;
-struct SharedMem;
+struct SharedMemory;
 class CClient;
 class CClient;
 class CScriptingModule;
 class CScriptingModule;
 struct CPathsInfo;
 struct CPathsInfo;
@@ -46,7 +46,8 @@ public:
 
 
 	CStopWatch th;
 	CStopWatch th;
 	boost::thread *serverThread; //thread that called system to run server
 	boost::thread *serverThread; //thread that called system to run server
-	SharedMem *shared; //interprocess memory (for waiting for server)
+	SharedMemory * shared;
+	std::string uuid;
 	bool verbose; //whether to print log msgs
 	bool verbose; //whether to print log msgs
 
 
 	//functions setting up local server
 	//functions setting up local server
@@ -148,9 +149,10 @@ public:
 	void newGame(CConnection *con, StartInfo *si); //con - connection to server
 	void newGame(CConnection *con, StartInfo *si); //con - connection to server
 
 
 	void loadNeutralBattleAI();
 	void loadNeutralBattleAI();
-	void installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, boost::optional<PlayerColor> color);
+	void installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, boost::optional<PlayerColor> color, bool battlecb = false);
 	void installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, boost::optional<PlayerColor> color, bool needCallback = true);
 	void installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, boost::optional<PlayerColor> color, bool needCallback = true);
 	std::string aiNameForPlayer(const PlayerSettings &ps, bool battleAI); //empty means no AI -> human
 	std::string aiNameForPlayer(const PlayerSettings &ps, bool battleAI); //empty means no AI -> human
+	std::string aiNameForPlayer(bool battleAI);
 
 
 	void endGame(bool closeConnection = true);
 	void endGame(bool closeConnection = true);
 	void stopConnection();
 	void stopConnection();

+ 46 - 29
client/NetPacksClient.cpp

@@ -96,6 +96,10 @@
 #define BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(function,...) 				\
 #define BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(function,...) 				\
 	CALL_ONLY_THAT_BATTLE_INTERFACE(GS(cl)->curB->sides[0].color, function, __VA_ARGS__)	\
 	CALL_ONLY_THAT_BATTLE_INTERFACE(GS(cl)->curB->sides[0].color, function, __VA_ARGS__)	\
 	CALL_ONLY_THAT_BATTLE_INTERFACE(GS(cl)->curB->sides[1].color, function, __VA_ARGS__)	\
 	CALL_ONLY_THAT_BATTLE_INTERFACE(GS(cl)->curB->sides[1].color, function, __VA_ARGS__)	\
+	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt)	\
+	{																					\
+		CALL_ONLY_THAT_BATTLE_INTERFACE(PlayerColor::SPECTATOR, function, __VA_ARGS__)	\
+	}																					\
 	BATTLE_INTERFACE_CALL_RECEIVERS(function, __VA_ARGS__)
 	BATTLE_INTERFACE_CALL_RECEIVERS(function, __VA_ARGS__)
 /*
 /*
  * NetPacksClient.cpp, part of VCMI engine
  * NetPacksClient.cpp, part of VCMI engine
@@ -283,13 +287,13 @@ void GiveBonus::applyCl(CClient *cl)
 void ChangeObjPos::applyFirstCl(CClient *cl)
 void ChangeObjPos::applyFirstCl(CClient *cl)
 {
 {
 	CGObjectInstance *obj = GS(cl)->getObjInstance(objid);
 	CGObjectInstance *obj = GS(cl)->getObjInstance(objid);
-	if(flags & 1)
+	if(flags & 1 && CGI->mh)
 		CGI->mh->hideObject(obj);
 		CGI->mh->hideObject(obj);
 }
 }
 void ChangeObjPos::applyCl(CClient *cl)
 void ChangeObjPos::applyCl(CClient *cl)
 {
 {
 	CGObjectInstance *obj = GS(cl)->getObjInstance(objid);
 	CGObjectInstance *obj = GS(cl)->getObjInstance(objid);
-	if(flags & 1)
+	if(flags & 1 && CGI->mh)
 		CGI->mh->printObject(obj);
 		CGI->mh->printObject(obj);
 
 
 	cl->invalidatePaths();
 	cl->invalidatePaths();
@@ -298,6 +302,10 @@ void ChangeObjPos::applyCl(CClient *cl)
 void PlayerEndsGame::applyCl(CClient *cl)
 void PlayerEndsGame::applyCl(CClient *cl)
 {
 {
 	CALL_IN_ALL_INTERFACES(gameOver, player, victoryLossCheckResult);
 	CALL_IN_ALL_INTERFACES(gameOver, player, victoryLossCheckResult);
+
+	// In auto testing mode we always close client if red player won or lose
+	if(!settings["session"]["testmap"].isNull() && player == PlayerColor(0))
+		handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not
 }
 }
 
 
 void RemoveBonus::applyCl(CClient *cl)
 void RemoveBonus::applyCl(CClient *cl)
@@ -335,7 +343,8 @@ void RemoveObject::applyFirstCl(CClient *cl)
 {
 {
 	const CGObjectInstance *o = cl->getObj(id);
 	const CGObjectInstance *o = cl->getObj(id);
 
 
-	CGI->mh->hideObject(o, true);
+	if(CGI->mh)
+		CGI->mh->hideObject(o, true);
 
 
 	//notify interfaces about removal
 	//notify interfaces about removal
 	for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++)
 	for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++)
@@ -357,18 +366,20 @@ void TryMoveHero::applyFirstCl(CClient *cl)
 	//check if playerint will have the knowledge about movement - if not, directly update maphandler
 	//check if playerint will have the knowledge about movement - if not, directly update maphandler
 	for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++)
 	for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++)
 	{
 	{
-		if(i->first >= PlayerColor::PLAYER_LIMIT)
-			continue;
-		TeamState *t = GS(cl)->getPlayerTeam(i->first);
-		if((t->fogOfWarMap[start.x-1][start.y][start.z] || t->fogOfWarMap[end.x-1][end.y][end.z])
-				&& GS(cl)->getPlayer(i->first)->human)
-			humanKnows = true;
+		auto ps = GS(cl)->getPlayer(i->first);
+		if(ps && (GS(cl)->isVisible(start - int3(1, 0, 0), i->first) || GS(cl)->isVisible(end - int3(1, 0, 0), i->first)))
+		{
+			if(ps->human)
+				humanKnows = true;
+		}
 	}
 	}
 
 
+	if(!CGI->mh)
+		return;
+
 	if(result == TELEPORTATION  ||  result == EMBARK  ||  result == DISEMBARK  ||  !humanKnows)
 	if(result == TELEPORTATION  ||  result == EMBARK  ||  result == DISEMBARK  ||  !humanKnows)
 		CGI->mh->hideObject(h, result == EMBARK && humanKnows);
 		CGI->mh->hideObject(h, result == EMBARK && humanKnows);
 
 
-
 	if(result == DISEMBARK)
 	if(result == DISEMBARK)
 		CGI->mh->printObject(h->boat);
 		CGI->mh->printObject(h->boat);
 }
 }
@@ -378,13 +389,14 @@ void TryMoveHero::applyCl(CClient *cl)
 	const CGHeroInstance *h = cl->getHero(id);
 	const CGHeroInstance *h = cl->getHero(id);
 	cl->invalidatePaths();
 	cl->invalidatePaths();
 
 
-	if(result == TELEPORTATION  ||  result == EMBARK  ||  result == DISEMBARK)
+	if(CGI->mh)
 	{
 	{
-		CGI->mh->printObject(h, result == DISEMBARK);
-	}
+		if(result == TELEPORTATION  ||  result == EMBARK  ||  result == DISEMBARK)
+			CGI->mh->printObject(h, result == DISEMBARK);
 
 
-	if(result == EMBARK)
-		CGI->mh->hideObject(h->boat);
+		if(result == EMBARK)
+			CGI->mh->hideObject(h->boat);
+	}
 
 
 	PlayerColor player = h->tempOwner;
 	PlayerColor player = h->tempOwner;
 
 
@@ -395,18 +407,17 @@ void TryMoveHero::applyCl(CClient *cl)
 	//notify interfaces about move
 	//notify interfaces about move
 	for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++)
 	for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++)
 	{
 	{
-		if(i->first >= PlayerColor::PLAYER_LIMIT) continue;
-		TeamState *t = GS(cl)->getPlayerTeam(i->first);
-		if(t->fogOfWarMap[start.x-1][start.y][start.z] || t->fogOfWarMap[end.x-1][end.y][end.z])
+		if(GS(cl)->isVisible(start - int3(1, 0, 0), i->first)
+			|| GS(cl)->isVisible(end - int3(1, 0, 0), i->first))
 		{
 		{
 			i->second->heroMoved(*this);
 			i->second->heroMoved(*this);
 		}
 		}
 	}
 	}
 
 
-	if(!humanKnows) //maphandler didn't get update from playerint, do it now
-	{				//TODO: restructure nicely
+	//maphandler didn't get update from playerint, do it now
+	//TODO: restructure nicely
+	if(!humanKnows && CGI->mh)
 		CGI->mh->printObject(h);
 		CGI->mh->printObject(h);
-	}
 }
 }
 
 
 void NewStructures::applyCl(CClient *cl)
 void NewStructures::applyCl(CClient *cl)
@@ -482,22 +493,22 @@ void HeroRecruited::applyCl(CClient *cl)
 			needsPrinting = false;
 			needsPrinting = false;
 		}
 		}
 	}
 	}
-	if (needsPrinting)
-	{
+	if(needsPrinting && CGI->mh)
 		CGI->mh->printObject(h);
 		CGI->mh->printObject(h);
-	}
 }
 }
 
 
 void GiveHero::applyCl(CClient *cl)
 void GiveHero::applyCl(CClient *cl)
 {
 {
 	CGHeroInstance *h = GS(cl)->getHero(id);
 	CGHeroInstance *h = GS(cl)->getHero(id);
-	CGI->mh->printObject(h);
+	if(CGI->mh)
+		CGI->mh->printObject(h);
 	cl->playerint[h->tempOwner]->heroCreated(h);
 	cl->playerint[h->tempOwner]->heroCreated(h);
 }
 }
 
 
 void GiveHero::applyFirstCl(CClient *cl)
 void GiveHero::applyFirstCl(CClient *cl)
 {
 {
-	CGI->mh->hideObject(GS(cl)->getHero(id));
+	if(CGI->mh)
+		CGI->mh->hideObject(GS(cl)->getHero(id));
 }
 }
 
 
 void InfoWindow::applyCl(CClient *cl)
 void InfoWindow::applyCl(CClient *cl)
@@ -588,6 +599,8 @@ void BattleStart::applyFirstCl(CClient *cl)
 		info->tile, info->sides[0].hero, info->sides[1].hero);
 		info->tile, info->sides[0].hero, info->sides[1].hero);
 	CALL_ONLY_THAT_BATTLE_INTERFACE(info->sides[1].color, battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject,
 	CALL_ONLY_THAT_BATTLE_INTERFACE(info->sides[1].color, battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject,
 		info->tile, info->sides[0].hero, info->sides[1].hero);
 		info->tile, info->sides[0].hero, info->sides[1].hero);
+	CALL_ONLY_THAT_BATTLE_INTERFACE(PlayerColor::SPECTATOR, battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject,
+		info->tile, info->sides[0].hero, info->sides[1].hero);
 	BATTLE_INTERFACE_CALL_RECEIVERS(battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject,
 	BATTLE_INTERFACE_CALL_RECEIVERS(battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject,
 		info->tile, info->sides[0].hero, info->sides[1].hero);
 		info->tile, info->sides[0].hero, info->sides[1].hero);
 }
 }
@@ -707,7 +720,7 @@ void BattleResultsApplied::applyCl(CClient *cl)
 {
 {
 	INTERFACE_CALL_IF_PRESENT(player1, battleResultsApplied);
 	INTERFACE_CALL_IF_PRESENT(player1, battleResultsApplied);
 	INTERFACE_CALL_IF_PRESENT(player2, battleResultsApplied);
 	INTERFACE_CALL_IF_PRESENT(player2, battleResultsApplied);
-	INTERFACE_CALL_IF_PRESENT(PlayerColor::UNFLAGGABLE, battleResultsApplied);
+	INTERFACE_CALL_IF_PRESENT(PlayerColor::SPECTATOR, battleResultsApplied);
 	if(GS(cl)->initialOpts->mode == StartInfo::DUEL)
 	if(GS(cl)->initialOpts->mode == StartInfo::DUEL)
 	{
 	{
 		handleQuit();
 		handleQuit();
@@ -808,7 +821,10 @@ void PlayerMessage::applyCl(CClient *cl)
 	logNetwork->debugStream() << "Player "<< player <<" sends a message: " << text;
 	logNetwork->debugStream() << "Player "<< player <<" sends a message: " << text;
 
 
 	std::ostringstream str;
 	std::ostringstream str;
-	str << cl->getPlayer(player)->nodeName() <<": " << text;
+	if(player.isSpectator())
+		str << "Spectator: " << text;
+	else
+		str << cl->getPlayer(player)->nodeName() <<": " << text;
 	if(LOCPLINT)
 	if(LOCPLINT)
 		LOCPLINT->cingconsole->print(str.str());
 		LOCPLINT->cingconsole->print(str.str());
 }
 }
@@ -904,7 +920,8 @@ void NewObject::applyCl(CClient *cl)
 	cl->invalidatePaths();
 	cl->invalidatePaths();
 
 
 	const CGObjectInstance *obj = cl->getObj(id);
 	const CGObjectInstance *obj = cl->getObj(id);
-	CGI->mh->printObject(obj, true);
+	if(CGI->mh)
+		CGI->mh->printObject(obj, true);
 
 
 	for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++)
 	for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++)
 	{
 	{

+ 16 - 2
client/battle/CBattleInterface.cpp

@@ -95,7 +95,7 @@ void CBattleInterface::addNewAnim(CBattleAnimation *anim)
 CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2,
 CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2,
 								   const CGHeroInstance *hero1, const CGHeroInstance *hero2,
 								   const CGHeroInstance *hero1, const CGHeroInstance *hero2,
 								   const SDL_Rect & myRect,
 								   const SDL_Rect & myRect,
-								   std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen)
+								   std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt)
 	: background(nullptr), queue(nullptr), attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0),
 	: background(nullptr), queue(nullptr), attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0),
       activeStack(nullptr), mouseHoveredStack(nullptr), stackToActivate(nullptr), selectedStack(nullptr), previouslyHoveredHex(-1),
       activeStack(nullptr), mouseHoveredStack(nullptr), stackToActivate(nullptr), selectedStack(nullptr), previouslyHoveredHex(-1),
 	  currentlyHoveredHex(-1), attackingHex(-1), stackCanCastSpell(false), creatureCasting(false), spellDestSelectMode(false), spellToCast(nullptr), sp(nullptr),
 	  currentlyHoveredHex(-1), attackingHex(-1), stackCanCastSpell(false), creatureCasting(false), spellDestSelectMode(false), spellToCast(nullptr), sp(nullptr),
@@ -105,12 +105,15 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 {
 {
 	OBJ_CONSTRUCTION;
 	OBJ_CONSTRUCTION;
 
 
-	if (!curInt)
+	if(spectatorInt)
+		curInt = spectatorInt;
+	else if(!curInt)
 	{
 	{
 		//May happen when we are defending during network MP game -> attacker interface is just not present
 		//May happen when we are defending during network MP game -> attacker interface is just not present
 		curInt = defenderInt;
 		curInt = defenderInt;
 	}
 	}
 
 
+
 	animsAreDisplayed.setn(false);
 	animsAreDisplayed.setn(false);
 	pos = myRect;
 	pos = myRect;
 	strongInterest = true;
 	strongInterest = true;
@@ -377,6 +380,8 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 	currentAction = INVALID;
 	currentAction = INVALID;
 	selectedAction = INVALID;
 	selectedAction = INVALID;
 	addUsedEvents(RCLICK | MOVE | KEYBOARD);
 	addUsedEvents(RCLICK | MOVE | KEYBOARD);
+
+	blockUI(settings["session"]["spectate"].Bool());
 }
 }
 
 
 CBattleInterface::~CBattleInterface()
 CBattleInterface::~CBattleInterface()
@@ -448,6 +453,7 @@ CBattleInterface::~CBattleInterface()
 		int terrain = LOCPLINT->cb->getTile(adventureInt->selection->visitablePos())->terType;
 		int terrain = LOCPLINT->cb->getTile(adventureInt->selection->visitablePos())->terType;
 		CCS->musich->playMusicFromSet("terrain", terrain, true);
 		CCS->musich->playMusicFromSet("terrain", terrain, true);
 	}
 	}
+	animsAreDisplayed.setn(false);
 }
 }
 
 
 void CBattleInterface::setPrintCellBorders(bool set)
 void CBattleInterface::setPrintCellBorders(bool set)
@@ -1246,6 +1252,11 @@ void CBattleInterface::battleFinished(const BattleResult& br)
 void CBattleInterface::displayBattleFinished()
 void CBattleInterface::displayBattleFinished()
 {
 {
 	CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
 	CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
+	if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool())
+	{
+		GH.popIntTotally(this);
+		return;
+	}
 
 
 	SDL_Rect temp_rect = genRect(561, 470, (screen->w - 800)/2 + 165, (screen->h - 600)/2 + 19);
 	SDL_Rect temp_rect = genRect(561, 470, (screen->w - 800)/2 + 165, (screen->h - 600)/2 + 19);
 	resWindow = new CBattleResultWindow(*bresult, temp_rect, *this->curInt);
 	resWindow = new CBattleResultWindow(*bresult, temp_rect, *this->curInt);
@@ -1531,6 +1542,9 @@ void CBattleInterface::setAnimSpeed(int set)
 
 
 int CBattleInterface::getAnimSpeed() const
 int CBattleInterface::getAnimSpeed() const
 {
 {
+	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-battle-speed"].isNull())
+		return vstd::round(settings["session"]["spectate-battle-speed"].Float() *100);
+
 	return vstd::round(settings["battle"]["animationSpeed"].Float() *100);
 	return vstd::round(settings["battle"]["animationSpeed"].Float() *100);
 }
 }
 
 

+ 1 - 1
client/battle/CBattleInterface.h

@@ -269,7 +269,7 @@ public:
 	ui32 animIDhelper; //for giving IDs for animations
 	ui32 animIDhelper; //for giving IDs for animations
 	static CondSh<bool> animsAreDisplayed; //for waiting with the end of battle for end of anims
 	static CondSh<bool> animsAreDisplayed; //for waiting with the end of battle for end of anims
 
 
-	CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen); //c-tor
+	CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt = nullptr); //c-tor
 	virtual ~CBattleInterface(); //d-tor
 	virtual ~CBattleInterface(); //d-tor
 
 
 	//std::vector<TimeInterested*> timeinterested; //animation handling
 	//std::vector<TimeInterested*> timeinterested; //animation handling

+ 1 - 2
client/battle/CBattleInterfaceClasses.cpp

@@ -211,8 +211,7 @@ void CBattleHero::clickRight(tribool down, bool previousState)
 	windowPosition.y = myOwner->pos.y + 135;
 	windowPosition.y = myOwner->pos.y + 135;
 
 
 	InfoAboutHero targetHero;
 	InfoAboutHero targetHero;
-
-	if (down && myOwner->myTurn)
+	if(down && (myOwner->myTurn || settings["session"]["spectate"].Bool()))
 	{
 	{
 		auto h = flip ? myOwner->defendingHeroInstance : myOwner->attackingHeroInstance;
 		auto h = flip ? myOwner->defendingHeroInstance : myOwner->attackingHeroInstance;
 		targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE);
 		targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE);

+ 43 - 1
client/gui/CGuiHandler.cpp

@@ -12,6 +12,7 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CConfigHandler.h"
 #include "../CMT.h"
 #include "../CMT.h"
 #include "../CPlayerInterface.h"
 #include "../CPlayerInterface.h"
+#include "../battle/CBattleInterface.h"
 
 
 extern std::queue<SDL_Event> events;
 extern std::queue<SDL_Event> events;
 extern boost::mutex eventsM;
 extern boost::mutex eventsM;
@@ -191,6 +192,46 @@ void CGuiHandler::handleEvent(SDL_Event *sEvent)
 	if (sEvent->type==SDL_KEYDOWN || sEvent->type==SDL_KEYUP)
 	if (sEvent->type==SDL_KEYDOWN || sEvent->type==SDL_KEYUP)
 	{
 	{
 		SDL_KeyboardEvent key = sEvent->key;
 		SDL_KeyboardEvent key = sEvent->key;
+		if(sEvent->type == SDL_KEYDOWN && key.keysym.sym >= SDLK_F1 && key.keysym.sym <= SDLK_F15 && settings["session"]["spectate"].Bool())
+		{
+			//TODO: we need some central place for all interface-independent hotkeys
+			Settings s = settings.write["session"];
+			switch(key.keysym.sym)
+			{
+			case SDLK_F5:
+				if(settings["session"]["spectate-locked-pim"].Bool())
+					LOCPLINT->pim->unlock();
+				else
+					LOCPLINT->pim->lock();
+				s["spectate-locked-pim"].Bool() = !settings["session"]["spectate-locked-pim"].Bool();
+				break;
+
+			case SDLK_F6:
+				s["spectate-ignore-hero"].Bool() = !settings["session"]["spectate-ignore-hero"].Bool();
+				break;
+
+			case SDLK_F7:
+				s["spectate-skip-battle"].Bool() = !settings["session"]["spectate-skip-battle"].Bool();
+				break;
+
+			case SDLK_F8:
+				s["spectate-skip-battle-result"].Bool() = !settings["session"]["spectate-skip-battle-result"].Bool();
+				break;
+
+			case SDLK_F9:
+				//not working yet since CClient::run remain locked after CBattleInterface removal
+				if(LOCPLINT->battleInt)
+				{
+					GH.popIntTotally(GH.topInt());
+					vstd::clear_pointer(LOCPLINT->battleInt);
+				}
+				break;
+
+			default:
+				break;
+			}
+			return;
+		}
 
 
 		//translate numpad keys
 		//translate numpad keys
 		if(key.keysym.sym == SDLK_KP_ENTER)
 		if(key.keysym.sym == SDLK_KP_ENTER)
@@ -361,7 +402,8 @@ void CGuiHandler::simpleRedraw()
 	//update only top interface and draw background
 	//update only top interface and draw background
 	if(objsToBlit.size() > 1)
 	if(objsToBlit.size() > 1)
 		blitAt(screen2,0,0,screen); //blit background
 		blitAt(screen2,0,0,screen); //blit background
-	objsToBlit.back()->show(screen); //blit active interface/window
+	if(!objsToBlit.empty())
+		objsToBlit.back()->show(screen); //blit active interface/window
 }
 }
 
 
 void CGuiHandler::handleMoveInterested(const SDL_MouseMotionEvent & motion)
 void CGuiHandler::handleMoveInterested(const SDL_MouseMotionEvent & motion)

+ 6 - 3
client/mapHandler.cpp

@@ -54,7 +54,7 @@ struct NeighborTilesInfo
 			if ( dx + pos.x < 0 || dx + pos.x >= sizes.x
 			if ( dx + pos.x < 0 || dx + pos.x >= sizes.x
 			  || dy + pos.y < 0 || dy + pos.y >= sizes.y)
 			  || dy + pos.y < 0 || dy + pos.y >= sizes.y)
 				return false;
 				return false;
-			return visibilityMap[dx+pos.x][dy+pos.y][pos.z];
+			return settings["session"]["spectate"].Bool() ? true : visibilityMap[dx+pos.x][dy+pos.y][pos.z];
 		};
 		};
 		d7 = getTile(-1, -1); //789
 		d7 = getTile(-1, -1); //789
 		d8 = getTile( 0, -1); //456
 		d8 = getTile( 0, -1); //456
@@ -563,7 +563,7 @@ void CMapHandler::CMapWorldViewBlitter::drawTileOverlay(SDL_Surface * targetSurf
 		const CGObjectInstance * obj = object.obj;
 		const CGObjectInstance * obj = object.obj;
 
 
 		const bool sameLevel = obj->pos.z == pos.z;
 		const bool sameLevel = obj->pos.z == pos.z;
-		const bool isVisible = (*info->visibilityMap)[pos.x][pos.y][pos.z];
+		const bool isVisible = settings["session"]["spectate"].Bool() ? true : (*info->visibilityMap)[pos.x][pos.y][pos.z];
 		const bool isVisitable = obj->visitableAt(pos.x, pos.y);
 		const bool isVisitable = obj->visitableAt(pos.x, pos.y);
 
 
 		if(sameLevel && isVisible && isVisitable)
 		if(sameLevel && isVisible && isVisitable)
@@ -895,7 +895,7 @@ void CMapHandler::CMapBlitter::blit(SDL_Surface * targetSurf, const MapDrawingIn
 			{
 			{
 				const TerrainTile2 & tile = parent->ttiles[pos.x][pos.y][pos.z];
 				const TerrainTile2 & tile = parent->ttiles[pos.x][pos.y][pos.z];
 
 
-				if (!(*info->visibilityMap)[pos.x][pos.y][topTile.z] && !info->showAllTerrain)
+				if(!settings["session"]["spectate"].Bool() && !(*info->visibilityMap)[pos.x][pos.y][topTile.z] && !info->showAllTerrain)
 					drawFow(targetSurf);
 					drawFow(targetSurf);
 
 
 				// overlay needs to be drawn over fow, because of artifacts-aura-like spells
 				// overlay needs to be drawn over fow, because of artifacts-aura-like spells
@@ -1099,6 +1099,9 @@ bool CMapHandler::CMapBlitter::canDrawObject(const CGObjectInstance * obj) const
 
 
 bool CMapHandler::CMapBlitter::canDrawCurrentTile() const
 bool CMapHandler::CMapBlitter::canDrawCurrentTile() const
 {
 {
+	if(settings["session"]["spectate"].Bool())
+		return true;
+
 	const NeighborTilesInfo neighbors(pos, parent->sizes, *info->visibilityMap);
 	const NeighborTilesInfo neighbors(pos, parent->sizes, *info->visibilityMap);
 	return !neighbors.areAllHidden();
 	return !neighbors.areAllHidden();
 }
 }

+ 14 - 2
client/windows/CAdvmapInterface.cpp

@@ -1021,7 +1021,12 @@ void CAdvMapInt::show(SDL_Surface * to)
 #endif
 #endif
 
 
 	for(int i = 0; i < 4; i++)
 	for(int i = 0; i < 4; i++)
-		gems[i]->setFrame(LOCPLINT->playerID.getNum());
+	{
+		if(settings["session"]["spectate"].Bool())
+			gems[i]->setFrame(PlayerColor(1).getNum());
+		else
+			gems[i]->setFrame(LOCPLINT->playerID.getNum());
+	}
 	if(updateScreen)
 	if(updateScreen)
 	{
 	{
 		int3 betterPos = LOCPLINT->repairScreenPos(position);
 		int3 betterPos = LOCPLINT->repairScreenPos(position);
@@ -1481,7 +1486,8 @@ void CAdvMapInt::setPlayer(PlayerColor Player)
 void CAdvMapInt::startTurn()
 void CAdvMapInt::startTurn()
 {
 {
 	state = INGAME;
 	state = INGAME;
-	if(LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID)
+	if(LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID
+		|| settings["session"]["spectate"].Bool())
 	{
 	{
 		adjustActiveness(false);
 		adjustActiveness(false);
 		minimap.setAIRadar(false);
 		minimap.setAIRadar(false);
@@ -1490,6 +1496,9 @@ void CAdvMapInt::startTurn()
 
 
 void CAdvMapInt::endingTurn()
 void CAdvMapInt::endingTurn()
 {
 {
+	if(settings["session"]["spectate"].Bool())
+		return;
+
 	if(LOCPLINT->cingconsole->active)
 	if(LOCPLINT->cingconsole->active)
 		LOCPLINT->cingconsole->deactivate();
 		LOCPLINT->cingconsole->deactivate();
 	LOCPLINT->makingTurn = false;
 	LOCPLINT->makingTurn = false;
@@ -1817,6 +1826,9 @@ const IShipyard * CAdvMapInt::ourInaccessibleShipyard(const CGObjectInstance *ob
 
 
 void CAdvMapInt::aiTurnStarted()
 void CAdvMapInt::aiTurnStarted()
 {
 {
+	if(settings["session"]["spectate"].Bool())
+		return;
+
 	adjustActiveness(true);
 	adjustActiveness(true);
 	CCS->musich->playMusicFromSet("enemy-turn", true);
 	CCS->musich->playMusicFromSet("enemy-turn", true);
 	adventureInt->minimap.setAIRadar(true);
 	adventureInt->minimap.setAIRadar(true);

+ 5 - 1
client/windows/CWindowObject.cpp

@@ -225,9 +225,13 @@ void CWindowObject::setShadow(bool on)
 
 
 void CWindowObject::showAll(SDL_Surface *to)
 void CWindowObject::showAll(SDL_Surface *to)
 {
 {
+	auto color = LOCPLINT ? LOCPLINT->playerID : PlayerColor(1);
+	if(settings["session"]["spectate"].Bool())
+		color = PlayerColor(1); // TODO: Spectator shouldn't need special code for UI colors
+
 	CIntObject::showAll(to);
 	CIntObject::showAll(to);
 	if ((options & BORDERED) && (pos.h != to->h || pos.w != to->w))
 	if ((options & BORDERED) && (pos.h != to->h || pos.w != to->w))
-		CMessage::drawBorder(LOCPLINT ? LOCPLINT->playerID : PlayerColor(1), to, pos.w+28, pos.h+29, pos.x-14, pos.y-15);
+		CMessage::drawBorder(color, to, pos.w+28, pos.h+29, pos.x-14, pos.y-15);
 }
 }
 
 
 void CWindowObject::close()
 void CWindowObject::close()

+ 2 - 0
client/windows/InfoWindows.cpp

@@ -335,6 +335,8 @@ void CRClickPopup::close()
 void CRClickPopup::createAndPush(const std::string &txt, const CInfoWindow::TCompsInfo &comps)
 void CRClickPopup::createAndPush(const std::string &txt, const CInfoWindow::TCompsInfo &comps)
 {
 {
 	PlayerColor player = LOCPLINT ? LOCPLINT->playerID : PlayerColor(1); //if no player, then use blue
 	PlayerColor player = LOCPLINT ? LOCPLINT->playerID : PlayerColor(1); //if no player, then use blue
+	if(settings["session"]["spectate"].Bool())//TODO: there must be better way to implement this
+		player = PlayerColor(1);
 
 
 	CSimpleWindow * temp = new CInfoWindow(txt, player, comps);
 	CSimpleWindow * temp = new CInfoWindow(txt, player, comps);
 	temp->center(Point(GH.current->motion)); //center on mouse
 	temp->center(Point(GH.current->motion)); //center on mouse

+ 1 - 1
lib/CBattleCallback.cpp

@@ -237,7 +237,7 @@ const CGTownInstance * CBattleInfoEssentials::battleGetDefendedTown() const
 BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const
 BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const
 {
 {
 	RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID);
 	RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID);
-	if(!player)
+	if(!player || player.get().isSpectator())
 		return BattlePerspective::ALL_KNOWING;
 		return BattlePerspective::ALL_KNOWING;
 	if(*player == getBattle()->sides[0].color)
 	if(*player == getBattle()->sides[0].color)
 		return BattlePerspective::LEFT_SIDE;
 		return BattlePerspective::LEFT_SIDE;

+ 1 - 1
lib/CGameInfoCallback.cpp

@@ -595,7 +595,7 @@ const CMapHeader * CGameInfoCallback::getMapHeader() const
 
 
 bool CGameInfoCallback::hasAccess(boost::optional<PlayerColor> playerId) const
 bool CGameInfoCallback::hasAccess(boost::optional<PlayerColor> playerId) const
 {
 {
-	return !player || gs->getPlayerRelations( *playerId, *player ) != PlayerRelations::ENEMIES;
+	return !player || player.get().isSpectator() || gs->getPlayerRelations( *playerId, *player ) != PlayerRelations::ENEMIES;
 }
 }
 
 
 EPlayerStatus::EStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbose) const
 EPlayerStatus::EStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbose) const

+ 3 - 0
lib/CGameState.cpp

@@ -2208,6 +2208,9 @@ bool CGameState::isVisible(int3 pos, PlayerColor player)
 {
 {
 	if(player == PlayerColor::NEUTRAL)
 	if(player == PlayerColor::NEUTRAL)
 		return false;
 		return false;
+	if(player.isSpectator())
+		return true;
+
 	return getPlayerTeam(player)->fogOfWarMap[pos.x][pos.y][pos.z];
 	return getPlayerTeam(player)->fogOfWarMap[pos.x][pos.y][pos.z];
 }
 }
 
 

+ 6 - 0
lib/GameConstants.cpp

@@ -29,6 +29,7 @@ const SlotID SlotID::SUMMONED_SLOT_PLACEHOLDER = SlotID(-3);
 const SlotID SlotID::WAR_MACHINES_SLOT = SlotID(-4);
 const SlotID SlotID::WAR_MACHINES_SLOT = SlotID(-4);
 const SlotID SlotID::ARROW_TOWERS_SLOT = SlotID(-5);
 const SlotID SlotID::ARROW_TOWERS_SLOT = SlotID(-5);
 
 
+const PlayerColor PlayerColor::SPECTATOR = PlayerColor(252);
 const PlayerColor PlayerColor::CANNOT_DETERMINE = PlayerColor(253);
 const PlayerColor PlayerColor::CANNOT_DETERMINE = PlayerColor(253);
 const PlayerColor PlayerColor::UNFLAGGABLE = PlayerColor(254);
 const PlayerColor PlayerColor::UNFLAGGABLE = PlayerColor(254);
 const PlayerColor PlayerColor::NEUTRAL = PlayerColor(255);
 const PlayerColor PlayerColor::NEUTRAL = PlayerColor(255);
@@ -72,6 +73,11 @@ bool PlayerColor::isValidPlayer() const
 	return num < PLAYER_LIMIT_I;
 	return num < PLAYER_LIMIT_I;
 }
 }
 
 
+bool PlayerColor::isSpectator() const
+{
+	return num == 252;
+}
+
 std::string PlayerColor::getStr(bool L10n) const
 std::string PlayerColor::getStr(bool L10n) const
 {
 {
 	std::string ret = "unnamed";
 	std::string ret = "unnamed";

+ 2 - 0
lib/GameConstants.h

@@ -257,12 +257,14 @@ class PlayerColor : public BaseForID<PlayerColor, ui8>
 		PLAYER_LIMIT_I = 8
 		PLAYER_LIMIT_I = 8
 	};
 	};
 
 
+	DLL_LINKAGE static const PlayerColor SPECTATOR; //252
 	DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253
 	DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253
 	DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks)
 	DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks)
 	DLL_LINKAGE static const PlayerColor NEUTRAL; //255
 	DLL_LINKAGE static const PlayerColor NEUTRAL; //255
 	DLL_LINKAGE static const PlayerColor PLAYER_LIMIT; //player limit per map
 	DLL_LINKAGE static const PlayerColor PLAYER_LIMIT; //player limit per map
 
 
 	DLL_LINKAGE bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral)
 	DLL_LINKAGE bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral)
+	DLL_LINKAGE bool isSpectator() const;
 
 
 	DLL_LINKAGE std::string getStr(bool L10n = false) const;
 	DLL_LINKAGE std::string getStr(bool L10n = false) const;
 	DLL_LINKAGE std::string getStrCap(bool L10n = false) const;
 	DLL_LINKAGE std::string getStrCap(bool L10n = false) const;

+ 32 - 12
lib/Interprocess.h

@@ -20,8 +20,8 @@ struct ServerReady
 {
 {
 	bool ready;
 	bool ready;
 	uint16_t port; //ui16?
 	uint16_t port; //ui16?
-	boost::interprocess::interprocess_mutex  mutex;
-	boost::interprocess::interprocess_condition  cond;
+	boost::interprocess::interprocess_mutex mutex;
+	boost::interprocess::interprocess_condition cond;
 
 
 	ServerReady()
 	ServerReady()
 	{
 	{
@@ -29,10 +29,19 @@ struct ServerReady
 		port = 0;
 		port = 0;
 	}
 	}
 
 
-	void setToTrueAndNotify(uint16_t Port)
+	void waitTillReady()
 	{
 	{
+		boost::interprocess::scoped_lock<boost::interprocess::interprocess_mutex> slock(mutex);
+		while(!ready)
 		{
 		{
-			boost::unique_lock<boost::interprocess::interprocess_mutex> lock(mutex); 
+			cond.wait(slock);
+		}
+	}
+
+	void setToReadyAndNotify(const uint16_t Port)
+	{
+		{
+			boost::unique_lock<boost::interprocess::interprocess_mutex> lock(mutex);
 			ready = true;
 			ready = true;
 			port = Port;
 			port = Port;
 		}
 		}
@@ -40,22 +49,33 @@ struct ServerReady
 	}
 	}
 };
 };
 
 
-struct SharedMem 
+struct SharedMemory
 {
 {
+	const char * name;
 	boost::interprocess::shared_memory_object smo;
 	boost::interprocess::shared_memory_object smo;
-	boost::interprocess::mapped_region *mr;
-	ServerReady *sr;
+	boost::interprocess::mapped_region * mr;
+	ServerReady * sr;
 	
 	
-	SharedMem() //c-tor
-		:smo(boost::interprocess::open_or_create,"vcmi_memory",boost::interprocess::read_write) 
+	SharedMemory(std::string Name, bool initialize = false)
+		: name(Name.c_str())
 	{
 	{
+		if(initialize)
+		{
+			//if the application has previously crashed, the memory may not have been removed. to avoid problems - try to destroy it
+			boost::interprocess::shared_memory_object::remove(name);
+		}
+		smo = boost::interprocess::shared_memory_object(boost::interprocess::open_or_create, name, boost::interprocess::read_write);
 		smo.truncate(sizeof(ServerReady));
 		smo.truncate(sizeof(ServerReady));
 		mr = new boost::interprocess::mapped_region(smo,boost::interprocess::read_write);
 		mr = new boost::interprocess::mapped_region(smo,boost::interprocess::read_write);
-		sr = new(mr->get_address())ServerReady();
+		if(initialize)
+			sr = new(mr->get_address())ServerReady();
+		else
+			sr = reinterpret_cast<ServerReady*>(mr->get_address());
 	};
 	};
-	~SharedMem() //d-tor
+
+	~SharedMemory()
 	{
 	{
 		delete mr;
 		delete mr;
-		boost::interprocess::shared_memory_object::remove("vcmi_memory");
+		boost::interprocess::shared_memory_object::remove(name);
 	}
 	}
 };
 };

+ 3 - 0
lib/NetPacksLib.cpp

@@ -1842,6 +1842,9 @@ DLL_LINKAGE void BattleSetStackProperty::applyGs(CGameState *gs)
 
 
 DLL_LINKAGE void PlayerCheated::applyGs(CGameState *gs)
 DLL_LINKAGE void PlayerCheated::applyGs(CGameState *gs)
 {
 {
+	if(!player.isValidPlayer())
+		return;
+
 	gs->getPlayer(player)->enteredLosingCheatCode = losingCheatCode;
 	gs->getPlayer(player)->enteredLosingCheatCode = losingCheatCode;
 	gs->getPlayer(player)->enteredWinningCheatCode = winningCheatCode;
 	gs->getPlayer(player)->enteredWinningCheatCode = winningCheatCode;
 }
 }

+ 15 - 12
server/CGameHandler.cpp

@@ -48,7 +48,7 @@
 #ifndef _MSC_VER
 #ifndef _MSC_VER
 #include <boost/thread/xtime.hpp>
 #include <boost/thread/xtime.hpp>
 #endif
 #endif
-extern bool end2;
+extern std::atomic<bool> serverShuttingDown;
 #ifdef min
 #ifdef min
 #undef min
 #undef min
 #endif
 #endif
@@ -1033,12 +1033,13 @@ void CGameHandler::handleConnection(std::set<PlayerColor> players, CConnection &
 
 
 	auto handleDisconnection = [&](const std::exception & e)
 	auto handleDisconnection = [&](const std::exception & e)
 	{
 	{
+		boost::unique_lock<boost::mutex> lock(*c.wmx);
 		assert(!c.connected); //make sure that connection has been marked as broken
 		assert(!c.connected); //make sure that connection has been marked as broken
 		logGlobal->error(e.what());
 		logGlobal->error(e.what());
 		conns -= &c;
 		conns -= &c;
 		for(auto playerConn : connections)
 		for(auto playerConn : connections)
 		{
 		{
-			if(playerConn.second == &c)
+			if(!serverShuttingDown && playerConn.second == &c)
 			{
 			{
 				PlayerCheated pc;
 				PlayerCheated pc;
 				pc.player = playerConn.first;
 				pc.player = playerConn.first;
@@ -1128,7 +1129,7 @@ void CGameHandler::handleConnection(std::set<PlayerColor> players, CConnection &
 	}
 	}
 	catch(...)
 	catch(...)
 	{
 	{
-		end2 = true;
+		serverShuttingDown = true;
 		handleException();
 		handleException();
 		throw;
 		throw;
 	}
 	}
@@ -1878,7 +1879,8 @@ void CGameHandler::run(bool resume)
 			sbuffer << color << " ";
 			sbuffer << color << " ";
 			{
 			{
 				boost::unique_lock<boost::recursive_mutex> lock(gsm);
 				boost::unique_lock<boost::recursive_mutex> lock(gsm);
-				connections[color] = cc;
+				if(!color.isSpectator()) // there can be more than one spectator
+					connections[color] = cc;
 			}
 			}
 		}
 		}
 		logGlobal->info(sbuffer.str());
 		logGlobal->info(sbuffer.str());
@@ -1901,7 +1903,7 @@ void CGameHandler::run(bool resume)
 	if (gs->scenarioOps->mode == StartInfo::DUEL)
 	if (gs->scenarioOps->mode == StartInfo::DUEL)
 	{
 	{
 		runBattle();
 		runBattle();
-		end2 = true;
+		serverShuttingDown = true;
 
 
 
 
 		while(conns.size() && (*conns.begin())->isOpen())
 		while(conns.size() && (*conns.begin())->isOpen())
@@ -1912,7 +1914,7 @@ void CGameHandler::run(bool resume)
 
 
 	auto playerTurnOrder = generatePlayerTurnOrder();
 	auto playerTurnOrder = generatePlayerTurnOrder();
 
 
-	while(!end2)
+	while(!serverShuttingDown)
 	{
 	{
 		if (!resume) newTurn();
 		if (!resume) newTurn();
 
 
@@ -1953,7 +1955,7 @@ void CGameHandler::run(bool resume)
 
 
 					//wait till turn is done
 					//wait till turn is done
 					boost::unique_lock<boost::mutex> lock(states.mx);
 					boost::unique_lock<boost::mutex> lock(states.mx);
-					while (states.players.at(playerColor).makingTurn && !end2)
+					while(states.players.at(playerColor).makingTurn && !serverShuttingDown)
 					{
 					{
 						static time_duration p = milliseconds(100);
 						static time_duration p = milliseconds(100);
 						states.cv.timed_wait(lock, p);
 						states.cv.timed_wait(lock, p);
@@ -1969,7 +1971,7 @@ void CGameHandler::run(bool resume)
 					activePlayer = true;
 					activePlayer = true;
 		}
 		}
 		if (!activePlayer)
 		if (!activePlayer)
-			end2 = true;
+			serverShuttingDown = true;
 	}
 	}
 	while(conns.size() && (*conns.begin())->isOpen())
 	while(conns.size() && (*conns.begin())->isOpen())
 		boost::this_thread::sleep(boost::posix_time::milliseconds(5)); //give time client to close socket
 		boost::this_thread::sleep(boost::posix_time::milliseconds(5)); //give time client to close socket
@@ -2733,7 +2735,7 @@ void CGameHandler::close()
 	{
 	{
 		exit(0);
 		exit(0);
 	}
 	}
-	end2 = true;
+	serverShuttingDown = true;
 
 
 	for (auto & elem : conns)
 	for (auto & elem : conns)
 	{
 	{
@@ -2744,7 +2746,6 @@ void CGameHandler::close()
 		elem->close();
 		elem->close();
 		elem->connected = false;
 		elem->connected = false;
 	}
 	}
-	exit(0);
 }
 }
 
 
 void CGameHandler::playerLeftGame(int cid)
 void CGameHandler::playerLeftGame(int cid)
@@ -4430,7 +4431,9 @@ void CGameHandler::playerMessage(PlayerColor player, const std::string &message,
 	{
 	{
 		SystemMessage temp_message(VLC->generaltexth->allTexts.at(260));
 		SystemMessage temp_message(VLC->generaltexth->allTexts.at(260));
 		sendAndApply(&temp_message);
 		sendAndApply(&temp_message);
-		checkVictoryLossConditionsForPlayer(player);//Player enter win code or got required art\creature
+
+		if(!player.isSpectator())
+			checkVictoryLossConditionsForPlayer(player);//Player enter win code or got required art\creature
 	}
 	}
 }
 }
 
 
@@ -5100,7 +5103,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player)
 
 
 			if (p->human)
 			if (p->human)
 			{
 			{
-				end2 = true;
+				serverShuttingDown = true;
 
 
 				if (gs->scenarioOps->campState)
 				if (gs->scenarioOps->campState)
 				{
 				{

+ 17 - 29
server/CVCMIServer.cpp

@@ -41,10 +41,7 @@
 
 
 std::string NAME_AFFIX = "server";
 std::string NAME_AFFIX = "server";
 std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; //application name
 std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; //application name
-#ifndef VCMI_ANDROID
-namespace intpr = boost::interprocess;
-#endif
-bool end2 = false;
+std::atomic<bool> serverShuttingDown(false);
 
 
 boost::program_options::variables_map cmdLineOptions;
 boost::program_options::variables_map cmdLineOptions;
 
 
@@ -107,7 +104,7 @@ void CPregameServer::handleConnection(CConnection *cpc)
 			}
 			}
 			else if(quitting) // Server must be stopped if host is leaving from lobby to avoid crash
 			else if(quitting) // Server must be stopped if host is leaving from lobby to avoid crash
 			{
 			{
-				end2 = true;
+				serverShuttingDown = true;
 			}
 			}
 		}
 		}
 	}
 	}
@@ -326,7 +323,7 @@ void CPregameServer::startListeningThread(CConnection * pc)
 }
 }
 
 
 CVCMIServer::CVCMIServer()
 CVCMIServer::CVCMIServer()
-	: port(3030), io(new boost::asio::io_service()), firstConnection(nullptr)
+	: port(3030), io(new boost::asio::io_service()), firstConnection(nullptr), shared(nullptr)
 {
 {
 	logNetwork->trace("CVCMIServer created!");
 	logNetwork->trace("CVCMIServer created!");
 	if(cmdLineOptions.count("port"))
 	if(cmdLineOptions.count("port"))
@@ -339,7 +336,7 @@ CVCMIServer::CVCMIServer()
 	catch(...)
 	catch(...)
 	{
 	{
 		logNetwork->info("Port %d is busy, trying to use random port instead", port);
 		logNetwork->info("Port %d is busy, trying to use random port instead", port);
-		if(cmdLineOptions.count("run-by-client") && !cmdLineOptions.count("use-shm"))
+		if(cmdLineOptions.count("run-by-client") && !cmdLineOptions.count("enable-shm"))
 		{
 		{
 			logNetwork->error("Cant pass port number to client without shared memory!", port);
 			logNetwork->error("Cant pass port number to client without shared memory!", port);
 			exit(0);
 			exit(0);
@@ -425,24 +422,14 @@ void CVCMIServer::newPregame()
 void CVCMIServer::start()
 void CVCMIServer::start()
 {
 {
 #ifndef VCMI_ANDROID
 #ifndef VCMI_ANDROID
-	ServerReady *sr = nullptr;
-	intpr::mapped_region *mr;
-	if(cmdLineOptions.count("use-shm"))
+	if(cmdLineOptions.count("enable-shm"))
 	{
 	{
-		try
-		{
-			intpr::shared_memory_object smo(intpr::open_only,"vcmi_memory",intpr::read_write);
-			smo.truncate(sizeof(ServerReady));
-			mr = new intpr::mapped_region(smo,intpr::read_write);
-			sr = reinterpret_cast<ServerReady*>(mr->get_address());
-		}
-		catch(...)
+		std::string sharedMemoryName = "vcmi_memory";
+		if(cmdLineOptions.count("enable-shm-uuid") && cmdLineOptions.count("uuid"))
 		{
 		{
-			intpr::shared_memory_object smo(intpr::create_only,"vcmi_memory",intpr::read_write);
-			smo.truncate(sizeof(ServerReady));
-			mr = new intpr::mapped_region(smo,intpr::read_write);
-			sr = new(mr->get_address())ServerReady();
+			sharedMemoryName += "_" + cmdLineOptions["uuid"].as<std::string>();
 		}
 		}
+		shared = new SharedMemory(sharedMemoryName);
 	}
 	}
 #endif
 #endif
 
 
@@ -460,10 +447,9 @@ void CVCMIServer::start()
 				logNetwork->info("Sending server ready message to client");
 				logNetwork->info("Sending server ready message to client");
 			}
 			}
 #else
 #else
-			if(cmdLineOptions.count("use-shm"))
+			if(shared)
 			{
 			{
-				sr->setToTrueAndNotify(port);
-				delete mr;
+				shared->sr->setToReadyAndNotify(port);
 			}
 			}
 #endif
 #endif
 
 
@@ -477,7 +463,7 @@ void CVCMIServer::start()
 			std::string name = NAME;
 			std::string name = NAME;
 			firstConnection = new CConnection(s, name.append(" STATE_WAITING"));
 			firstConnection = new CConnection(s, name.append(" STATE_WAITING"));
 			logNetwork->info("Got connection!");
 			logNetwork->info("Got connection!");
-			while(!end2)
+			while(!serverShuttingDown)
 			{
 			{
 				ui8 mode;
 				ui8 mode;
 				*firstConnection >> mode;
 				*firstConnection >> mode;
@@ -557,7 +543,9 @@ static void handleCommandOptions(int argc, char *argv[])
 		("help,h", "display help and exit")
 		("help,h", "display help and exit")
 		("version,v", "display version information and exit")
 		("version,v", "display version information and exit")
 		("run-by-client", "indicate that server launched by client on same machine")
 		("run-by-client", "indicate that server launched by client on same machine")
-		("use-shm", "enable usage of shared memory")
+		("uuid", po::value<std::string>(), "")
+		("enable-shm-uuid", "use UUID for shared memory identifier")
+		("enable-shm", "enable usage of shared memory")
 		("port", po::value<ui16>(), "port at which server will listen to connections from client")
 		("port", po::value<ui16>(), "port at which server will listen to connections from client")
 		("resultsFile", po::value<std::string>()->default_value("./results.txt"), "file to which the battle result will be appended. Used only in the DUEL mode.");
 		("resultsFile", po::value<std::string>()->default_value("./results.txt"), "file to which the battle result will be appended. Used only in the DUEL mode.");
 
 
@@ -649,7 +637,7 @@ int main(int argc, char** argv)
 
 
 		try
 		try
 		{
 		{
-			while (!end2)
+			while(!serverShuttingDown)
 			{
 			{
 				server.start();
 				server.start();
 			}
 			}
@@ -658,7 +646,7 @@ int main(int argc, char** argv)
 		catch (boost::system::system_error &e) //for boost errors just log, not crash - probably client shut down connection
 		catch (boost::system::system_error &e) //for boost errors just log, not crash - probably client shut down connection
 		{
 		{
 			logNetwork->error(e.what());
 			logNetwork->error(e.what());
-			end2 = true;
+			serverShuttingDown = true;
 		}
 		}
 		catch (...)
 		catch (...)
 		{
 		{

+ 2 - 0
server/CVCMIServer.h

@@ -17,6 +17,7 @@ class CMapInfo;
 class CConnection;
 class CConnection;
 struct CPackForSelectionScreen;
 struct CPackForSelectionScreen;
 class CGameHandler;
 class CGameHandler;
+struct SharedMemory;
 
 
 namespace boost
 namespace boost
 {
 {
@@ -46,6 +47,7 @@ class CVCMIServer
 	ui16 port;
 	ui16 port;
 	boost::asio::io_service *io;
 	boost::asio::io_service *io;
 	TAcceptor * acceptor;
 	TAcceptor * acceptor;
+	SharedMemory * shared;
 
 
 	CConnection *firstConnection;
 	CConnection *firstConnection;
 public:
 public:

+ 5 - 2
server/NetPacksServer.cpp

@@ -283,8 +283,11 @@ bool CastAdvSpell::applyGh( CGameHandler *gh )
 
 
 bool PlayerMessage::applyGh( CGameHandler *gh )
 bool PlayerMessage::applyGh( CGameHandler *gh )
 {
 {
-	ERROR_IF_NOT(player);
-	if(gh->getPlayerAt(c) != player) ERROR_AND_RETURN;
+	if(!player.isSpectator()) // TODO: clearly not a great way to verify permissions
+	{
+		ERROR_IF_NOT(player);
+		if(gh->getPlayerAt(c) != player) ERROR_AND_RETURN;
+	}
 	gh->playerMessage(player,text, currObj);
 	gh->playerMessage(player,text, currObj);
 	return true;
 	return true;
 }
 }