Browse Source

Merge pull request #306 from vcmi/feature/debugOptions

Debug features: auto tests, headless and spectator modes
ArseniyShestakov 8 years ago
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
 
 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 <utility>
 #include <vector>
+#include <atomic>
 
 //The only available version is 3, as of Boost 1.50
 #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/math/special_functions/round.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
 #  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;
 boost::mutex eventsM;
 
-bool gNoGUI = false;
 CondSh<bool> serverAlive(false);
 static po::variables_map vm;
 
@@ -114,6 +113,29 @@ void endGame();
 #include <getopt.h>
 #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)
 {
 	StartInfo si;
@@ -150,7 +172,7 @@ void init()
 	logGlobal->infoStream()<<"Initializing VCMI_Lib: "<<tmh.getDiff();
 
 
-	if(!gNoGUI)
+	if(!settings["session"]["headless"].Bool())
 	{
 		pomtime.getDiff();
 		CCS->curh = new CCursorHandler;
@@ -238,10 +260,19 @@ int main(int argc, char** argv)
 	opts.add_options()
 		("help,h", "display help 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")
 		("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")
-		("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")
 		("oneGoodAI", "puts one default AI and the rest will be EmptyAI")
 		("autoSkip", "automatically skip turns in GUI")
@@ -281,11 +312,6 @@ int main(int argc, char** argv)
 		prog_version();
 		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"))
 	{
 		CServerHandler::DO_NOT_START_SERVER = true;
@@ -314,9 +340,18 @@ int main(int argc, char** argv)
 	// Init filesystem and settings
 	preinitDLL(::console);
 	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
-	Settings session = settings.write["session"];
 	session["serverport"].Integer() = vm.count("serverport") ? vm["serverport"].as<si64>() : 0;
 	session["saveprefix"].String() = vm.count("saveprefix") ? vm["saveprefix"].as<std::string>() : "";
 	session["savefrequency"].Integer() = vm.count("savefrequency") ? vm["savefrequency"].as<si64>() : 1;
@@ -364,7 +399,7 @@ int main(int argc, char** argv)
 		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))
 		{
@@ -427,7 +462,7 @@ int main(int argc, char** argv)
 #ifdef DISABLE_VIDEO
 	CCS->videoh = new CEmptyVideoPlayer;
 #else
-	if (!gNoGUI && !vm.count("disable-video"))
+	if (!settings["session"]["headless"].Bool() && !vm.count("disable-video"))
 		CCS->videoh = new CVideoPlayer;
 	else
 		CCS->videoh = new CEmptyVideoPlayer;
@@ -456,7 +491,7 @@ int main(int argc, char** argv)
 	init();
 #endif
 
-	if(!gNoGUI )
+	if(!settings["session"]["headless"].Bool())
 	{
 		if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool())
 			playIntro();
@@ -480,7 +515,6 @@ int main(int argc, char** argv)
 
 	if(!vm.count("battle"))
 	{
-		Settings session = settings.write["session"];
 		session["autoSkip"].Bool()  = vm.count("autoSkip");
 		session["oneGoodAI"].Bool() = vm.count("oneGoodAI");
 		session["aiSolo"].Bool() = false;
@@ -488,17 +522,39 @@ int main(int argc, char** argv)
 		bfs::path fileToStartFrom; //none by default
 		if(vm.count("start"))
 			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
 		{
-			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
@@ -511,7 +567,7 @@ int main(int argc, char** argv)
 		startGame(si);
 	}
 
-	if(!gNoGUI)
+	if(!settings["session"]["headless"].Bool())
 	{
 		mainLoop();
 	}
@@ -554,6 +610,20 @@ void printInfoAboutIntObject(const CIntObject *obj, int level)
 		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)
 {
 	std::istringstream readed;
@@ -674,10 +744,6 @@ void processCommand(const std::string &message)
 		*ptr = 666;
 		//disaster!
 	}
-	else if(cn == "onlyai")
-	{
-		vm.insert(std::pair<std::string, po::variable_value>("onlyAI", po::variable_value()));
-	}
 	else if(cn == "mp" && adventureInt)
 	{
 		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)
 	{
 		YourTurn yt;
@@ -1275,7 +1327,7 @@ void startGame(StartInfo * options, CConnection *serv/* = nullptr*/)
 		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>();
 
@@ -1327,7 +1379,7 @@ void handleQuit(bool ask/* = true*/)
 		dispose();
 		vstd::clear_pointer(console);
 		boost::this_thread::sleep(boost::posix_time::milliseconds(750));
-		if(!gNoGUI)
+		if(!settings["session"]["headless"].Bool())
 		{
 			cleanupRenderer();
 			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 *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
 
+void removeGUI();
 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)
 {
+	if(playerColor.isSpectator())
+		playerColor = PlayerColor(1);
 	std::vector<const IImage*> &box = piecesOfBox.at(playerColor.getNum());
 
 	// 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)
 		return;
 
+	if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-ignore-hero"].Bool())
+		return;
+
 	const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero
 	int3 hp = details.start;
 
@@ -321,7 +324,12 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details)
 	}
 
 	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();
 	else
 		speed = settings["adventure"]["enemySpeed"].Float();
@@ -334,7 +342,6 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details)
 		return; // no animation
 	}
 
