Przeglądaj źródła

Merge branch 'develop' of https://github.com/vcmi/vcmi into develop

DJWarmonger 8 lat temu
rodzic
commit
e901fcd293
82 zmienionych plików z 1309 dodań i 823 usunięć
  1. 5 7
      .travis.yml
  2. 3 4
      AI/BattleAI/BattleAI.cpp
  3. 5 0
      ChangeLog
  4. 4 0
      Global.h
  5. 104 53
      client/CMT.cpp
  6. 1 3
      client/CMT.h
  7. 2 0
      client/CMessage.cpp
  8. 23 12
      client/CPlayerInterface.cpp
  9. 1 0
      client/CPlayerInterface.h
  10. 15 12
      client/CPreGame.cpp
  11. 3 3
      client/CPreGame.h
  12. 140 71
      client/Client.cpp
  13. 9 6
      client/Client.h
  14. 46 29
      client/NetPacksClient.cpp
  15. 0 2
      client/VCMI_client.vcxproj
  16. 2 8
      client/VCMI_client.vcxproj.filters
  17. 38 14
      client/battle/CBattleInterface.cpp
  18. 1 1
      client/battle/CBattleInterface.h
  19. 4 8
      client/battle/CBattleInterfaceClasses.cpp
  20. 91 59
      client/gui/CGuiHandler.cpp
  21. 3 0
      client/gui/CGuiHandler.h
  22. 22 12
      client/gui/CIntObject.cpp
  23. 10 7
      client/gui/CIntObject.h
  24. 6 3
      client/mapHandler.cpp
  25. 1 1
      client/widgets/AdventureMapClasses.cpp
  26. 1 1
      client/widgets/Buttons.cpp
  27. 10 9
      client/widgets/CArtifactHolder.cpp
  28. 53 31
      client/windows/CAdvmapInterface.cpp
  29. 5 8
      client/windows/CAdvmapInterface.h
  30. 2 1
      client/windows/CCastleInterface.cpp
  31. 3 3
      client/windows/CSpellWindow.cpp
  32. 5 1
      client/windows/CWindowObject.cpp
  33. 1 1
      client/windows/GUIClasses.cpp
  34. 2 0
      client/windows/InfoWindows.cpp
  35. 9 5
      config/artifacts.json
  36. 7 1
      config/schemas/artifact.json
  37. 8 4
      config/schemas/settings.json
  38. 1 1
      launcher/settingsView/csettingsview_moc.cpp
  39. 26 12
      lib/BattleInfo.cpp
  40. 45 66
      lib/CArtHandler.cpp
  41. 12 8
      lib/CArtHandler.h
  42. 2 103
      lib/CBattleCallback.cpp
  43. 0 4
      lib/CBattleCallback.h
  44. 20 0
      lib/CCreatureHandler.cpp
  45. 13 0
      lib/CCreatureHandler.h
  46. 1 1
      lib/CGameInfoCallback.cpp
  47. 40 6
      lib/CGameState.cpp
  48. 2 2
      lib/CGameState.h
  49. 1 1
      lib/CTownHandler.cpp
  50. 6 0
      lib/GameConstants.cpp
  51. 2 0
      lib/GameConstants.h
  52. 35 12
      lib/Interprocess.h
  53. 22 0
      lib/NetPacks.h
  54. 9 0
      lib/NetPacksLib.cpp
  55. 1 1
      lib/VCMI_lib.vcxproj
  56. 1 1
      lib/VCMI_lib.vcxproj.filters
  57. 41 40
      lib/mapObjects/CGHeroInstance.cpp
  58. 1 1
      lib/mapObjects/MiscObjects.cpp
  59. 3 1
      lib/mapping/CMapInfo.cpp
  60. 27 14
      lib/mapping/CMapService.cpp
  61. 6 3
      lib/mapping/CMapService.h
  62. 10 2
      lib/mapping/MapFormatH3M.cpp
  63. 5 0
      lib/mapping/MapFormatJson.cpp
  64. 2 0
      lib/registerTypes/RegisterTypes.h
  65. 0 1
      lib/registerTypes/TypesClientPacks1.cpp
  66. 0 1
      lib/registerTypes/TypesClientPacks2.cpp
  67. 0 1
      lib/registerTypes/TypesMapObjects1.cpp
  68. 0 1
      lib/registerTypes/TypesMapObjects2.cpp
  69. 0 1
      lib/registerTypes/TypesMapObjects3.cpp
  70. 0 1
      lib/registerTypes/TypesServerPacks.cpp
  71. 1 1
      lib/serializer/CSerializer.h
  72. 8 4
      lib/serializer/Connection.cpp
  73. 2 1
      lib/serializer/Connection.h
  74. 47 3
      lib/spells/CSpellHandler.cpp
  75. 6 2
      lib/spells/ISpellMechanics.cpp
  76. 2 1
      lib/spells/ISpellMechanics.h
  77. 125 58
      server/CGameHandler.cpp
  78. 7 0
      server/CGameHandler.h
  79. 115 83
      server/CVCMIServer.cpp
  80. 3 1
      server/CVCMIServer.h
  81. 11 2
      server/NetPacksServer.cpp
  82. 3 2
      test/CMapEditManagerTest.cpp

+ 5 - 7
.travis.yml

@@ -36,9 +36,6 @@ matrix:
     - os: linux
       compiler: clang
       env: VCMI_PLATFORM='linux' REAL_CC=clang-3.6 REAL_CXX=clang++-3.6 PACKAGE=clang-3.6 SUPPORT=libstdc++-4.8-dev VCMI_CMAKE_FLAGS='-DENABLE_TEST=0'
-    - os: linux
-      compiler: clang
-      env: VCMI_PLATFORM='linux' REAL_CC=clang-3.5 REAL_CXX=clang++-3.5 PACKAGE=clang-3.5 SUPPORT=libstdc++-4.8-dev VCMI_CMAKE_FLAGS='-DENABLE_TEST=0'
     - os: linux
       compiler: clang
       env: VCMI_PLATFORM='linux' REAL_CC=clang-3.4 REAL_CXX=clang++-3.4 PACKAGE=clang-3.4 SUPPORT=libstdc++-4.8-dev VCMI_CMAKE_FLAGS='-DENABLE_TEST=0'
@@ -57,14 +54,15 @@ addons:
       name: vcmi/vcmi
       description: Build submitted via Travis CI
     notification_email: [email protected]
-    build_command_prepend: "cov-configure --compiler clang-3.6 --comptype clangcc && cov-configure --comptype clangcxx --compiler clang++-3.6 && cmake -G Ninja .. -DCMAKE_BUILD_TYPE=DEBUG -DENABLE_LAUNCHER=1 -DENABLE_TEST=0"
-    build_command: ninja -j 2
+    build_command_prepend: "cov-configure --compiler clang-3.6 --comptype clangcc && cov-configure --comptype clangcxx --compiler clang++-3.6 && cmake -G Ninja .. -DCMAKE_BUILD_TYPE=DEBUG -DENABLE_LAUNCHER=0 -DENABLE_TEST=0"
+    build_command: ninja -j 3
     branch_pattern: coverity_scan
 
 notifications:
   email:
     recipients:
-      - [email protected]
-      - [email protected]
+      - [email protected]
     on_success: change
     on_failure: always
+  slack:
+    secure: "KHXFe14FFKtw5mErWbj730+utqy7i/3AUobWfAMAGvWI5sJYlhbBU+KvvCoD2SlRQg3mQqgwVw8NBJF1Mffs7WcRmrFFFmuMqZxFLAfKBd3T0CxWpAGfnfNgDmlfV4OfEgQWk1pakEPOymhxbbmLUuCjykZDuTcioxAk0UAHDwY="

+ 3 - 4
AI/BattleAI/BattleAI.cpp

@@ -186,16 +186,15 @@ void CBattleAI::attemptCastingSpell()
 	if(!hero)
 		return;
 
-	if(!cb->battleCanCastSpell())
+	if(cb->battleCanCastSpell(hero, ECastingMode::HERO_CASTING) != ESpellCastProblem::OK)
 		return;
 
 	LOGL("Casting spells sounds like fun. Let's see...");
 	//Get all spells we can cast
 	std::vector<const CSpell*> possibleSpells;
-	vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [this] (const CSpell *s) -> bool
+	vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [this, hero] (const CSpell *s) -> bool
 	{
-		auto problem = getCbc()->battleCanCastThisSpell(s);
-		return problem == ESpellCastProblem::OK;
+		return s->canBeCast(getCbc().get(), ECastingMode::HERO_CASTING, hero) == ESpellCastProblem::OK;
 	});
 	LOGFL("I can cast %d spells.", possibleSpells.size());
 

+ 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

+ 104 - 53
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")
@@ -254,9 +285,9 @@ int main(int argc, char** argv)
         ("loadplayer", po::value<int>(),"specifies which player we are in multiplayer loaded games (0=Red, etc.)")
         ("loadserverip",po::value<std::string>(),"IP for loaded game server")
 		("loadserverport",po::value<std::string>(),"port for loaded game server")
-		("testingport",po::value<std::string>(),"port for testing, override specified in config file")
-		("testingfileprefix",po::value<std::string>(),"prefix for auto save files")
-		("testingsavefrequency",po::value<int>(),"how often auto save should be created");
+		("serverport", po::value<si64>(), "override port specified in config file")
+		("saveprefix", po::value<std::string>(), "prefix for auto save files")
+		("savefrequency", po::value<si64>(), "limit auto save creation to each N days");
 
 	if(argc > 1)
 	{
@@ -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,16 +340,21 @@ int main(int argc, char** argv)
 	// Init filesystem and settings
 	preinitDLL(::console);
 	settings.init();
-
-	// Init special testing settings
-	Settings testingSettings = settings.write["testing"];
-	if(vm.count("testingport") && vm.count("testingfileprefix"))
+	Settings session = settings.write["session"];
+	session["onlyai"].Bool() = vm.count("onlyAI");
+	if(vm.count("headless"))
 	{
-		testingSettings["enabled"].Bool() = true;
-		testingSettings["port"].String() = vm["testingport"].as<std::string>();
-		testingSettings["prefix"].String() = vm["testingfileprefix"].as<std::string>();
-		testingSettings["savefrequency"].Float() = vm.count("testingsavefrequency") ? vm["testingsavefrequency"].as<int>() : 1;
+		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
+	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;
 
 	// Initialize logging based on settings
 	logConfig.configure();
@@ -368,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))
 		{
@@ -431,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;
@@ -460,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();
@@ -484,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;
@@ -492,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
@@ -515,7 +567,7 @@ int main(int argc, char** argv)
 		startGame(si);
 	}
 
-	if(!gNoGUI)
+	if(!settings["session"]["headless"].Bool())
 	{
 		mainLoop();
 	}
@@ -558,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;
@@ -678,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))
@@ -817,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;
@@ -1273,10 +1321,13 @@ static void mainLoop()
 
 void startGame(StartInfo * options, CConnection *serv/* = nullptr*/)
 {
-	serverAlive.waitWhileTrue();
-	serverAlive.setn(true);
+	if(!CServerHandler::DO_NOT_START_SERVER)
+	{
+		serverAlive.waitWhileTrue();
+		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>();
 
@@ -1306,7 +1357,7 @@ void startGame(StartInfo * options, CConnection *serv/* = nullptr*/)
         if(!vm.count("loadplayer"))
             client->loadGame(fname);
         else
-            client->loadGame(fname,vm.count("loadserver"),vm.count("loadhumanplayerindices") ? vm["loadhumanplayerindices"].as<std::vector<int>>() : std::vector<int>(),vm.count("loadnumplayers") ? vm["loadnumplayers"].as<int>() : 1,vm["loadplayer"].as<int>(),vm.count("loadserverip") ? vm["loadserverip"].as<std::string>() : "", vm.count("loadserverport") ? vm["loadserverport"].as<std::string>() : "3030");
+			client->loadGame(fname,vm.count("loadserver"),vm.count("loadhumanplayerindices") ? vm["loadhumanplayerindices"].as<std::vector<int>>() : std::vector<int>(),vm.count("loadnumplayers") ? vm["loadnumplayers"].as<int>() : 1,vm["loadplayer"].as<int>(),vm.count("loadserverip") ? vm["loadserverip"].as<std::string>() : "", vm.count("loadserverport") ? vm["loadserverport"].as<ui16>() : CServerHandler::getDefaultPort());
 		break;
 	}
 
@@ -1328,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.

+ 23 - 12
client/CPlayerInterface.cpp

@@ -171,12 +171,7 @@ void CPlayerInterface::yourTurn()
 		GH.curInt = this;
 		adventureInt->selection = nullptr;
 
-		std::string prefix = "";
-		if (settings["testing"]["enabled"].Bool())
-		{
-			prefix = settings["testing"]["prefix"].String();
-		}
-
+		std::string prefix = settings["session"]["saveprefix"].String();
 		if (firstCall)
 		{
 			if (howManyPeople == 1)
@@ -192,7 +187,7 @@ void CPlayerInterface::yourTurn()
 			}
 			firstCall = 0;
 		}
-		else if (settings["testing"].isNull() || cb->getDate() % static_cast<int>(settings["testing"]["savefrequency"].Float()) == 0)
+		else if (cb->getDate() % static_cast<int>(settings["session"]["savefrequency"].Integer()) == 0)
 		{
 			LOCPLINT->cb->save("Saves/" + prefix + "Autosave_" + boost::lexical_cast<std::string>(autosaveCount++ + 1));
 			autosaveCount %= 5;
@@ -250,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;
 
@@ -326,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();
@@ -339,7 +342,6 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details)
 		return; // no animation
 	}
 
-
 	adventureInt->centerOn(hero); //actualizing screen pos
 	adventureInt->minimap.redraw();
 	adventureInt->heroList.redraw();
@@ -476,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;
@@ -1642,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;
 	}
@@ -2136,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)
 			{
@@ -2157,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);

+ 15 - 12
client/CPreGame.cpp

@@ -83,7 +83,7 @@ struct EvilHlpStruct
 
 	void reset()
 	{
-		vstd::clear_pointer(serv);
+//		vstd::clear_pointer(serv);
 		vstd::clear_pointer(sInfo);
 	}
 
@@ -131,10 +131,10 @@ void setPlayer(PlayerSettings &pset, ui8 player, const std::map<ui8, std::string
 		playerColor = pset.color;
 }
 
-void updateStartInfo(std::string filename, StartInfo & sInfo, const CMapHeader * mapHeader, const std::map<ui8, std::string> &playerNames)
+void updateStartInfo(std::string filename, StartInfo & sInfo, const std::unique_ptr<CMapHeader> & mapHeader, const std::map<ui8, std::string> &playerNames)
 {
 	sInfo.playerInfos.clear();
-	if(!mapHeader)
+	if(!mapHeader.get())
 	{
 		return;
 	}
@@ -559,7 +559,7 @@ void CGPreGame::removeFromGui()
 	GH.popInt(GH.topInt()); //remove background
 }
 
-CSelectionScreen::CSelectionScreen(CMenuScreen::EState Type, CMenuScreen::EMultiMode MultiPlayer /*= CMenuScreen::SINGLE_PLAYER*/, const std::map<ui8, std::string> * Names /*= nullptr*/, const std::string & Address /*=""*/, const std::string & Port /*= ""*/)
+CSelectionScreen::CSelectionScreen(CMenuScreen::EState Type, CMenuScreen::EMultiMode MultiPlayer /*= CMenuScreen::SINGLE_PLAYER*/, const std::map<ui8, std::string> * Names /*= nullptr*/, const std::string & Address /*=""*/, const ui16 Port)
 	: ISelectionScreenInfo(Names), serverHandlingThread(nullptr), mx(new boost::recursive_mutex),
 	  serv(nullptr), ongoingClosing(false), myNameID(255)
 {
@@ -799,7 +799,12 @@ void CSelectionScreen::changeSelection(const CMapInfo * to)
 	   SEL->sInfo.difficulty = to->scenarioOpts->difficulty;
 	if(screenType != CMenuScreen::campaignList)
 	{
-		updateStartInfo(to ? to->fileURI : "", sInfo, to ? to->mapHeader.get() : nullptr);
+		std::unique_ptr<CMapHeader> emptyHeader;
+        if(to)
+			updateStartInfo(to->fileURI, sInfo, to->mapHeader);
+		else
+			updateStartInfo("", sInfo, emptyHeader);
+
 		if(screenType == CMenuScreen::newGame)
 		{
 			if(to && to->isRandomMap)
@@ -3219,7 +3224,7 @@ void CHotSeatPlayers::enterSelectionScreen()
 void CBonusSelection::init()
 {
 	highlightedRegion = nullptr;
-	ourHeader = nullptr;
+	ourHeader.reset();
 	diffLb = nullptr;
 	diffRb = nullptr;
 	bonuses = nullptr;
@@ -3342,7 +3347,6 @@ CBonusSelection::CBonusSelection(const std::string & campaignFName)
 CBonusSelection::~CBonusSelection()
 {
 	SDL_FreeSurface(background);
-	delete ourHeader;
 	sFlags->unload();
 }
 
@@ -3423,10 +3427,9 @@ void CBonusSelection::selectMap(int mapNr, bool initialSelect)
 		scenarioName += ':' + boost::lexical_cast<std::string>(selectedMap);
 
 		//get header
-		delete ourHeader;
 		std::string & headerStr = ourCampaign->camp->mapPieces.find(mapNr)->second;
 		auto buffer = reinterpret_cast<const ui8 *>(headerStr.data());
-		ourHeader = CMapService::loadMapHeader(buffer, headerStr.size(), scenarioName).release();
+		ourHeader = CMapService::loadMapHeader(buffer, headerStr.size(), scenarioName);
 
 		std::map<ui8, std::string> names;
 		names[1] = settings["general"]["playerName"].String();
@@ -3922,7 +3925,7 @@ ISelectionScreenInfo::~ISelectionScreenInfo()
 	SEL = nullptr;
 }
 
-void ISelectionScreenInfo::updateStartInfo(std::string filename, StartInfo & sInfo, const CMapHeader * mapHeader)
+void ISelectionScreenInfo::updateStartInfo(std::string filename, StartInfo & sInfo, const std::unique_ptr<CMapHeader> & mapHeader)
 {
 	::updateStartInfo(filename, sInfo, mapHeader, playerNames);
 }
@@ -4320,7 +4323,7 @@ CSimpleJoinScreen::CSimpleJoinScreen(CMenuScreen::EMultiMode mode)
 	cancel = new CButton(Point(142, 142), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CGuiHandler::popIntTotally, std::ref(GH), this), SDLK_ESCAPE);
 	bar = new CGStatusBar(new CPicture(Rect(7, 186, 218, 18), 0));
 
-	port->setText(boost::lexical_cast<std::string>(settings["server"]["port"].Float()), true);
+	port->setText(CServerHandler::getDefaultPortStr(), true);
 	address->setText(settings["server"]["server"].String(), true);
 	address->giveFocus();
 }
@@ -4332,7 +4335,7 @@ void CSimpleJoinScreen::enterSelectionScreen(CMenuScreen::EMultiMode mode)
 
 	GH.popIntTotally(this);
 
-	GH.pushInt(new CSelectionScreen(CMenuScreen::newGame, mode, nullptr, textAddress, textPort));
+	GH.pushInt(new CSelectionScreen(CMenuScreen::newGame, mode, nullptr, textAddress, boost::lexical_cast<ui16>(textPort)));
 }
 void CSimpleJoinScreen::onChange(const std::string & newText)
 {

+ 3 - 3
client/CPreGame.h

@@ -341,7 +341,7 @@ public:
 	virtual void postChatMessage(const std::string &txt){};
 
 	void setPlayer(PlayerSettings &pset, ui8 player);
-	void updateStartInfo( std::string filename, StartInfo & sInfo, const CMapHeader * mapHeader );
+	void updateStartInfo(std::string filename, StartInfo & sInfo, const std::unique_ptr<CMapHeader> & mapHeader);
 
 	ui8 getIdOfFirstUnallocatedPlayer(); //returns 0 if none
 	bool isGuest() const;
@@ -371,7 +371,7 @@ public:
 	bool ongoingClosing;
 	ui8 myNameID; //used when networking - otherwise all player are "mine"
 
-	CSelectionScreen(CMenuScreen::EState Type, CMenuScreen::EMultiMode MultiPlayer = CMenuScreen::SINGLE_PLAYER, const std::map<ui8, std::string> * Names = nullptr, const std::string & Address = "", const std::string & Port = "");
+	CSelectionScreen(CMenuScreen::EState Type, CMenuScreen::EMultiMode MultiPlayer = CMenuScreen::SINGLE_PLAYER, const std::map<ui8, std::string> * Names = nullptr, const std::string & Address = "", const ui16 Port = 0);
 	~CSelectionScreen();
 	void toggleTab(CIntObject *tab);
 	void changeSelection(const CMapInfo *to);
@@ -539,7 +539,7 @@ private:
 	int selectedMap;
 	boost::optional<int> selectedBonus;
 	StartInfo startInfo;
-	CMapHeader * ourHeader;
+	std::unique_ptr<CMapHeader> ourHeader;
 };
 
 /// Campaign selection screen

+ 140 - 71
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
 
@@ -251,24 +249,16 @@ void CClient::endGame(bool closeConnection /*= true*/)
 }
 
 #if 1
-void CClient::loadGame(const std::string & fname, const bool server, const std::vector<int>& humanplayerindices, const int loadNumPlayers, int player_, const std::string & ipaddr, const std::string & port)
+void CClient::loadGame(const std::string & fname, const bool server, const std::vector<int>& humanplayerindices, const int loadNumPlayers, int player_, const std::string & ipaddr, const ui16 port)
 {
 	PlayerColor player(player_); //intentional shadowing
 	logNetwork->infoStream() << "Loading procedure started!";
 
-	std::string realPort;
-	if(settings["testing"]["enabled"].Bool())
-		realPort = settings["testing"]["port"].String();
-	else if(port.size())
-		realPort = port;
-	else
-		realPort = boost::lexical_cast<std::string>(settings["server"]["port"].Float());
-
 	CServerHandler sh;
 	if(server)
 		sh.startServer();
 	else
-		serv = sh.justConnectToServer(ipaddr, realPort);
+		serv = sh.justConnectToServer(ipaddr, port);
 
 	CStopWatch tmh;
 	std::unique_ptr<CLoadFile> loader;
@@ -391,7 +381,7 @@ void CClient::newGame( CConnection *con, StartInfo *si )
 	else
 	{
 		serv = con;
-		networkMode = (con->connectionID == 1) ? HOST : GUEST;
+		networkMode = con->isHost() ? HOST : GUEST;
 	}
 
 	CConnection &c = *serv;
@@ -416,10 +406,9 @@ void CClient::newGame( CConnection *con, StartInfo *si )
 
 	// Initialize game state
 	gs = new CGameState();
-	logNetwork->infoStream() <<"\tCreating gamestate: "<<tmh.getDiff();
+	logNetwork->info("\tCreating gamestate: %i",tmh.getDiff());
 
-	gs->scenarioOps = si;
-	gs->init(si);
+	gs->init(si, settings["general"]["saveRandomMaps"].Bool());
 	logNetwork->infoStream() <<"Initializing GameState (together): "<<tmh.getDiff();
 
 	// Now after possible random map gen, we know exact player count.
@@ -441,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();
 	}
@@ -481,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);
@@ -493,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();
 	}
 
@@ -644,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))
@@ -693,13 +712,22 @@ void CClient::stopConnection()
 {
 	terminate = true;
 
-	if (serv) //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";
+		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
@@ -708,16 +736,13 @@ void CClient::stopConnection()
 			connectionHandler->join();
 
 		logNetwork->infoStream() << "Connection handler thread joined";
-
-		delete connectionHandler;
-		connectionHandler = nullptr;
+		vstd::clear_pointer(connectionHandler);
 	}
 
 	if (serv) //and delete connection
 	{
 		serv->close();
-		delete serv;
-		serv = nullptr;
+		vstd::clear_pointer(serv);
 		logNetwork->warnStream() << "Our socket has been closed.";
 	}
 }
@@ -750,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);
+			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);
+		}
 	}
 
 	auto callBattleStart = [&](PlayerColor color, ui8 side){
@@ -768,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))
 	{
@@ -780,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()
@@ -877,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);
@@ -893,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*/)
@@ -924,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";
@@ -966,11 +1016,8 @@ void CServerHandler::waitForServer()
 	th.update();
 
 #ifndef VCMI_ANDROID
-	intpr::scoped_lock<intpr::interprocess_mutex> slock(shared->sr->mutex);
-	while(!shared->sr->ready)
-	{
-		shared->sr->cond.wait(slock);
-	}
+	if(shared)
+		shared->sr->waitTillReady();
 #else
 	logNetwork->infoStream() << "waiting for server";
 	while (!androidTestServerReadyFlag.load())
@@ -987,16 +1034,15 @@ void CServerHandler::waitForServer()
 
 CConnection * CServerHandler::connectToServer()
 {
-#ifndef VCMI_ANDROID
-	if(!shared->sr->ready)
-		waitForServer();
-#else
 	waitForServer();
-#endif
 
 	th.update(); //put breakpoint here to attach to server before it does something stupid
 
-	CConnection *ret = justConnectToServer(settings["server"]["server"].String(), port);
+#ifndef VCMI_ANDROID
+	CConnection *ret = justConnectToServer(settings["server"]["server"].String(), shared ? shared->sr->port : 0);
+#else
+	CConnection *ret = justConnectToServer(settings["server"]["server"].String());
+#endif
 
 	if(verbose)
 		logNetwork->infoStream()<<"\tConnecting to the server: "<<th.getDiff();
@@ -1004,24 +1050,43 @@ CConnection * CServerHandler::connectToServer()
 	return ret;
 }
 
+ui16 CServerHandler::getDefaultPort()
+{
+	if(settings["session"]["serverport"].Integer())
+		return settings["session"]["serverport"].Integer();
+	else
+		return settings["server"]["port"].Integer();
+}
+
+std::string CServerHandler::getDefaultPortStr()
+{
+	return boost::lexical_cast<std::string>(getDefaultPort());
+}
+
 CServerHandler::CServerHandler(bool runServer /*= false*/)
 {
 	serverThread = nullptr;
 	shared = nullptr;
-	if(settings["testing"]["enabled"].Bool())
-		port = settings["testing"]["port"].String();
-	else
-		port = boost::lexical_cast<std::string>(settings["server"]["port"].Float());
 	verbose = true;
+	uuid = boost::uuids::to_string(boost::uuids::random_generator()());
 
 #ifndef VCMI_ANDROID
-	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
+	if(DO_NOT_START_SERVER || settings["session"]["disable-shm"].Bool())
+		return;
+
+	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(...)
 	{
+		vstd::clear_pointer(shared);
 		logNetwork->error("Cannot open interprocess memory.");
 		handleException();
 		throw;
@@ -1040,7 +1105,18 @@ 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() + " --port=" + port + " > \"" + logName + '\"';
+	std::string comm = VCMIDirs::get().serverPath().string()
+		+ " --port=" + getDefaultPortStr()
+		+ " --run-by-client"
+		+ " --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)
 	{
@@ -1056,16 +1132,8 @@ void CServerHandler::callServer()
 #endif
 }
 
-CConnection * CServerHandler::justConnectToServer(const std::string &host, const std::string &port)
+CConnection * CServerHandler::justConnectToServer(const std::string &host, const ui16 port)
 {
-	std::string realPort;
-	if(settings["testing"]["enabled"].Bool())
-		realPort = settings["testing"]["port"].String();
-	else if(port.size())
-		realPort = port;
-	else
-		realPort = boost::lexical_cast<std::string>(settings["server"]["port"].Float());
-
 	CConnection *ret = nullptr;
 	while(!ret)
 	{
@@ -1073,8 +1141,9 @@ CConnection * CServerHandler::justConnectToServer(const std::string &host, const
 		{
 			logNetwork->infoStream() << "Establishing connection...";
 			ret = new CConnection(	host.size() ? host : settings["server"]["server"].String(),
-									realPort,
+									port ? port : getDefaultPort(),
 									NAME);
+			ret->connectionID = 1; // TODO: Refactoring for the server so IDs set outside of CConnection
 		}
 		catch(...)
 		{

+ 9 - 6
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,9 +46,9 @@ 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
-	std::string port; //port number in text form
 
 	//functions setting up local server
 	void startServer(); //creates a thread with callServer
@@ -56,7 +56,9 @@ public:
 	CConnection * connectToServer(); //connects to server
 
 	//////////////////////////////////////////////////////////////////////////
-	static CConnection * justConnectToServer(const std::string &host = "", const std::string &port = ""); //connects to given host without taking any other actions (like setting up server)
+	static CConnection * justConnectToServer(const std::string &host = "", const ui16 port = 0); //connects to given host without taking any other actions (like setting up server)
+	static ui16 getDefaultPort();
+	static std::string getDefaultPortStr();
 
 	CServerHandler(bool runServer = false);
 	virtual ~CServerHandler();
@@ -147,14 +149,15 @@ 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();
 	void save(const std::string & fname);
-	void loadGame(const std::string & fname, const bool server = true, const std::vector<int>& humanplayerindices = std::vector<int>(), const int loadnumplayers = 1, int player_ = -1, const std::string & ipaddr = "", const std::string & port = "");
+	void loadGame(const std::string & fname, const bool server = true, const std::vector<int>& humanplayerindices = std::vector<int>(), const int loadnumplayers = 1, int player_ = -1, const std::string & ipaddr = "", const ui16 port = 0);
 	void run();
 	void campaignMapFinished( std::shared_ptr<CCampaignState> camp );
 	void finishCampaign( std::shared_ptr<CCampaignState> camp );

+ 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++)
 	{

+ 0 - 2
client/VCMI_client.vcxproj

@@ -292,6 +292,4 @@
     </ProjectReference>
   </ItemGroup>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
-  <ImportGroup Label="ExtensionTargets">
-  </ImportGroup>
 </Project>

+ 2 - 8
client/VCMI_client.vcxproj.filters

@@ -109,17 +109,10 @@
       <Filter>gui</Filter>
     </ClCompile>
     <ClCompile Include="..\CCallback.cpp" />
-  </ItemGroup>
-  <ItemGroup>
-    <ClCompile Include="CQuestLog.cpp">
-      <Filter>Source Files</Filter>
-    </ClCompile>
+    <ClCompile Include="SDLRWwrapper.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="VCMI_client.rc" />
-    <ClInclude Include="CQuestLog.h">
-      <Filter>Header Files</Filter>
-    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <None Include="vcmi.ico" />
@@ -254,5 +247,6 @@
       <Filter>gui</Filter>
     </ClInclude>
     <ClInclude Include="..\CCallback.h" />
+    <ClInclude Include="SDLRWwrapper.h" />
   </ItemGroup>
 </Project>

+ 38 - 14
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)
@@ -871,19 +877,24 @@ void CBattleInterface::bSpellf()
 	if (spellDestSelectMode) //we are casting a spell
 		return;
 
-	CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
-
 	if (!myTurn)
 		return;
 
 	auto myHero = currentHero();
-	ESpellCastProblem::ESpellCastProblem spellCastProblem;
-	if (curInt->cb->battleCanCastSpell(&spellCastProblem))
+	if(!myHero)
+		return;
+
+	CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
+
+	ESpellCastProblem::ESpellCastProblem spellCastProblem = curInt->cb->battleCanCastSpell(myHero, ECastingMode::HERO_CASTING);
+
+	if(spellCastProblem == ESpellCastProblem::OK)
 	{
 		GH.pushInt(new CSpellWindow(myHero, curInt.get()));
 	}
 	else if (spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED)
 	{
+		//TODO: move to spell mechanics, add more information to spell cast problem
 		//Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible
 		auto blockingBonus = currentHero()->getBonusLocalFirst(Selector::type(Bonus::BLOCK_ALL_MAGIC));
 		if (!blockingBonus)
@@ -1241,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);
@@ -1526,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);
 }
 
@@ -1658,8 +1677,7 @@ void CBattleInterface::enterCreatureCastingMode()
 	{
 		const ISpellCaster *caster = activeStack;
 		const CSpell *spell = SpellID(creatureSpellToCast).toSpell();
-
-		const bool isCastingPossible = (curInt->cb->battleCanCastThisSpellHere(caster, spell, ECastingMode::CREATURE_ACTIVE_CASTING, BattleHex::INVALID) == ESpellCastProblem::OK);
+		const bool isCastingPossible = (spell->canBeCastAt(curInt->cb.get(), caster, ECastingMode::CREATURE_ACTIVE_CASTING, BattleHex::INVALID) == ESpellCastProblem::OK);
 
 		if (isCastingPossible)
 		{
@@ -1849,11 +1867,17 @@ void CBattleInterface::showQueue()
 
 void CBattleInterface::blockUI(bool on)
 {
-	ESpellCastProblem::ESpellCastProblem spellcastingProblem;
-	bool canCastSpells = curInt->cb->battleCanCastSpell(&spellcastingProblem);
-	//if magic is blocked, we leave button active, so the message can be displayed (cf bug #97)
-	if (!canCastSpells)
-		canCastSpells = spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED;
+	bool canCastSpells = false;
+	auto hero = curInt->cb->battleGetMyHero();
+
+	if(hero)
+	{
+		ESpellCastProblem::ESpellCastProblem spellcastingProblem = curInt->cb->battleCanCastSpell(hero, ECastingMode::HERO_CASTING);
+
+		//if magic is blocked, we leave button active, so the message can be displayed after button click
+		canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED;
+	}
+
 	bool canWait = activeStack ? !activeStack->waited() : false;
 
 	bOptions->block(on);
@@ -2511,7 +2535,7 @@ bool CBattleInterface::isCastingPossibleHere(const CStack *sactive, const CStack
 		else
 		{
 			const ECastingMode::ECastingMode mode = creatureCasting ? ECastingMode::CREATURE_ACTIVE_CASTING : ECastingMode::HERO_CASTING;
-			isCastingPossible = (curInt->cb->battleCanCastThisSpellHere(caster, sp, mode, myNumber) == ESpellCastProblem::OK);
+			isCastingPossible = (sp->canBeCastAt(curInt->cb.get(), caster, mode, myNumber) == ESpellCastProblem::OK);
 		}
 	}
 	else

+ 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

+ 4 - 8
client/battle/CBattleInterfaceClasses.cpp

@@ -191,7 +191,7 @@ void CBattleHero::clickLeft(tribool down, bool previousState)
 	if(myOwner->spellDestSelectMode) //we are casting a spell
 		return;
 
-	if(myHero != nullptr && !down &&  myOwner->myTurn && myOwner->getCurrentPlayerInterface()->cb->battleCanCastSpell()) //check conditions
+	if(myHero != nullptr && !down &&  myOwner->myTurn && myOwner->getCurrentPlayerInterface()->cb->battleCanCastSpell(myHero, ECastingMode::HERO_CASTING) == ESpellCastProblem::OK) //check conditions
 	{
 		for(int it=0; it<GameConstants::BFIELD_SIZE; ++it) //do nothing when any hex is hovered - hero's animation overlaps battlefield
 		{
@@ -211,14 +211,10 @@ 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()))
 	{
-		if (myHero != nullptr)
-			targetHero.initFromHero(myHero, InfoAboutHero::EInfoLevel::INBATTLE);
-		else
-			targetHero = myOwner->enemyHero();
-
+		auto h = flip ? myOwner->defendingHeroInstance : myOwner->attackingHeroInstance;
+		targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE);
 		GH.pushInt(new CHeroInfoWindow(targetHero, &windowPosition));
 	}
 }

+ 91 - 59
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;
@@ -59,6 +60,7 @@ void CGuiHandler::processLists(const ui16 activityFlag, std::function<void (std:
 {
 	processList(CIntObject::LCLICK,activityFlag,&lclickable,cb);
 	processList(CIntObject::RCLICK,activityFlag,&rclickable,cb);
+	processList(CIntObject::MCLICK,activityFlag,&mclickable,cb);
 	processList(CIntObject::HOVER,activityFlag,&hoverable,cb);
 	processList(CIntObject::MOVE,activityFlag,&motioninterested,cb);
 	processList(CIntObject::KEYBOARD,activityFlag,&keyinterested,cb);
@@ -191,6 +193,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)
@@ -219,18 +261,18 @@ void CGuiHandler::handleEvent(SDL_Event *sEvent)
 		CCS->curh->cursorMove(sEvent->motion.x, sEvent->motion.y);
 		handleMouseMotion(sEvent);
 	}
-	else if (sEvent->type==SDL_MOUSEBUTTONDOWN)
+	else if(sEvent->type == SDL_MOUSEBUTTONDOWN)
 	{
-		if(sEvent->button.button == SDL_BUTTON_LEFT)
+		switch(sEvent->button.button)
 		{
-
-			if(lastClick == sEvent->motion  &&  (SDL_GetTicks() - lastClickTime) < 300)
+		case SDL_BUTTON_LEFT:
+			if(lastClick == sEvent->motion && (SDL_GetTicks() - lastClickTime) < 300)
 			{
 				std::list<CIntObject*> hlp = doubleClickInterested;
-				for(auto i=hlp.begin(); i != hlp.end() && current; i++)
+				for(auto i = hlp.begin(); i != hlp.end() && current; i++)
 				{
-					if(!vstd::contains(doubleClickInterested,*i)) continue;
-					if (isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y))
+					if(!vstd::contains(doubleClickInterested, *i)) continue;
+					if(isItIn(&(*i)->pos, sEvent->motion.x, sEvent->motion.y))
 					{
 						(*i)->onDoubleClick();
 					}
@@ -241,31 +283,16 @@ void CGuiHandler::handleEvent(SDL_Event *sEvent)
 			lastClick = sEvent->motion;
 			lastClickTime = SDL_GetTicks();
 
-			std::list<CIntObject*> hlp = lclickable;
-			for(auto i=hlp.begin(); i != hlp.end() && current; i++)
-			{
-				if(!vstd::contains(lclickable,*i)) continue;
-				if (isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y))
-				{
-					prev = (*i)->pressedL;
-					(*i)->pressedL = true;
-					(*i)->clickLeft(true, prev);
-				}
-			}
-		}
-		else if (sEvent->button.button == SDL_BUTTON_RIGHT)
-		{
-			std::list<CIntObject*> hlp = rclickable;
-			for(auto i=hlp.begin(); i != hlp.end() && current; i++)
-			{
-				if(!vstd::contains(rclickable,*i)) continue;
-				if (isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y))
-				{
-					prev = (*i)->pressedR;
-					(*i)->pressedR = true;
-					(*i)->clickRight(true, prev);
-				}
-			}
+			handleMouseButtonClick(lclickable, EIntObjMouseBtnType::LEFT, true);
+			break;
+		case SDL_BUTTON_RIGHT:
+			handleMouseButtonClick(rclickable, EIntObjMouseBtnType::RIGHT, true);
+			break;
+		case SDL_BUTTON_MIDDLE:
+			handleMouseButtonClick(mclickable, EIntObjMouseBtnType::MIDDLE, true);
+			break;
+		default:
+			break;
 		}
 	}
 	else if (sEvent->type == SDL_MOUSEWHEEL)
@@ -295,40 +322,44 @@ void CGuiHandler::handleEvent(SDL_Event *sEvent)
 		}
 	}
 	//todo: muiltitouch
-	else if ((sEvent->type==SDL_MOUSEBUTTONUP) && (sEvent->button.button == SDL_BUTTON_LEFT))
+	else if(sEvent->type == SDL_MOUSEBUTTONUP)
 	{
-		std::list<CIntObject*> hlp = lclickable;
-		for(auto i=hlp.begin(); i != hlp.end() && current; i++)
+		switch(sEvent->button.button)
 		{
-			if(!vstd::contains(lclickable,*i)) continue;
-			prev = (*i)->pressedL;
-			(*i)->pressedL = false;
-			if (isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y))
-			{
-				(*i)->clickLeft(false, prev);
-			}
-			else
-				(*i)->clickLeft(boost::logic::indeterminate, prev);
+		case SDL_BUTTON_LEFT:
+			handleMouseButtonClick(lclickable, EIntObjMouseBtnType::LEFT, false);
+			break;
+		case SDL_BUTTON_RIGHT:
+			handleMouseButtonClick(rclickable, EIntObjMouseBtnType::RIGHT, false);
+			break;
+		case SDL_BUTTON_MIDDLE:
+			handleMouseButtonClick(mclickable, EIntObjMouseBtnType::MIDDLE, false);
+			break;
 		}
 	}
-	else if ((sEvent->type==SDL_MOUSEBUTTONUP) && (sEvent->button.button == SDL_BUTTON_RIGHT))
+	current = nullptr;
+} //event end
+
+void CGuiHandler::handleMouseButtonClick(CIntObjectList & interestedObjs, EIntObjMouseBtnType btn, bool isPressed)
+{
+	auto hlp = interestedObjs;
+	for(auto i = hlp.begin(); i != hlp.end() && current; i++)
 	{
-		std::list<CIntObject*> hlp = rclickable;
-		for(auto i=hlp.begin(); i != hlp.end() && current; i++)
+		if(!vstd::contains(interestedObjs, *i)) continue;
+
+		auto prev = (*i)->mouseState(btn);
+		if(!isPressed)
+			(*i)->updateMouseState(btn, isPressed);
+		if(isItIn(&(*i)->pos, current->motion.x, current->motion.y))
 		{
-			if(!vstd::contains(rclickable,*i)) continue;
-			prev = (*i)->pressedR;
-			(*i)->pressedR = false;
-			if (isItIn(&(*i)->pos,sEvent->motion.x,sEvent->motion.y))
-			{
-				(*i)->clickRight(false, prev);
-			}
-			else
-				(*i)->clickRight(boost::logic::indeterminate, prev);
+			if(isPressed)
+				(*i)->updateMouseState(btn, isPressed);
+			(*i)->click(btn, isPressed, prev);
 		}
+		else if(!isPressed)
+			(*i)->click(btn, boost::logic::indeterminate, prev);
 	}
-	current = nullptr;
-} //event end
+}
 
 void CGuiHandler::handleMouseMotion(SDL_Event *sEvent)
 {
@@ -361,7 +392,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)