-
 	adventureInt->centerOn(hero); //actualizing screen pos
 	adventureInt->minimap.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;
 	return pos;
 }
+
+void CPlayerInterface::activateForSpectator()
+{
+	adventureInt->state = CAdvMapInt::INGAME;
+	adventureInt->activate();
+	adventureInt->minimap.activate();
+}
+
 void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val)
 {
 	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
-	if (adventureInt && !adventureInt->selection && GH.topInt() == adventureInt)
+	if(adventureInt && GH.topInt() == adventureInt
+		&& (!adventureInt->selection && !settings["session"]["spectate"].Bool()))
 	{
 		return;
 	}
@@ -2131,7 +2147,7 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul
 
 		--howManyPeople;
 
-		if (howManyPeople == 0) //all human players eliminated
+		if(howManyPeople == 0 && !settings["session"]["spectate"].Bool()) //all human players eliminated
 		{
 			if (adventureInt)
 			{
@@ -2152,7 +2168,7 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul
 		}
 		else
 		{
-			if (howManyPeople == 0) //all human players eliminated
+			if(howManyPeople == 0 && !settings["session"]["spectate"].Bool()) //all human players eliminated
 			{
 				requestReturningToMainMenu();
 			}

+ 1 - 0
client/CPlayerInterface.h

@@ -236,6 +236,7 @@ public:
 	void updateInfo(const CGObjectInstance * specific);
 	void init(std::shared_ptr<CCallback> CB) override;
 	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
 	void showInfoDialog(const std::string &text, CComponent * component);

+ 105 - 51
client/Client.cpp

@@ -41,9 +41,7 @@
 #include "CMT.h"
 
 extern std::string NAME;
-#ifndef VCMI_ANDROID
-namespace intpr = boost::interprocess;
-#else
+#ifdef VCMI_ANDROID
 #include "lib/CAndroidVMHelper.h"
 #endif
 
@@ -432,10 +430,13 @@ void CClient::newGame( CConnection *con, StartInfo *si )
 	// Init map handler
 	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());
 		logNetwork->infoStream() << "Initializing mapHandler (together): " << tmh.getDiff();
 	}
@@ -472,7 +473,7 @@ void CClient::newGame( CConnection *con, StartInfo *si )
 
 	if(si->mode == StartInfo::DUEL)
 	{
-		if(!gNoGUI)
+		if(!settings["session"]["headless"].Bool())
 		{
 			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
 			auto p = std::make_shared<CPlayerInterface>(PlayerColor::NEUTRAL);
@@ -484,6 +485,10 @@ void CClient::newGame( CConnection *con, StartInfo *si )
 	}
 	else
 	{
+		if(settings["session"]["spectate"].Bool())
+		{
+			installNewPlayerInterface(std::make_shared<CPlayerInterface>(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true);
+		}
 		loadNeutralBattleAI();
 	}
 
@@ -635,10 +640,33 @@ void CClient::serialize(BinaryDeserializer & h, const int version, const std::se
 			nInt->human = isHuman;
 			nInt->playerID = pid;
 
-			if(playerIDs.count(pid))
-				installNewPlayerInterface(nInt, pid);
-
 			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))
@@ -684,19 +712,22 @@ void CClient::stopConnection()
 {
 	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);
-		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
@@ -744,14 +775,29 @@ void CClient::battleStarted(const BattleInfo * info)
 			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){
@@ -762,6 +808,8 @@ void CClient::battleStarted(const BattleInfo * info)
 	callBattleStart(leftSide.color, 0);
 	callBattleStart(rightSide.color, 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))
 	{
@@ -774,6 +822,9 @@ void CClient::battleFinished()
 	for(auto & side : gs->curB->sides)
 		if(battleCallbacks.count(side.color))
 			battleCallbacks[side.color]->setBattle(nullptr);
+
+	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
+		battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr);
 }
 
 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);
 	PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE);