+ 3 - 0
client/gui/CGuiHandler.h

@@ -10,6 +10,7 @@ class CIntObject;
 class IUpdateable;
 class IShowActivatable;
 class IShowable;
+enum class EIntObjMouseBtnType;
 template <typename T> struct CondSh;
 
 /*
@@ -53,6 +54,7 @@ private:
 	//active GUI elements (listening for events
 	CIntObjectList lclickable,
 				   rclickable,
+				   mclickable,
 				   hoverable,
 				   keyinterested,
 				   motioninterested,
@@ -62,6 +64,7 @@ private:
 	               textInterested;
 
 
+	void handleMouseButtonClick(CIntObjectList & interestedObjs, EIntObjMouseBtnType btn, bool isPressed);
 	void processLists(const ui16 activityFlag, std::function<void (std::list<CIntObject*> *)> cb);
 public:
 	void handleElementActivate(CIntObject * elem, ui16 activityFlag);

+ 22 - 12
client/gui/CIntObject.cpp

@@ -16,7 +16,7 @@ CIntObject::CIntObject(int used_, Point pos_):
 	parent(parent_m),
 	active(active_m)
 {
-	pressedL = pressedR = hovered = captureAllKeys = strongInterest = false;
+	hovered = captureAllKeys = strongInterest = false;
 	toNextTick = timerDelay = 0;
 	used = used_;
 
@@ -134,6 +134,23 @@ CIntObject::~CIntObject()
 		parent_m->removeChild(this);
 }
 
+void CIntObject::click(EIntObjMouseBtnType btn, tribool down, bool previousState)
+{
+	switch(btn)
+	{
+	default:
+	case EIntObjMouseBtnType::LEFT:
+		clickLeft(down, previousState);
+		break;
+	case EIntObjMouseBtnType::MIDDLE:
+		clickMiddle(down, previousState);
+		break;
+	case EIntObjMouseBtnType::RIGHT:
+		clickRight(down, previousState);
+		break;
+	}
+}
+
 void CIntObject::printAtLoc( const std::string & text, int x, int y, EFonts font, SDL_Color kolor/*=Colors::WHITE*/, SDL_Surface * dst/*=screen*/ )
 {
 	graphics->fonts[font]->renderTextLeft(dst, text, kolor, Point(pos.x + x, pos.y + y));
@@ -340,16 +357,9 @@ void CKeyShortcut::keyPressed(const SDL_KeyboardEvent & key)
 	if(vstd::contains(assignedKeys,key.keysym.sym)
 	 || vstd::contains(assignedKeys, CGuiHandler::numToDigit(key.keysym.sym)))
 	{
-		bool prev = pressedL;
-		if(key.state == SDL_PRESSED)
-		{
-			pressedL = true;
-			clickLeft(true, prev);
-		}
-		else
-		{
-			pressedL = false;
-			clickLeft(false, prev);
-		}
+		bool prev = mouseState(EIntObjMouseBtnType::LEFT);		
+		updateMouseState(EIntObjMouseBtnType::LEFT, key.state == SDL_PRESSED);
+		clickLeft(key.state == SDL_PRESSED, prev);
+		
 	}
 }

+ 10 - 7
client/gui/CIntObject.h

@@ -61,6 +61,7 @@ public:
 	virtual ~IShowActivatable(){}; //d-tor
 };
 
+enum class EIntObjMouseBtnType { LEFT, MIDDLE, RIGHT };
 //typedef ui16 ActivityFlag;
 
 // Base UI element
@@ -73,6 +74,8 @@ class CIntObject : public IShowActivatable //interface object
 	int toNextTick;
 	int timerDelay;
 
+	std::map<EIntObjMouseBtnType, bool> currentMouseState;
+
 	void onTimer(int timePassed);
 
 	//non-const versions of fields to allow changing them in CIntObject
@@ -104,13 +107,13 @@ public:
 	CIntObject(int used=0, Point offset=Point());
 	virtual ~CIntObject(); //d-tor
 
-	//l-clicks handling
-	/*const*/ bool pressedL; //for determining if object is L-pressed
-	virtual void clickLeft(tribool down, bool previousState){}
+	void updateMouseState(EIntObjMouseBtnType btn, bool state) { currentMouseState[btn] = state; }
+	bool mouseState(EIntObjMouseBtnType btn) const { return currentMouseState.count(btn) ? currentMouseState.at(btn) : false; }
 
-	//r-clicks handling
-	/*const*/ bool pressedR; //for determining if object is R-pressed
-	virtual void clickRight(tribool down, bool previousState){}
+	virtual void click(EIntObjMouseBtnType btn, tribool down, bool previousState);
+	virtual void clickLeft(tribool down, bool previousState) {}
+	virtual void clickRight(tribool down, bool previousState) {}
+	virtual void clickMiddle(tribool down, bool previousState) {}
 
 	//hover handling
 	/*const*/ bool hovered;  //for determining if object is hovered
@@ -138,7 +141,7 @@ public:
 	//double click
 	virtual void onDoubleClick(){}
 
-	enum {LCLICK=1, RCLICK=2, HOVER=4, MOVE=8, KEYBOARD=16, TIME=32, GENERAL=64, WHEEL=128, DOUBLECLICK=256, TEXTINPUT=512, ALL=0xffff};
+	enum {LCLICK=1, RCLICK=2, HOVER=4, MOVE=8, KEYBOARD=16, TIME=32, GENERAL=64, WHEEL=128, DOUBLECLICK=256, TEXTINPUT=512, MCLICK=1024, ALL=0xffff};
 	const ui16 & active;
 	void addUsedEvents(ui16 newActions);
 	void removeUsedEvents(ui16 newActions);

+ 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();
 }

+ 1 - 1
client/widgets/AdventureMapClasses.cpp

@@ -587,7 +587,7 @@ void CMinimap::hover(bool on)
 
 void CMinimap::mouseMoved(const SDL_MouseMotionEvent & sEvent)
 {
-	if (pressedL)
+	if(mouseState(EIntObjMouseBtnType::LEFT))
 		moveAdvMapSelection();
 }
 

+ 1 - 1
client/widgets/Buttons.cpp

@@ -623,7 +623,7 @@ void CSlider::clickLeft(tribool down, bool previousState)
 			return;
 		// 		if (rw>1) return;
 		// 		if (rw<0) return;
-		slider->clickLeft(true, slider->pressedL);
+		slider->clickLeft(true, slider->mouseState(EIntObjMouseBtnType::LEFT));
 		moveTo(rw * positions  +  0.5);
 		return;
 	}

+ 10 - 9
client/widgets/CArtifactHolder.cpp

@@ -161,16 +161,18 @@ void CHeroArtPlace::clickLeft(tribool down, bool previousState)
 				{
 					const CArtifact * const cur = ourOwner->commonInfo->src.art->artType;
 
-					switch(cur->id)
+					if(cur->id == ArtifactID::CATAPULT)
 					{
-					case ArtifactID::CATAPULT:
 						//should not happen, catapult cannot be selected
-						assert(cur->id != ArtifactID::CATAPULT);
-						break;
-					case ArtifactID::BALLISTA: case ArtifactID::AMMO_CART: case ArtifactID::FIRST_AID_TENT: //war machines cannot go to backpack
+						logGlobal->error("Attempt to move Catapult");
+					}
+					else if(cur->isBig())
+					{
+						//war machines cannot go to backpack
 						LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[153]) % cur->Name()));
-						break;
-					default:
+					}
+					else
+					{
 						setMeAsDest();
 						vstd::amin(ourOwner->commonInfo->dst.slotID, ArtifactPosition(
 							ourOwner->curHero->artifactsInBackpack.size() + GameConstants::BACKPACK_START));
@@ -184,7 +186,6 @@ void CHeroArtPlace::clickLeft(tribool down, bool previousState)
 							deselect();
 						else
 							ourOwner->realizeCurrentTransaction();
-						break;
 					}
 				}
 			}
@@ -366,7 +367,7 @@ bool CHeroArtPlace::fitsHere(const CArtifactInstance * art) const
 
 	// Anything but War Machines can be placed in backpack.
 	if (slotID >= GameConstants::BACKPACK_START)
-		return !CGI->arth->isBigArtifact(art->artType->id);
+		return !art->artType->isBig();
 
 	return art->canBePutAt(ArtifactLocation(ourOwner->curHero, slotID), true);
 }

+ 53 - 31
client/windows/CAdvmapInterface.cpp

@@ -98,7 +98,7 @@ CTerrainRect::CTerrainRect()
 	pos.w=ADVOPT.advmapW;
 	pos.h=ADVOPT.advmapH;
 	moveX = moveY = 0;
-	addUsedEvents(LCLICK | RCLICK | HOVER | MOVE);
+	addUsedEvents(LCLICK | RCLICK | MCLICK | HOVER | MOVE);
 }
 
 CTerrainRect::~CTerrainRect()
@@ -124,17 +124,10 @@ void CTerrainRect::clickLeft(tribool down, bool previousState)
 #ifdef VCMI_ANDROID
 	if(adventureInt->swipeEnabled)
 	{
-		if(down == true)
+		if(handleSwipeStateChange(down == true))
 		{
-			swipeInitialRealPos = int3(GH.current->motion.x, GH.current->motion.y, 0);
-			swipeInitialMapPos = int3(adventureInt->position);
 			return; // if swipe is enabled, we don't process "down" events and wait for "up" (to make sure this wasn't a swiping gesture)
 		}
-		else if(isSwiping) // only accept this touch if it wasn't a swipe
-		{
-			isSwiping = false;
-			return;
-		}
 	}
 	else
 	{
@@ -165,22 +158,32 @@ void CTerrainRect::clickRight(tribool down, bool previousState)
 		adventureInt->tileRClicked(mp);
 }
 
+void CTerrainRect::clickMiddle(tribool down, bool previousState)
+{
+	handleSwipeStateChange(down == true);
+}
+
 void CTerrainRect::mouseMoved(const SDL_MouseMotionEvent & sEvent)
 {
 	handleHover(sEvent);
 
-#ifdef VCMI_ANDROID
-	if(!adventureInt->swipeEnabled || sEvent.state == 0)
+	if(!adventureInt->swipeEnabled)
 		return;
 
 	handleSwipeMove(sEvent);
-#endif // !VCMI_ANDROID
 }
 
-#ifdef VCMI_ANDROID
-
 void CTerrainRect::handleSwipeMove(const SDL_MouseMotionEvent & sEvent)
 {
+#ifdef VCMI_ANDROID
+	if(sEvent.state == 0) // any "button" is enough on android
+#else //!VCMI_ANDROID
+	if((sEvent.state & SDL_BUTTON_MMASK) == 0) // swipe only works with middle mouse on other platforms
+#endif //!VCMI_ANDROID
+	{
+		return;
+	}
+
 	if(!isSwiping)
 	{
 		// try to distinguish if this touch was meant to be a swipe or just fat-fingering press
@@ -201,7 +204,21 @@ void CTerrainRect::handleSwipeMove(const SDL_MouseMotionEvent & sEvent)
 	}
 }
 
-#endif // VCMI_ANDROID
+bool CTerrainRect::handleSwipeStateChange(bool btnPressed)
+{
+	if(btnPressed)
+	{
+		swipeInitialRealPos = int3(GH.current->motion.x, GH.current->motion.y, 0);
+		swipeInitialMapPos = int3(adventureInt->position);
+		return true;
+	}
+	else if(isSwiping) // only accept this touch if it wasn't a swipe
+	{
+		isSwiping = false;
+		return true;
+	}
+	return false;
+}
 
 void CTerrainRect::handleHover(const SDL_MouseMotionEvent &sEvent)
 {
@@ -534,11 +551,9 @@ CAdvMapInt::CAdvMapInt():
 	infoBar(Rect(ADVOPT.infoboxX, ADVOPT.infoboxY, 192, 192)), state(NA),
   spellBeingCasted(nullptr), position(int3(0, 0, 0)), selection(nullptr),
   updateScreen(false), anim(0), animValHitCount(0), heroAnim(0), heroAnimValHitCount(0),
-	activeMapPanel(nullptr), duringAITurn(false), scrollingDir(0), scrollingState(false)
-#ifdef VCMI_ANDROID
-	, swipeEnabled(settings["general"]["swipe"].Bool()), swipeMovementRequested(false),
+	activeMapPanel(nullptr), duringAITurn(false), scrollingDir(0), scrollingState(false), 
+	swipeEnabled(settings["general"]["swipe"].Bool()), swipeMovementRequested(false),
 	swipeTargetPosition(int3(-1, -1, -1))
-#endif
 {
   adventureInt = this;
 	pos.x = pos.y = 0;
@@ -1007,21 +1022,24 @@ void CAdvMapInt::show(SDL_Surface * to)
 	}
 	++heroAnim;
 
-#ifdef VCMI_ANDROID
 	if(swipeEnabled)
 	{
 		handleSwipeUpdate();
 	}
+#ifdef VCMI_ANDROID // on android, map-moving mode is exclusive (TODO technically it might work with both enabled; to be checked)
 	else
+#endif // VCMI_ANDROID
 	{
-#endif // !VCMI_ANDROID
 		handleMapScrollingUpdate();
-#ifdef VCMI_ANDROID
 	}
-#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);
@@ -1084,14 +1102,13 @@ void CAdvMapInt::handleMapScrollingUpdate()
 	}
 }
 
-#ifdef VCMI_ANDROID
-
 void CAdvMapInt::handleSwipeUpdate()
 {
 	if(swipeMovementRequested)
 	{
-		position.x = swipeTargetPosition.x;
-		position.y = swipeTargetPosition.y;
+		auto fixedPos = LOCPLINT->repairScreenPos(swipeTargetPosition);
+		position.x = fixedPos.x;
+		position.y = fixedPos.y;
 		CCS->curh->changeGraphic(ECursor::DEFAULT, 0);
 		updateScreen = true;
 		minimap.redraw();
@@ -1099,8 +1116,6 @@ void CAdvMapInt::handleSwipeUpdate()
 	}
 }
 
-#endif
-
 void CAdvMapInt::selectionChanged()
 {
 	const CGTownInstance *to = LOCPLINT->towns[townList.getSelectedIndex()];
@@ -1481,7 +1496,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 +1506,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 +1836,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 - 8
client/windows/CAdvmapInterface.h

@@ -62,10 +62,10 @@ class CTerrainRect
 	bool isSwiping;
 	static constexpr float SwipeTouchSlop = 16.0f;
 
-	void handleHover(const SDL_MouseMotionEvent &sEvent);
-#ifdef VCMI_ANDROID
-	void handleSwipeMove(const SDL_MouseMotionEvent &sEvent);
-#endif // VCMI_ANDROID
+	void handleHover(const SDL_MouseMotionEvent & sEvent);
+	void handleSwipeMove(const SDL_MouseMotionEvent & sEvent);
+	/// handles start/finish of swipe (press/release of corresponding button); returns true if state change was handled
+	bool handleSwipeStateChange(bool btnPressed);
 public:
 	int tilesw, tilesh; //width and height of terrain to blit in tiles
 	int3 curHoveredTile;
@@ -77,6 +77,7 @@ public:
 	void deactivate() override;
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
+	void clickMiddle(tribool down, bool previousState) override;
 	void hover(bool on) override;
 	void mouseMoved (const SDL_MouseMotionEvent & sEvent) override;
 	void show(SDL_Surface * to) override;
@@ -132,11 +133,9 @@ public:
 	enum{LEFT=1, RIGHT=2, UP=4, DOWN=8};
 	ui8 scrollingDir; //uses enum: LEFT RIGHT, UP, DOWN
 	bool scrollingState;
-#ifdef VCMI_ANDROID
 	bool swipeEnabled;
 	bool swipeMovementRequested;
 	int3 swipeTargetPosition;
-#endif // !VCMI_ANDROID
 
 	enum{NA, INGAME, WAITING} state;
 
@@ -260,9 +259,7 @@ public:
 	void changeMode(EAdvMapMode newMode, float newScale = 0.36f);
 
 	void handleMapScrollingUpdate();
-#ifdef VCMI_ANDROID
 	void handleSwipeUpdate();
-#endif
 
 };
 

+ 2 - 1
client/windows/CCastleInterface.cpp

@@ -754,7 +754,8 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
 	}
 	int price = CGI->arth->artifacts[artifactID]->price;
 	bool possible = LOCPLINT->cb->getResourceAmount(Res::GOLD) >= price && !hero->hasArt(artifactID);
-	GH.pushInt(new CBlacksmithDialog(possible, CArtHandler::machineIDToCreature(artifactID), artifactID, hero->id));
+	CreatureID cre = artifactID.toArtifact()->warMachine;
+	GH.pushInt(new CBlacksmithDialog(possible, cre, artifactID, hero->id));
 }
 
 void CCastleBuildings::enterBuilding(BuildingID building)

+ 3 - 3
client/windows/CSpellWindow.cpp

@@ -337,7 +337,7 @@ void CSpellWindow::computeSpellsPerArea()
 	spellsCurSite.reserve(mySpells.size());
 	for(const CSpell * spell : mySpells)
 	{
-		if(spell->combatSpell ^ !battleSpellsOnly
+		if(spell->isCombatSpell() ^ !battleSpellsOnly
 			&& ((selectedTab == 4) || spell->school.at((ESpellSchool)selectedTab))
 			)
 		{
@@ -547,9 +547,9 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 		}
 
 		//we will cast a spell
-		if(mySpell->combatSpell && owner->myInt->battleInt && owner->myInt->cb->battleCanCastSpell()) //if battle window is open
+		if(mySpell->isCombatSpell() && owner->myInt->battleInt) //if battle window is open
 		{
-			ESpellCastProblem::ESpellCastProblem problem = owner->myInt->cb->battleCanCastThisSpell(mySpell);
+			ESpellCastProblem::ESpellCastProblem problem = mySpell->canBeCast(owner->myInt->cb.get(), ECastingMode::HERO_CASTING, owner->myHero);
 			switch (problem)
 			{
 			case ESpellCastProblem::OK:

+ 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()

+ 1 - 1
client/windows/GUIClasses.cpp

@@ -205,7 +205,7 @@ void CRecruitmentWindow::buy()
 	CreatureID crid =  selected->creature->idNumber;
 	SlotID dstslot = dst-> getSlotFor(crid);
 
-	if(!dstslot.validSlot() && !vstd::contains(CGI->arth->bigArtifacts,CGI->arth->creatureToMachineID(crid))) //no available slot
+	if(!dstslot.validSlot() && (selected->creature->warMachine == ArtifactID::NONE)) //no available slot
 	{
 		std::string txt;
 		if(dst->ID == Obj::HERO)

+ 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

+ 9 - 5
config/artifacts.json

@@ -17,22 +17,26 @@
 	"catapult":
 	{
 		"index" : 3,
-		"type" : ["HERO"]
+		"type" : ["HERO"],
+		"warMachine" : "catapult"
 	},
 	"ballista":
 	{
 		"index" : 4,
-		"type" : ["HERO"]
+		"type" : ["HERO"],
+		"warMachine" : "ballista"
 	},
 	"ammoCart":
 	{
 		"index" : 5,
-		"type" : ["HERO"]
+		"type" : ["HERO"],
+		"warMachine" : "ammoCart"
 	},
 	"firstAidTent":
 	{
 		"index" : 6,
-		"type" : ["HERO"]
+		"type" : ["HERO"],
+		"warMachine" : "firstAidTent"
 	},
 	"centaurAxe":
 	{
@@ -1333,7 +1337,7 @@
 				"subtype" : 1,
 				"val" : 0,
 				"valueType" : "BASE_NUMBER"
-			}			
+			}
 		],
 		"index" : 93,
 		"type" : ["HERO"]

+ 7 - 1
config/schemas/artifact.json

@@ -4,7 +4,7 @@
 	"title" : "VCMI artifact format",
 	"description" : "Format used to define new artifacts in VCMI",
 	"required" : [ "class", "text", "type", "value" ],
-	
+
 	"definitions" : {
 		"growingBonusList" : {
 			"type" : "array",
@@ -117,6 +117,12 @@
 		"value": {
 			"type":"number",
 			"description": "Cost of this artifact, in gold"
+		},
+		"warMachine":
+		{
+			"type":"string",
+			"description": "Creature id to use on battle field. If set, this artifact is war machine"
 		}
+
 	}
 }

+ 8 - 4
config/schemas/settings.json

@@ -5,8 +5,8 @@
 	"$schema": "http://json-schema.org/draft-04/schema",
 	"required" : [ "general", "video", "adventure", "pathfinder", "battle", "server", "logging", "launcher" ],
 	"definitions" : {
-		"logLevelEnum" : { 
-			"type" : "string", 
+		"logLevelEnum" : {
+			"type" : "string",
 			"enum" : [ "trace", "debug", "info", "warn", "error" ]
 		}
 	},
@@ -17,7 +17,7 @@
 			"type" : "object",
 			"default": {},
 			"additionalProperties" : false,
-			"required" : [ "playerName", "showfps", "music", "sound", "encoding", "swipe" ],
+			"required" : [ "playerName", "showfps", "music", "sound", "encoding", "swipe", "saveRandomMaps" ],
 			"properties" : {
 				"playerName" : {
 					"type":"string",
@@ -41,8 +41,12 @@
 				},
 				"swipe" : {
 					"type" : "boolean",
-					"default" : false
+					"default" : true
 				},
+				"saveRandomMaps" : {
+					"type" : "boolean",
+					"default" : false
+				}
 			}
 		},
 		"video" : {

+ 1 - 1
launcher/settingsView/csettingsview_moc.cpp

@@ -55,7 +55,7 @@ void CSettingsView::loadSettings()
 	ui->comboBoxEnemyAI->setCurrentIndex(enemyAIIndex);
 	ui->comboBoxPlayerAI->setCurrentIndex(playerAIIndex);
 
-	ui->spinBoxNetworkPort->setValue(settings["server"]["port"].Float());
+	ui->spinBoxNetworkPort->setValue(settings["server"]["port"].Integer());
 
 	ui->comboBoxAutoCheck->setCurrentIndex(settings["launcher"]["autoCheckRepositories"].Bool());
 	// all calls to plainText will trigger textChanged() signal overwriting config. Create backup before editing widget

+ 26 - 12
lib/BattleInfo.cpp

@@ -432,22 +432,36 @@ BattleInfo * BattleInfo::setupBattle( int3 tile, ETerrainType terrain, BFieldTyp
 	if(!creatureBank)
 	{
 		//Checks if hero has artifact and create appropriate stack
-		auto handleWarMachine= [&](int side, ArtifactPosition artslot, CreatureID cretype, BattleHex hex)
+		auto handleWarMachine= [&](int side, ArtifactPosition artslot, BattleHex hex)
 		{
-			if(heroes[side] && heroes[side]->getArt(artslot))
-				stacks.push_back(curB->generateNewStack(CStackBasicDescriptor(cretype, 1), !side, SlotID::WAR_MACHINES_SLOT, hex));
+			const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot);
+
+			if(nullptr != warMachineArt)
+			{
+				CreatureID cre = warMachineArt->artType->warMachine;
+
+				if(cre != CreatureID::NONE)
+					stacks.push_back(curB->generateNewStack(CStackBasicDescriptor(cre, 1), !side, SlotID::WAR_MACHINES_SLOT, hex));
+			}
 		};
 
-		handleWarMachine(0, ArtifactPosition::MACH1, CreatureID::BALLISTA, 52);
-		handleWarMachine(0, ArtifactPosition::MACH2, CreatureID::AMMO_CART, 18);
-		handleWarMachine(0, ArtifactPosition::MACH3, CreatureID::FIRST_AID_TENT, 154);
-		if(town && town->hasFort())
-			handleWarMachine(0, ArtifactPosition::MACH4, CreatureID::CATAPULT, 120);
+		if(heroes[0])
+		{
 
-		if(!town) //defending hero shouldn't receive ballista (bug #551)
-			handleWarMachine(1, ArtifactPosition::MACH1, CreatureID::BALLISTA, 66);
-		handleWarMachine(1, ArtifactPosition::MACH2, CreatureID::AMMO_CART, 32);
-		handleWarMachine(1, ArtifactPosition::MACH3, CreatureID::FIRST_AID_TENT, 168);
+			handleWarMachine(0, ArtifactPosition::MACH1, 52);
+			handleWarMachine(0, ArtifactPosition::MACH2, 18);
+			handleWarMachine(0, ArtifactPosition::MACH3, 154);
+			if(town && town->hasFort())
+				handleWarMachine(0, ArtifactPosition::MACH4, 120);
+		}
+
+		if(heroes[1])
+		{
+			if(!town) //defending hero shouldn't receive ballista (bug #551)
+				handleWarMachine(1, ArtifactPosition::MACH1, 66);
+			handleWarMachine(1, ArtifactPosition::MACH2, 32);
+			handleWarMachine(1, ArtifactPosition::MACH3, 168);
+		}
 	}
 	//war machines added
 

+ 45 - 66
lib/CArtHandler.cpp

@@ -15,6 +15,7 @@
 #include "CGeneralTextHandler.h"
 #include "VCMI_Lib.h"
 #include "CModHandler.h"
+#include "CCreatureHandler.h"
 #include "spells/CSpellHandler.h"
 #include "mapObjects/MapObjects.h"
 #include "NetPacksBase.h"
@@ -61,14 +62,21 @@ const std::string & CArtifact::EventText() const
 	return eventText;
 }
 
-bool CArtifact::isBig () const
+bool CArtifact::isBig() const
 {
-	return VLC->arth->isBigArtifact(id);
+	return warMachine != CreatureID::NONE;
 }
 
-bool CArtifact::isTradable () const
+bool CArtifact::isTradable() const
 {
-	return VLC->arth->isTradableArtifact(id);
+	switch(id)
+	{
+	case ArtifactID::SPELLBOOK:
+	case ArtifactID::GRAIL:
+		return false;
+	default:
+		return !isBig();
+	}
 }
 
 CArtifact::CArtifact()
@@ -120,6 +128,26 @@ void CArtifact::addNewBonus(const std::shared_ptr<Bonus>& b)
 	CBonusSystemNode::addNewBonus(b);
 }
 
+void CArtifact::fillWarMachine()
+{
+	switch (id)
+	{
+	case ArtifactID::CATAPULT:
+		warMachine = CreatureID::CATAPULT;
+		break;
+	case ArtifactID::BALLISTA:
+		warMachine = CreatureID::BALLISTA;
+		break;
+	case ArtifactID::FIRST_AID_TENT:
+		warMachine = CreatureID::FIRST_AID_TENT;
+		break;
+	case ArtifactID::AMMO_CART:
+		warMachine = CreatureID::AMMO_CART;
+		break;
+	}
+	warMachine = CreatureID::NONE; //this artifact is not a creature
+}
+
 void CGrowingArtifact::levelUpArtifact (CArtifactInstance * art)
 {
 	auto b = std::make_shared<Bonus>();
@@ -146,11 +174,6 @@ void CGrowingArtifact::levelUpArtifact (CArtifactInstance * art)
 
 CArtHandler::CArtHandler()
 {
-	//VLC->arth = this;
-
-	// War machines are the default big artifacts.
-	for (ArtifactID i = ArtifactID::CATAPULT; i <= ArtifactID::FIRST_AID_TENT; i.advance(1))
-		bigArtifacts.insert(i);
 }
 
 CArtHandler::~CArtHandler()
@@ -310,6 +333,19 @@ CArtifact * CArtHandler::loadFromJson(const JsonNode & node, const std::string &
 		auto bonus = JsonUtils::parseBonus(b);
 		art->addNewBonus(bonus);
 	}
+
+	const JsonNode & warMachine = node["warMachine"];
+	if(warMachine.getType() == JsonNode::DATA_STRING && warMachine.String() != "")
+	{
+		VLC->modh->identifiers.requestIdentifier("creature", warMachine, [=](si32 id)
+		{
+			art->warMachine = CreatureID(id);
+
+			//this assumes that creature object is stored before registration
+			VLC->creh->creatures.at(id)->warMachine = art->id;
+		});
+	}
+
 	return art;
 }
 
@@ -453,47 +489,6 @@ void CArtHandler::loadGrowingArt(CGrowingArtifact * art, const JsonNode & node)
 	}
 }
 