@@ -887,7 +938,7 @@ void CClient::installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInte
 	battleCallbacks[colorUsed] = 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*/)
@@ -918,6 +969,11 @@ std::string CClient::aiNameForPlayer(const PlayerSettings &ps, bool battleAI)
 			return ps.name;
 	}
 
+	return aiNameForPlayer(battleAI);
+}
+
+std::string CClient::aiNameForPlayer(bool battleAI)
+{
 	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 badAI = battleAI ? "StupidAI" : "EmptyAI";
@@ -961,13 +1017,7 @@ void CServerHandler::waitForServer()
 
 #ifndef VCMI_ANDROID
 	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
 	logNetwork->infoStream() << "waiting for server";
 	while (!androidTestServerReadyFlag.load())
@@ -984,15 +1034,7 @@ void CServerHandler::waitForServer()
 
 CConnection * CServerHandler::connectToServer()
 {
-#ifndef VCMI_ANDROID
-	if(shared)
-	{
-		if(!shared->sr->ready)
-			waitForServer();
-	}
-#else
 	waitForServer();
-#endif
 
 	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;
 	shared = nullptr;
 	verbose = true;
+	uuid = boost::uuids::to_string(boost::uuids::random_generator()());
 
 #ifndef VCMI_ANDROID
-	if(DO_NOT_START_SERVER)
+	if(DO_NOT_START_SERVER || settings["session"]["disable-shm"].Bool())
 		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
 	{
-		shared = new SharedMem();
+		shared = new SharedMemory(sharedMemoryName, true);
 	}
 	catch(...)
 	{
@@ -1057,11 +1105,17 @@ void CServerHandler::callServer()
 #ifndef VCMI_ANDROID
 	setThreadName("CServerHandler::callServer");
 	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()
 		+ " --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());
 	if (result == 0)

+ 5 - 3
client/Client.h

@@ -28,7 +28,7 @@ class CGameInterface;
 class CConnection;
 class CCallback;
 struct BattleAction;
-struct SharedMem;
+struct SharedMemory;
 class CClient;
 class CScriptingModule;
 struct CPathsInfo;
@@ -46,7 +46,8 @@ public:
 
 	CStopWatch th;
 	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
 
 	//functions setting up local server
@@ -148,9 +149,10 @@ public:
 	void newGame(CConnection *con, StartInfo *si); //con - connection to server
 
 	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);
 	std::string aiNameForPlayer(const PlayerSettings &ps, bool battleAI); //empty means no AI -> human
+	std::string aiNameForPlayer(bool battleAI);
 
 	void endGame(bool closeConnection = true);
 	void stopConnection();

+ 46 - 29
client/NetPacksClient.cpp

@@ -96,6 +96,10 @@
 #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[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__)
 /*
  * NetPacksClient.cpp, part of VCMI engine
@@ -283,13 +287,13 @@ void GiveBonus::applyCl(CClient *cl)
 void ChangeObjPos::applyFirstCl(CClient *cl)
 {
 	CGObjectInstance *obj = GS(cl)->getObjInstance(objid);
-	if(flags & 1)
+	if(flags & 1 && CGI->mh)
 		CGI->mh->hideObject(obj);
 }
 void ChangeObjPos::applyCl(CClient *cl)
 {
 	CGObjectInstance *obj = GS(cl)->getObjInstance(objid);
-	if(flags & 1)
+	if(flags & 1 && CGI->mh)
 		CGI->mh->printObject(obj);
 
 	cl->invalidatePaths();
@@ -298,6 +302,10 @@ void ChangeObjPos::applyCl(CClient *cl)
 void PlayerEndsGame::applyCl(CClient *cl)
 {
 	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)
@@ -335,7 +343,8 @@ void RemoveObject::applyFirstCl(CClient *cl)
 {
 	const CGObjectInstance *o = cl->getObj(id);
 
-	CGI->mh->hideObject(o, true);
+	if(CGI->mh)
+		CGI->mh->hideObject(o, true);
 
 	//notify interfaces about removal
 	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
 	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)
 		CGI->mh->hideObject(h, result == EMBARK && humanKnows);
 
-
 	if(result == DISEMBARK)
 		CGI->mh->printObject(h->boat);
 }
@@ -378,13 +389,14 @@ void TryMoveHero::applyCl(CClient *cl)
 	const CGHeroInstance *h = cl->getHero(id);
 	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;
 
@@ -395,18 +407,17 @@ void TryMoveHero::applyCl(CClient *cl)
 	//notify interfaces about move
 	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);
 		}
 	}
 
-	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);
-	}
 }
 
 void NewStructures::applyCl(CClient *cl)
@@ -482,22 +493,22 @@ void HeroRecruited::applyCl(CClient *cl)
 			needsPrinting = false;
 		}
 	}
-	if (needsPrinting)
-	{
+	if(needsPrinting && CGI->mh)
 		CGI->mh->printObject(h);
-	}
 }
 
 void GiveHero::applyCl(CClient *cl)
 {
 	CGHeroInstance *h = GS(cl)->getHero(id);
-	CGI->mh->printObject(h);
+	if(CGI->mh)
+		CGI->mh->printObject(h);
 	cl->playerint[h->tempOwner]->heroCreated(h);
 }
 
 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)
@@ -588,6 +599,8 @@ void BattleStart::applyFirstCl(CClient *cl)
 		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,
 		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,
 		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(player2, battleResultsApplied);
-	INTERFACE_CALL_IF_PRESENT(PlayerColor::UNFLAGGABLE, battleResultsApplied);
+	INTERFACE_CALL_IF_PRESENT(PlayerColor::SPECTATOR, battleResultsApplied);
 	if(GS(cl)->initialOpts->mode == StartInfo::DUEL)
 	{
 		handleQuit();
@@ -808,7 +821,10 @@ void PlayerMessage::applyCl(CClient *cl)
 	logNetwork->debugStream() << "Player "<< player <<" sends a message: " << text;
 
 	std::ostringstream str;
-	str << cl->getPlayer(player)->nodeName() <<": " << text;
+	if(player.isSpectator())
+		str << "Spectator: " << text;
+	else
+		str << cl->getPlayer(player)->nodeName() <<": " << text;
 	if(LOCPLINT)
 		LOCPLINT->cingconsole->print(str.str());
 }
@@ -904,7 +920,8 @@ void NewObject::applyCl(CClient *cl)
 	cl->invalidatePaths();
 
 	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++)
 	{

+ 16 - 2
client/battle/CBattleInterface.cpp

@@ -95,7 +95,7 @@ void CBattleInterface::addNewAnim(CBattleAnimation *anim)
 CBattleInterface::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> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt)
 	: background(nullptr), queue(nullptr), attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0),
       activeStack(nullptr), mouseHoveredStack(nullptr), stackToActivate(nullptr), selectedStack(nullptr), previouslyHoveredHex(-1),
 	  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;
 
-	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
 		curInt = defenderInt;
 	}
 
+
 	animsAreDisplayed.setn(false);
 	pos = myRect;
 	strongInterest = true;
@@ -377,6 +380,8 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 	currentAction = INVALID;
 	selectedAction = INVALID;
 	addUsedEvents(RCLICK | MOVE | KEYBOARD);
+
+	blockUI(settings["session"]["spectate"].Bool());
 }
 
 CBattleInterface::~CBattleInterface()
@@ -448,6 +453,7 @@ CBattleInterface::~CBattleInterface()
 		int terrain = LOCPLINT->cb->getTile(adventureInt->selection->visitablePos())->terType;
 		CCS->musich->playMusicFromSet("terrain", terrain, true);
 	}
+	animsAreDisplayed.setn(false);
 }
 
 void CBattleInterface::setPrintCellBorders(bool set)
@@ -1246,6 +1252,11 @@ void CBattleInterface::battleFinished(const BattleResult& br)
 void CBattleInterface::displayBattleFinished()
 {
 	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);
 	resWindow = new CBattleResultWindow(*bresult, temp_rect, *this->curInt);
@@ -1531,6 +1542,9 @@ void CBattleInterface::setAnimSpeed(int set)
 
 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);
 }
 

+ 1 - 1
client/battle/CBattleInterface.h

@@ -269,7 +269,7 @@ public:
 	ui32 animIDhelper; //for giving IDs for animations
 	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
 
 	//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;
 
 	InfoAboutHero targetHero;
-
-	if (down && myOwner->myTurn)
+	if(down && (myOwner->myTurn || settings["session"]["spectate"].Bool()))
 	{
 		auto h = flip ? myOwner->defendingHeroInstance : myOwner->attackingHeroInstance;
 		targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE);

+ 43 - 1
client/gui/CGuiHandler.cpp

@@ -12,6 +12,7 @@
 #include "../../lib/CConfigHandler.h"
 #include "../CMT.h"
 #include "../CPlayerInterface.h"
+#include "../battle/CBattleInterface.h"
 
 extern std::queue<SDL_Event> events;
 extern boost::mutex eventsM;
@@ -191,6 +192,46 @@ void CGuiHandler::handleEvent(SDL_Event *sEvent)
 	if (sEvent->type==SDL_KEYDOWN || sEvent->type==SDL_KEYUP)
 	{
 		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
 		if(key.keysym.sym == SDLK_KP_ENTER)
@@ -361,7 +402,8 @@ void CGuiHandler::simpleRedraw()
 	//update only top interface and draw background
 	if(objsToBlit.size() > 1)
 		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)

+ 6 - 3
client/mapHandler.cpp

@@ -54,7 +54,7 @@ struct NeighborTilesInfo
 			if ( dx + pos.x < 0 || dx + pos.x >= sizes.x
 			  || dy + pos.y < 0 || dy + pos.y >= sizes.y)
 				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
 		d8 = getTile( 0, -1); //456
@@ -563,7 +563,7 @@ void CMapHandler::CMapWorldViewBlitter::drawTileOverlay(SDL_Surface * targetSurf
 		const CGObjectInstance * obj = object.obj;
 
 		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);
 
 		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];
 
-				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);
 
 				// 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
 {
+	if(settings["session"]["spectate"].Bool())
+		return true;
+
 	const NeighborTilesInfo neighbors(pos, parent->sizes, *info->visibilityMap);
 	return !neighbors.areAllHidden();
 }

+ 14 - 2
client/windows/CAdvmapInterface.cpp

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

+ 2 - 0
client/windows/InfoWindows.cpp

@@ -335,6 +335,8 @@ void CRClickPopup::close()
 void CRClickPopup::createAndPush(const std::string &txt, const CInfoWindow::TCompsInfo &comps)
 {
 	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);
 	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
 {
 	RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID);
-	if(!player)
+	if(!player || player.get().isSpectator())
 		return BattlePerspective::ALL_KNOWING;
 	if(*player == getBattle()->sides[0].color)
 		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
 {
-	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

+ 3 - 0
lib/CGameState.cpp

@@ -2208,6 +2208,9 @@ bool CGameState::isVisible(int3 pos, PlayerColor player)
 {
 	if(player == PlayerColor::NEUTRAL)
 		return false;
+	if(player.isSpectator())
+		return true;
+
 	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::ARROW_TOWERS_SLOT = SlotID(-5);
 
+const PlayerColor PlayerColor::SPECTATOR = PlayerColor(252);
 const PlayerColor PlayerColor::CANNOT_DETERMINE = PlayerColor(253);
 const PlayerColor PlayerColor::UNFLAGGABLE = PlayerColor(254);
 const PlayerColor PlayerColor::NEUTRAL = PlayerColor(255);
@@ -72,6 +73,11 @@ bool PlayerColor::isValidPlayer() const
 	return num < PLAYER_LIMIT_I;
 }
 
+bool PlayerColor::isSpectator() const
+{
+	return num == 252;
+}
+
 std::string PlayerColor::getStr(bool L10n) const
 {
 	std::string ret = "unnamed";

+ 2 - 0
lib/GameConstants.h

@@ -257,12 +257,14 @@ class PlayerColor : public BaseForID<PlayerColor, ui8>
 		PLAYER_LIMIT_I = 8
 	};
 
+	DLL_LINKAGE static const PlayerColor SPECTATOR; //252
 	DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253
 	DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks)
 	DLL_LINKAGE static const PlayerColor NEUTRAL; //255
 	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 isSpectator() const;
 
 	DLL_LINKAGE std::string getStr(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;
 	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()
 	{
@@ -29,10 +29,19 @@ struct ServerReady
 		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;
 			port = Port;
 		}
@@ -40,22 +49,33 @@ struct ServerReady
 	}
 };
 
-struct SharedMem 
+struct SharedMemory
 {
+	const char * name;
 	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));
 		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;
-		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)
 {
+	if(!player.isValidPlayer())
+		return;
+
 	gs->getPlayer(player)->enteredLosingCheatCode = losingCheatCode;
 	gs->getPlayer(player)->enteredWinningCheatCode = winningCheatCode;
 }

+ 15 - 12
server/CGameHandler.cpp

@@ -48,7 +48,7 @@
 #ifndef _MSC_VER
 #include <boost/thread/xtime.hpp>
 #endif
-extern bool end2;
+extern std::atomic<bool> serverShuttingDown;
 #ifdef min
 #undef min
 #endif
@@ -1033,12 +1033,13 @@ void CGameHandler::handleConnection(std::set<PlayerColor> players, CConnection &
 
 	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
 		logGlobal->error(e.what());
 		conns -= &c;
 		for(auto playerConn : connections)
 		{
-			if(playerConn.second == &c)
+			if(!serverShuttingDown && playerConn.second == &c)
 			{
 				PlayerCheated pc;
 				pc.player = playerConn.first;
@@ -1128,7 +1129,7 @@ void CGameHandler::handleConnection(std::set<PlayerColor> players, CConnection &
 	}
 	catch(...)
 	{
-		end2 = true;
+		serverShuttingDown = true;
 		handleException();
 		throw;
 	}
@@ -1878,7 +1879,8 @@ void CGameHandler::run(bool resume)
 			sbuffer << color << " ";
 			{
 				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());
@@ -1901,7 +1903,7 @@ void CGameHandler::run(bool resume)
 	if (gs->scenarioOps->mode == StartInfo::DUEL)
 	{
 		runBattle();
-		end2 = true;
+		serverShuttingDown = true;
 
 
 		while(conns.size() && (*conns.begin())->isOpen())
@@ -1912,7 +1914,7 @@ void CGameHandler::run(bool resume)
 
 	auto playerTurnOrder = generatePlayerTurnOrder();
 
-	while(!end2)
+	while(!serverShuttingDown)
 	{
 		if (!resume) newTurn();
 
@@ -1953,7 +1955,7 @@ void CGameHandler::run(bool resume)
 
 					//wait till turn is done
 					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);
 						states.cv.timed_wait(lock, p);
@@ -1969,7 +1971,7 @@ void CGameHandler::run(bool resume)
 					activePlayer = true;
 		}
 		if (!activePlayer)
-			end2 = true;
+			serverShuttingDown = true;
 	}
 	while(conns.size() && (*conns.begin())->isOpen())
 		boost::this_thread::sleep(boost::posix_time::milliseconds(5)); //give time client to close socket
@@ -2733,7 +2735,7 @@ void CGameHandler::close()
 	{
 		exit(0);
 	}
-	end2 = true;
+	serverShuttingDown = true;
 
 	for (auto & elem : conns)
 	{
@@ -2744,7 +2746,6 @@ void CGameHandler::close()
 		elem->close();
 		elem->connected = false;
 	}
-	exit(0);
 }
 
 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));
 		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)
 			{
-				end2 = true;
+				serverShuttingDown = true;
 
 				if (gs->scenarioOps->campState)
 				{

+ 17 - 29
server/CVCMIServer.cpp

@@ -41,10 +41,7 @@
 
 std::string NAME_AFFIX = "server";
 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;
 
@@ -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
 			{
-				end2 = true;
+				serverShuttingDown = true;
 			}
 		}
 	}
@@ -326,7 +323,7 @@ void CPregameServer::startListeningThread(CConnection * pc)
 }
 
 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!");
 	if(cmdLineOptions.count("port"))
@@ -339,7 +336,7 @@ CVCMIServer::CVCMIServer()
 	catch(...)
 	{
 		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);
 			exit(0);
@@ -425,24 +422,14 @@ void CVCMIServer::newPregame()
 void CVCMIServer::start()
 {
 #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
 
@@ -460,10 +447,9 @@ void CVCMIServer::start()
 				logNetwork->info("Sending server ready message to client");
 			}
 #else
-			if(cmdLineOptions.count("use-shm"))
+			if(shared)
 			{
-				sr->setToTrueAndNotify(port);
-				delete mr;
+				shared->sr->setToReadyAndNotify(port);
 			}
 #endif
 
@@ -477,7 +463,7 @@ void CVCMIServer::start()
 			std::string name = NAME;
 			firstConnection = new CConnection(s, name.append(" STATE_WAITING"));
 			logNetwork->info("Got connection!");
-			while(!end2)
+			while(!serverShuttingDown)
 			{
 				ui8 mode;
 				*firstConnection >> mode;
@@ -557,7 +543,9 @@ static void handleCommandOptions(int argc, char *argv[])
 		("help,h", "display help and exit")
 		("version,v", "display version information and exit")
 		("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")
 		("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
 		{
-			while (!end2)
+			while(!serverShuttingDown)
 			{
 				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
 		{
 			logNetwork->error(e.what());
-			end2 = true;
+			serverShuttingDown = true;
 		}
 		catch (...)
 		{

+ 2 - 0
server/CVCMIServer.h

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

+ 5 - 2
server/NetPacksServer.cpp

@@ -283,8 +283,11 @@ bool CastAdvSpell::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);
 	return true;
 }