-//TODO: use bimap
-ArtifactID CArtHandler::creatureToMachineID(CreatureID id)
-{
-	switch (id)
-	{
-	case CreatureID::CATAPULT: //Catapult
-		return ArtifactID::CATAPULT;
-		break;
-	case CreatureID::BALLISTA: //Ballista
-		return ArtifactID::BALLISTA;
-		break;
-	case CreatureID::FIRST_AID_TENT: //First Aid tent
-		return ArtifactID::FIRST_AID_TENT;
-		break;
-	case CreatureID::AMMO_CART: //Ammo cart
-		return ArtifactID::AMMO_CART;
-		break;
-	}
-	return ArtifactID::NONE; //this creature is not artifact
-}
-
-CreatureID CArtHandler::machineIDToCreature(ArtifactID id)
-{
-	switch (id)
-	{
-	case ArtifactID::CATAPULT:
-		return CreatureID::CATAPULT;
-		break;
-	case ArtifactID::BALLISTA:
-		return CreatureID::BALLISTA;
-		break;
-	case ArtifactID::FIRST_AID_TENT:
-		return CreatureID::FIRST_AID_TENT;
-		break;
-	case ArtifactID::AMMO_CART:
-		return CreatureID::AMMO_CART;
-		break;
-	}
-	return CreatureID::NONE; //this artifact is not a creature
-}
-
 ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function<bool(ArtifactID)> accepts)
 {
 	auto getAllowedArts = [&](std::vector<ConstTransitivePtr<CArtifact> > &out, std::vector<CArtifact*> *arts, CArtifact::EartClass flag)
@@ -635,22 +630,6 @@ bool CArtHandler::legalArtifact(ArtifactID id)
 		art->aClass <= CArtifact::ART_RELIC);
 }
 
-bool CArtHandler::isTradableArtifact(ArtifactID id) const
-{
-	switch (id)
-	{
-	case ArtifactID::SPELLBOOK:
-	case ArtifactID::GRAIL:
-	case ArtifactID::CATAPULT:
-	case ArtifactID::BALLISTA:
-	case ArtifactID::AMMO_CART:
-	case ArtifactID::FIRST_AID_TENT:
-		return false;
-	default:
-		return true;
-	}
-}
-
 void CArtHandler::initAllowedArtifactsList(const std::vector<bool> &allowed)
 {
 	allowedArtifacts.clear();

+ 12 - 8
lib/CArtHandler.h

@@ -61,6 +61,7 @@ public:
 	std::vector<CArtifact *> constituentOf; // Reverse map of constituents - combined arts that include this art
 	EartClass aClass;
 	ArtifactID id;
+	CreatureID warMachine;
 
 	const std::string &Name() const; //getter
 	const std::string &Description() const; //getter
@@ -84,12 +85,23 @@ public:
 		{
 			h & identifier;
 		}
+
+		if(version >= 771)
+		{
+			h & warMachine;
+		}
+		else if(!h.saving)
+		{
+			fillWarMachine();
+		}
 	}
 
 	CArtifact();
 	~CArtifact();
 
 	friend class CArtHandler;
+private:
+	void fillWarMachine();
 };
 
 class DLL_LINKAGE CGrowingArtifact : public CArtifact //for example commander artifacts getting bonuses after battle
@@ -213,7 +225,6 @@ public:
 
 	std::vector< ConstTransitivePtr<CArtifact> > artifacts;
 	std::vector<CArtifact *> allowedArtifacts;
-	std::set<ArtifactID> bigArtifacts; // Artifacts that cannot be moved to backpack, e.g. war machines.
 	std::set<ArtifactID> growingArtifacts;
 
 	void addBonuses(CArtifact *art, const JsonNode &bonusList);
@@ -231,13 +242,7 @@ public:
 	ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function<bool(ArtifactID)> accepts);
 
 	bool legalArtifact(ArtifactID id);
-	//void getAllowedArts(std::vector<ConstTransitivePtr<CArtifact> > &out, std::vector<CArtifact*> *arts, int flag);
-	//void getAllowed(std::vector<ConstTransitivePtr<CArtifact> > &out, int flags);
-	bool isBigArtifact (ArtifactID artID) const {return bigArtifacts.find(artID) != bigArtifacts.end();}
-	bool isTradableArtifact (ArtifactID id) const;
 	void initAllowedArtifactsList(const std::vector<bool> &allowed); //allowed[art_id] -> 0 if not allowed, 1 if allowed
-	static ArtifactID creatureToMachineID(CreatureID id);
-	static CreatureID machineIDToCreature(ArtifactID id);
 	void makeItCreatureArt (CArtifact * a, bool onlyCreature = true);
 	void makeItCreatureArt (ArtifactID aid, bool onlyCreature = true);
 	void makeItCommanderArt (CArtifact * a, bool onlyCommander = true);
@@ -264,7 +269,6 @@ public:
 	{
 		h & artifacts & allowedArtifacts & treasures & minors & majors & relics
 			& growingArtifacts;
-		//if(!h.saving) sortArts();
 	}
 
 private:

+ 2 - 103
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;
@@ -1708,59 +1708,6 @@ std::vector<BattleHex> CBattleInfoCallback::getAttackableBattleHexes() const
 	return attackableBattleHexes;
 }
 
-ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell(const ISpellCaster * caster, const CSpell * spell, ECastingMode::ECastingMode mode) const
-{
-	RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID);
-	if(caster == nullptr)
-	{
-		logGlobal->errorStream() << "CBattleInfoCallback::battleCanCastThisSpell: no spellcaster.";
-		return ESpellCastProblem::INVALID;
-	}
-	const PlayerColor player = caster->getOwner();
-	const si8 side = playerToSide(player);
-
-	if(side < 0)
-		return ESpellCastProblem::INVALID;
-
-	if(!battleDoWeKnowAbout(side))
-		return ESpellCastProblem::INVALID;
-
-	ESpellCastProblem::ESpellCastProblem genProblem = battleCanCastSpell(caster, mode);
-	if(genProblem != ESpellCastProblem::OK)
-		return genProblem;
-
-	switch(mode)
-	{
-	case ECastingMode::HERO_CASTING:
-		{
-			const CGHeroInstance * castingHero = dynamic_cast<const CGHeroInstance *>(caster);//todo: unify hero|creature spell cost
-			if(!castingHero)
-			{
-				logGlobal->error("battleCanCastThisSpell: invalid caster");
-				return ESpellCastProblem::INVALID;
-			}
-
-			if(!castingHero->getArt(ArtifactPosition::SPELLBOOK))
-				return ESpellCastProblem::NO_SPELLBOOK;
-			if(!castingHero->canCastThisSpell(spell))
-				return ESpellCastProblem::HERO_DOESNT_KNOW_SPELL;
-			if(castingHero->mana < battleGetSpellCost(spell, castingHero)) //not enough mana
-				return ESpellCastProblem::NOT_ENOUGH_MANA;
-		}
-		break;
-	}
-
-	if(!spell->combatSpell)
-		return ESpellCastProblem::ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL;
-
-	//effect like Recanter's Cloak. Blocks also passive casting.
-	//TODO: check creature abilities to block
-	if(battleMaxSpellLevel(side) < spell->level)
-		return ESpellCastProblem::SPELL_LEVEL_LIMIT_EXCEEDED;
-
-	return spell->canBeCast(this, mode, caster);
-}
-
 ui32 CBattleInfoCallback::battleGetSpellCost(const CSpell * sp, const CGHeroInstance * caster) const
 {
 	RETURN_IF_NOT_BATTLE(-1);
@@ -1789,22 +1736,6 @@ ui32 CBattleInfoCallback::battleGetSpellCost(const CSpell * sp, const CGHeroInst
 	return ret - manaReduction + manaIncrease;
 }
 
-ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpellHere(const ISpellCaster * caster, const CSpell * spell, ECastingMode::ECastingMode mode, BattleHex dest) const
-{
-	RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID);
-	if(caster == nullptr)
-	{
-		logGlobal->errorStream() << "CBattleInfoCallback::battleCanCastThisSpellHere: no spellcaster.";
-		return ESpellCastProblem::INVALID;
-	}
-
-	ESpellCastProblem::ESpellCastProblem problem = battleCanCastThisSpell(caster, spell, mode);
-	if(problem != ESpellCastProblem::OK)
-		return problem;
-
-	return spell->canBeCastAt(this, caster, mode, dest);
-}
-
 const CStack * CBattleInfoCallback::getStackIf(std::function<bool(const CStack*)> pred) const
 {
 	RETURN_IF_NOT_BATTLE(nullptr);
@@ -1887,7 +1818,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c
 
 		if(subject->hasBonus(Selector::source(Bonus::SPELL_EFFECT, spellID), Selector::all, cachingStr.str())
 			//TODO: this ability has special limitations
-			|| battleCanCastThisSpellHere(subject, spellID.toSpell(), ECastingMode::CREATURE_ACTIVE_CASTING, subject->position) != ESpellCastProblem::OK)
+			|| spellID.toSpell()->canBeCastAt(this, subject, ECastingMode::CREATURE_ACTIVE_CASTING, subject->position) != ESpellCastProblem::OK)
 			continue;
 
 		switch (spellID)
@@ -2127,18 +2058,6 @@ ReachabilityInfo::Parameters::Parameters(const CStack *Stack)
 	knownAccessible = stack->getHexes();
 }
 
-ESpellCastProblem::ESpellCastProblem CPlayerBattleCallback::battleCanCastThisSpell(const CSpell * spell) const
-{
-	RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID);
-	ASSERT_IF_CALLED_WITH_PLAYER
-
-	const ISpellCaster * hero = battleGetMyHero();
-	if(hero == nullptr)
-		return ESpellCastProblem::INVALID;
-	else
-		return CBattleInfoCallback::battleCanCastThisSpell(hero, spell, ECastingMode::HERO_CASTING);
-}
-
 bool CPlayerBattleCallback::battleCanFlee() const
 {
 	RETURN_IF_NOT_BATTLE(false);
@@ -2169,26 +2088,6 @@ int CPlayerBattleCallback::battleGetSurrenderCost() const
 	return CBattleInfoCallback::battleGetSurrenderCost(*player);
 }
 
-bool CPlayerBattleCallback::battleCanCastSpell(ESpellCastProblem::ESpellCastProblem *outProblem /*= nullptr*/) const
-{
-	RETURN_IF_NOT_BATTLE(false);
-	ASSERT_IF_CALLED_WITH_PLAYER
-
-	const CGHeroInstance * hero = battleGetMyHero();
-	if(!hero)
-	{
-		if(outProblem)
-			*outProblem = ESpellCastProblem::NO_HERO_TO_CAST_SPELL;
-		return false;
-	}
-
-	auto problem = CBattleInfoCallback::battleCanCastSpell(hero, ECastingMode::HERO_CASTING);
-	if(outProblem)
-		*outProblem = problem;
-
-	return problem == ESpellCastProblem::OK;
-}
-
 const CGHeroInstance * CPlayerBattleCallback::battleGetMyHero() const
 {
 	return CBattleInfoEssentials::battleGetFightingHero(battleGetMySide());

+ 0 - 4
lib/CBattleCallback.h

@@ -293,8 +293,6 @@ public:
 	si8 battleMaxSpellLevel(ui8 side) const; //calculates minimum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned
 	ui32 battleGetSpellCost(const CSpell * sp, const CGHeroInstance * caster) const; //returns cost of given spell
 	ESpellCastProblem::ESpellCastProblem battleCanCastSpell(const ISpellCaster * caster, ECastingMode::ECastingMode mode) const; //returns true if there are no general issues preventing from casting a spell
-	ESpellCastProblem::ESpellCastProblem battleCanCastThisSpell(const ISpellCaster * caster, const CSpell * spell, ECastingMode::ECastingMode mode) const; //checks if given player can cast given spell
-	ESpellCastProblem::ESpellCastProblem battleCanCastThisSpellHere(const ISpellCaster * caster, const CSpell * spell, ECastingMode::ECastingMode mode, BattleHex dest) const; //checks if given player can cast given spell at given tile in given mode
 
 	SpellID battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const;
 	SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const;
@@ -336,11 +334,9 @@ class DLL_LINKAGE CPlayerBattleCallback : public CBattleInfoCallback
 public:
 	bool battleCanFlee() const; //returns true if caller can flee from the battle
 	TStacks battleGetStacks(EStackOwnership whose = MINE_AND_ENEMY, bool onlyAlive = true) const; //returns stacks on battlefield
-	ESpellCastProblem::ESpellCastProblem battleCanCastThisSpell(const CSpell * spell) const; //determines if given spell can be cast (and returns problem description)
 
 	int battleGetSurrenderCost() const; //returns cost of surrendering battle, -1 if surrendering is not possible
 
-	bool battleCanCastSpell(ESpellCastProblem::ESpellCastProblem *outProblem = nullptr) const; //returns true, if caller can cast a spell. If not, if pointer is given via arg, the reason will be written.
 	const CGHeroInstance * battleGetMyHero() const;
 	InfoAboutHero battleGetEnemyHero() const;
 };

+ 20 - 0
lib/CCreatureHandler.cpp

@@ -152,6 +152,26 @@ void CCreature::setId(CreatureID ID)
 	CBonusSystemNode::treeHasChanged();
 }
 
+void CCreature::fillWarMachine()
+{
+	switch (idNumber)
+	{
+	case CreatureID::CATAPULT: //Catapult
+		warMachine = ArtifactID::CATAPULT;
+		break;
+	case CreatureID::BALLISTA: //Ballista
+		warMachine = ArtifactID::BALLISTA;
+		break;
+	case CreatureID::FIRST_AID_TENT: //First Aid tent
+		warMachine = ArtifactID::FIRST_AID_TENT;
+		break;
+	case CreatureID::AMMO_CART: //Ammo cart
+		warMachine = ArtifactID::AMMO_CART;
+		break;
+	}
+	warMachine = ArtifactID::NONE; //this creature is not artifact
+}
+
 static void AddAbility(CCreature *cre, const JsonVector &ability_vec)
 {
 	auto nsf = std::make_shared<Bonus>();

+ 13 - 0
lib/CCreatureHandler.h

@@ -98,6 +98,8 @@ public:
 		}
 	} sounds;
 
+	ArtifactID warMachine;
+
 	bool isItNativeTerrain(int terrain) const;
 	bool isDoubleWide() const; //returns true if unit is double wide on battlefield
 	bool isFlying() const; //returns true if it is a flying unit
@@ -142,9 +144,20 @@ public:
 		{
 			h & identifier;
 		}
+		if(version >= 771)
+		{
+			h & warMachine;
+		}
+		else if(!h.saving)
+		{
+			fillWarMachine();
+		}
 	}
 
 	CCreature();
+
+private:
+	void fillWarMachine();
 };
 
 class DLL_LINKAGE CCreatureHandler : public IHandlerBase

+ 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

+ 40 - 6
lib/CGameState.cpp

@@ -16,7 +16,6 @@
 #include "StartInfo.h"
 #include "NetPacks.h"
 #include "registerTypes/RegisterTypes.h"
-#include "mapping/CMapInfo.h"
 #include "BattleInfo.h"
 #include "JsonNode.h"
 #include "filesystem/Filesystem.h"
@@ -24,8 +23,10 @@
 #include "rmg/CMapGenerator.h"
 #include "CStopWatch.h"
 #include "mapping/CMapEditManager.h"
+#include "mapping/CMapService.h"
 #include "serializer/CTypeList.h"
 #include "serializer/CMemorySerializer.h"
+#include "VCMIDirs.h"
 
 #ifdef min
 #undef min
@@ -698,7 +699,7 @@ CGameState::~CGameState()
 		ptr.second.dellNull();
 }
 
-void CGameState::init(StartInfo * si)
+void CGameState::init(StartInfo * si, bool allowSavingRandomMap)
 {
 	logGlobal->infoStream() << "\tUsing random seed: "<< si->seedToBeUsed;
 	getRandomGenerator().setSeed(si->seedToBeUsed);
@@ -709,7 +710,7 @@ void CGameState::init(StartInfo * si)
 	switch(scenarioOps->mode)
 	{
 	case StartInfo::NEW_GAME:
-		initNewGame();
+		initNewGame(allowSavingRandomMap);
 		break;
 	case StartInfo::CAMPAIGN:
 		initCampaign();
@@ -771,7 +772,7 @@ void CGameState::init(StartInfo * si)
 	}
 }
 
-void CGameState::initNewGame()
+void CGameState::initNewGame(bool allowSavingRandomMap)
 {
 	if(scenarioOps->createRandomMap())
 	{
@@ -780,8 +781,37 @@ void CGameState::initNewGame()
 
 		// Gen map
 		CMapGenerator mapGenerator;
-		map = mapGenerator.generate(scenarioOps->mapGenOptions.get(), scenarioOps->seedToBeUsed).release();
 
+		std::unique_ptr<CMap> randomMap = mapGenerator.generate(scenarioOps->mapGenOptions.get(), scenarioOps->seedToBeUsed);
+
+		if(allowSavingRandomMap)
+		{
+			try
+			{
+				auto path = VCMIDirs::get().userCachePath() / "RandomMaps";
+				boost::filesystem::create_directories(path);
+
+				std::shared_ptr<CMapGenOptions> options = scenarioOps->mapGenOptions;
+
+				const std::string templateName = options->getMapTemplate()->getName();
+				const ui32 seed = scenarioOps->seedToBeUsed;
+
+				const std::string fileName = boost::str(boost::format("%s_%d.vmap") % templateName % seed );
+				const auto fullPath = path / fileName;
+
+				CMapService::saveMap(randomMap, fullPath);
+
+				logGlobal->info("Random map has been saved to:");
+				logGlobal->info(fullPath.string());
+			}
+			catch(...)
+			{
+				logGlobal->error("Saving random map failed with exception");
+				handleException();
+			}
+		}
+
+		map = randomMap.release();
 		// Update starting options
 		for(int i = 0; i < map->players.size(); ++i)
 		{
@@ -809,7 +839,8 @@ void CGameState::initNewGame()
 	else
 	{
 		logGlobal->infoStream() << "Open map file: " << scenarioOps->mapname;
-		map = CMapService::loadMap(scenarioOps->mapname).release();
+		const ResourceID mapURI(scenarioOps->mapname, EResType::MAP);
+		map = CMapService::loadMap(mapURI).release();
 	}
 }
 
@@ -2177,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];
 }
 

+ 2 - 2
lib/CGameState.h

@@ -201,7 +201,7 @@ public:
 	CGameState();
 	virtual ~CGameState();
 
-	void init(StartInfo * si);
+	void init(StartInfo * si, bool allowSavingRandomMap = false);
 
 	ConstTransitivePtr<StartInfo> scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized)
 	PlayerColor currentPlayer; //ID of player currently having turn
@@ -283,7 +283,7 @@ private:
 
 	// ----- initialization -----
 
-	void initNewGame();
+	void initNewGame(bool allowSavingRandomMap);
 	void initCampaign();
 	void initDuel();
 	void checkMapChecksum();

+ 1 - 1
lib/CTownHandler.cpp

@@ -584,7 +584,7 @@ void CTownHandler::loadTown(CTown &town, const JsonNode & source)
 	VLC->modh->identifiers.requestIdentifier("creature", source["warMachine"],
 	[&town](si32 creature)
 	{
-		town.warMachine = CArtHandler::creatureToMachineID(CreatureID(creature));
+		town.warMachine = CreatureID(creature).toCreature()->warMachine;
 	});
 
 	town.moatDamage = source["moatDamage"].Float();

+ 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;

+ 35 - 12
lib/Interprocess.h

@@ -19,40 +19,63 @@
 struct ServerReady
 {
 	bool ready;
-	boost::interprocess::interprocess_mutex  mutex;
-	boost::interprocess::interprocess_condition  cond;
+	uint16_t port; //ui16?
+	boost::interprocess::interprocess_mutex mutex;
+	boost::interprocess::interprocess_condition cond;
 
 	ServerReady()
 	{
 		ready = false;
+		port = 0;
 	}
 
-	void setToTrueAndNotify()
+	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;
 		}
 		cond.notify_all();
 	}
 };
 
-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);
 	}
 };

+ 22 - 0
lib/NetPacks.h

@@ -152,6 +152,21 @@ struct PlayerBlocked : public CPackForClient
 	}
 };
 
+struct PlayerCheated : public CPackForClient
+{
+	PlayerCheated() : losingCheatCode(false), winningCheatCode(false) {}
+	DLL_LINKAGE void applyGs(CGameState *gs);
+
+	PlayerColor player;
+	bool losingCheatCode;
+	bool winningCheatCode;
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & player & losingCheatCode & winningCheatCode;
+	}
+};
+
 struct YourTurn : public CPackForClient
 {
 	YourTurn(){}
@@ -1774,6 +1789,13 @@ struct CloseServer : public CPackForServer
 	{}
 };
 
+struct LeaveGame : public CPackForServer
+{
+	bool applyGh(CGameHandler *gh);
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{}
+};
+
 struct EndTurn : public CPackForServer
 {
 	bool applyGh(CGameHandler *gh);

+ 9 - 0
lib/NetPacksLib.cpp

@@ -1840,6 +1840,15 @@ 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;
+}
+
 DLL_LINKAGE void YourTurn::applyGs(CGameState *gs)
 {
 	gs->currentPlayer = player;

+ 1 - 1
lib/VCMI_lib.vcxproj

@@ -424,4 +424,4 @@
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
   </ImportGroup>
-</Project>
+</Project>

+ 1 - 1
lib/VCMI_lib.vcxproj.filters

@@ -669,4 +669,4 @@
       <Filter>Header Files</Filter>
     </ClInclude>
   </ItemGroup>
-</Project>
+</Project>

+ 41 - 40
lib/mapObjects/CGHeroInstance.cpp

@@ -352,34 +352,43 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor *dst /*=
 
 		int count = rand.nextInt(stack.minAmount, stack.maxAmount);
 
-		if(stack.creature >= CreatureID::CATAPULT &&
-		   stack.creature <= CreatureID::ARROW_TOWERS) //war machine
+		const CCreature * creature = stack.creature.toCreature();
+
+		if(creature == nullptr)
+		{
+			logGlobal->error("Hero %s has invalid creature with id %d in initial army", name, stack.creature.toEnum());
+			continue;
+		}
+
+		if(creature->warMachine != ArtifactID::NONE) //war machine
 		{
 			warMachinesGiven++;
 			if(dst != this)
 				continue;
 
 			int slot = -1;
-			ArtifactID aid = ArtifactID::NONE;
-			switch (stack.creature)
+			ArtifactID aid = creature->warMachine;
+			const CArtifact * art = aid.toArtifact();
+
+			if(art != nullptr && !art->possibleSlots.at(ArtBearer::HERO).empty())
 			{
-			case CreatureID::CATAPULT:
-				slot = ArtifactPosition::MACH4;
-				aid = ArtifactID::CATAPULT;
-				break;
-			default:
-				aid = CArtHandler::creatureToMachineID(stack.creature);
-				slot = 9 + aid;
-				break;
+				//TODO: should we try another possible slots?
+				ArtifactPosition slot = art->possibleSlots.at(ArtBearer::HERO).front();
+
+				if(!getArt(slot))
+					putArtifact(slot, CArtifactInstance::createNewArtifactInstance(aid));
+				else
+					logGlobal->warnStream() << "Hero " << name << " already has artifact at " << slot << ", omitting giving " << aid;
 			}
-			auto convSlot = ArtifactPosition(slot);
-			if(!getArt(convSlot))
-				putArtifact(convSlot, CArtifactInstance::createNewArtifactInstance(aid));
 			else
-				logGlobal->warnStream() << "Hero " << name << " already has artifact at " << slot << ", omitting giving " << aid;
+			{
+				logGlobal->error("Hero %s has invalid war machine in initial army", name);
+			}
 		}
 		else
+		{
 			dst->setCreature(SlotID(stackNo-warMachinesGiven), stack.creature, count);
+		}
 	}
 }
 
@@ -1610,42 +1619,34 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler)
 		}
 	}
 
+	//primary skills
+	if(handler.saving)
 	{
-		if(handler.saving)
+		const bool haveSkills = hasBonus(Selector::type(Bonus::PRIMARY_SKILL).And(Selector::sourceType(Bonus::HERO_BASE_SKILL)));
+
+		if(haveSkills)
 		{
-			bool haveSkills = false;
+			auto primarySkills = handler.enterStruct("primarySkills");
 
 			for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i)
 			{
-				if(valOfBonuses(Selector::typeSubtype(Bonus::PRIMARY_SKILL, i).And(Selector::sourceType(Bonus::HERO_BASE_SKILL))) != 0)
-				{
-					haveSkills = true;
-					break;
-				}
-			}
+				int value = valOfBonuses(Selector::typeSubtype(Bonus::PRIMARY_SKILL, i).And(Selector::sourceType(Bonus::HERO_BASE_SKILL)));
 
-			if(haveSkills)
-			{
-				auto primarySkills = handler.enterStruct("primarySkills");
-
-				for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i)
-				{
-					int value = valOfBonuses(Selector::typeSubtype(Bonus::PRIMARY_SKILL, i).And(Selector::sourceType(Bonus::HERO_BASE_SKILL)));
-
-					handler.serializeInt(PrimarySkill::names[i], value, 0);
-				}
+				handler.serializeInt(PrimarySkill::names[i], value, 0);
 			}
 		}
-		else
-		{
-			auto primarySkills = handler.enterStruct("primarySkills");
+	}
+	else
+	{
+		auto primarySkills = handler.enterStruct("primarySkills");
 
+		if(primarySkills.get().getType() == JsonNode::DATA_STRUCT)
+		{
 			for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i)
 			{
 				int value = 0;
-				handler.serializeInt(PrimarySkill::names[i], value, 0);
-				if(value != 0)
-					pushPrimSkill(static_cast<PrimarySkill::PrimarySkill>(i), value);
+				primarySkills->serializeInt(PrimarySkill::names[i], value, 0);
+				pushPrimSkill(static_cast<PrimarySkill::PrimarySkill>(i), value);
 			}
 		}
 	}

+ 1 - 1
lib/mapObjects/MiscObjects.cpp

@@ -1477,7 +1477,7 @@ std::string CGWitchHut::getHoverText(PlayerColor player) const
 std::string CGWitchHut::getHoverText(const CGHeroInstance * hero) const
 {
 	std::string hoverName = getHoverText(hero->tempOwner);
-	if(hero->getSecSkillLevel(SecondarySkill(ability))) //hero knows that ability
+	if(wasVisited(hero->tempOwner) && hero->getSecSkillLevel(SecondarySkill(ability))) //hero knows that ability
 		hoverName += "\n\n" + VLC->generaltexth->allTexts[357]; // (Already learned)
 	return hoverName;
 }

+ 3 - 1
lib/mapping/CMapInfo.cpp

@@ -1,6 +1,7 @@
 #include "StdInc.h"
 #include "CMapInfo.h"
 
+#include "../filesystem/ResourceID.h"
 #include "../StartInfo.h"
 #include "../GameConstants.h"
 #include "CMapService.h"
@@ -58,7 +59,7 @@ CMapInfo::~CMapInfo()
 void CMapInfo::mapInit(const std::string & fname)
 {
 	fileURI = fname;
-	mapHeader = CMapService::loadMapHeader(fname);
+	mapHeader = CMapService::loadMapHeader(ResourceID(fname, EResType::MAP));
 	countPlayers();
 }
 
@@ -81,3 +82,4 @@ CMapInfo & CMapInfo::operator=(CMapInfo &&tmp)
 	return *this;
 }
 
+#undef STEAL

+ 27 - 14
lib/mapping/CMapService.cpp

@@ -5,6 +5,7 @@
 #include "../filesystem/CBinaryReader.h"
 #include "../filesystem/CCompressedStream.h"
 #include "../filesystem/CMemoryStream.h"
+#include "../filesystem/CMemoryBuffer.h"
 
 #include "CMap.h"
 
@@ -12,24 +13,16 @@
 #include "MapFormatJson.h"
 
 
-std::unique_ptr<CMap> CMapService::loadMap(const std::string & name)
+std::unique_ptr<CMap> CMapService::loadMap(const ResourceID & name)
 {
 	auto stream = getStreamFromFS(name);
-	std::unique_ptr<CMap> map(getMapLoader(stream)->loadMap());
-	std::unique_ptr<CMapHeader> header(map.get());
-
-	getMapPatcher(name)->patchMapHeader(header);
-	header.release();
-
-	return map;
+	return getMapLoader(stream)->loadMap();
 }
 
-std::unique_ptr<CMapHeader> CMapService::loadMapHeader(const std::string & name)
+std::unique_ptr<CMapHeader> CMapService::loadMapHeader(const ResourceID & name)
 {
 	auto stream = getStreamFromFS(name);
-	std::unique_ptr<CMapHeader> header = getMapLoader(stream)->loadMapHeader();
-	getMapPatcher(name)->patchMapHeader(header);
-	return header;
+	return getMapLoader(stream)->loadMapHeader();
 }
 
 std::unique_ptr<CMap> CMapService::loadMap(const ui8 * buffer, int size, const std::string & name)
@@ -38,6 +31,7 @@ std::unique_ptr<CMap> CMapService::loadMap(const ui8 * buffer, int size, const s
 	std::unique_ptr<CMap> map(getMapLoader(stream)->loadMap());
 	std::unique_ptr<CMapHeader> header(map.get());
 
+	//might be original campaign and require patch
 	getMapPatcher(name)->patchMapHeader(header);
 	header.release();
 
@@ -48,13 +42,32 @@ std::unique_ptr<CMapHeader> CMapService::loadMapHeader(const ui8 * buffer, int s
 {
 	auto stream = getStreamFromMem(buffer, size);
 	std::unique_ptr<CMapHeader> header = getMapLoader(stream)->loadMapHeader();
+
+	//might be original campaign and require patch
 	getMapPatcher(name)->patchMapHeader(header);
 	return header;
 }
 
-std::unique_ptr<CInputStream> CMapService::getStreamFromFS(const std::string & name)
+void CMapService::saveMap(const std::unique_ptr<CMap> & map, boost::filesystem::path fullPath)
+{
+	CMemoryBuffer serializeBuffer;
+	{
+		CMapSaverJson saver(&serializeBuffer);
+		saver.saveMap(map);
+	}
+	{
+		boost::filesystem::remove(fullPath);
+		boost::filesystem::ofstream tmp(fullPath, boost::filesystem::ofstream::binary);
+
+		tmp.write((const char *)serializeBuffer.getBuffer().data(),serializeBuffer.getSize());
+		tmp.flush();
+		tmp.close();
+	}
+}
+
+std::unique_ptr<CInputStream> CMapService::getStreamFromFS(const ResourceID & name)
 {
-	return CResourceHandler::get()->load(ResourceID(name, EResType::MAP));
+	return CResourceHandler::get()->load(name);
 }
 
 std::unique_ptr<CInputStream> CMapService::getStreamFromMem(const ui8 * buffer, int size)

+ 6 - 3
lib/mapping/CMapService.h

@@ -11,6 +11,8 @@
 
 #pragma once
 
+class ResourceID;
+
 class CMap;
 class CMapHeader;
 class CInputStream;
@@ -31,7 +33,7 @@ public:
 	 * @param name the name of the map
 	 * @return a unique ptr to the loaded map class
 	 */
-	static std::unique_ptr<CMap> loadMap(const std::string & name);
+	static std::unique_ptr<CMap> loadMap(const ResourceID & name);
 
 	/**
 	 * Loads the VCMI/H3 map header specified by the name.
@@ -39,7 +41,7 @@ public:
 	 * @param name the name of the map
 	 * @return a unique ptr to the loaded map header class
 	 */
-	static std::unique_ptr<CMapHeader> loadMapHeader(const std::string & name);
+	static std::unique_ptr<CMapHeader> loadMapHeader(const ResourceID & name);
 
 	/**
 	 * Loads the VCMI/H3 map file from a buffer. This method is temporarily
@@ -69,6 +71,7 @@ public:
 	 */
 	static std::unique_ptr<CMapHeader> loadMapHeader(const ui8 * buffer, int size, const std::string & name);
 
+	static void saveMap(const std::unique_ptr<CMap> & map, boost::filesystem::path fullPath);
 private:
 	/**
 	 * Gets a map input stream object specified by a map name.
@@ -76,7 +79,7 @@ private:
 	 * @param name the name of the map
 	 * @return a unique ptr to the input stream class
 	 */
-	static std::unique_ptr<CInputStream> getStreamFromFS(const std::string & name);
+	static std::unique_ptr<CInputStream> getStreamFromFS(const ResourceID & name);
 
 	/**
 	 * Gets a map input stream from a buffer.

+ 10 - 2
lib/mapping/MapFormatH3M.cpp

@@ -872,9 +872,17 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot)
 	bool isArt  =  aid != artmask;
 	if(isArt)
 	{
-		if(vstd::contains(VLC->arth->bigArtifacts, aid) && slot >= GameConstants::BACKPACK_START)
+		const CArtifact * art = ArtifactID(aid).toArtifact();
+
+		if(nullptr == art)
+		{
+			logGlobal->warnStream() << "Invalid artifact in hero's backpack, ignoring...";
+			return false;
+		}
+
+		if(art->isBig() && slot >= GameConstants::BACKPACK_START)
 		{
-			logGlobal->warnStream() << "Warning: A big artifact (war machine) in hero's backpack, ignoring...";
+			logGlobal->warnStream() << "A big artifact (war machine) in hero's backpack, ignoring...";
 			return false;
 		}
 		if(aid == 0 && slot == ArtifactPosition::MISC5)

+ 5 - 0
lib/mapping/MapFormatJson.cpp

@@ -1212,6 +1212,8 @@ void CMapSaverJson::saveMap(const std::unique_ptr<CMap>& map)
 
 void CMapSaverJson::writeHeader()
 {
+	logGlobal->trace("Saving header");
+
 	JsonNode header;
 	JsonSerializer handler(mapObjectResolver.get(), header);
 
@@ -1283,6 +1285,7 @@ JsonNode CMapSaverJson::writeTerrainLevel(const int index)
 
 void CMapSaverJson::writeTerrain()
 {
+	logGlobal->trace("Saving terrain");
 	//todo: multilevel map save support
 
 	JsonNode surface = writeTerrainLevel(0);
@@ -1297,12 +1300,14 @@ void CMapSaverJson::writeTerrain()
 
 void CMapSaverJson::writeObjects()
 {
+	logGlobal->trace("Saving objects");
 	JsonNode data(JsonNode::DATA_STRUCT);
 
 	JsonSerializer handler(mapObjectResolver.get(), data);
 
 	for(CGObjectInstance * obj : map->objects)
 	{
+		logGlobal->trace("\t%s", obj->instanceName);
 		auto temp = handler.enterStruct(obj->instanceName);
 
 		obj->serializeJson(handler);

+ 2 - 0
lib/registerTypes/RegisterTypes.h

@@ -210,6 +210,7 @@ void registerTypesClientPacks1(Serializer &s)
 	s.template registerType<CPackForClient, PackageApplied>();
 	s.template registerType<CPackForClient, SystemMessage>();
 	s.template registerType<CPackForClient, PlayerBlocked>();
+	s.template registerType<CPackForClient, PlayerCheated>();
 	s.template registerType<CPackForClient, YourTurn>();
 	s.template registerType<CPackForClient, SetResources>();
 	s.template registerType<CPackForClient, SetPrimSkill>();
@@ -314,6 +315,7 @@ void registerTypesServerPacks(Serializer &s)
 {
 	s.template registerType<CPack, CPackForServer>();
 	s.template registerType<CPackForServer, CloseServer>();
+	s.template registerType<CPackForServer, LeaveGame>();
 	s.template registerType<CPackForServer, EndTurn>();
 	s.template registerType<CPackForServer, DismissHero>();
 	s.template registerType<CPackForServer, MoveHero>();

+ 0 - 1
lib/registerTypes/TypesClientPacks1.cpp

@@ -1,7 +1,6 @@
 #include "StdInc.h"
 #include "RegisterTypes.h"
 
-#include "../mapping/CMapInfo.h"
 #include "../StartInfo.h"
 #include "../CGameState.h"
 #include "../mapping/CMap.h"

+ 0 - 1
lib/registerTypes/TypesClientPacks2.cpp

@@ -1,7 +1,6 @@
 #include "StdInc.h"
 #include "RegisterTypes.h"
 
-#include "../mapping/CMapInfo.h"
 #include "../StartInfo.h"
 #include "../CStack.h"
 #include "../BattleInfo.h"

+ 0 - 1
lib/registerTypes/TypesMapObjects1.cpp

@@ -1,7 +1,6 @@
 #include "StdInc.h"
 #include "RegisterTypes.h"
 
-#include "../mapping/CMapInfo.h"
 #include "../StartInfo.h"
 #include "../CGameState.h"
 #include "../mapping/CMap.h"

+ 0 - 1
lib/registerTypes/TypesMapObjects2.cpp

@@ -1,7 +1,6 @@
 #include "StdInc.h"
 #include "RegisterTypes.h"
 
-#include "../mapping/CMapInfo.h"
 #include "../StartInfo.h"
 #include "../CStack.h"
 #include "../BattleInfo.h"

+ 0 - 1
lib/registerTypes/TypesMapObjects3.cpp

@@ -1,7 +1,6 @@
 #include "StdInc.h"
 #include "RegisterTypes.h"
 
-#include "../mapping/CMapInfo.h"
 #include "../StartInfo.h"
 #include "../CGameState.h"
 #include "../mapping/CMap.h"

+ 0 - 1
lib/registerTypes/TypesServerPacks.cpp

@@ -1,7 +1,6 @@
 #include "StdInc.h"
 #include "RegisterTypes.h"
 
-#include "../mapping/CMapInfo.h"
 #include "../StartInfo.h"
 #include "../CGameState.h"
 #include "../mapping/CMap.h"

+ 1 - 1
lib/serializer/CSerializer.h

@@ -14,7 +14,7 @@
 #include "../ConstTransitivePtr.h"
 #include "../GameConstants.h"
 
-const ui32 SERIALIZATION_VERSION = 770;
+const ui32 SERIALIZATION_VERSION = 771;
 const ui32 MINIMAL_SERIALIZATION_VERSION = 753;
 const std::string SAVEGAME_MAGIC = "VCMISVG";
 

+ 8 - 4
lib/serializer/Connection.cpp

@@ -61,14 +61,14 @@ void CConnection::init()
 	iser.fileVersion = SERIALIZATION_VERSION;
 }
 
-CConnection::CConnection(std::string host, std::string port, std::string Name)
+CConnection::CConnection(std::string host, ui16 port, std::string Name)
 :iser(this), oser(this), io_service(new asio::io_service), name(Name)
 {
 	int i;
 	boost::system::error_code error = asio::error::host_not_found;
 	socket = new tcp::socket(*io_service);
 	tcp::resolver resolver(*io_service);
-	tcp::resolver::iterator end, pom, endpoint_iterator = resolver.resolve(tcp::resolver::query(host,port),error);
+	tcp::resolver::iterator end, pom, endpoint_iterator = resolver.resolve(tcp::resolver::query(host, std::to_string(port)),error);
 	if(error)
 	{
 		logNetwork->errorStream() << "Problem with resolving: \n" << error;
@@ -191,8 +191,7 @@ void CConnection::close()
 	if(socket)
 	{
 		socket->close();
-		delete socket;
-		socket = nullptr;
+		vstd::clear_pointer(socket);
 	}
 }
 
@@ -201,6 +200,11 @@ bool CConnection::isOpen() const
 	return socket && connected;
 }
 
+bool CConnection::isHost() const
+{
+	return connectionID == 1;
+}
+
 void CConnection::reportState(CLogger * out)
 {
 	out->debugStream() << "CConnection";

+ 2 - 1
lib/serializer/Connection.h

@@ -68,12 +68,13 @@ public:
 
 	bool receivedStop, sendStop;
 
-	CConnection(std::string host, std::string port, std::string Name);
+	CConnection(std::string host, ui16 port, std::string Name);
 	CConnection(TAcceptor * acceptor, boost::asio::io_service *Io_service, std::string Name);
 	CConnection(TSocket * Socket, std::string Name); //use immediately after accepting connection into socket
 
 	void close();
 	bool isOpen() const;
+	bool isHost() const;
 	template<class T>
 	CConnection &operator&(const T&);
 	virtual ~CConnection(void);

+ 47 - 3
lib/spells/CSpellHandler.cpp

@@ -157,10 +157,50 @@ ui32 CSpell::calculateDamage(const ISpellCaster * caster, const CStack * affecte
 
 ESpellCastProblem::ESpellCastProblem CSpell::canBeCast(const CBattleInfoCallback * cb, ECastingMode::ECastingMode mode, const ISpellCaster * caster) const
 {
-	const ESpellCastProblem::ESpellCastProblem generalProblem = mechanics->canBeCast(cb, mode, caster);
+	ESpellCastProblem::ESpellCastProblem genProblem = cb->battleCanCastSpell(caster, mode);
+	if(genProblem != ESpellCastProblem::OK)
+		return genProblem;
 
-	if(generalProblem != ESpellCastProblem::OK)
-		return generalProblem;
+	switch(mode)
+	{
+	case ECastingMode::HERO_CASTING:
+		{
+			const CGHeroInstance * castingHero = dynamic_cast<const CGHeroInstance *>(caster);//todo: unify hero|creature spell cost
+			if(!castingHero)
+			{
+				logGlobal->debug("CSpell::canBeCast: invalid caster");
+				return ESpellCastProblem::NO_HERO_TO_CAST_SPELL;
+			}
+
+			if(!castingHero->getArt(ArtifactPosition::SPELLBOOK))
+				return ESpellCastProblem::NO_SPELLBOOK;
+			if(!castingHero->canCastThisSpell(this))
+				return ESpellCastProblem::HERO_DOESNT_KNOW_SPELL;
+			if(castingHero->mana < cb->battleGetSpellCost(this, castingHero)) //not enough mana
+				return ESpellCastProblem::NOT_ENOUGH_MANA;
+		}
+		break;
+	}
+
+	if(!isCombatSpell())
+		return ESpellCastProblem::ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL;
+
+	const PlayerColor player = caster->getOwner();
+	const si8 side = cb->playerToSide(player);
+
+	if(side < 0)
+		return ESpellCastProblem::INVALID;
+
+	//effect like Recanter's Cloak. Blocks also passive casting.
+	//TODO: check creature abilities to block
+	//TODO: check any possible caster
+	if(cb->battleMaxSpellLevel(side) < level)
+		return ESpellCastProblem::SPELL_LEVEL_LIMIT_EXCEEDED;
+
+	const ESpellCastProblem::ESpellCastProblem specificProblem = mechanics->canBeCast(cb, mode, caster);
+
+	if(specificProblem != ESpellCastProblem::OK)
+		return specificProblem;
 
 	//check for creature target existence
 	//allow to cast spell if there is at least one smart target
@@ -368,6 +408,10 @@ void CSpell::getEffects(std::vector<Bonus> & lst, const int level) const
 
 ESpellCastProblem::ESpellCastProblem CSpell::canBeCastAt(const CBattleInfoCallback * cb, const ISpellCaster * caster, ECastingMode::ECastingMode mode, BattleHex destination) const
 {
+	ESpellCastProblem::ESpellCastProblem problem = canBeCast(cb, mode, caster);
+	if(problem != ESpellCastProblem::OK)
+		return problem;
+
 	SpellTargetingContext ctx(this, mode, caster, caster->getSpellSchoolLevel(this), destination);
 
 	return mechanics->canBeCast(cb, ctx);

+ 6 - 2
lib/spells/ISpellMechanics.cpp

@@ -87,10 +87,14 @@ void BattleSpellCastParameters::cast(const SpellCastEnvironment * env)
 	spell->battleCast(env, *this);
 }
 
-void BattleSpellCastParameters::castIfPossible(const SpellCastEnvironment * env)
+bool BattleSpellCastParameters::castIfPossible(const SpellCastEnvironment * env)
 {
-	if(ESpellCastProblem::OK == cb->battleCanCastThisSpell(caster, spell, mode))
+	if(ESpellCastProblem::OK == spell->canBeCast(cb, mode, caster))
+	{
 		cast(env);
+		return true;
+	}
+	return false;
 }
 
 BattleHex BattleSpellCastParameters::getFirstDestinationHex() const

+ 2 - 1
lib/spells/ISpellMechanics.h

@@ -57,7 +57,8 @@ public:
 	void cast(const SpellCastEnvironment * env);
 
 	///cast with silent check for permitted cast
-	void castIfPossible(const SpellCastEnvironment * env);
+	///returns true if cast was permitted
+	bool castIfPossible(const SpellCastEnvironment * env);
 
 	BattleHex getFirstDestinationHex() const;
 

+ 125 - 58
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
@@ -1031,6 +1031,25 @@ void CGameHandler::handleConnection(std::set<PlayerColor> players, CConnection &
 {
 	setThreadName("CGameHandler::handleConnection");
 
+	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(!serverShuttingDown && playerConn.second == &c)
+			{
+				PlayerCheated pc;
+				pc.player = playerConn.first;
+				pc.losingCheatCode = true;
+				sendAndApply(&pc);
+				checkVictoryLossConditionsForPlayer(playerConn.first);
+			}
+		}
+	};
+
 	try
 	{
 		while(1)//server should never shut connection first //was: while(!end2)
@@ -1042,6 +1061,8 @@ void CGameHandler::handleConnection(std::set<PlayerColor> players, CConnection &
 
 			{
 				boost::unique_lock<boost::mutex> lock(*c.rmx);
+				if(!c.connected)
+					throw clientDisconnectedException();
 				c >> player >> requestID >> pack; //get the package
 
 				if (!pack)
@@ -1060,6 +1081,11 @@ void CGameHandler::handleConnection(std::set<PlayerColor> players, CConnection &
 			//prepare struct informing that action was applied
 			auto sendPackageResponse = [&](bool succesfullyApplied)
 			{
+				//dont reply to disconnected client
+				//TODO: this must be implemented as option of CPackForServer
+				if(dynamic_cast<LeaveGame *>(pack) || dynamic_cast<CloseServer *>(pack))
+					return;
+
 				PackageApplied applied;
 				applied.player = player;
 				applied.result = succesfullyApplied;
@@ -1095,13 +1121,15 @@ void CGameHandler::handleConnection(std::set<PlayerColor> players, CConnection &
 	}
 	catch(boost::system::system_error &e) //for boost errors just log, not crash - probably client shut down connection
 	{
-		assert(!c.connected); //make sure that connection has been marked as broken
-		logGlobal->error(e.what());
-		end2 = true;
+		handleDisconnection(e);
+	}
+	catch(clientDisconnectedException & e)
+	{
+		handleDisconnection(e);
 	}
 	catch(...)
 	{
-		end2 = true;
+		serverShuttingDown = true;
 		handleException();
 		throw;
 	}
@@ -1851,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());
@@ -1874,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())
@@ -1885,7 +1914,7 @@ void CGameHandler::run(bool resume)
 
 	auto playerTurnOrder = generatePlayerTurnOrder();
 
-	while(!end2)
+	while(!serverShuttingDown)
 	{
 		if (!resume) newTurn();
 
@@ -1926,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);
@@ -1942,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
@@ -2631,6 +2660,9 @@ void CGameHandler::sendToAllClients(CPackForClient * info)
 	logNetwork->trace("Sending to all clients a package of type %s", typeid(*info).name());
 	for (auto & elem : conns)
 	{
+		if(!elem->isOpen())
+			continue;
+
 		boost::unique_lock<boost::mutex> lock(*(elem)->wmx);
 		*elem << info;
 	}
@@ -2703,11 +2735,31 @@ void CGameHandler::close()
 	{
 		exit(0);
 	}
+	serverShuttingDown = true;
 
-	//for (CConnection *cc : conns)
-	//	if (cc && cc->socket && cc->socket->is_open())
-	//		cc->socket->close();
-	//exit(0);
+	for (auto & elem : conns)
+	{
+		if(!elem->isOpen())
+			continue;
+
+		boost::unique_lock<boost::mutex> lock(*(elem)->wmx);
+		elem->close();
+		elem->connected = false;
+	}
+}
+
+void CGameHandler::playerLeftGame(int cid)
+{
+	for (auto & elem : conns)
+	{
+		if(elem->isOpen() && elem->connectionID == cid)
+		{
+			boost::unique_lock<boost::mutex> lock(*(elem)->wmx);
+			elem->close();
+			elem->connected = false;
+			break;
+		}
+	}
 }
 
 bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player)
@@ -3083,7 +3135,7 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst
 	const CGDwelling * dw = static_cast<const CGDwelling *>(getObj(objid));
 	const CArmedInstance *dst = nullptr;
 	const CCreature *c = VLC->creh->creatures.at(crid);
-	bool warMachine = c->hasBonusOfType(Bonus::SIEGE_WEAPON);
+	const bool warMachine = c->warMachine != ArtifactID::NONE;
 
 	//TODO: test for owning
 	//TODO: check if dst can recruit objects (e.g. hero is actually visiting object, town and source are same, etc)
@@ -3134,24 +3186,18 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst
 	if (warMachine)
 	{
 		const CGHeroInstance *h = dynamic_cast<const CGHeroInstance*>(dst);
-		if (!h)
-			COMPLAIN_RET("Only hero can buy war machines");
 
-		switch(crid)
-		{
-		case CreatureID::BALLISTA:
-			giveHeroNewArtifact(h, VLC->arth->artifacts[ArtifactID::BALLISTA], ArtifactPosition::MACH1);
-			break;
-		case CreatureID::FIRST_AID_TENT:
-			giveHeroNewArtifact(h, VLC->arth->artifacts[ArtifactID::FIRST_AID_TENT], ArtifactPosition::MACH3);
-			break;
-		case CreatureID::AMMO_CART:
-			giveHeroNewArtifact(h, VLC->arth->artifacts[ArtifactID::AMMO_CART], ArtifactPosition::MACH2);
-			break;
-		default:
-			complain("This war machine cannot be recruited!");
-			return false;
-		}
+		COMPLAIN_RET_FALSE_IF(!h, "Only hero can buy war machines");
+
+		ArtifactID artId = c->warMachine;
+
+		COMPLAIN_RET_FALSE_IF(artId == ArtifactID::CATAPULT, "Catapult cannot be recruited!");
+
+		const CArtifact * art = artId.toArtifact();
+
+		COMPLAIN_RET_FALSE_IF(nullptr == art, "Invalid war machine artifact");
+
+		return giveHeroNewArtifact(h, art);
 	}
 	else
 	{
@@ -3392,7 +3438,10 @@ bool CGameHandler::assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition
 bool CGameHandler::buyArtifact(ObjectInstanceID hid, ArtifactID aid)
 {
 	const CGHeroInstance * hero = getHero(hid);
+	COMPLAIN_RET_FALSE_IF(nullptr == hero, "Invalid hero index");
 	const CGTownInstance * town = hero->visitedTown;
+	COMPLAIN_RET_FALSE_IF(nullptr == town, "Hero not in town");
+
 	if (aid==ArtifactID::SPELLBOOK)
 	{
 		if ((!town->hasBuilt(BuildingID::MAGES_GUILD_1) && complain("Cannot buy a spellbook, no mage guild in the town!"))
@@ -3407,26 +3456,24 @@ bool CGameHandler::buyArtifact(ObjectInstanceID hid, ArtifactID aid)
 		giveSpells(town,hero);
 		return true;
 	}
-	else if (aid < 7  &&  aid > 3) //war machine
+	else
 	{
-		int price = VLC->arth->artifacts[aid]->price;
+		const CArtifact * art = aid.toArtifact();
+		COMPLAIN_RET_FALSE_IF(nullptr == art, "Invalid artifact index to buy");
+		COMPLAIN_RET_FALSE_IF(art->warMachine == CreatureID::NONE, "War machine artifact required");
+		COMPLAIN_RET_FALSE_IF(hero->hasArt(aid),"Hero already has this machine!");
+		const int price = art->price;
+		COMPLAIN_RET_FALSE_IF(getPlayer(hero->getOwner())->resources.at(Res::GOLD) < price, "Not enough gold!");
 
-		if ((hero->getArt(ArtifactPosition(9+aid)) && complain("Hero already has this machine!"))
-		 || (getPlayer(hero->getOwner())->resources.at(Res::GOLD) < price && complain("Not enough gold!")))
-		{
-			return false;
-		}
 		if  ((town->hasBuilt(BuildingID::BLACKSMITH) && town->town->warMachine == aid)
 		 || ((town->hasBuilt(BuildingID::BALLISTA_YARD, ETownType::STRONGHOLD)) && aid == ArtifactID::BALLISTA))
 		{
 			giveResource(hero->getOwner(),Res::GOLD,-price);
-			giveHeroNewArtifact(hero, VLC->arth->artifacts[aid], ArtifactPosition(9+aid));
-			return true;
+			return giveHeroNewArtifact(hero, art);
 		}
 		else
 			COMPLAIN_RET("This machine is unavailable here!");
 	}
-	return false;
 }
 
 bool CGameHandler::buyArtifact(const IMarket *m, const CGHeroInstance *h, Res::ERes rid, ArtifactID aid)
@@ -4379,7 +4426,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
 	}
 }
 
@@ -4407,7 +4456,7 @@ bool CGameHandler::makeCustomAction(BattleAction &ba)
 			if (ba.selectedStack >= 0)
 				parameters.aimToStack(gs->curB->battleGetStackByID(ba.selectedStack, false));
 
-			ESpellCastProblem::ESpellCastProblem escp = gs->curB->battleCanCastThisSpell(h, s, ECastingMode::HERO_CASTING);//todo: should we check aimed cast(battleCanCastThisSpellHere)?
+			ESpellCastProblem::ESpellCastProblem escp = s->canBeCast(gs->curB, ECastingMode::HERO_CASTING, h);//todo: should we check aimed cast?
 			if (escp != ESpellCastProblem::OK)
 			{
 				logGlobal->warn("Spell cannot be cast! Problem: %d", escp);
@@ -4580,14 +4629,14 @@ void CGameHandler::stackTurnTrigger(const CStack *st)
 				const CSpell * spell = SpellID(spellID).toSpell();
 				bl.remove_if([&bonus](const Bonus* b){return b==bonus.get();});
 
-				if (gs->curB->battleCanCastThisSpell(st, spell, ECastingMode::ENCHANTER_CASTING) == ESpellCastProblem::OK)
-				{
-					BattleSpellCastParameters parameters(gs->curB, st, spell);
-					parameters.spellLvl = bonus->val;
-					parameters.effectLevel = bonus->val;//todo: recheck
-					parameters.mode = ECastingMode::ENCHANTER_CASTING;
-					parameters.cast(spellEnv);
+				BattleSpellCastParameters parameters(gs->curB, st, spell);
+				parameters.spellLvl = bonus->val;
+				parameters.effectLevel = bonus->val;//todo: recheck
+				parameters.mode = ECastingMode::ENCHANTER_CASTING;
 
+				cast = parameters.castIfPossible(spellEnv);
+				if(cast)
+				{
 					//todo: move to mechanics
 					BattleSetStackProperty ssp;
 					ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER;
@@ -4595,8 +4644,6 @@ void CGameHandler::stackTurnTrigger(const CStack *st)
 					ssp.val = bonus->additionalInfo; //increase cooldown counter
 					ssp.stackID = st->ID;
 					sendAndApply(&ssp);
-
-					cast = true;
 				}
 			}
 		}
@@ -5051,7 +5098,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player)
 
 			if (p->human)
 			{
-				end2 = true;
+				serverShuttingDown = true;
 
 				if (gs->scenarioOps->campState)
 				{
@@ -5251,7 +5298,7 @@ void CGameHandler::attackCasting(const BattleAttack & bat, Bonus::BonusType atta
 			vstd::amin(chance, 100);
 
 			const CSpell * spell = SpellID(spellID).toSpell();
-			if (gs->curB->battleCanCastThisSpellHere(attacker, spell, ECastingMode::AFTER_ATTACK_CASTING, oneOfAttacked->position) != ESpellCastProblem::OK)
+			if(spell->canBeCastAt(gs->curB, attacker, ECastingMode::AFTER_ATTACK_CASTING, oneOfAttacked->position) != ESpellCastProblem::OK)
 				continue;
 
 			//check if spell should be cast (probability handling)
@@ -6001,6 +6048,18 @@ void CGameHandler::putArtifact(const ArtifactLocation &al, const CArtifactInstan
 	sendAndApply(&pa);
 }
 
+bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * art)
+{
+	COMPLAIN_RET_FALSE_IF(art->possibleSlots.at(ArtBearer::HERO).empty(), "Not a hero artifact!");
+
+	ArtifactPosition slot = art->possibleSlots.at(ArtBearer::HERO).front();
+
+	COMPLAIN_RET_FALSE_IF(nullptr != h->getArt(slot, false), "Hero already has artifact in slot");
+
+	giveHeroNewArtifact(h, art, slot);
+	return true;
+}
+
 void CGameHandler::giveHeroNewArtifact(const CGHeroInstance *h, const CArtifact *artType, ArtifactPosition pos)
 {
 	CArtifactInstance *a = nullptr;
@@ -6162,12 +6221,18 @@ void CGameHandler::handleCheatCode(std::string & cheat, PlayerColor player, cons
 	else if (cheat == "vcmisilmaril")
 	{
 		///Player wins
-		gs->getPlayer(player)->enteredWinningCheatCode = 1;
+		PlayerCheated pc;
+		pc.player = player;
+		pc.winningCheatCode = true;
+		sendAndApply(&pc);
 	}
 	else if (cheat == "vcmimelkor")
 	{
 		///Player looses
-		gs->getPlayer(player)->enteredLosingCheatCode = 1;
+		PlayerCheated pc;
+		pc.player = player;
+		pc.losingCheatCode = true;
+		sendAndApply(&pc);
 	}
 	else if (cheat == "vcmieagles" || cheat == "vcmiungoliant")
 	{
@@ -6357,10 +6422,12 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, Battl
 		}
 		else if (st->slot == SlotID::WAR_MACHINES_SLOT)
 		{
-			auto warMachine = VLC->arth->creatureToMachineID(st->type->idNumber);
+			auto warMachine = st->type->warMachine;
 
 			if (warMachine == ArtifactID::NONE)
+			{
 				logGlobal->error("Invalid creature in war machine virtual slot. Stack: %s", st->nodeName());
+			}
 			//catapult artifact remain even if "creature" killed in siege
 			else if (warMachine != ArtifactID::CATAPULT && !st->count)
 			{

+ 7 - 0
server/CGameHandler.h

@@ -143,6 +143,7 @@ public:
 
 	void removeAfterVisit(const CGObjectInstance *object) override;
 
+	bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * art);
 	void giveHeroNewArtifact(const CGHeroInstance *h, const CArtifact *artType, ArtifactPosition pos) override;
 	void giveHeroArtifact(const CGHeroInstance *h, const CArtifactInstance *a, ArtifactPosition pos) override;
 	void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override;
@@ -223,6 +224,7 @@ public:
 	bool arrangeStacks( ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player);
 	void save(const std::string &fname);
 	void close();
+	void playerLeftGame(int cid);
 	void handleTimeEvents();
 	void handleTownEvents(CGTownInstance *town, NewTurn &n);
 	bool complain(const std::string &problem); //sends message to all clients, prints on the logs and return true
@@ -298,4 +300,9 @@ private:
 	void checkVictoryLossConditionsForAll();
 };
 
+class clientDisconnectedException : public std::exception
+{
+
+};
+
 void makeStackDoNothing();

+ 115 - 83
server/CVCMIServer.cpp

@@ -41,11 +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;
-int port = 3030;
+std::atomic<bool> serverShuttingDown(false);
 
 boost::program_options::variables_map cmdLineOptions;
 
@@ -106,6 +102,10 @@ void CPregameServer::handleConnection(CConnection *cpc)
 				auto unlock = vstd::makeUnlockGuard(mx);
 				while(state == RUNNING) boost::this_thread::sleep(boost::posix_time::milliseconds(50));
 			}
+			else if(quitting) // Server must be stopped if host is leaving from lobby to avoid crash
+			{
+				serverShuttingDown = true;
+			}
 		}
 	}
 	catch (const std::exception& e)
@@ -206,20 +206,29 @@ void CPregameServer::connectionAccepted(const boost::system::error_code& ec)
 		return;
 	}
 
-	logNetwork->info("We got a new connection! :)");
-	CConnection *pc = new CConnection(upcomingConnection, NAME);
-	initConnection(pc);
-	upcomingConnection = nullptr;
+	try
+	{
+		logNetwork->info("We got a new connection! :)");
+		std::string name = NAME;
+		CConnection *pc = new CConnection(upcomingConnection, name.append(" STATE_PREGAME"));
+		initConnection(pc);
+		upcomingConnection = nullptr;
 
-	startListeningThread(pc);
+		startListeningThread(pc);
 
-	*pc << (ui8)pc->connectionID << curmap;
+		*pc << (ui8)pc->connectionID << curmap;
 
-	announceTxt(pc->name + " joins the game");
-	auto pj = new PlayerJoined();
-	pj->playerName = pc->name;
-	pj->connectionID = pc->connectionID;
-	toAnnounce.push_back(pj);
+		announceTxt(pc->name + " joins the game");
+		auto pj = new PlayerJoined();
+		pj->playerName = pc->name;
+		pj->connectionID = pc->connectionID;
+		toAnnounce.push_back(pj);
+	}
+	catch(std::exception& e)
+	{
+		upcomingConnection = nullptr;
+		logNetwork->info("I guess it was just my imagination!");
+	}
 
 	start_async_accept();
 }
@@ -314,9 +323,28 @@ void CPregameServer::startListeningThread(CConnection * pc)
 }
 
 CVCMIServer::CVCMIServer()
-: io(new boost::asio::io_service()), acceptor(new TAcceptor(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port))), firstConnection(nullptr)
+	: port(3030), io(new boost::asio::io_service()), firstConnection(nullptr), shared(nullptr)
 {
 	logNetwork->trace("CVCMIServer created!");
+	if(cmdLineOptions.count("port"))
+		port = cmdLineOptions["port"].as<ui16>();
+	logNetwork->info("Port %d will be used", port);
+	try
+	{
+		acceptor = new TAcceptor(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port));
+	}
+	catch(...)
+	{
+		logNetwork->info("Port %d is busy, trying to use random port instead", port);
+		if(cmdLineOptions.count("run-by-client") && !cmdLineOptions.count("enable-shm"))
+		{
+			logNetwork->error("Cant pass port number to client without shared memory!", port);
+			exit(0);
+		}
+		acceptor = new TAcceptor(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0));
+		port = acceptor->local_endpoint().port();
+	}
+	logNetwork->info("Listening for connections at port %d", port);
 }
 CVCMIServer::~CVCMIServer()
 {
@@ -394,70 +422,77 @@ void CVCMIServer::newPregame()
 void CVCMIServer::start()
 {
 #ifndef VCMI_ANDROID
-	ServerReady *sr = nullptr;
-	intpr::mapped_region *mr;
-	try
+	if(cmdLineOptions.count("enable-shm"))
 	{
-		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(...)
-	{
-		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();
+		std::string sharedMemoryName = "vcmi_memory";
+		if(cmdLineOptions.count("enable-shm-uuid") && cmdLineOptions.count("uuid"))
+		{
+			sharedMemoryName += "_" + cmdLineOptions["uuid"].as<std::string>();
+		}
+		shared = new SharedMemory(sharedMemoryName);
 	}
 #endif
 
 	boost::system::error_code error;
-	logNetwork->info("Listening for connections at port %d", acceptor->local_endpoint().port());
-	auto s = new boost::asio::ip::tcp::socket(acceptor->get_io_service());
-	boost::thread acc(std::bind(vaccept,acceptor,s,&error));
-#ifndef VCMI_ANDROID
-	sr->setToTrueAndNotify();
-	delete mr;
+	for (;;)
+	{
+		try
+		{
+			auto s = new boost::asio::ip::tcp::socket(acceptor->get_io_service());
+			boost::thread acc(std::bind(vaccept,acceptor,s,&error));
+#ifdef VCMI_ANDROID
+			{ // in block to clean-up vm helper after use, because we don't need to keep this thread attached to vm
+				CAndroidVMHelper envHelper;
+				envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "onServerReady");
+				logNetwork->info("Sending server ready message to client");
+			}
 #else
-	{ // in block to clean-up vm helper after use, because we don't need to keep this thread attached to vm
-		CAndroidVMHelper envHelper;
-		envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "onServerReady");
-		logNetwork->info("Sending server ready message to client");
-	}
+			if(shared)
+			{
+				shared->sr->setToReadyAndNotify(port);
+			}
 #endif
 
-	acc.join();
-	if (error)
-	{
-		logNetwork->warnStream() << "Got connection but there is an error " << error;
-		return;
-	}
-	logNetwork->info("We've accepted someone... ");
-	firstConnection = new CConnection(s, NAME);
-	logNetwork->info("Got connection!");
-	while (!end2)
-	{
-		ui8 mode;
-		*firstConnection >> mode;
-		switch (mode)
-		{
-		case 0:
-			firstConnection->close();
-			exit(0);
-		case 1:
-			firstConnection->close();
-			return;
-		case 2:
-			newGame();
-			break;
-		case 3:
-			loadGame();
-			break;
-		case 4:
-			newPregame();
+			acc.join();
+			if (error)
+			{
+				logNetwork->warnStream()<<"Got connection but there is an error " << error;
+				return;
+			}
+			logNetwork->info("We've accepted someone... ");
+			std::string name = NAME;
+			firstConnection = new CConnection(s, name.append(" STATE_WAITING"));
+			logNetwork->info("Got connection!");
+			while(!serverShuttingDown)
+			{
+				ui8 mode;
+				*firstConnection >> mode;
+				switch (mode)
+				{
+				case 0:
+					firstConnection->close();
+					exit(0);
+				case 1:
+					firstConnection->close();
+					return;
+				case 2:
+					newGame();
+					break;
+				case 3:
+					loadGame();
+					break;
+				case 4:
+					newPregame();
+					break;
+				}
+			}
 			break;
 		}
+		catch(std::exception& e)
+		{
+			vstd::clear_pointer(firstConnection);
+			logNetwork->info("I guess it was just my imagination!");
+		}
 	}
 }
 
@@ -507,7 +542,11 @@ static void handleCommandOptions(int argc, char *argv[])
 	opts.add_options()
 		("help,h", "display help and exit")
 		("version,v", "display version information and exit")
-		("port", po::value<int>()->default_value(3030), "port at which server will listen to connections from client")
+		("run-by-client", "indicate that server launched by client on same machine")
+		("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.");
 
 	if(argc > 1)
@@ -584,12 +623,7 @@ int main(int argc, char** argv)
 	logConfig.configureDefault();
 	logGlobal->info(NAME);
 
-
 	handleCommandOptions(argc, argv);
-	if (cmdLineOptions.count("port"))
-		port = cmdLineOptions["port"].as<int>();
-	logNetwork->info("Port %d will be used.", port);
-
 	preinitDLL(console);
 	settings.init();
 	logConfig.configure();
@@ -603,7 +637,7 @@ int main(int argc, char** argv)
 
 		try
 		{
-			while (!end2)
+			while(!serverShuttingDown)
 			{
 				server.start();
 			}
@@ -612,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 (...)
 		{
@@ -630,11 +664,9 @@ int main(int argc, char** argv)
 	CAndroidVMHelper envHelper;
 	envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "killServer");
 #endif
-	delete VLC;
-	VLC = nullptr;
+	vstd::clear_pointer(VLC);
 	CResourceHandler::clear();
-
-  return 0;
+	return 0;
 }
 
 #ifdef VCMI_ANDROID

+ 3 - 1
server/CVCMIServer.h

@@ -17,6 +17,7 @@ class CMapInfo;
 class CConnection;
 struct CPackForSelectionScreen;
 class CGameHandler;
+struct SharedMemory;
 
 namespace boost
 {
@@ -43,8 +44,10 @@ typedef boost::asio::basic_stream_socket < boost::asio::ip::tcp , boost::asio::s
 
 class CVCMIServer
 {
+	ui16 port;
 	boost::asio::io_service *io;
 	TAcceptor * acceptor;
+	SharedMemory * shared;
 
 	CConnection *firstConnection;
 public:
@@ -73,7 +76,6 @@ public:
 	std::list<CPackForSelectionScreen*> toAnnounce;
 	boost::recursive_mutex mx;
 
-	//std::vector<CMapInfo> maps;
 	TAcceptor *acceptor;
 	TSocket *upcomingConnection;
 

+ 11 - 2
server/NetPacksServer.cpp

@@ -65,6 +65,12 @@ bool CloseServer::applyGh( CGameHandler *gh )
 	return true;
 }
 
+bool LeaveGame::applyGh( CGameHandler *gh )
+{
+	gh->playerLeftGame(c->connectionID);
+	return true;
+}
+
 bool EndTurn::applyGh( CGameHandler *gh )
 {
 	PlayerColor player = GS(gh)->currentPlayer;
@@ -277,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;
 }

+ 3 - 2
test/CMapEditManagerTest.cpp

@@ -115,9 +115,10 @@ BOOST_AUTO_TEST_CASE(CMapEditManager_DrawTerrain_View)
 	logGlobal->info("CMapEditManager_DrawTerrain_View start");
 	try
 	{
+		const ResourceID testMap("test/TerrainViewTest", EResType::MAP);
 		// Load maps and json config
-		const auto originalMap = CMapService::loadMap("test/TerrainViewTest");
-		auto map = CMapService::loadMap("test/TerrainViewTest");
+		const auto originalMap = CMapService::loadMap(testMap);
+		auto map = CMapService::loadMap(testMap);
 		logGlobal->info("Loaded test map successfully.");
 
 		// Validate edit manager