Przeglądaj źródła

Merge remote-tracking branch 'origin/develop' into dimension-door-changes

Dydzio 1 rok temu
rodzic
commit
70b86e5a87
100 zmienionych plików z 1534 dodań i 711 usunięć
  1. 9 0
      AI/EmptyAI/StdInc.cpp
  2. 9 0
      AI/EmptyAI/StdInc.h
  3. 0 21
      AI/Nullkiller/AIUtility.h
  4. 5 4
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  5. 8 0
      AI/Nullkiller/Analyzers/HeroManager.cpp
  6. 14 29
      AI/Nullkiller/Analyzers/HeroManager.h
  7. 96 52
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  8. 9 4
      AI/Nullkiller/Analyzers/ObjectClusterizer.h
  9. 3 1
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  10. 1 0
      AI/Nullkiller/Behaviors/ClusterBehavior.cpp
  11. 2 2
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  12. 6 1
      AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp
  13. 1 0
      AI/Nullkiller/CMakeLists.txt
  14. 2 3
      AI/Nullkiller/Goals/Build.cpp
  15. 3 3
      AI/Nullkiller/Goals/Build.h
  16. 45 21
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  17. 2 3
      AI/Nullkiller/Goals/GatherArmy.cpp
  18. 3 3
      AI/Nullkiller/Goals/GatherArmy.h
  19. 202 183
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  20. 96 29
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  21. 26 10
      AI/Nullkiller/Pathfinding/AIPathfinder.cpp
  22. 12 2
      AI/Nullkiller/Pathfinding/AIPathfinder.h
  23. 19 4
      AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp
  24. 15 0
      AI/Nullkiller/Pathfinding/Actions/BoatActions.h
  25. 5 0
      AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp
  26. 1 1
      AI/Nullkiller/Pathfinding/Actions/QuestAction.h
  27. 13 0
      AI/Nullkiller/Pathfinding/Actions/SpecialAction.h
  28. 277 51
      AI/Nullkiller/Pathfinding/ObjectGraph.cpp
  29. 21 0
      AI/Nullkiller/Pathfinding/ObjectGraph.h
  30. 4 1
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  31. 4 1
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h
  32. 10 0
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
  33. 9 0
      AI/Nullkiller/StdInc.cpp
  34. 9 0
      AI/Nullkiller/StdInc.h
  35. 50 0
      AI/Nullkiller/pforeach.h
  36. 9 0
      AI/StupidAI/StdInc.cpp
  37. 9 0
      AI/StupidAI/StdInc.h
  38. 1 1
      AI/VCAI/AIhelper.cpp
  39. 9 0
      AI/VCAI/MapObjectsEvaluator.cpp
  40. 11 7
      AI/VCAI/Pathfinding/AINodeStorage.cpp
  41. 3 1
      AI/VCAI/Pathfinding/AINodeStorage.h
  42. 9 0
      AI/VCAI/StdInc.cpp
  43. 9 0
      AI/VCAI/StdInc.h
  44. 1 1
      CI/linux-qt6/before_install.sh
  45. 1 1
      CI/linux/before_install.sh
  46. 40 23
      CMakeLists.txt
  47. 1 0
      Mods/vcmi/config/vcmi/english.json
  48. 1 0
      Mods/vcmi/config/vcmi/ukrainian.json
  49. 1 1
      client/CFocusableHelper.cpp
  50. 1 0
      client/CFocusableHelper.h
  51. 13 2
      client/CMT.cpp
  52. 1 1
      client/Client.cpp
  53. 4 0
      client/NetPacksClient.cpp
  54. 9 0
      client/StdInc.cpp
  55. 9 0
      client/StdInc.h
  56. 69 26
      client/globalLobby/GlobalLobbyClient.cpp
  57. 12 0
      client/globalLobby/GlobalLobbyClient.h
  58. 17 5
      client/globalLobby/GlobalLobbyLoginWindow.cpp
  59. 1 0
      client/globalLobby/GlobalLobbyLoginWindow.h
  60. 1 1
      client/globalLobby/GlobalLobbyWidget.cpp
  61. 5 2
      client/globalLobby/GlobalLobbyWindow.cpp
  62. 1 1
      client/gui/FramerateManager.cpp
  63. 1 0
      client/ios/GameChatKeyboardHandler.h
  64. 1 1
      client/ios/startSDL.mm
  65. 1 1
      client/render/Colors.h
  66. 3 3
      client/widgets/CExchangeController.h
  67. 4 40
      config/schemas/settings.json
  68. 1 7
      debian/control
  69. 1 3
      debian/rules
  70. 9 0
      launcher/StdInc.cpp
  71. 9 0
      launcher/StdInc.h
  72. 0 8
      launcher/modManager/cmodlist.h
  73. 1 4
      launcher/modManager/imageviewer_moc.h
  74. 98 80
      lib/CMakeLists.txt
  75. 9 0
      lib/StdInc.cpp
  76. 9 0
      lib/StdInc.h
  77. 3 2
      lib/VCMIDirs.cpp
  78. 2 1
      lib/battle/PossiblePlayerBattleAction.h
  79. 0 1
      lib/bonuses/IBonusBearer.h
  80. 2 0
      lib/json/JsonValidator.cpp
  81. 1 1
      lib/modding/CModVersion.cpp
  82. 37 34
      lib/pathfinder/CPathfinder.cpp
  83. 1 1
      lib/pathfinder/CPathfinder.h
  84. 3 1
      lib/pathfinder/INodeStorage.h
  85. 13 13
      lib/pathfinder/NodeStorage.cpp
  86. 3 1
      lib/pathfinder/NodeStorage.h
  87. 1 0
      lib/spells/ObstacleCasterProxy.h
  88. 3 3
      lib/spells/ViewSpellInt.h
  89. 9 0
      lib/vstd/DateUtils.cpp
  90. 9 0
      lib/vstd/StringUtils.cpp
  91. 1 1
      lobby/EntryPoint.cpp
  92. 4 1
      lobby/LobbyServer.cpp
  93. 9 0
      lobby/StdInc.cpp
  94. 9 0
      mapeditor/StdInc.cpp
  95. 9 0
      mapeditor/StdInc.h
  96. 1 1
      mapeditor/main.cpp
  97. 1 1
      mapeditor/mapsettings/victoryconditions.cpp
  98. 9 0
      scripting/erm/StdInc.cpp
  99. 9 0
      scripting/erm/StdInc.h
  100. 9 0
      scripting/lua/StdInc.cpp

+ 9 - 0
AI/EmptyAI/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 // Creates the precompiled header
 // Creates the precompiled header
 #include "StdInc.h"
 #include "StdInc.h"

+ 9 - 0
AI/EmptyAI/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #pragma once
 #pragma once
 
 
 #include "../../Global.h"
 #include "../../Global.h"

+ 0 - 21
AI/Nullkiller/AIUtility.h

@@ -50,7 +50,6 @@
 
 
 #include <chrono>
 #include <chrono>
 
 
-using namespace tbb;
 
 
 using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
 
@@ -246,26 +245,6 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
 // todo: move to obj manager
 // todo: move to obj manager
 bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObjectInstance * obj);
 bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObjectInstance * obj);
 
 
-template<typename TFunc>
-void pforeachTilePos(const int3 & mapSize, TFunc fn)
-{
-	for(int z = 0; z < mapSize.z; ++z)
-	{
-		parallel_for(blocked_range<size_t>(0, mapSize.x), [&](const blocked_range<size_t>& r)
-		{
-			int3 pos(0, 0, z);
-
-			for(pos.x = r.begin(); pos.x != r.end(); ++pos.x)
-			{
-				for(pos.y = 0; pos.y < mapSize.y; ++pos.y)
-				{
-					fn(pos);
-				}
-			}
-		});
-	}
-}
-
 class CDistanceSorter
 class CDistanceSorter
 {
 {
 	const CGHeroInstance * hero;
 	const CGHeroInstance * hero;

+ 5 - 4
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -11,6 +11,7 @@
 #include "DangerHitMapAnalyzer.h"
 #include "DangerHitMapAnalyzer.h"
 
 
 #include "../Engine/Nullkiller.h"
 #include "../Engine/Nullkiller.h"
+#include "../pforeach.h"
 #include "../../../lib/CRandomGenerator.h"
 #include "../../../lib/CRandomGenerator.h"
 
 
 namespace NKAI
 namespace NKAI
@@ -82,9 +83,9 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 
 		boost::this_thread::interruption_point();
 		boost::this_thread::interruption_point();
 
 
-		pforeachTilePos(mapSize, [&](const int3 & pos)
+		pforeachTilePaths(mapSize, ai, [&](const int3 & pos, const std::vector<AIPath> & paths)
 		{
 		{
-			for(AIPath & path : ai->pathfinder->getPathInfo(pos))
+			for(const AIPath & path : paths)
 			{
 			{
 				if(path.getFirstBlockedAction())
 				if(path.getFirstBlockedAction())
 					continue;
 					continue;
@@ -194,14 +195,14 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 
 
 	ai->pathfinder->updatePaths(townHeroes, ps);
 	ai->pathfinder->updatePaths(townHeroes, ps);
 
 
-	pforeachTilePos(mapSize, [&](const int3 & pos)
+	pforeachTilePaths(mapSize, ai, [&](const int3 & pos, const std::vector<AIPath> & paths)
 		{
 		{
 			float ourDistance = std::numeric_limits<float>::max();
 			float ourDistance = std::numeric_limits<float>::max();
 			float enemyDistance = std::numeric_limits<float>::max();
 			float enemyDistance = std::numeric_limits<float>::max();
 			const CGTownInstance * enemyTown = nullptr;
 			const CGTownInstance * enemyTown = nullptr;
 			const CGTownInstance * ourTown = nullptr;
 			const CGTownInstance * ourTown = nullptr;
 
 
-			for(AIPath & path : ai->pathfinder->getPathInfo(pos))
+			for(const AIPath & path : paths)
 			{
 			{
 				if(!path.targetHero || path.getFirstBlockedAction())
 				if(!path.targetHero || path.getFirstBlockedAction())
 					continue;
 					continue;

+ 8 - 0
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -109,6 +109,7 @@ void HeroManager::update()
 	for(auto & hero : myHeroes)
 	for(auto & hero : myHeroes)
 	{
 	{
 		scores[hero] = evaluateFightingStrength(hero);
 		scores[hero] = evaluateFightingStrength(hero);
+		knownFightingStrength[hero->id] = hero->getFightingStrength();
 	}
 	}
 
 
 	auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
 	auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
@@ -192,6 +193,13 @@ bool HeroManager::heroCapReached() const
 		|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
 		|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
 }
 }
 
 
+float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
+{
+	auto cached = knownFightingStrength.find(hero->id);
+
+	return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength();
+}
+
 float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
 float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
 {
 {
 	auto hasFly = hero->spellbookContainsSpell(SpellID::FLY);
 	auto hasFly = hero->spellbookContainsSpell(SpellID::FLY);

+ 14 - 29
AI/Nullkiller/Analyzers/HeroManager.h

@@ -20,23 +20,6 @@
 namespace NKAI
 namespace NKAI
 {
 {
 
 
-class DLL_EXPORT IHeroManager //: public: IAbstractManager
-{
-public:
-	virtual ~IHeroManager() = default;
-	virtual const std::map<HeroPtr, HeroRole> & getHeroRoles() const = 0;
-	virtual int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const = 0;
-	virtual HeroRole getHeroRole(const HeroPtr & hero) const = 0;
-	virtual void update() = 0;
-	virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
-	virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
-	virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
-	virtual bool heroCapReached() const = 0;
-	virtual const CGHeroInstance * findHeroWithGrail() const = 0;
-	virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0;
-	virtual float getMagicStrength(const CGHeroInstance * hero) const = 0;
-};
-
 class DLL_EXPORT ISecondarySkillRule
 class DLL_EXPORT ISecondarySkillRule
 {
 {
 public:
 public:
@@ -55,7 +38,7 @@ public:
 	float evaluateSecSkill(const CGHeroInstance * hero, SecondarySkill skill) const;
 	float evaluateSecSkill(const CGHeroInstance * hero, SecondarySkill skill) const;
 };
 };
 
 
-class DLL_EXPORT HeroManager : public IHeroManager
+class DLL_EXPORT HeroManager
 {
 {
 private:
 private:
 	static const SecondarySkillEvaluator wariorSkillsScores;
 	static const SecondarySkillEvaluator wariorSkillsScores;
@@ -64,20 +47,22 @@ private:
 	CCallback * cb; //this is enough, but we downcast from CCallback
 	CCallback * cb; //this is enough, but we downcast from CCallback
 	const Nullkiller * ai;
 	const Nullkiller * ai;
 	std::map<HeroPtr, HeroRole> heroRoles;
 	std::map<HeroPtr, HeroRole> heroRoles;
+	std::map<ObjectInstanceID, float> knownFightingStrength;
 
 
 public:
 public:
 	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB), ai(ai) {}
 	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB), ai(ai) {}
-	const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
-	HeroRole getHeroRole(const HeroPtr & hero) const override;
-	int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;
-	void update() override;
-	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
-	float evaluateHero(const CGHeroInstance * hero) const override;
-	bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
-	bool heroCapReached() const override;
-	const CGHeroInstance * findHeroWithGrail() const override;
-	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override;
-	float getMagicStrength(const CGHeroInstance * hero) const override;
+	const std::map<HeroPtr, HeroRole> & getHeroRoles() const;
+	HeroRole getHeroRole(const HeroPtr & hero) const;
+	int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const;
+	void update();
+	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
+	float evaluateHero(const CGHeroInstance * hero) const;
+	bool canRecruitHero(const CGTownInstance * t = nullptr) const;
+	bool heroCapReached() const;
+	const CGHeroInstance * findHeroWithGrail() const;
+	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const;
+	float getMagicStrength(const CGHeroInstance * hero) const;
+	float getFightingStrengthCached(const CGHeroInstance * hero) const;
 
 
 private:
 private:
 	float evaluateFightingStrength(const CGHeroInstance * hero) const;
 	float evaluateFightingStrength(const CGHeroInstance * hero) const;

+ 96 - 52
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -90,64 +90,74 @@ std::vector<std::shared_ptr<ObjectCluster>> ObjectClusterizer::getLockedClusters
 	return result;
 	return result;
 }
 }
 
 
-const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) const
+std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPathNodeInfo & node) const
 {
 {
-	for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++)
+	std::vector<const CGObjectInstance *> blockers = {};
+
+	if(node.layer == EPathfindingLayer::LAND || node.layer == EPathfindingLayer::SAIL)
 	{
 	{
-		std::vector<const CGObjectInstance *> blockers = {};
+		auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
+
+		blockers = ai->cb->getVisitableObjs(node.coord);
 
 
-		if(node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL)
+		if(guardPos.valid())
 		{
 		{
-			auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord);
-			
-			blockers = ai->cb->getVisitableObjs(node->coord);
+			auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
 
 
-			if(guardPos.valid())
+			if(guard)
 			{
 			{
-				auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord));
-
-				if(guard)
-				{
-					blockers.insert(blockers.begin(), guard);
-				}
+				blockers.insert(blockers.begin(), guard);
 			}
 			}
 		}
 		}
+	}
 
 
-		if(node->specialAction && node->actionIsBlocked)
-		{
-			auto blockerObject = node->specialAction->targetObject();
+	if(node.specialAction && node.actionIsBlocked)
+	{
+		auto blockerObject = node.specialAction->targetObject();
 
 
-			if(blockerObject)
-			{
-				blockers.insert(blockers.begin(), blockerObject);
-			}
+		if(blockerObject)
+		{
+			blockers.insert(blockers.begin(), blockerObject);
 		}
 		}
+	}
 
 
-		if(blockers.empty())
-			continue;
-
-		auto blocker = blockers.front();
+	if(blockers.empty())
+		return std::optional< const CGObjectInstance *>();
 
 
-		if(isObjectPassable(ai, blocker))
-			continue;
+	auto blocker = blockers.front();
 
 
-		if(blocker->ID == Obj::GARRISON
-			|| blocker->ID == Obj::GARRISON2)
-		{
-			if(dynamic_cast<const CArmedInstance *>(blocker)->getArmyStrength() == 0)
-				continue;
-			else
-				return blocker;
-		}
+	if(isObjectPassable(ai, blocker))
+		return std::optional< const CGObjectInstance *>();
 
 
-		if(blocker->ID == Obj::MONSTER
-			|| blocker->ID == Obj::BORDERGUARD
-			|| blocker->ID == Obj::BORDER_GATE
-			|| blocker->ID == Obj::SHIPYARD
-			|| (blocker->ID == Obj::QUEST_GUARD && node->actionIsBlocked))
-		{
+	if(blocker->ID == Obj::GARRISON
+		|| blocker->ID == Obj::GARRISON2)
+	{
+		if(dynamic_cast<const CArmedInstance *>(blocker)->getArmyStrength() == 0)
+			return std::optional< const CGObjectInstance *>();
+		else
 			return blocker;
 			return blocker;
-		}
+	}
+
+	if(blocker->ID == Obj::MONSTER
+		|| blocker->ID == Obj::BORDERGUARD
+		|| blocker->ID == Obj::BORDER_GATE
+		|| blocker->ID == Obj::SHIPYARD
+		|| (blocker->ID == Obj::QUEST_GUARD && node.actionIsBlocked))
+	{
+		return blocker;
+	}
+
+	return std::optional< const CGObjectInstance *>();
+}
+
+const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) const
+{
+	for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++)
+	{
+		auto blocker = getBlocker(*node);
+
+		if(blocker)
+			return *blocker;
 	}
 	}
 
 
 	return nullptr;
 	return nullptr;
@@ -225,15 +235,17 @@ void ObjectClusterizer::clusterize()
 		ai->memory->visitableObjs.end());
 		ai->memory->visitableObjs.end());
 
 
 #if NKAI_TRACE_LEVEL == 0
 #if NKAI_TRACE_LEVEL == 0
-	parallel_for(blocked_range<size_t>(0, objs.size()), [&](const blocked_range<size_t> & r) {
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, objs.size()), [&](const tbb::blocked_range<size_t> & r) {
 #else
 #else
-	blocked_range<size_t> r(0, objs.size());
+	tbb::blocked_range<size_t> r(0, objs.size());
 #endif
 #endif
 		auto priorityEvaluator = ai->priorityEvaluators->acquire();
 		auto priorityEvaluator = ai->priorityEvaluators->acquire();
+		auto heroes = ai->cb->getHeroesInfo();
+		std::vector<AIPath> pathCache;
 
 
 		for(int i = r.begin(); i != r.end(); i++)
 		for(int i = r.begin(); i != r.end(); i++)
 		{
 		{
-			clusterizeObject(objs[i], priorityEvaluator.get());
+			clusterizeObject(objs[i], priorityEvaluator.get(), pathCache, heroes);
 		}
 		}
 #if NKAI_TRACE_LEVEL == 0
 #if NKAI_TRACE_LEVEL == 0
 	});
 	});
@@ -257,7 +269,11 @@ void ObjectClusterizer::clusterize()
 	logAi->trace("Clusterization complete in %ld", timeElapsed(start));
 	logAi->trace("Clusterization complete in %ld", timeElapsed(start));
 }
 }
 
 
-void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator)
+void ObjectClusterizer::clusterizeObject(
+	const CGObjectInstance * obj,
+	PriorityEvaluator * priorityEvaluator,
+	std::vector<AIPath> & pathCache,
+	std::vector<const CGHeroInstance *> & heroes)
 {
 {
 	if(!shouldVisitObject(obj))
 	if(!shouldVisitObject(obj))
 	{
 	{
@@ -271,9 +287,14 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
 	logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
 	logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
 #endif
 #endif
 
 
-	auto paths = ai->pathfinder->getPathInfo(obj->visitablePos(), true);
+	if(ai->settings->isObjectGraphAllowed())
+	{
+		ai->pathfinder->calculateQuickPathsWithBlocker(pathCache, heroes, obj->visitablePos());
+	}
+	else
+		ai->pathfinder->calculatePathInfo(pathCache, obj->visitablePos(), false);
 
 
-	if(paths.empty())
+	if(pathCache.empty())
 	{
 	{
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace("No paths found.");
 		logAi->trace("No paths found.");
@@ -281,17 +302,17 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
 		return;
 		return;
 	}
 	}
 
 
-	std::sort(paths.begin(), paths.end(), [](const AIPath & p1, const AIPath & p2) -> bool
+	std::sort(pathCache.begin(), pathCache.end(), [](const AIPath & p1, const AIPath & p2) -> bool
 		{
 		{
 			return p1.movementCost() < p2.movementCost();
 			return p1.movementCost() < p2.movementCost();
 		});
 		});
 
 
 	if(vstd::contains(IgnoredObjectTypes, obj->ID))
 	if(vstd::contains(IgnoredObjectTypes, obj->ID))
 	{
 	{
-		farObjects.addObject(obj, paths.front(), 0);
+		farObjects.addObject(obj, pathCache.front(), 0);
 
 
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
-		logAi->trace("Object ignored. Moved to far objects with path %s", paths.front().toString());
+		logAi->trace("Object ignored. Moved to far objects with path %s", pathCache.front().toString());
 #endif
 #endif
 
 
 		return;
 		return;
@@ -299,12 +320,35 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
 
 
 	std::set<const CGHeroInstance *> heroesProcessed;
 	std::set<const CGHeroInstance *> heroesProcessed;
 
 
-	for(auto & path : paths)
+	for(auto & path : pathCache)
 	{
 	{
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace("Checking path %s", path.toString());
 		logAi->trace("Checking path %s", path.toString());
 #endif
 #endif
 
 
+		if(ai->heroManager->getHeroRole(path.targetHero) == HeroRole::SCOUT)
+		{
+			if(path.movementCost() > 2.0f)
+			{
+#if NKAI_TRACE_LEVEL >= 2
+				logAi->trace("Path is too far %f", path.movementCost());
+#endif
+				continue;
+			}
+		}
+		else if(path.movementCost() > 4.0f && obj->ID != Obj::TOWN)
+		{
+			auto strategicalValue = valueEvaluator.getStrategicalValue(obj);
+
+			if(strategicalValue < 0.3f)
+			{
+#if NKAI_TRACE_LEVEL >= 2
+				logAi->trace("Object value is too low %f", strategicalValue);
+#endif
+				continue;
+			}
+		}
+
 		if(!shouldVisit(ai, path.targetHero, obj))
 		if(!shouldVisit(ai, path.targetHero, obj))
 		{
 		{
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2

+ 9 - 4
AI/Nullkiller/Analyzers/ObjectClusterizer.h

@@ -10,6 +10,7 @@
 #pragma once
 #pragma once
 
 
 #include "../Pathfinding/AINodeStorage.h"
 #include "../Pathfinding/AINodeStorage.h"
+#include "../Engine/PriorityEvaluator.h"
 
 
 namespace NKAI
 namespace NKAI
 {
 {
@@ -49,8 +50,6 @@ public:
 
 
 using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
 using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
 
 
-class PriorityEvaluator;
-
 class ObjectClusterizer
 class ObjectClusterizer
 {
 {
 private:
 private:
@@ -60,6 +59,7 @@ private:
 	ObjectCluster farObjects;
 	ObjectCluster farObjects;
 	ClusterMap blockedObjects;
 	ClusterMap blockedObjects;
 	const Nullkiller * ai;
 	const Nullkiller * ai;
+	RewardEvaluator valueEvaluator;
 
 
 public:
 public:
 	void clusterize();
 	void clusterize();
@@ -67,12 +67,17 @@ public:
 	std::vector<const CGObjectInstance *> getFarObjects() const;
 	std::vector<const CGObjectInstance *> getFarObjects() const;
 	std::vector<std::shared_ptr<ObjectCluster>> getLockedClusters() const;
 	std::vector<std::shared_ptr<ObjectCluster>> getLockedClusters() const;
 	const CGObjectInstance * getBlocker(const AIPath & path) const;
 	const CGObjectInstance * getBlocker(const AIPath & path) const;
+	std::optional<const CGObjectInstance *> getBlocker(const AIPathNodeInfo & node) const;
 
 
-	ObjectClusterizer(const Nullkiller * ai): ai(ai) {}
+	ObjectClusterizer(const Nullkiller * ai): ai(ai), valueEvaluator(ai) {}
 
 
 private:
 private:
 	bool shouldVisitObject(const CGObjectInstance * obj) const;
 	bool shouldVisitObject(const CGObjectInstance * obj) const;
-	void clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator);
+	void clusterizeObject(
+		const CGObjectInstance * obj,
+		PriorityEvaluator * priorityEvaluator,
+		std::vector<AIPath> & pathCache,
+		std::vector<const CGHeroInstance *> & heroes);
 };
 };
 
 
 }
 }

+ 3 - 1
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -180,6 +180,8 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 
 
 		logAi->debug("Scanning objects, count %d", objs.size());
 		logAi->debug("Scanning objects, count %d", objs.size());
 
 
+		std::vector<AIPath> paths;
+
 		for(auto objToVisit : objs)
 		for(auto objToVisit : objs)
 		{
 		{
 			if(!objectMatchesFilter(objToVisit))
 			if(!objectMatchesFilter(objToVisit))
@@ -193,7 +195,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 			bool useObjectGraph = ai->nullkiller->settings->isObjectGraphAllowed()
 			bool useObjectGraph = ai->nullkiller->settings->isObjectGraphAllowed()
 				&& ai->nullkiller->getScanDepth() != ScanDepth::SMALL;
 				&& ai->nullkiller->getScanDepth() != ScanDepth::SMALL;
 
 
-			auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, useObjectGraph);
+			ai->nullkiller->pathfinder->calculatePathInfo(paths, pos, useObjectGraph);
 
 
 			std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
 			std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
 			std::shared_ptr<ExecuteHeroChain> closestWay;
 			std::shared_ptr<ExecuteHeroChain> closestWay;

+ 1 - 0
AI/Nullkiller/Behaviors/ClusterBehavior.cpp

@@ -43,6 +43,7 @@ Goals::TGoalVec ClusterBehavior::decomposeCluster(std::shared_ptr<ObjectCluster>
 {
 {
 	auto center = cluster->calculateCenter();
 	auto center = cluster->calculateCenter();
 	auto paths = ai->nullkiller->pathfinder->getPathInfo(center->visitablePos(), ai->nullkiller->settings->isObjectGraphAllowed());
 	auto paths = ai->nullkiller->pathfinder->getPathInfo(center->visitablePos(), ai->nullkiller->settings->isObjectGraphAllowed());
+
 	auto blockerPos = cluster->blocker->visitablePos();
 	auto blockerPos = cluster->blocker->visitablePos();
 	std::vector<AIPath> blockerPaths;
 	std::vector<AIPath> blockerPaths;
 
 

+ 2 - 2
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -69,7 +69,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 	logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString());
 	logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString());
 #endif
 #endif
 
 
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
+	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, ai->nullkiller->settings->isObjectGraphAllowed());
 
 
 #if NKAI_TRACE_LEVEL >= 1
 #if NKAI_TRACE_LEVEL >= 1
 	logAi->trace("Gather army found %d paths", paths.size());
 	logAi->trace("Gather army found %d paths", paths.size());
@@ -231,7 +231,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 	logAi->trace("Checking ways to upgrade army in town %s, %s", upgrader->getObjectName(), pos.toString());
 	logAi->trace("Checking ways to upgrade army in town %s, %s", upgrader->getObjectName(), pos.toString());
 #endif
 #endif
 	
 	
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
+	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, ai->nullkiller->settings->isObjectGraphAllowed());
 	auto goals = CaptureObjectsBehavior::getVisitGoals(paths);
 	auto goals = CaptureObjectsBehavior::getVisitGoals(paths);
 
 
 	std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
 	std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;

+ 6 - 1
AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp

@@ -35,18 +35,23 @@ Goals::TGoalVec StayAtTownBehavior::decompose() const
 	if(!towns.size())
 	if(!towns.size())
 		return tasks;
 		return tasks;
 
 
+	std::vector<AIPath> paths;
+
 	for(auto town : towns)
 	for(auto town : towns)
 	{
 	{
 		if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
 		if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
 			continue;
 			continue;
 
 
-		auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos());
+		ai->nullkiller->pathfinder->calculatePathInfo(paths, town->visitablePos());
 
 
 		for(auto & path : paths)
 		for(auto & path : paths)
 		{
 		{
 			if(town->visitingHero && town->visitingHero.get() != path.targetHero)
 			if(town->visitingHero && town->visitingHero.get() != path.targetHero)
 				continue;
 				continue;
 
 
+			if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit())
+				continue;
+
 			if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
 			if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
 			{
 			{
 				if(path.targetHero->mana == path.targetHero->manaLimit())
 				if(path.targetHero->mana == path.targetHero->manaLimit())

+ 1 - 0
AI/Nullkiller/CMakeLists.txt

@@ -81,6 +81,7 @@ set(Nullkiller_HEADERS
 		Pathfinding/Rules/AIPreviousNodeRule.h
 		Pathfinding/Rules/AIPreviousNodeRule.h
 		Pathfinding/ObjectGraph.h
 		Pathfinding/ObjectGraph.h
 		AIUtility.h
 		AIUtility.h
+		pforeach.h
 		Analyzers/ArmyManager.h
 		Analyzers/ArmyManager.h
 		Analyzers/HeroManager.h
 		Analyzers/HeroManager.h
 		Engine/Settings.h
 		Engine/Settings.h

+ 2 - 3
AI/Nullkiller/Goals/Build.cpp

@@ -1,6 +1,3 @@
-namespace Nullkiller
-{
-
 /*
 /*
 * Build.cpp, part of VCMI engine
 * Build.cpp, part of VCMI engine
 *
 *
@@ -23,6 +20,8 @@ namespace Nullkiller
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/StringConstants.h"
 #include "../../../lib/StringConstants.h"
 
 
+namespace Nullkiller
+{
 
 
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<AIGateway> ai;
 extern boost::thread_specific_ptr<AIGateway> ai;

+ 3 - 3
AI/Nullkiller/Goals/Build.h

@@ -1,6 +1,3 @@
-namespace Nullkiller
-{
-
 /*
 /*
 * Build.h, part of VCMI engine
 * Build.h, part of VCMI engine
 *
 *
@@ -14,6 +11,9 @@ namespace Nullkiller
 
 
 #include "CGoal.h"
 #include "CGoal.h"
 
 
+namespace Nullkiller
+{
+
 struct HeroPtr;
 struct HeroPtr;
 class AIGateway;
 class AIGateway;
 class FuzzyHelper;
 class FuzzyHelper;

+ 45 - 21
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -67,40 +67,40 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 
 
 	for(int i = chainPath.nodes.size() - 1; i >= 0; i--)
 	for(int i = chainPath.nodes.size() - 1; i >= 0; i--)
 	{
 	{
-		auto & node = chainPath.nodes[i];
+		auto  * node = &chainPath.nodes[i];
 
 
-		const CGHeroInstance * hero = node.targetHero;
+		const CGHeroInstance * hero = node->targetHero;
 		HeroPtr heroPtr = hero;
 		HeroPtr heroPtr = hero;
 
 
-		if(node.parentIndex >= i)
+		if(node->parentIndex >= i)
 		{
 		{
-			logAi->error("Invalid parentIndex while executing node " + node.coord.toString());
+			logAi->error("Invalid parentIndex while executing node " + node->coord.toString());
 		}
 		}
 
 
 		if(vstd::contains(blockedIndexes, i))
 		if(vstd::contains(blockedIndexes, i))
 		{
 		{
-			blockedIndexes.insert(node.parentIndex);
+			blockedIndexes.insert(node->parentIndex);
 			ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 			ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 
 
 			continue;
 			continue;
 		}
 		}
 
 
-		logAi->debug("Executing chain node %d. Moving hero %s to %s", i, hero->getNameTranslated(), node.coord.toString());
+		logAi->debug("Executing chain node %d. Moving hero %s to %s", i, hero->getNameTranslated(), node->coord.toString());
 
 
 		try
 		try
 		{
 		{
 			if(hero->movementPointsRemaining() > 0)
 			if(hero->movementPointsRemaining() > 0)
 			{
 			{
-				ai->nullkiller->setActive(hero, node.coord);
+				ai->nullkiller->setActive(hero, node->coord);
 
 
-				if(node.specialAction)
+				if(node->specialAction)
 				{
 				{
-					if(node.actionIsBlocked)
+					if(node->actionIsBlocked)
 					{
 					{
 						throw cannotFulfillGoalException("Path is nondeterministic.");
 						throw cannotFulfillGoalException("Path is nondeterministic.");
 					}
 					}
 					
 					
-					node.specialAction->execute(hero);
+					node->specialAction->execute(hero);
 					
 					
 					if(!heroPtr.validAndSet())
 					if(!heroPtr.validAndSet())
 					{
 					{
@@ -109,10 +109,34 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						return;
 						return;
 					}
 					}
 				}
 				}
+				else if(i > 0 && ai->nullkiller->settings->isObjectGraphAllowed())
+				{
+					auto chainMask = i < chainPath.nodes.size() - 1 ? chainPath.nodes[i + 1].chainMask : node->chainMask;
+
+					for(auto j = i - 1; j >= 0; j--)
+					{
+						auto & nextNode = chainPath.nodes[j];
+
+						if(nextNode.specialAction || nextNode.chainMask != chainMask)
+							break;
+
+						auto targetNode = cb->getPathsInfo(hero)->getPathInfo(nextNode.coord);
+
+						if(!targetNode->reachable()
+							|| targetNode->getCost() > nextNode.cost)
+							break;
+
+						i = j;
+						node = &nextNode;
+
+						if(targetNode->action == EPathNodeAction::BATTLE || targetNode->action == EPathNodeAction::TELEPORT_BATTLE)
+							break;
+					}
+				}
 
 
-				if(node.turns == 0 && node.coord != hero->visitablePos())
+				if(node->turns == 0 && node->coord != hero->visitablePos())
 				{
 				{
-					auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node.coord);
+					auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node->coord);
 
 
 					if(targetNode->accessible == EPathAccessibility::NOT_SET
 					if(targetNode->accessible == EPathAccessibility::NOT_SET
 						|| targetNode->accessible == EPathAccessibility::BLOCKED
 						|| targetNode->accessible == EPathAccessibility::BLOCKED
@@ -122,7 +146,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						logAi->error(
 						logAi->error(
 							"Unable to complete chain. Expected hero %s to arrive to %s in 0 turns but he cannot do this",
 							"Unable to complete chain. Expected hero %s to arrive to %s in 0 turns but he cannot do this",
 							hero->getNameTranslated(),
 							hero->getNameTranslated(),
-							node.coord.toString());
+							node->coord.toString());
 
 
 						return;
 						return;
 					}
 					}
@@ -132,7 +156,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 				{
 				{
 					try
 					try
 					{
 					{
-						if(moveHeroToTile(hero, node.coord))
+						if(moveHeroToTile(hero, node->coord))
 						{
 						{
 							continue;
 							continue;
 						}
 						}
@@ -149,11 +173,11 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						if(hero->movementPointsRemaining() > 0)
 						if(hero->movementPointsRemaining() > 0)
 						{
 						{
 							CGPath path;
 							CGPath path;
-							bool isOk = cb->getPathsInfo(hero)->getPath(path, node.coord);
+							bool isOk = cb->getPathsInfo(hero)->getPath(path, node->coord);
 
 
 							if(isOk && path.nodes.back().turns > 0)
 							if(isOk && path.nodes.back().turns > 0)
 							{
 							{
-								logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movementPointsRemaining(), node.coord.toString());
+								logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movementPointsRemaining(), node->coord.toString());
 
 
 								ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 								ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 								return;
 								return;
@@ -165,15 +189,15 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 				}
 				}
 			}
 			}
 
 
-			if(node.coord == hero->visitablePos())
+			if(node->coord == hero->visitablePos())
 				continue;
 				continue;
 
 
-			if(node.turns == 0)
+			if(node->turns == 0)
 			{
 			{
 				logAi->error(
 				logAi->error(
 					"Unable to complete chain. Expected hero %s to arive to %s but he is at %s",
 					"Unable to complete chain. Expected hero %s to arive to %s but he is at %s",
 					hero->getNameTranslated(),
 					hero->getNameTranslated(),
-					node.coord.toString(),
+					node->coord.toString(),
 					hero->visitablePos().toString());
 					hero->visitablePos().toString());
 
 
 				return;
 				return;
@@ -181,13 +205,13 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 			
 			
 			// no exception means we were not able to reach the tile
 			// no exception means we were not able to reach the tile
 			ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 			ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
-			blockedIndexes.insert(node.parentIndex);
+			blockedIndexes.insert(node->parentIndex);
 		}
 		}
 		catch(const goalFulfilledException &)
 		catch(const goalFulfilledException &)
 		{
 		{
 			if(!heroPtr.validAndSet())
 			if(!heroPtr.validAndSet())
 			{
 			{
-				logAi->debug("Hero %s was killed while attempting to reach %s", heroPtr.name, node.coord.toString());
+				logAi->debug("Hero %s was killed while attempting to reach %s", heroPtr.name, node->coord.toString());
 
 
 				return;
 				return;
 			}
 			}

+ 2 - 3
AI/Nullkiller/Goals/GatherArmy.cpp

@@ -1,6 +1,3 @@
-namespace Nullkiller
-{
-
 /*
 /*
 * GatherArmy.cpp, part of VCMI engine
 * GatherArmy.cpp, part of VCMI engine
 *
 *
@@ -22,6 +19,8 @@ namespace Nullkiller
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/StringConstants.h"
 #include "../../../lib/StringConstants.h"
 
 
+namespace Nullkiller
+{
 
 
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<AIGateway> ai;
 extern boost::thread_specific_ptr<AIGateway> ai;

+ 3 - 3
AI/Nullkiller/Goals/GatherArmy.h

@@ -1,6 +1,3 @@
-namespace Nullkiller
-{
-
 /*
 /*
 * GatherArmy.h, part of VCMI engine
 * GatherArmy.h, part of VCMI engine
 *
 *
@@ -14,6 +11,9 @@ namespace Nullkiller
 
 
 #include "CGoal.h"
 #include "CGoal.h"
 
 
+namespace Nullkiller
+{
+
 struct HeroPtr;
 struct HeroPtr;
 class AIGateway;
 class AIGateway;
 class FuzzyHelper;
 class FuzzyHelper;

+ 202 - 183
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -24,7 +24,8 @@
 namespace NKAI
 namespace NKAI
 {
 {
 
 
-std::shared_ptr<boost::multi_array<AIPathNode, 5>> AISharedStorage::shared;
+std::shared_ptr<boost::multi_array<AIPathNode, 4>> AISharedStorage::shared;
+uint64_t AISharedStorage::version = 0;
 boost::mutex AISharedStorage::locker;
 boost::mutex AISharedStorage::locker;
 std::set<int3> commitedTiles;
 std::set<int3> commitedTiles;
 std::set<int3> commitedTilesInitial;
 std::set<int3> commitedTilesInitial;
@@ -40,11 +41,24 @@ const bool DO_NOT_SAVE_TO_COMMITED_TILES = false;
 AISharedStorage::AISharedStorage(int3 sizes)
 AISharedStorage::AISharedStorage(int3 sizes)
 {
 {
 	if(!shared){
 	if(!shared){
-		shared.reset(new boost::multi_array<AIPathNode, 5>(
-			boost::extents[EPathfindingLayer::NUM_LAYERS][sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
-	}
+		shared.reset(new boost::multi_array<AIPathNode, 4>(
+			boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
+
+		nodes = shared;
 
 
-	nodes = shared;
+		foreach_tile_pos([&](const int3 & pos)
+			{
+				for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++)
+				{
+					auto & node = get(pos)[i];
+						
+					node.version = -1;
+					node.coord = pos;
+				}
+			});
+	}
+	else
+		nodes = shared;
 }
 }
 
 
 AISharedStorage::~AISharedStorage()
 AISharedStorage::~AISharedStorage()
@@ -80,6 +94,9 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
 AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
 AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
 	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
 	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
 {
 {
+	accesibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
+		boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
+
 	dangerEvaluator.reset(new FuzzyHelper(ai));
 	dangerEvaluator.reset(new FuzzyHelper(ai));
 }
 }
 
 
@@ -90,6 +107,8 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
 	if(heroChainPass)
 	if(heroChainPass)
 		return;
 		return;
 
 
+	AISharedStorage::version++;
+
 	//TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline
 	//TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline
 	const PlayerColor fowPlayer = ai->playerID;
 	const PlayerColor fowPlayer = ai->playerID;
 	const auto & fow = static_cast<const CGameInfoCallback *>(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap;
 	const auto & fow = static_cast<const CGameInfoCallback *>(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap;
@@ -97,7 +116,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
 
 
 	//Each thread gets different x, but an array of y located next to each other in memory
 	//Each thread gets different x, but an array of y located next to each other in memory
 
 
-	parallel_for(blocked_range<size_t>(0, sizes.x), [&](const blocked_range<size_t>& r)
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, sizes.x), [&](const tbb::blocked_range<size_t>& r)
 	{
 	{
 		int3 pos;
 		int3 pos;
 
 
@@ -152,9 +171,9 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 {
 {
 	int bucketIndex = ((uintptr_t)actor) % AIPathfinding::BUCKET_COUNT;
 	int bucketIndex = ((uintptr_t)actor) % AIPathfinding::BUCKET_COUNT;
 	int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
 	int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
-	auto chains = nodes.get(pos, layer);
+	auto chains = nodes.get(pos);
 
 
-	if(chains[0].blocked())
+	if(blocked(pos, layer))
 	{
 	{
 		return std::nullopt;
 		return std::nullopt;
 	}
 	}
@@ -163,15 +182,17 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 	{
 	{
 		AIPathNode & node = chains[i + bucketOffset];
 		AIPathNode & node = chains[i + bucketOffset];
 
 
-		if(node.actor == actor)
+		if(node.version != AISharedStorage::version)
 		{
 		{
+			node.reset(layer, getAccessibility(pos, layer));
+			node.version = AISharedStorage::version;
+			node.actor = actor;
+
 			return &node;
 			return &node;
 		}
 		}
 
 
-		if(!node.actor)
+		if(node.actor == actor && node.layer == layer)
 		{
 		{
-			node.actor = actor;
-
 			return &node;
 			return &node;
 		}
 		}
 	}
 	}
@@ -226,21 +247,6 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 	return initialNodes;
 	return initialNodes;
 }
 }
 
 
-void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, EPathAccessibility accessibility)
-{
-	for(AIPathNode & heroNode : nodes.get(coord, layer))
-{
-		heroNode.actor = nullptr;
-		heroNode.danger = 0;
-		heroNode.manaCost = 0;
-		heroNode.specialAction.reset();
-		heroNode.armyLoss = 0;
-		heroNode.chainOther = nullptr;
-		heroNode.dayFlags = DayFlags::NONE;
-		heroNode.update(coord, layer, accessibility);
-	}
-}
-
 void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInfo & source)
 void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInfo & source)
 {
 {
 	const AIPathNode * srcNode = getAINode(source.node);
 	const AIPathNode * srcNode = getAINode(source.node);
@@ -306,30 +312,31 @@ void AINodeStorage::commit(
 	}
 	}
 }
 }
 
 
-std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
+void AINodeStorage::calculateNeighbours(
+	std::vector<CGPathNode *> & result,
 	const PathNodeInfo & source,
 	const PathNodeInfo & source,
+	EPathfindingLayer layer,
 	const PathfinderConfig * pathfinderConfig,
 	const PathfinderConfig * pathfinderConfig,
 	const CPathfinderHelper * pathfinderHelper)
 	const CPathfinderHelper * pathfinderHelper)
 {
 {
-	std::vector<CGPathNode *> neighbours;
-	neighbours.reserve(16);
+	std::vector<int3> accessibleNeighbourTiles;
+
+	result.clear();
+	accessibleNeighbourTiles.reserve(8);
+
+	pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);
+
 	const AIPathNode * srcNode = getAINode(source.node);
 	const AIPathNode * srcNode = getAINode(source.node);
-	auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
 
 
 	for(auto & neighbour : accessibleNeighbourTiles)
 	for(auto & neighbour : accessibleNeighbourTiles)
 	{
 	{
-		for(EPathfindingLayer i = EPathfindingLayer::LAND; i < EPathfindingLayer::NUM_LAYERS; i.advance(1))
-		{
-			auto nextNode = getOrCreateNode(neighbour, i, srcNode->actor);
+		auto nextNode = getOrCreateNode(neighbour, layer, srcNode->actor);
 
 
-			if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
-				continue;
+		if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
+			continue;
 
 
-			neighbours.push_back(nextNode.value());
-		}
+		result.push_back(nextNode.value());
 	}
 	}
-	
-	return neighbours;
 }
 }
 
 
 constexpr std::array phisycalLayers = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
 constexpr std::array phisycalLayers = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
@@ -346,19 +353,16 @@ bool AINodeStorage::increaseHeroChainTurnLimit()
 	{
 	{
 		foreach_tile_pos([&](const int3 & pos)
 		foreach_tile_pos([&](const int3 & pos)
 		{
 		{
-			auto chains = nodes.get(pos, layer);
-
-			if(!chains[0].blocked())
-			{
-				for(AIPathNode & node : chains)
+			iterateValidNodesUntil(pos, layer, [&](AIPathNode & node)
 				{
 				{
 					if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
 					if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
 					{
 					{
 						commitedTiles.insert(pos);
 						commitedTiles.insert(pos);
-						break;
+						return true;
 					}
 					}
-				}
-			}
+
+					return false;
+				});
 		});
 		});
 	}
 	}
 
 
@@ -374,22 +378,17 @@ bool AINodeStorage::calculateHeroChainFinal()
 	{
 	{
 		foreach_tile_pos([&](const int3 & pos)
 		foreach_tile_pos([&](const int3 & pos)
 		{
 		{
-			auto chains = nodes.get(pos, layer);
-
-			if(!chains[0].blocked())
-			{
-				for(AIPathNode & node : chains)
+			iterateValidNodes(pos, layer, [&](AIPathNode & node)
 				{
 				{
 					if(node.turns > heroChainTurn
 					if(node.turns > heroChainTurn
 						&& !node.locked
 						&& !node.locked
 						&& node.action != EPathNodeAction::UNKNOWN
 						&& node.action != EPathNodeAction::UNKNOWN
 						&& node.actor->actorExchangeCount > 1
 						&& node.actor->actorExchangeCount > 1
-						&& !hasBetterChain(&node, &node, chains))
+						&& !hasBetterChain(&node, node))
 					{
 					{
 						heroChain.push_back(&node);
 						heroChain.push_back(&node);
 					}
 					}
-				}
-			}
+				});
 		});
 		});
 	}
 	}
 
 
@@ -413,7 +412,6 @@ struct DelayedWork
 class HeroChainCalculationTask
 class HeroChainCalculationTask
 {
 {
 private:
 private:
-	AISharedStorage & nodes;
 	AINodeStorage & storage;
 	AINodeStorage & storage;
 	std::vector<AIPathNode *> existingChains;
 	std::vector<AIPathNode *> existingChains;
 	std::vector<ExchangeCandidate> newChains;
 	std::vector<ExchangeCandidate> newChains;
@@ -425,14 +423,14 @@ private:
 
 
 public:
 public:
 	HeroChainCalculationTask(
 	HeroChainCalculationTask(
-		AINodeStorage & storage, AISharedStorage & nodes, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
-		:existingChains(), newChains(), delayedWork(), nodes(nodes), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
+		AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
+		:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
 	{
 	{
 		existingChains.reserve(AIPathfinding::NUM_CHAINS);
 		existingChains.reserve(AIPathfinding::NUM_CHAINS);
 		newChains.reserve(AIPathfinding::NUM_CHAINS);
 		newChains.reserve(AIPathfinding::NUM_CHAINS);
 	}
 	}
 
 
-	void execute(const blocked_range<size_t>& r)
+	void execute(const tbb::blocked_range<size_t>& r)
 	{
 	{
 		std::random_device randomDevice;
 		std::random_device randomDevice;
 		std::mt19937 randomEngine(randomDevice());
 		std::mt19937 randomEngine(randomDevice());
@@ -443,21 +441,19 @@ public:
 
 
 			for(auto layer : phisycalLayers)
 			for(auto layer : phisycalLayers)
 			{
 			{
-				auto chains = nodes.get(pos, layer);
+				existingChains.clear();
 
 
-				// fast cut inactive nodes
-				if(chains[0].blocked())
+				storage.iterateValidNodes(pos, layer, [this](AIPathNode & node)
+					{
+						if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
+							existingChains.push_back(&node);
+					});
+
+				if(existingChains.empty())
 					continue;
 					continue;
 
 
-				existingChains.clear();
 				newChains.clear();
 				newChains.clear();
 
 
-				for(AIPathNode & node : chains)
-				{
-					if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
-						existingChains.push_back(&node);
-				}
-
 				std::shuffle(existingChains.begin(), existingChains.end(), randomEngine);
 				std::shuffle(existingChains.begin(), existingChains.end(), randomEngine);
 
 
 				for(AIPathNode * node : existingChains)
 				for(AIPathNode * node : existingChains)
@@ -530,10 +526,10 @@ bool AINodeStorage::calculateHeroChain()
 
 
 		std::shuffle(data.begin(), data.end(), randomEngine);
 		std::shuffle(data.begin(), data.end(), randomEngine);
 
 
-		parallel_for(blocked_range<size_t>(0, data.size()), [&](const blocked_range<size_t>& r)
+		tbb::parallel_for(tbb::blocked_range<size_t>(0, data.size()), [&](const tbb::blocked_range<size_t>& r)
 		{
 		{
 			//auto r = blocked_range<size_t>(0, data.size());
 			//auto r = blocked_range<size_t>(0, data.size());
-			HeroChainCalculationTask task(*this, nodes, data, chainMask, heroChainTurn);
+			HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
 
 
 			task.execute(r);
 			task.execute(r);
 
 
@@ -546,8 +542,8 @@ bool AINodeStorage::calculateHeroChain()
 	}
 	}
 	else
 	else
 	{
 	{
-		auto r = blocked_range<size_t>(0, data.size());
-		HeroChainCalculationTask task(*this, nodes, data, chainMask, heroChainTurn);
+		auto r = tbb::blocked_range<size_t>(0, data.size());
+		HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
 
 
 		task.execute(r);
 		task.execute(r);
 		task.flushResult(heroChain);
 		task.flushResult(heroChain);
@@ -612,14 +608,20 @@ bool AINodeStorage::selectNextActor()
 	return false;
 	return false;
 }
 }
 
 
+uint64_t AINodeStorage::evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const
+{
+	float fightingStrength = ai->heroManager->getFightingStrengthCached(hero);
+	double ratio = (double)danger / (armyValue * fightingStrength);
+
+	return (uint64_t)(armyValue * ratio * ratio);
+}
+
 void HeroChainCalculationTask::cleanupInefectiveChains(std::vector<ExchangeCandidate> & result) const
 void HeroChainCalculationTask::cleanupInefectiveChains(std::vector<ExchangeCandidate> & result) const
 {
 {
 	vstd::erase_if(result, [&](const ExchangeCandidate & chainInfo) -> bool
 	vstd::erase_if(result, [&](const ExchangeCandidate & chainInfo) -> bool
 	{
 	{
-		auto pos = chainInfo.coord;
-		auto chains = nodes.get(pos, EPathfindingLayer::LAND);
-		auto isNotEffective = storage.hasBetterChain(chainInfo.carrierParent, &chainInfo, chains)
-			|| storage.hasBetterChain(chainInfo.carrierParent, &chainInfo, result);
+		auto isNotEffective = storage.hasBetterChain(chainInfo.carrierParent, chainInfo)
+			|| storage.hasBetterChain(chainInfo.carrierParent, chainInfo, result);
 
 
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 		if(isNotEffective)
 		if(isNotEffective)
@@ -645,7 +647,7 @@ void HeroChainCalculationTask::calculateHeroChain(
 {
 {
 	for(AIPathNode * node : variants)
 	for(AIPathNode * node : variants)
 	{
 	{
-		if(node == srcNode || !node->actor)
+		if(node == srcNode || !node->actor || node->version != AISharedStorage::version)
 			continue;
 			continue;
 
 
 		if((node->actor->chainMask & chainMask) == 0 && (srcNode->actor->chainMask & chainMask) == 0)
 		if((node->actor->chainMask & chainMask) == 0 && (srcNode->actor->chainMask & chainMask) == 0)
@@ -1174,7 +1176,7 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
 
 
 	if(actorsVector.size() * initialNodes.size() > 1000)
 	if(actorsVector.size() * initialNodes.size() > 1000)
 	{
 	{
-		parallel_for(blocked_range<size_t>(0, actorsVector.size()), [&](const blocked_range<size_t> & r)
+		tbb::parallel_for(tbb::blocked_range<size_t>(0, actorsVector.size()), [&](const tbb::blocked_range<size_t> & r)
 			{
 			{
 				for(int i = r.begin(); i != r.end(); i++)
 				for(int i = r.begin(); i != r.end(); i++)
 				{
 				{
@@ -1195,95 +1197,116 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
 
 
 bool AINodeStorage::hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
 bool AINodeStorage::hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
 {
 {
-	auto pos = destination.coord;
-	auto chains = nodes.get(pos, EPathfindingLayer::LAND);
+	auto candidateNode = getAINode(destination.node);
 
 
-	return hasBetterChain(source.node, getAINode(destination.node), chains);
+	return hasBetterChain(source.node, *candidateNode);
 }
 }
 
 
-template<class NodeRange>
 bool AINodeStorage::hasBetterChain(
 bool AINodeStorage::hasBetterChain(
-	const CGPathNode * source, 
-	const AIPathNode * candidateNode,
-	const NodeRange & chains) const
+	const CGPathNode * source,
+	const AIPathNode & candidateNode) const
 {
 {
-	auto candidateActor = candidateNode->actor;
+	return iterateValidNodesUntil(
+		candidateNode.coord,
+		candidateNode.layer,
+		[this, &source, candidateNode](const AIPathNode & node) -> bool
+		{
+			return isOtherChainBetter(source, candidateNode, node);
+		});
+}
 
 
-	for(const AIPathNode & node : chains)
+template<class NodeRange>
+bool AINodeStorage::hasBetterChain(
+	const CGPathNode * source,
+	const AIPathNode & candidateNode,
+	const NodeRange & nodes) const
+{
+	for(const AIPathNode & node : nodes)
 	{
 	{
-		auto sameNode = node.actor == candidateNode->actor;
-
-		if(sameNode	|| node.action == EPathNodeAction::UNKNOWN || !node.actor || !node.actor->hero)
-		{
-			continue;
-		}
+		if(isOtherChainBetter(source, candidateNode, node))
+			return true;
+	}
 
 
-		if(node.danger <= candidateNode->danger && candidateNode->actor == node.actor->battleActor)
-		{
-			if(node.getCost() < candidateNode->getCost())
-			{
-#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
-				logAi->trace(
-					"Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i",
-					source->coord.toString(),
-					candidateNode->coord.toString(),
-					candidateNode->actor->hero->getNameTranslated(),
-					candidateNode->actor->chainMask,
-					candidateNode->actor->armyValue,
-					node.moveRemains - candidateNode->moveRemains);
-#endif
-				return true;
-			}
-		}
+	return false;
+}
 
 
-		if(candidateActor->chainMask != node.actor->chainMask && heroChainPass != EHeroChainPass::FINAL)
-			continue;
+bool AINodeStorage::isOtherChainBetter(
+	const CGPathNode * source,
+	const AIPathNode & candidateNode,
+	const AIPathNode & other) const
+{
+	auto sameNode = other.actor == candidateNode.actor;
 
 
-		auto nodeActor = node.actor;
-		auto nodeArmyValue = nodeActor->armyValue - node.armyLoss;
-		auto candidateArmyValue = candidateActor->armyValue - candidateNode->armyLoss;
+	if(sameNode || other.action == EPathNodeAction::UNKNOWN || !other.actor || !other.actor->hero)
+	{
+		return false;
+	}
 
 
-		if(nodeArmyValue > candidateArmyValue
-			&& node.getCost() <= candidateNode->getCost())
+	if(other.danger <= candidateNode.danger && candidateNode.actor == other.actor->battleActor)
+	{
+		if(other.getCost() < candidateNode.getCost())
 		{
 		{
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 			logAi->trace(
 			logAi->trace(
-				"Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i",
+				"Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i",
 				source->coord.toString(),
 				source->coord.toString(),
-				candidateNode->coord.toString(),
-				candidateNode->actor->hero->getNameTranslated(),
-				candidateNode->actor->chainMask,
-				candidateNode->actor->armyValue,
-				node.moveRemains - candidateNode->moveRemains);
+				candidateNode.coord.toString(),
+				candidateNode.actor->hero->getNameTranslated(),
+				candidateNode.actor->chainMask,
+				candidateNode.actor->armyValue,
+				other.moveRemains - candidateNode.moveRemains);
 #endif
 #endif
 			return true;
 			return true;
 		}
 		}
+	}
+
+	if(candidateNode.actor->chainMask != other.actor->chainMask && heroChainPass != EHeroChainPass::FINAL)
+		return false;
 
 
-		if(heroChainPass == EHeroChainPass::FINAL)
+	auto nodeActor = other.actor;
+	auto nodeArmyValue = nodeActor->armyValue - other.armyLoss;
+	auto candidateArmyValue = candidateNode.actor->armyValue - candidateNode.armyLoss;
+
+	if(nodeArmyValue > candidateArmyValue
+		&& other.getCost() <= candidateNode.getCost())
+	{
+#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
+		logAi->trace(
+			"Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i",
+			source->coord.toString(),
+			candidateNode.coord.toString(),
+			candidateNode.actor->hero->getNameTranslated(),
+			candidateNode.actor->chainMask,
+			candidateNode.actor->armyValue,
+			other.moveRemains - candidateNode.moveRemains);
+#endif
+		return true;
+	}
+
+	if(heroChainPass == EHeroChainPass::FINAL)
+	{
+		if(nodeArmyValue == candidateArmyValue
+			&& nodeActor->heroFightingStrength >= candidateNode.actor->heroFightingStrength
+			&& other.getCost() <= candidateNode.getCost())
 		{
 		{
-			if(nodeArmyValue == candidateArmyValue
-				&& nodeActor->heroFightingStrength >= candidateActor->heroFightingStrength
-				&& node.getCost() <= candidateNode->getCost())
+			if(vstd::isAlmostEqual(nodeActor->heroFightingStrength, candidateNode.actor->heroFightingStrength)
+				&& vstd::isAlmostEqual(other.getCost(), candidateNode.getCost())
+				&& &other < &candidateNode)
 			{
 			{
-				if(vstd::isAlmostEqual(nodeActor->heroFightingStrength, candidateActor->heroFightingStrength)
-					&& vstd::isAlmostEqual(node.getCost(), candidateNode->getCost())
-					&& &node < candidateNode)
-				{
-					continue;
-				}
+				return false;
+			}
 
 
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
-				logAi->trace(
-					"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i",
-					source->coord.toString(),
-					candidateNode->coord.toString(),
-					candidateNode->actor->hero->getNameTranslated(),
-					candidateNode->actor->chainMask,
-					candidateNode->actor->armyValue,
-					node.moveRemains - candidateNode->moveRemains);
+			logAi->trace(
+				"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i",
+				source->coord.toString(),
+				candidateNode.coord.toString(),
+				candidateNode.actor->hero->getNameTranslated(),
+				candidateNode.actor->chainMask,
+				candidateNode.actor->armyValue,
+				other.moveRemains - candidateNode.moveRemains);
 #endif
 #endif
-				return true;
-			}
+			return true;
 		}
 		}
 	}
 	}
 
 
@@ -1292,12 +1315,15 @@ bool AINodeStorage::hasBetterChain(
 
 
 bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const
 bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const
 {
 {
-	auto chains = nodes.get(pos, layer);
+	auto chains = nodes.get(pos);
 
 
 	for(const AIPathNode & node : chains)
 	for(const AIPathNode & node : chains)
 	{
 	{
-		if(node.action != EPathNodeAction::UNKNOWN 
-			&& node.actor && node.actor->hero == hero.h)
+		if(node.version == AISharedStorage::version
+			&& node.layer == layer
+			&& node.action != EPathNodeAction::UNKNOWN 
+			&& node.actor
+			&& node.actor->hero == hero.h)
 		{
 		{
 			return true;
 			return true;
 		}
 		}
@@ -1306,22 +1332,23 @@ bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, con
 	return false;
 	return false;
 }
 }
 
 
-std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand) const
+void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 & pos, bool isOnLand) const
 {
 {
-	std::vector<AIPath> paths;
-
-	paths.reserve(AIPathfinding::NUM_CHAINS / 4);
-
-	auto chains = nodes.get(pos, isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL);
+	auto layer = isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL;
+	auto chains = nodes.get(pos);
 
 
 	for(const AIPathNode & node : chains)
 	for(const AIPathNode & node : chains)
 	{
 	{
-		if(node.action == EPathNodeAction::UNKNOWN || !node.actor || !node.actor->hero)
+		if(node.version != AISharedStorage::version
+			|| node.layer != layer
+			|| node.action == EPathNodeAction::UNKNOWN
+			|| !node.actor
+			|| !node.actor->hero)
 		{
 		{
 			continue;
 			continue;
 		}
 		}
 
 
-		AIPath path;
+		AIPath & path = paths.emplace_back();
 
 
 		path.targetHero = node.actor->hero;
 		path.targetHero = node.actor->hero;
 		path.heroArmy = node.actor->creatureSet;
 		path.heroArmy = node.actor->creatureSet;
@@ -1332,11 +1359,7 @@ std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand)
 		path.exchangeCount = node.actor->actorExchangeCount;
 		path.exchangeCount = node.actor->actorExchangeCount;
 		
 		
 		fillChainInfo(&node, path, -1);
 		fillChainInfo(&node, path, -1);
-
-		paths.push_back(path);
 	}
 	}
-
-	return paths;
 }
 }
 
 
 void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const
 void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const
@@ -1349,33 +1372,29 @@ void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int pa
 		if(node->chainOther)
 		if(node->chainOther)
 			fillChainInfo(node->chainOther, path, parentIndex);
 			fillChainInfo(node->chainOther, path, parentIndex);
 
 
-		//if(node->actor->hero->visitablePos() != node->coord)
-		{
-			AIPathNodeInfo pathNode;
-
-			pathNode.cost = node->getCost();
-			pathNode.targetHero = node->actor->hero;
-			pathNode.chainMask = node->actor->chainMask;
-			pathNode.specialAction = node->specialAction;
-			pathNode.turns = node->turns;
-			pathNode.danger = node->danger;
-			pathNode.coord = node->coord;
-			pathNode.parentIndex = parentIndex;
-			pathNode.actionIsBlocked = false;
-			pathNode.layer = node->layer;
-
-			if(pathNode.specialAction)
-			{
-				auto targetNode =node->theNodeBefore ?  getAINode(node->theNodeBefore) : node;
+		AIPathNodeInfo pathNode;
 
 
-				pathNode.actionIsBlocked = !pathNode.specialAction->canAct(targetNode);
-			}
+		pathNode.cost = node->getCost();
+		pathNode.targetHero = node->actor->hero;
+		pathNode.chainMask = node->actor->chainMask;
+		pathNode.specialAction = node->specialAction;
+		pathNode.turns = node->turns;
+		pathNode.danger = node->danger;
+		pathNode.coord = node->coord;
+		pathNode.parentIndex = parentIndex;
+		pathNode.actionIsBlocked = false;
+		pathNode.layer = node->layer;
 
 
-			parentIndex = path.nodes.size();
+		if(pathNode.specialAction)
+		{
+			auto targetNode =node->theNodeBefore ?  getAINode(node->theNodeBefore) : node;
 
 
-			path.nodes.push_back(pathNode);
+			pathNode.actionIsBlocked = !pathNode.specialAction->canAct(targetNode);
 		}
 		}
-		
+
+		parentIndex = path.nodes.size();
+
+		path.nodes.push_back(pathNode);
 		node = getAINode(node->theNodeBefore);
 		node = getAINode(node->theNodeBefore);
 	}
 	}
 }
 }

+ 96 - 29
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -28,7 +28,7 @@ namespace NKAI
 namespace AIPathfinding
 namespace AIPathfinding
 {
 {
 	const int BUCKET_COUNT = 3;
 	const int BUCKET_COUNT = 3;
-	const int BUCKET_SIZE = 5;
+	const int BUCKET_SIZE = 7;
 	const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
 	const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
 	const int CHAIN_MAX_DEPTH = 4;
 	const int CHAIN_MAX_DEPTH = 4;
 }
 }
@@ -49,15 +49,24 @@ struct AIPathNode : public CGPathNode
 	const AIPathNode * chainOther;
 	const AIPathNode * chainOther;
 	std::shared_ptr<const SpecialAction> specialAction;
 	std::shared_ptr<const SpecialAction> specialAction;
 	const ChainActor * actor;
 	const ChainActor * actor;
+	uint64_t version;
 
 
-	STRONG_INLINE
-	bool blocked() const
+	void addSpecialAction(std::shared_ptr<const SpecialAction> action);
+
+	inline void reset(EPathfindingLayer layer, EPathAccessibility accessibility)
 	{
 	{
-		return accessible == EPathAccessibility::NOT_SET
-			|| accessible == EPathAccessibility::BLOCKED;
+		CGPathNode::reset();
+
+		actor = nullptr;
+		danger = 0;
+		manaCost = 0;
+		specialAction.reset();
+		armyLoss = 0;
+		chainOther = nullptr;
+		dayFlags = DayFlags::NONE;
+		this->layer = layer;
+		accessible = accessibility;
 	}
 	}
-
-	void addSpecialAction(std::shared_ptr<const SpecialAction> action);
 };
 };
 
 
 struct AIPathNodeInfo
 struct AIPathNodeInfo
@@ -133,21 +142,21 @@ enum EHeroChainPass
 
 
 class AISharedStorage
 class AISharedStorage
 {
 {
-	// 1 - layer (air, water, land)
-	// 2-4 - position on map[z][x][y]
-	// 5 - chain (normal, battle, spellcast and combinations)
-	static std::shared_ptr<boost::multi_array<AIPathNode, 5>> shared;
-	std::shared_ptr<boost::multi_array<AIPathNode, 5>> nodes;
+	// 1-3 - position on map[z][x][y]
+	// 4 - chain + layer (normal, battle, spellcast and combinations, water, air)
+	static std::shared_ptr<boost::multi_array<AIPathNode, 4>> shared;
+	std::shared_ptr<boost::multi_array<AIPathNode, 4>> nodes;
 public:
 public:
 	static boost::mutex locker;
 	static boost::mutex locker;
+	static uint64_t version;
 
 
 	AISharedStorage(int3 mapSize);
 	AISharedStorage(int3 mapSize);
 	~AISharedStorage();
 	~AISharedStorage();
 
 
 	STRONG_INLINE
 	STRONG_INLINE
-	boost::detail::multi_array::sub_array<AIPathNode, 1> get(int3 tile, EPathfindingLayer layer) const
+	boost::detail::multi_array::sub_array<AIPathNode, 1> get(int3 tile) const
 	{
 	{
-		return (*nodes)[layer][tile.z][tile.x][tile.y];
+		return (*nodes)[tile.z][tile.x][tile.y];
 	}
 	}
 };
 };
 
 
@@ -156,6 +165,8 @@ class AINodeStorage : public INodeStorage
 private:
 private:
 	int3 sizes;
 	int3 sizes;
 
 
+	std::unique_ptr<boost::multi_array<EPathAccessibility, 4>> accesibility;
+
 	const CPlayerSpecificInfoCallback * cb;
 	const CPlayerSpecificInfoCallback * cb;
 	const Nullkiller * ai;
 	const Nullkiller * ai;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
@@ -182,8 +193,10 @@ public:
 
 
 	std::vector<CGPathNode *> getInitialNodes() override;
 	std::vector<CGPathNode *> getInitialNodes() override;
 
 
-	virtual std::vector<CGPathNode *> calculateNeighbours(
+	virtual void calculateNeighbours(
+		std::vector<CGPathNode *> & result,
 		const PathNodeInfo & source,
 		const PathNodeInfo & source,
+		EPathfindingLayer layer,
 		const PathfinderConfig * pathfinderConfig,
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) override;
 		const CPathfinderHelper * pathfinderHelper) override;
 
 
@@ -222,23 +235,37 @@ public:
 		return aiNode->actor->hero;
 		return aiNode->actor->hero;
 	}
 	}
 
 
-	bool hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
-
-	bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
+	inline bool blocked(const int3 & tile, EPathfindingLayer layer) const
 	{
 	{
-		return hasBetterChain(source, destination);
+		EPathAccessibility accessible = getAccessibility(tile, layer);
+
+		return accessible == EPathAccessibility::NOT_SET
+			|| accessible == EPathAccessibility::BLOCKED;
 	}
 	}
 
 
-	bool isDistanceLimitReached(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
+	bool hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
+	bool hasBetterChain(const CGPathNode * source, const AIPathNode & candidateNode) const;
 
 
 	template<class NodeRange>
 	template<class NodeRange>
 	bool hasBetterChain(
 	bool hasBetterChain(
 		const CGPathNode * source, 
 		const CGPathNode * source, 
-		const AIPathNode * destinationNode,
+		const AIPathNode & destinationNode,
 		const NodeRange & chains) const;
 		const NodeRange & chains) const;
 
 
+	bool isOtherChainBetter(
+		const CGPathNode * source,
+		const AIPathNode & candidateNode,
+		const AIPathNode & other) const;
+
+	bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
+	{
+		return hasBetterChain(source, destination);
+	}
+
+	bool isDistanceLimitReached(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
+
 	std::optional<AIPathNode *> getOrCreateNode(const int3 & coord, const EPathfindingLayer layer, const ChainActor * actor);
 	std::optional<AIPathNode *> getOrCreateNode(const int3 & coord, const EPathfindingLayer layer, const ChainActor * actor);
-	std::vector<AIPath> getChainInfo(const int3 & pos, bool isOnLand) const;
+	void calculateChainInfo(std::vector<AIPath> & result, const int3 & pos, bool isOnLand) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const;
 	void setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes);
 	void setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes);
 	void setScoutTurnDistanceLimit(uint8_t distanceLimit) { turnDistanceLimit[HeroRole::SCOUT] = distanceLimit; }
 	void setScoutTurnDistanceLimit(uint8_t distanceLimit) { turnDistanceLimit[HeroRole::SCOUT] = distanceLimit; }
@@ -256,17 +283,19 @@ public:
 		return dangerEvaluator->evaluateDanger(tile, hero, checkGuards);
 		return dangerEvaluator->evaluateDanger(tile, hero, checkGuards);
 	}
 	}
 
 
-	inline uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const
-	{
-		double ratio = (double)danger / (armyValue * hero->getFightingStrength());
+	uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const;
 
 
-		return (uint64_t)(armyValue * ratio * ratio);
+	inline EPathAccessibility getAccessibility(const int3 & tile, EPathfindingLayer layer) const
+	{
+		return (*this->accesibility)[tile.z][tile.x][tile.y][layer];
 	}
 	}
 
 
-	STRONG_INLINE
-	void resetTile(const int3 & tile, EPathfindingLayer layer, EPathAccessibility accessibility);
+	inline void resetTile(const int3 & tile, EPathfindingLayer layer, EPathAccessibility tileAccessibility)
+	{
+		(*this->accesibility)[tile.z][tile.x][tile.y][layer] = tileAccessibility;
+	}
 
 
-	STRONG_INLINE int getBucket(const ChainActor * actor) const
+	inline int getBucket(const ChainActor * actor) const
 	{
 	{
 		return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT;
 		return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT;
 	}
 	}
@@ -274,6 +303,44 @@ public:
 	void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);
 	void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);
 	void fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const;
 	void fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const;
 
 
+	template<typename Fn>
+	void iterateValidNodes(const int3 & pos, EPathfindingLayer layer, Fn fn)
+	{
+		if(blocked(pos, layer))
+			return;
+
+		auto chains = nodes.get(pos);
+
+		for(AIPathNode & node : chains)
+		{
+			if(node.version != AISharedStorage::version || node.layer != layer)
+				continue;
+
+			fn(node);
+		}
+	}
+
+	template<typename Fn>
+	bool iterateValidNodesUntil(const int3 & pos, EPathfindingLayer layer, Fn predicate) const
+	{
+		if(blocked(pos, layer))
+			return false;
+
+		auto chains = nodes.get(pos);
+
+		for(AIPathNode & node : chains)
+		{
+			if(node.version != AISharedStorage::version || node.layer != layer)
+				continue;
+
+			if(predicate(node))
+				return true;
+		}
+
+		return false;
+	}
+
+
 private:
 private:
 	template<class TVector>
 	template<class TVector>
 	void calculateTownPortal(
 	void calculateTownPortal(

+ 26 - 10
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -33,16 +33,31 @@ bool AIPathfinder::isTileAccessible(const HeroPtr & hero, const int3 & tile) con
 		|| storage->isTileAccessible(hero, tile, EPathfindingLayer::SAIL);
 		|| storage->isTileAccessible(hero, tile, EPathfindingLayer::SAIL);
 }
 }
 
 
-std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile, bool includeGraph) const
+void AIPathfinder::calculateQuickPathsWithBlocker(std::vector<AIPath> & result, const std::vector<const CGHeroInstance *> & heroes, const int3 & tile)
+{
+	result.clear();
+
+	for(auto hero : heroes)
+	{
+		auto graph = heroGraphs.find(hero->id);
+
+		if(graph != heroGraphs.end())
+			graph->second->quickAddChainInfoWithBlocker(result, tile, hero, ai);
+	}
+}
+
+void AIPathfinder::calculatePathInfo(std::vector<AIPath> & result, const int3 & tile, bool includeGraph) const
 {
 {
 	const TerrainTile * tileInfo = cb->getTile(tile, false);
 	const TerrainTile * tileInfo = cb->getTile(tile, false);
 
 
+	result.clear();
+
 	if(!tileInfo)
 	if(!tileInfo)
 	{
 	{
-		return std::vector<AIPath>();
+		return;
 	}
 	}
 
 
-	auto info = storage->getChainInfo(tile, !tileInfo->isWater());
+	storage->calculateChainInfo(result, tile, !tileInfo->isWater());
 
 
 	if(includeGraph)
 	if(includeGraph)
 	{
 	{
@@ -51,11 +66,9 @@ std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile, bool includeGra
 			auto graph = heroGraphs.find(hero->id);
 			auto graph = heroGraphs.find(hero->id);
 
 
 			if(graph != heroGraphs.end())
 			if(graph != heroGraphs.end())
-				graph->second.addChainInfo(info, tile, hero, ai);
+				graph->second->addChainInfo(result, tile, hero, ai);
 		}
 		}
 	}
 	}
-
-	return info;
 }
 }
 
 
 void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings)
 void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings)
@@ -134,21 +147,24 @@ void AIPathfinder::updateGraphs(const std::map<const CGHeroInstance *, HeroRole>
 
 
 	for(auto hero : heroes)
 	for(auto hero : heroes)
 	{
 	{
-		if(heroGraphs.try_emplace(hero.first->id, GraphPaths()).second)
+		if(heroGraphs.try_emplace(hero.first->id).second)
+		{
+			heroGraphs[hero.first->id] = std::make_unique<GraphPaths>();
 			heroesVector.push_back(hero.first);
 			heroesVector.push_back(hero.first);
+		}
 	}
 	}
 
 
-	parallel_for(blocked_range<size_t>(0, heroesVector.size()), [this, &heroesVector](const blocked_range<size_t> & r)
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, heroesVector.size()), [this, &heroesVector](const tbb::blocked_range<size_t> & r)
 		{
 		{
 			for(auto i = r.begin(); i != r.end(); i++)
 			for(auto i = r.begin(); i != r.end(); i++)
-				heroGraphs.at(heroesVector[i]->id).calculatePaths(heroesVector[i], ai);
+				heroGraphs.at(heroesVector[i]->id)->calculatePaths(heroesVector[i], ai);
 		});
 		});
 
 
 	if(NKAI_GRAPH_TRACE_LEVEL >= 1)
 	if(NKAI_GRAPH_TRACE_LEVEL >= 1)
 	{
 	{
 		for(auto hero : heroes)
 		for(auto hero : heroes)
 		{
 		{
-			heroGraphs[hero.first->id].dumpToLog();
+			heroGraphs[hero.first->id]->dumpToLog();
 		}
 		}
 	}
 	}
 
 

+ 12 - 2
AI/Nullkiller/Pathfinding/AIPathfinder.h

@@ -40,20 +40,30 @@ private:
 	std::shared_ptr<AINodeStorage> storage;
 	std::shared_ptr<AINodeStorage> storage;
 	CPlayerSpecificInfoCallback * cb;
 	CPlayerSpecificInfoCallback * cb;
 	Nullkiller * ai;
 	Nullkiller * ai;
-	std::map<ObjectInstanceID, GraphPaths>  heroGraphs;
+	std::map<ObjectInstanceID, std::unique_ptr<GraphPaths>>  heroGraphs;
 
 
 public:
 public:
 	AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * ai);
 	AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * ai);
-	std::vector<AIPath> getPathInfo(const int3 & tile, bool includeGraph = false) const;
+	void calculatePathInfo(std::vector<AIPath> & paths, const int3 & tile, bool includeGraph = false) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const;
 	void updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings);
 	void updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings);
 	void updateGraphs(const std::map<const CGHeroInstance *, HeroRole> & heroes);
 	void updateGraphs(const std::map<const CGHeroInstance *, HeroRole> & heroes);
+	void calculateQuickPathsWithBlocker(std::vector<AIPath> & result, const std::vector<const CGHeroInstance *> & heroes, const int3 & tile);
 	void init();
 	void init();
 
 
 	std::shared_ptr<AINodeStorage>getStorage()
 	std::shared_ptr<AINodeStorage>getStorage()
 	{
 	{
 		return storage;
 		return storage;
 	}
 	}
+
+	std::vector<AIPath> getPathInfo(const int3 & tile, bool includeGraph = false)
+	{
+		std::vector<AIPath> result;
+
+		calculatePathInfo(result, tile, includeGraph);
+
+		return result;
+	}
 };
 };
 
 
 }
 }

+ 19 - 4
AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp

@@ -37,10 +37,8 @@ namespace AIPathfinding
 		return Goals::sptr(Goals::Invalid());
 		return Goals::sptr(Goals::Invalid());
 	}
 	}
 
 
-	bool BuildBoatAction::canAct(const AIPathNode * source) const
+	bool BuildBoatAction::canAct(const CGHeroInstance * hero, const TResources & reservedResources) const
 	{
 	{
-		auto hero = source->actor->hero;
-
 		if(cb->getPlayerRelations(hero->tempOwner, shipyard->getObject()->getOwner()) == PlayerRelations::ENEMIES)
 		if(cb->getPlayerRelations(hero->tempOwner, shipyard->getObject()->getOwner()) == PlayerRelations::ENEMIES)
 		{
 		{
 #if NKAI_TRACE_LEVEL > 1
 #if NKAI_TRACE_LEVEL > 1
@@ -53,7 +51,7 @@ namespace AIPathfinding
 
 
 		shipyard->getBoatCost(boatCost);
 		shipyard->getBoatCost(boatCost);
 
 
-		if(!cb->getResourceAmount().canAfford(source->actor->armyCost + boatCost))
+		if(!cb->getResourceAmount().canAfford(reservedResources + boatCost))
 		{
 		{
 #if NKAI_TRACE_LEVEL > 1
 #if NKAI_TRACE_LEVEL > 1
 			logAi->trace("Can not build a boat. Not enough resources.");
 			logAi->trace("Can not build a boat. Not enough resources.");
@@ -65,6 +63,18 @@ namespace AIPathfinding
 		return true;
 		return true;
 	}
 	}
 
 
+	bool BuildBoatAction::canAct(const AIPathNode * source) const
+	{
+		return canAct(source->actor->hero, source->actor->armyCost);
+	}
+
+	bool BuildBoatAction::canAct(const AIPathNodeInfo & source) const
+	{
+		TResources res;
+
+		return canAct(source.targetHero, res);
+	}
+
 	const CGObjectInstance * BuildBoatAction::targetObject() const
 	const CGObjectInstance * BuildBoatAction::targetObject() const
 	{
 	{
 		return dynamic_cast<const CGObjectInstance*>(shipyard);
 		return dynamic_cast<const CGObjectInstance*>(shipyard);
@@ -75,6 +85,11 @@ namespace AIPathfinding
 		return sourceActor->resourceActor;
 		return sourceActor->resourceActor;
 	}
 	}
 
 
+	std::shared_ptr<SpecialAction> BuildBoatActionFactory::create(const Nullkiller * ai)
+	{
+		return std::make_shared<BuildBoatAction>(ai->cb.get(), dynamic_cast<const IShipyard * >(ai->cb->getObj(shipyard)));
+	}
+
 	void SummonBoatAction::execute(const CGHeroInstance * hero) const
 	void SummonBoatAction::execute(const CGHeroInstance * hero) const
 	{
 	{
 		Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai);
 		Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai);

+ 15 - 0
AI/Nullkiller/Pathfinding/Actions/BoatActions.h

@@ -57,6 +57,8 @@ namespace AIPathfinding
 		}
 		}
 
 
 		bool canAct(const AIPathNode * source) const override;
 		bool canAct(const AIPathNode * source) const override;
+		bool canAct(const AIPathNodeInfo & source) const override;
+		bool canAct(const CGHeroInstance * hero, const TResources & reservedResources) const;
 
 
 		void execute(const CGHeroInstance * hero) const override;
 		void execute(const CGHeroInstance * hero) const override;
 
 
@@ -68,6 +70,19 @@ namespace AIPathfinding
 
 
 		const CGObjectInstance * targetObject() const override;
 		const CGObjectInstance * targetObject() const override;
 	};
 	};
+
+	class BuildBoatActionFactory : public ISpecialActionFactory
+	{
+		ObjectInstanceID shipyard;
+
+	public:
+		BuildBoatActionFactory(ObjectInstanceID shipyard)
+			:shipyard(shipyard)
+		{
+		}
+
+		std::shared_ptr<SpecialAction> create(const Nullkiller * ai) override;
+	};
 }
 }
 
 
 }
 }

+ 5 - 0
AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp

@@ -23,6 +23,11 @@ namespace AIPathfinding
 		return canAct(node->actor->hero);
 		return canAct(node->actor->hero);
 	}
 	}
 
 
+	bool QuestAction::canAct(const AIPathNodeInfo & node) const
+	{
+		return canAct(node.targetHero);
+	}
+
 	bool QuestAction::canAct(const CGHeroInstance * hero) const
 	bool QuestAction::canAct(const CGHeroInstance * hero) const
 	{
 	{
 		if(questInfo.obj->ID == Obj::BORDER_GATE || questInfo.obj->ID == Obj::BORDERGUARD)
 		if(questInfo.obj->ID == Obj::BORDER_GATE || questInfo.obj->ID == Obj::BORDERGUARD)

+ 1 - 1
AI/Nullkiller/Pathfinding/Actions/QuestAction.h

@@ -29,7 +29,7 @@ namespace AIPathfinding
 		}
 		}
 
 
 		bool canAct(const AIPathNode * node) const override;
 		bool canAct(const AIPathNode * node) const override;
-
+		bool canAct(const AIPathNodeInfo & node) const override;
 		bool canAct(const CGHeroInstance * hero) const;
 		bool canAct(const CGHeroInstance * hero) const;
 
 
 		Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
 		Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;

+ 13 - 0
AI/Nullkiller/Pathfinding/Actions/SpecialAction.h

@@ -22,6 +22,7 @@ namespace NKAI
 {
 {
 
 
 struct AIPathNode;
 struct AIPathNode;
+struct AIPathNodeInfo;
 class ChainActor;
 class ChainActor;
 
 
 class SpecialAction
 class SpecialAction
@@ -34,6 +35,11 @@ public:
 		return true;
 		return true;
 	}
 	}
 
 
+	virtual bool canAct(const AIPathNodeInfo & source) const
+	{
+		return true;
+	}
+
 	virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const;
 	virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const;
 
 
 	virtual void execute(const CGHeroInstance * hero) const;
 	virtual void execute(const CGHeroInstance * hero) const;
@@ -89,4 +95,11 @@ public:
 		const AIPathNode * srcNode) const override;
 		const AIPathNode * srcNode) const override;
 };
 };
 
 
+class ISpecialActionFactory
+{
+public:
+	virtual std::shared_ptr<SpecialAction> create(const Nullkiller * ai) = 0;
+	virtual ~ISpecialActionFactory() = default;
+};
+
 }
 }

+ 277 - 51
AI/Nullkiller/Pathfinding/ObjectGraph.cpp

@@ -16,6 +16,8 @@
 #include "../Engine/Nullkiller.h"
 #include "../Engine/Nullkiller.h"
 #include "../../../lib/logging/VisualLogger.h"
 #include "../../../lib/logging/VisualLogger.h"
 #include "Actions/QuestAction.h"
 #include "Actions/QuestAction.h"
+#include "../pforeach.h"
+#include "Actions/BoatActions.h"
 
 
 namespace NKAI
 namespace NKAI
 {
 {
@@ -32,6 +34,7 @@ class ObjectGraphCalculator
 private:
 private:
 	ObjectGraph * target;
 	ObjectGraph * target;
 	const Nullkiller * ai;
 	const Nullkiller * ai;
+	std::mutex syncLock;
 
 
 	std::map<const CGHeroInstance *, HeroRole> actors;
 	std::map<const CGHeroInstance *, HeroRole> actors;
 	std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
 	std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
@@ -41,7 +44,7 @@ private:
 
 
 public:
 public:
 	ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
 	ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
-		:ai(ai), target(target)
+		:ai(ai), target(target), syncLock()
 	{
 	{
 	}
 	}
 
 
@@ -65,17 +68,51 @@ public:
 	{
 	{
 		updatePaths();
 		updatePaths();
 
 
-		foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+		std::vector<AIPath> pathCache;
+
+		foreach_tile_pos(ai->cb.get(), [this, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
 			{
 			{
-				calculateConnections(pos);
+				calculateConnections(pos, pathCache);
 			});
 			});
 
 
 		removeExtraConnections();
 		removeExtraConnections();
 	}
 	}
 
 
+	float getNeighborConnectionsCost(const int3 & pos, std::vector<AIPath> & pathCache)
+	{
+		float neighborCost = std::numeric_limits<float>::max();
+
+		if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+		{
+			logAi->trace("Checking junction %s", pos.toString());
+		}
+
+		foreach_neighbour(
+			ai->cb.get(),
+			pos,
+			[this, &neighborCost, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
+			{
+				ai->pathfinder->calculatePathInfo(pathCache, neighbor);
+
+				auto costTotal = this->getConnectionsCost(pathCache);
+
+				if(costTotal.connectionsCount > 2 && costTotal.avg < neighborCost)
+				{
+					neighborCost = costTotal.avg;
+
+					if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+					{
+						logAi->trace("Better node found at %s", neighbor.toString());
+					}
+				}
+			});
+
+		return neighborCost;
+	}
+
 	void addMinimalDistanceJunctions()
 	void addMinimalDistanceJunctions()
 	{
 	{
-		foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+		pforeachTilePaths(ai->cb->getMapSize(), ai, [this](const int3 & pos, std::vector<AIPath> & paths)
 			{
 			{
 				if(target->hasNodeAt(pos))
 				if(target->hasNodeAt(pos))
 					return;
 					return;
@@ -83,35 +120,12 @@ public:
 				if(ai->cb->getGuardingCreaturePosition(pos).valid())
 				if(ai->cb->getGuardingCreaturePosition(pos).valid())
 					return;
 					return;
 
 
-				ConnectionCostInfo currentCost = getConnectionsCost(pos);
+				ConnectionCostInfo currentCost = getConnectionsCost(paths);
 
 
 				if(currentCost.connectionsCount <= 2)
 				if(currentCost.connectionsCount <= 2)
 					return;
 					return;
 
 
-				float neighborCost = currentCost.avg + 0.001f;
-
-				if(NKAI_GRAPH_TRACE_LEVEL >= 2)
-				{
-					logAi->trace("Checking junction %s", pos.toString());
-				}
-
-				foreach_neighbour(
-					ai->cb.get(),
-					pos,
-					[this, &neighborCost](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
-					{
-						auto costTotal = this->getConnectionsCost(neighbor);
-
-						if(costTotal.avg < neighborCost)
-						{
-							neighborCost = costTotal.avg;
-
-							if(NKAI_GRAPH_TRACE_LEVEL >= 2)
-							{
-								logAi->trace("Better node found at %s", neighbor.toString());
-							}
-						}
-					});
+				float neighborCost = getNeighborConnectionsCost(pos, paths);
 
 
 				if(currentCost.avg < neighborCost)
 				if(currentCost.avg < neighborCost)
 				{
 				{
@@ -132,20 +146,20 @@ private:
 		ai->pathfinder->updatePaths(actors, ps);
 		ai->pathfinder->updatePaths(actors, ps);
 	}
 	}
 
 
-	void calculateConnections(const int3 & pos)
+	void calculateConnections(const int3 & pos, std::vector<AIPath> & pathCache)
 	{
 	{
 		if(target->hasNodeAt(pos))
 		if(target->hasNodeAt(pos))
 		{
 		{
 			foreach_neighbour(
 			foreach_neighbour(
 				ai->cb.get(),
 				ai->cb.get(),
 				pos,
 				pos,
-				[this, &pos](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
+				[this, &pos, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
 				{
 				{
 					if(target->hasNodeAt(neighbor))
 					if(target->hasNodeAt(neighbor))
 					{
 					{
-						auto paths = ai->pathfinder->getPathInfo(neighbor);
+						ai->pathfinder->calculatePathInfo(pathCache, neighbor);
 
 
-						for(auto & path : paths)
+						for(auto & path : pathCache)
 						{
 						{
 							if(pos == path.targetHero->visitablePos())
 							if(pos == path.targetHero->visitablePos())
 							{
 							{
@@ -155,15 +169,46 @@ private:
 					}
 					}
 				});
 				});
 
 
+			auto obj = ai->cb->getTopObj(pos);
+
+			if((obj && obj->ID == Obj::BOAT) || target->isVirtualBoat(pos))
+			{
+				ai->pathfinder->calculatePathInfo(pathCache, pos);
+
+				for(AIPath & path : pathCache)
+				{
+					auto from = path.targetHero->visitablePos();
+					auto fromObj = actorObjectMap[path.targetHero];
+
+					auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos, path.targetHero, true);
+					auto updated = target->tryAddConnection(
+						from,
+						pos,
+						path.movementCost(),
+						danger);
+
+					if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated)
+					{
+						logAi->trace(
+							"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
+							fromObj ? fromObj->getObjectName() : "J", from.toString(),
+							"Boat", pos.toString(),
+							pos.toString(),
+							path.movementCost());
+					}
+				}
+			}
+
 			return;
 			return;
 		}
 		}
 
 
 		auto guardPos = ai->cb->getGuardingCreaturePosition(pos);
 		auto guardPos = ai->cb->getGuardingCreaturePosition(pos);
-		auto paths = ai->pathfinder->getPathInfo(pos);
+		
+		ai->pathfinder->calculatePathInfo(pathCache, pos);
 
 
-		for(AIPath & path1 : paths)
+		for(AIPath & path1 : pathCache)
 		{
 		{
-			for(AIPath & path2 : paths)
+			for(AIPath & path2 : pathCache)
 			{
 			{
 				if(path1.targetHero == path2.targetHero)
 				if(path1.targetHero == path2.targetHero)
 					continue;
 					continue;
@@ -185,7 +230,9 @@ private:
 					if(!cb->getTile(pos)->isWater())
 					if(!cb->getTile(pos)->isWater())
 						continue;
 						continue;
 
 
-					if(obj1 && (obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD))
+					auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1);
+
+					if(!startingObjIsBoat)
 						continue;
 						continue;
 				}
 				}
 
 
@@ -273,13 +320,25 @@ private:
 		assert(objectActor->visitablePos() == visitablePos);
 		assert(objectActor->visitablePos() == visitablePos);
 
 
 		actorObjectMap[objectActor] = obj;
 		actorObjectMap[objectActor] = obj;
-		actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::SHIPYARD ? HeroRole::MAIN : HeroRole::SCOUT;
+		actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::BOAT ? HeroRole::MAIN : HeroRole::SCOUT;
 
 
 		target->addObject(obj);
 		target->addObject(obj);
+
+		auto shipyard = dynamic_cast<const IShipyard *>(obj);
+		
+		if(shipyard && shipyard->bestLocation().valid())
+		{
+			int3 virtualBoat = shipyard->bestLocation();
+			
+			addJunctionActor(virtualBoat, true);
+			target->addVirtualBoat(virtualBoat, obj);
+		}
 	}
 	}
 
 
-	void addJunctionActor(const int3 & visitablePos)
+	void addJunctionActor(const int3 & visitablePos, bool isVirtualBoat = false)
 	{
 	{
+		std::lock_guard<std::mutex> lock(syncLock);
+
 		auto internalCb = temporaryActorHeroes.front()->cb;
 		auto internalCb = temporaryActorHeroes.front()->cb;
 		auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
 		auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
 
 
@@ -290,7 +349,7 @@ private:
 		objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
 		objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
 		objectActor->initObj(rng);
 		objectActor->initObj(rng);
 
 
-		if(cb->getTile(visitablePos)->isWater())
+		if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater())
 		{
 		{
 			objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
 			objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
 		}
 		}
@@ -298,14 +357,13 @@ private:
 		assert(objectActor->visitablePos() == visitablePos);
 		assert(objectActor->visitablePos() == visitablePos);
 
 
 		actorObjectMap[objectActor] = nullptr;
 		actorObjectMap[objectActor] = nullptr;
-		actors[objectActor] = HeroRole::SCOUT;
+		actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT;
 
 
 		target->registerJunction(visitablePos);
 		target->registerJunction(visitablePos);
 	}
 	}
 
 
-	ConnectionCostInfo getConnectionsCost(const int3 & pos) const
+	ConnectionCostInfo getConnectionsCost(std::vector<AIPath> & paths) const
 	{
 	{
-		auto paths = ai->pathfinder->getPathInfo(pos);
 		std::map<int3, float> costs;
 		std::map<int3, float> costs;
 
 
 		for(auto & path : paths)
 		for(auto & path : paths)
@@ -349,7 +407,15 @@ bool ObjectGraph::tryAddConnection(
 	float cost,
 	float cost,
 	uint64_t danger)
 	uint64_t danger)
 {
 {
-	return nodes[from].connections[to].update(cost, danger);
+	auto result =  nodes[from].connections[to].update(cost, danger);
+	auto & connection = nodes[from].connections[to];
+
+	if(result && isVirtualBoat(to) && !connection.specialAction)
+	{
+		connection.specialAction = std::make_shared<AIPathfinding::BuildBoatActionFactory>(virtualBoats[to]);
+	}
+
+	return result;
 }
 }
 
 
 void ObjectGraph::removeConnection(const int3 & from, const int3 & to)
 void ObjectGraph::removeConnection(const int3 & from, const int3 & to)
@@ -374,19 +440,30 @@ void ObjectGraph::updateGraph(const Nullkiller * ai)
 
 
 void ObjectGraph::addObject(const CGObjectInstance * obj)
 void ObjectGraph::addObject(const CGObjectInstance * obj)
 {
 {
-	nodes[obj->visitablePos()].init(obj);
+	if(!hasNodeAt(obj->visitablePos()))
+		nodes[obj->visitablePos()].init(obj);
+}
+
+void ObjectGraph::addVirtualBoat(const int3 & pos, const CGObjectInstance * shipyard)
+{
+	if(!isVirtualBoat(pos))
+	{
+		virtualBoats[pos] = shipyard->id;
+	}
 }
 }
 
 
 void ObjectGraph::registerJunction(const int3 & pos)
 void ObjectGraph::registerJunction(const int3 & pos)
 {
 {
-	nodes[pos].initJunction();
+	if(!hasNodeAt(pos))
+		nodes[pos].initJunction();
+
 }
 }
 
 
 void ObjectGraph::removeObject(const CGObjectInstance * obj)
 void ObjectGraph::removeObject(const CGObjectInstance * obj)
 {
 {
 	nodes[obj->visitablePos()].objectExists = false;
 	nodes[obj->visitablePos()].objectExists = false;
 
 
-	if(obj->ID == Obj::BOAT)
+	if(obj->ID == Obj::BOAT && !isVirtualBoat(obj->visitablePos()))
 	{
 	{
 		vstd::erase_if(nodes[obj->visitablePos()].connections, [&](const std::pair<int3, ObjectLink> & link) -> bool
 		vstd::erase_if(nodes[obj->visitablePos()].connections, [&](const std::pair<int3, ObjectLink> & link) -> bool
 			{
 			{
@@ -459,9 +536,35 @@ bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const Graph
 	return pathNodes.at(lhs.coord)[lhs.nodeType].cost > pathNodes.at(rhs.coord)[rhs.nodeType].cost;
 	return pathNodes.at(lhs.coord)[lhs.nodeType].cost > pathNodes.at(rhs.coord)[rhs.nodeType].cost;
 }
 }
 
 
+GraphPaths::GraphPaths()
+	: visualKey(""), graph(), pathNodes()
+{
+}
+
+std::shared_ptr<SpecialAction> getCompositeAction(
+	const Nullkiller * ai,
+	std::shared_ptr<ISpecialActionFactory> linkActionFactory,
+	std::shared_ptr<SpecialAction> transitionAction)
+{
+	if(!linkActionFactory)
+		return transitionAction;
+
+	auto linkAction = linkActionFactory->create(ai);
+
+	if(!transitionAction)
+		return linkAction;
+
+	std::vector<std::shared_ptr<const SpecialAction>> actionsArray = {
+		transitionAction,
+		linkAction
+	};
+
+	return std::make_shared<CompositeAction>(actionsArray);
+}
+
 void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai)
 void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai)
 {
 {
-	graph = *ai->baseGraph;
+	graph.copyFrom(*ai->baseGraph);
 	graph.connectHeroes(ai);
 	graph.connectHeroes(ai);
 
 
 	visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
 	visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
@@ -508,15 +611,16 @@ void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkil
 
 
 		node.isInQueue = false;
 		node.isInQueue = false;
 
 
-		graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, ObjectLink o)
+		graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, const ObjectLink & o)
 			{
 			{
-				auto targetNodeType = o.danger || transitionAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
+				auto compositeAction = getCompositeAction(ai, o.specialAction, transitionAction);
+				auto targetNodeType = o.danger || compositeAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
 				auto targetPointer = GraphPathNodePointer(target, targetNodeType);
 				auto targetPointer = GraphPathNodePointer(target, targetNodeType);
 				auto & targetNode = getOrCreateNode(targetPointer);
 				auto & targetNode = getOrCreateNode(targetPointer);
 
 
 				if(targetNode.tryUpdate(pos, node, o))
 				if(targetNode.tryUpdate(pos, node, o))
 				{
 				{
-					targetNode.specialAction = transitionAction;
+					targetNode.specialAction = compositeAction;
 
 
 					auto targetGraphNode = graph.getNode(target);
 					auto targetGraphNode = graph.getNode(target);
 
 
@@ -651,6 +755,11 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 				n.parentIndex = -1;
 				n.parentIndex = -1;
 				n.specialAction = getNode(*graphTile).specialAction;
 				n.specialAction = getNode(*graphTile).specialAction;
 
 
+				if(n.specialAction)
+				{
+					n.actionIsBlocked = !n.specialAction->canAct(n);
+				}
+
 				for(auto & node : path.nodes)
 				for(auto & node : path.nodes)
 				{
 				{
 					node.parentIndex++;
 					node.parentIndex++;
@@ -668,4 +777,121 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 	}
 	}
 }
 }
 
 
+void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const
+{
+	auto nodes = pathNodes.find(tile);
+
+	if(nodes == pathNodes.end())
+		return;
+
+	for(auto & targetNode : nodes->second)
+	{
+		if(!targetNode.reachable())
+			continue;
+
+		std::vector<GraphPathNodePointer> tilesToPass;
+
+		uint64_t danger = targetNode.danger;
+		float cost = targetNode.cost;
+		bool allowBattle = false;
+
+		auto current = GraphPathNodePointer(nodes->first, targetNode.nodeType);
+
+		while(true)
+		{
+			auto currentTile = pathNodes.find(current.coord);
+
+			if(currentTile == pathNodes.end())
+				break;
+
+			auto currentNode = currentTile->second[current.nodeType];
+
+			allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE;
+			vstd::amax(danger, currentNode.danger);
+			vstd::amax(cost, currentNode.cost);
+
+			tilesToPass.push_back(current);
+
+			if(currentNode.cost < 2.0f)
+				break;
+
+			current = currentNode.previous;
+		}
+		
+		if(tilesToPass.empty())
+			continue;
+
+		auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
+
+		for(auto & entryPath : entryPaths)
+		{
+			if(entryPath.targetHero != hero)
+				continue;
+
+			auto & path = paths.emplace_back();
+
+			path.targetHero = entryPath.targetHero;
+			path.heroArmy = entryPath.heroArmy;
+			path.exchangeCount = entryPath.exchangeCount;
+			path.armyLoss = entryPath.armyLoss + ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger);
+			path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle);
+			path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
+
+			AIPathNodeInfo n;
+
+			n.targetHero = hero;
+			n.parentIndex = -1;
+
+			// final node
+			n.coord = tile;
+			n.cost = targetNode.cost;
+			n.danger = targetNode.danger;
+			n.parentIndex = path.nodes.size();
+			path.nodes.push_back(n);
+
+			for(auto entryNode = entryPath.nodes.rbegin(); entryNode != entryPath.nodes.rend(); entryNode++)
+			{
+				auto blocker = ai->objectClusterizer->getBlocker(*entryNode);
+
+				if(blocker)
+				{
+					// blocker node
+					path.nodes.push_back(*entryNode);
+					path.nodes.back().parentIndex = path.nodes.size() - 1;
+					break;
+				}
+			}
+			
+			if(path.nodes.size() > 1)
+				continue;
+
+			for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
+			{
+				auto & node = getNode(*graphTile);
+
+				n.coord = graphTile->coord;
+				n.cost = node.cost;
+				n.turns = static_cast<ui8>(node.cost);
+				n.danger = node.danger;
+				n.specialAction = node.specialAction;
+				n.parentIndex = path.nodes.size();
+
+				if(n.specialAction)
+				{
+					n.actionIsBlocked = !n.specialAction->canAct(n);
+				}
+
+				auto blocker = ai->objectClusterizer->getBlocker(n);
+
+				if(!blocker)
+					continue;
+
+				// blocker node
+				path.nodes.push_back(n);
+				break;
+			}
+		}
+	}
+}
+
 }
 }

+ 21 - 0
AI/Nullkiller/Pathfinding/ObjectGraph.h

@@ -22,6 +22,7 @@ struct ObjectLink
 {
 {
 	float cost = 100000; // some big number
 	float cost = 100000; // some big number
 	uint64_t danger = 0;
 	uint64_t danger = 0;
+	std::shared_ptr<ISpecialActionFactory> specialAction;
 
 
 	bool update(float newCost, uint64_t newDanger)
 	bool update(float newCost, uint64_t newDanger)
 	{
 	{
@@ -62,17 +63,35 @@ struct ObjectNode
 class ObjectGraph
 class ObjectGraph
 {
 {
 	std::unordered_map<int3, ObjectNode> nodes;
 	std::unordered_map<int3, ObjectNode> nodes;
+	std::unordered_map<int3, ObjectInstanceID> virtualBoats;
 
 
 public:
 public:
+	ObjectGraph()
+		:nodes(), virtualBoats()
+	{
+	}
+
 	void updateGraph(const Nullkiller * ai);
 	void updateGraph(const Nullkiller * ai);
 	void addObject(const CGObjectInstance * obj);
 	void addObject(const CGObjectInstance * obj);
 	void registerJunction(const int3 & pos);
 	void registerJunction(const int3 & pos);
+	void addVirtualBoat(const int3 & pos, const CGObjectInstance * shipyard);
 	void connectHeroes(const Nullkiller * ai);
 	void connectHeroes(const Nullkiller * ai);
 	void removeObject(const CGObjectInstance * obj);
 	void removeObject(const CGObjectInstance * obj);
 	bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger);
 	bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger);
 	void removeConnection(const int3 & from, const int3 & to);
 	void removeConnection(const int3 & from, const int3 & to);
 	void dumpToLog(std::string visualKey) const;
 	void dumpToLog(std::string visualKey) const;
 
 
+	bool isVirtualBoat(const int3 & tile) const
+	{
+		return vstd::contains(virtualBoats, tile);
+	}
+
+	void copyFrom(const ObjectGraph & other)
+	{
+		nodes = other.nodes;
+		virtualBoats = other.virtualBoats;
+	}
+
 	template<typename Func>
 	template<typename Func>
 	void iterateConnections(const int3 & pos, Func fn)
 	void iterateConnections(const int3 & pos, Func fn)
 	{
 	{
@@ -167,8 +186,10 @@ class GraphPaths
 	std::string visualKey;
 	std::string visualKey;
 
 
 public:
 public:
+	GraphPaths();
 	void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai);
 	void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai);
 	void addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
 	void addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
+	void quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
 	void dumpToLog() const;
 	void dumpToLog() const;
 
 
 private:
 private:

+ 4 - 1
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -17,7 +17,10 @@ namespace NKAI
 {
 {
 namespace AIPathfinding
 namespace AIPathfinding
 {
 {
-	AILayerTransitionRule::AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage)
+	AILayerTransitionRule::AILayerTransitionRule(
+		CPlayerSpecificInfoCallback * cb,
+		Nullkiller * ai,
+		std::shared_ptr<AINodeStorage> nodeStorage)
 		:cb(cb), ai(ai), nodeStorage(nodeStorage)
 		:cb(cb), ai(ai), nodeStorage(nodeStorage)
 	{
 	{
 		setup();
 		setup();

+ 4 - 1
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h

@@ -34,7 +34,10 @@ namespace AIPathfinding
 		std::map<const CGHeroInstance *, std::shared_ptr<const AirWalkingAction>> airWalkingActions;
 		std::map<const CGHeroInstance *, std::shared_ptr<const AirWalkingAction>> airWalkingActions;
 
 
 	public:
 	public:
-		AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage);
+		AILayerTransitionRule(
+			CPlayerSpecificInfoCallback * cb,
+			Nullkiller * ai,
+			std::shared_ptr<AINodeStorage> nodeStorage);
 
 
 		virtual void process(
 		virtual void process(
 			const PathNodeInfo & source,
 			const PathNodeInfo & source,

+ 10 - 0
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -40,6 +40,16 @@ namespace AIPathfinding
 
 
 			return;
 			return;
 		}
 		}
+		
+		if(!allowBypassObjects
+			&& destination.action == EPathNodeAction::EMBARK
+			&& source.node->layer == EPathfindingLayer::LAND
+			&& destination.node->layer == EPathfindingLayer::SAIL)
+		{
+			destination.blocked = true;
+
+			return;
+		}
 
 
 		auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
 		auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
 
 

+ 9 - 0
AI/Nullkiller/StdInc.cpp

@@ -1 +1,10 @@
+/*
+ * StdInc.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #include "StdInc.h"
 #include "StdInc.h"

+ 9 - 0
AI/Nullkiller/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #pragma  once
 #pragma  once
 #include "../../Global.h"
 #include "../../Global.h"
 VCMI_LIB_USING_NAMESPACE
 VCMI_LIB_USING_NAMESPACE

+ 50 - 0
AI/Nullkiller/pforeach.h

@@ -0,0 +1,50 @@
+#pragma once
+
+#include "Engine/Nullkiller.h"
+
+namespace NKAI
+{
+
+template<typename TFunc>
+void pforeachTilePos(const int3 & mapSize, TFunc fn)
+{
+	for(int z = 0; z < mapSize.z; ++z)
+	{
+		tbb::parallel_for(tbb::blocked_range<size_t>(0, mapSize.x), [&](const tbb::blocked_range<size_t> & r)
+			{
+				int3 pos(0, 0, z);
+
+				for(pos.x = r.begin(); pos.x != r.end(); ++pos.x)
+				{
+					for(pos.y = 0; pos.y < mapSize.y; ++pos.y)
+					{
+						fn(pos);
+					}
+				}
+			});
+	}
+}
+
+template<typename TFunc>
+void pforeachTilePaths(const int3 & mapSize, const Nullkiller * ai, TFunc fn)
+{
+	for(int z = 0; z < mapSize.z; ++z)
+	{
+		tbb::parallel_for(tbb::blocked_range<size_t>(0, mapSize.x), [&](const tbb::blocked_range<size_t> & r)
+			{
+				int3 pos(0, 0, z);
+				std::vector<AIPath> paths;
+
+				for(pos.x = r.begin(); pos.x != r.end(); ++pos.x)
+				{
+					for(pos.y = 0; pos.y < mapSize.y; ++pos.y)
+					{
+						ai->pathfinder->calculatePathInfo(paths, pos);
+						fn(pos, paths);
+					}
+				}
+			});
+	}
+}
+
+}

+ 9 - 0
AI/StupidAI/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 // Creates the precompiled header
 // Creates the precompiled header
 #include "StdInc.h"
 #include "StdInc.h"

+ 9 - 0
AI/StupidAI/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #pragma once
 #pragma once
 
 
 #include "../../Global.h"
 #include "../../Global.h"

+ 1 - 1
AI/VCAI/AIhelper.cpp

@@ -1,5 +1,5 @@
 /*
 /*
-* AIhelper.h, part of VCMI engine
+* AIhelper.cpp, part of VCMI engine
 *
 *
 * Authors: listed in file AUTHORS in main folder
 * Authors: listed in file AUTHORS in main folder
 *
 *

+ 9 - 0
AI/VCAI/MapObjectsEvaluator.cpp

@@ -1,3 +1,12 @@
+/*
+ * MapObjectsEvaluator.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #include "StdInc.h"
 #include "StdInc.h"
 #include "MapObjectsEvaluator.h"
 #include "MapObjectsEvaluator.h"
 #include "../../lib/GameConstants.h"
 #include "../../lib/GameConstants.h"

+ 11 - 7
AI/VCAI/Pathfinding/AINodeStorage.cpp

@@ -155,15 +155,21 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf
 	});
 	});
 }
 }
 
 
-std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
+void AINodeStorage::calculateNeighbours(
+	std::vector<CGPathNode *> & result,
 	const PathNodeInfo & source,
 	const PathNodeInfo & source,
+	EPathfindingLayer layer,
 	const PathfinderConfig * pathfinderConfig,
 	const PathfinderConfig * pathfinderConfig,
 	const CPathfinderHelper * pathfinderHelper)
 	const CPathfinderHelper * pathfinderHelper)
 {
 {
-	std::vector<CGPathNode *> neighbours;
-	neighbours.reserve(16);
+	std::vector<int3> accessibleNeighbourTiles;
+
+	result.clear();
+	accessibleNeighbourTiles.reserve(8);
+
+	pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);
+
 	const AIPathNode * srcNode = getAINode(source.node);
 	const AIPathNode * srcNode = getAINode(source.node);
-	auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
 
 
 	for(auto & neighbour : accessibleNeighbourTiles)
 	for(auto & neighbour : accessibleNeighbourTiles)
 	{
 	{
@@ -174,11 +180,9 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
 			if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
 			if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
 				continue;
 				continue;
 
 
-			neighbours.push_back(nextNode.value());
+			result.push_back(nextNode.value());
 		}
 		}
 	}
 	}
-
-	return neighbours;
 }
 }
 
 
 void AINodeStorage::setHero(HeroPtr heroPtr, const VCAI * _ai)
 void AINodeStorage::setHero(HeroPtr heroPtr, const VCAI * _ai)

+ 3 - 1
AI/VCAI/Pathfinding/AINodeStorage.h

@@ -89,8 +89,10 @@ public:
 
 
 	std::vector<CGPathNode *> getInitialNodes() override;
 	std::vector<CGPathNode *> getInitialNodes() override;
 
 
-	virtual std::vector<CGPathNode *> calculateNeighbours(
+	virtual void calculateNeighbours(
+		std::vector<CGPathNode *> & result,
 		const PathNodeInfo & source,
 		const PathNodeInfo & source,
+		EPathfindingLayer layer,
 		const PathfinderConfig * pathfinderConfig,
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) override;
 		const CPathfinderHelper * pathfinderHelper) override;
 
 

+ 9 - 0
AI/VCAI/StdInc.cpp

@@ -1 +1,10 @@
+/*
+ * StdInc.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #include "StdInc.h"
 #include "StdInc.h"

+ 9 - 0
AI/VCAI/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #pragma  once
 #pragma  once
 #include "../../Global.h"
 #include "../../Global.h"
 
 

+ 1 - 1
CI/linux-qt6/before_install.sh

@@ -3,7 +3,7 @@
 sudo apt-get update
 sudo apt-get update
 
 
 # Dependencies
 # Dependencies
-sudo apt-get install libboost-all-dev \
+sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \
 qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \

+ 1 - 1
CI/linux/before_install.sh

@@ -3,7 +3,7 @@
 sudo apt-get update
 sudo apt-get update
 
 
 # Dependencies
 # Dependencies
-sudo apt-get install libboost-all-dev \
+sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 qtbase5-dev \
 qtbase5-dev \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \

+ 40 - 23
CMakeLists.txt

@@ -38,11 +38,13 @@ endif()
 
 
 # Platform-independent options
 # Platform-independent options
 
 
+option(ENABLE_CLIENT "Enable compilation of game client" ON)
 option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF)
 option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF)
 option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF)
 option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF)
 option(ENABLE_TRANSLATIONS "Enable generation of translations for launcher and editor" ON)
 option(ENABLE_TRANSLATIONS "Enable generation of translations for launcher and editor" ON)
 option(ENABLE_NULLKILLER_AI "Enable compilation of Nullkiller AI library" ON)
 option(ENABLE_NULLKILLER_AI "Enable compilation of Nullkiller AI library" ON)
 option(ENABLE_GITVERSION "Enable Version.cpp with Git commit hash" ON)
 option(ENABLE_GITVERSION "Enable Version.cpp with Git commit hash" ON)
+option(ENABLE_MINIMAL_LIB "Build only core parts of vcmi library that are required for game lobby" OFF)
 
 
 # Compilation options
 # Compilation options
 
 
@@ -247,6 +249,10 @@ if(ENABLE_SINGLE_APP_BUILD)
 	add_definitions(-DENABLE_SINGLE_APP_BUILD)
 	add_definitions(-DENABLE_SINGLE_APP_BUILD)
 endif()
 endif()
 
 
+if(ENABLE_MINIMAL_LIB)
+	add_definitions(-DENABLE_MINIMAL_LIB)
+endif()
+
 if(APPLE_IOS)
 if(APPLE_IOS)
 	set(CMAKE_MACOSX_RPATH 1)
 	set(CMAKE_MACOSX_RPATH 1)
 	set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0)
 	set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0)
@@ -450,12 +456,6 @@ if(TARGET zlib::zlib)
 	add_library(ZLIB::ZLIB ALIAS zlib::zlib)
 	add_library(ZLIB::ZLIB ALIAS zlib::zlib)
 endif()
 endif()
 
 
-set(FFMPEG_COMPONENTS avutil swscale avformat avcodec)
-if(APPLE_IOS AND NOT USING_CONAN)
-	list(APPEND FFMPEG_COMPONENTS swresample)
-endif()
-find_package(ffmpeg COMPONENTS ${FFMPEG_COMPONENTS})
-
 option(FORCE_BUNDLED_MINIZIP "Force bundled Minizip library" OFF)
 option(FORCE_BUNDLED_MINIZIP "Force bundled Minizip library" OFF)
 if(NOT FORCE_BUNDLED_MINIZIP)
 if(NOT FORCE_BUNDLED_MINIZIP)
 	find_package(minizip)
 	find_package(minizip)
@@ -464,18 +464,26 @@ if(NOT FORCE_BUNDLED_MINIZIP)
 	endif()
 	endif()
 endif()
 endif()
 
 
-find_package(SDL2 REQUIRED)
-find_package(SDL2_image REQUIRED)
-if(TARGET SDL2_image::SDL2_image)
-	add_library(SDL2::Image ALIAS SDL2_image::SDL2_image)
-endif()
-find_package(SDL2_mixer REQUIRED)
-if(TARGET SDL2_mixer::SDL2_mixer)
-	add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer)
-endif()
-find_package(SDL2_ttf REQUIRED)
-if(TARGET SDL2_ttf::SDL2_ttf)
-	add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
+if (ENABLE_CLIENT)
+	set(FFMPEG_COMPONENTS avutil swscale avformat avcodec)
+	if(APPLE_IOS AND NOT USING_CONAN)
+		list(APPEND FFMPEG_COMPONENTS swresample)
+	endif()
+	find_package(ffmpeg COMPONENTS ${FFMPEG_COMPONENTS})
+
+	find_package(SDL2 REQUIRED)
+	find_package(SDL2_image REQUIRED)
+	if(TARGET SDL2_image::SDL2_image)
+		add_library(SDL2::Image ALIAS SDL2_image::SDL2_image)
+	endif()
+	find_package(SDL2_mixer REQUIRED)
+	if(TARGET SDL2_mixer::SDL2_mixer)
+		add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer)
+	endif()
+	find_package(SDL2_ttf REQUIRED)
+	if(TARGET SDL2_ttf::SDL2_ttf)
+		add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
+	endif()
 endif()
 endif()
 
 
 if(ENABLE_LOBBY)
 if(ENABLE_LOBBY)
@@ -493,7 +501,7 @@ if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
 	endif()
 	endif()
 endif()
 endif()
 
 
-if(ENABLE_NULLKILLER_AI)
+if(ENABLE_NULLKILLER_AI AND ENABLE_CLIENT)
 	find_package(TBB REQUIRED)
 	find_package(TBB REQUIRED)
 endif()
 endif()
 
 
@@ -603,10 +611,15 @@ if(APPLE_IOS)
 	add_subdirectory(ios)
 	add_subdirectory(ios)
 endif()
 endif()
 
 
-add_subdirectory_with_folder("AI" AI)
+if (ENABLE_CLIENT)
+	add_subdirectory_with_folder("AI" AI)
+endif()
 
 
 add_subdirectory(lib)
 add_subdirectory(lib)
-add_subdirectory(server)
+
+if (ENABLE_CLIENT OR ENABLE_SERVER)
+	add_subdirectory(server)
+endif()
 
 
 if(ENABLE_ERM)
 if(ENABLE_ERM)
 	add_subdirectory(scripting/erm)
 	add_subdirectory(scripting/erm)
@@ -633,7 +646,9 @@ if(ENABLE_LOBBY)
 	add_subdirectory(lobby)
 	add_subdirectory(lobby)
 endif()
 endif()
 
 
-add_subdirectory(client)
+if (ENABLE_CLIENT)
+	add_subdirectory(client)
+endif()
 
 
 if(ENABLE_SERVER)
 if(ENABLE_SERVER)
 	add_subdirectory(serverapp)
 	add_subdirectory(serverapp)
@@ -677,7 +692,9 @@ if(ANDROID)
 	")
 	")
 else()
 else()
 	install(DIRECTORY config DESTINATION ${DATA_DIR})
 	install(DIRECTORY config DESTINATION ${DATA_DIR})
-	install(DIRECTORY Mods DESTINATION ${DATA_DIR})
+	if (ENABLE_CLIENT OR ENABLE_SERVER)
+		install(DIRECTORY Mods DESTINATION ${DATA_DIR})
+	endif()
 endif()
 endif()
 if(ENABLE_LUA)
 if(ENABLE_LUA)
 	install(DIRECTORY scripts DESTINATION ${DATA_DIR})
 	install(DIRECTORY scripts DESTINATION ${DATA_DIR})

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

@@ -79,6 +79,7 @@
 	"vcmi.lobby.login.error" : "Connection error: %s",
 	"vcmi.lobby.login.error" : "Connection error: %s",
 	"vcmi.lobby.login.create" : "New Account",
 	"vcmi.lobby.login.create" : "New Account",
 	"vcmi.lobby.login.login" : "Login",
 	"vcmi.lobby.login.login" : "Login",
+	"vcmi.lobby.login.as" : "Login as %s",
 	"vcmi.lobby.header.rooms" : "Game Rooms - %d",
 	"vcmi.lobby.header.rooms" : "Game Rooms - %d",
 	"vcmi.lobby.header.channels" : "Chat Channels",
 	"vcmi.lobby.header.channels" : "Chat Channels",
 	"vcmi.lobby.header.chat.global" : "Global Game Chat - %s", // %s -> language name
 	"vcmi.lobby.header.chat.global" : "Global Game Chat - %s", // %s -> language name

+ 1 - 0
Mods/vcmi/config/vcmi/ukrainian.json

@@ -78,6 +78,7 @@
 	"vcmi.lobby.login.error" : "Помилка з'єднання: %s",
 	"vcmi.lobby.login.error" : "Помилка з'єднання: %s",
 	"vcmi.lobby.login.create" : "Створити акаунт",
 	"vcmi.lobby.login.create" : "Створити акаунт",
 	"vcmi.lobby.login.login" : "Увійти",
 	"vcmi.lobby.login.login" : "Увійти",
+	"vcmi.lobby.login.as" : "Увійти як %s",
 	"vcmi.lobby.header.rooms" : "Активні кімнати - %d",
 	"vcmi.lobby.header.rooms" : "Активні кімнати - %d",
 	"vcmi.lobby.header.channels" : "Канали чату",
 	"vcmi.lobby.header.channels" : "Канали чату",
 	"vcmi.lobby.header.chat.global" : "Глобальний ігровий чат - %s", // %s -> language name
 	"vcmi.lobby.header.chat.global" : "Глобальний ігровий чат - %s", // %s -> language name

+ 1 - 1
client/CFocusableHelper.cpp

@@ -7,9 +7,9 @@
  * Full text of license available in license.txt file, in main folder
  * Full text of license available in license.txt file, in main folder
  *
  *
  */
  */
+#include "StdInc.h"
 #include "CFocusableHelper.h"
 #include "CFocusableHelper.h"
 #include "../Global.h"
 #include "../Global.h"
-#include "StdInc.h"
 #include "widgets/TextControls.h"
 #include "widgets/TextControls.h"
 
 
 void removeFocusFromActiveInput()
 void removeFocusFromActiveInput()

+ 1 - 0
client/CFocusableHelper.h

@@ -7,5 +7,6 @@
  * Full text of license available in license.txt file, in main folder
  * Full text of license available in license.txt file, in main folder
  *
  *
  */
  */
+#pragma once
 
 
 void removeFocusFromActiveInput();
 void removeFocusFromActiveInput();

+ 13 - 2
client/CMT.cpp

@@ -56,6 +56,7 @@ namespace po = boost::program_options;
 namespace po_style = boost::program_options::command_line_style;
 namespace po_style = boost::program_options::command_line_style;
 
 
 static std::atomic<bool> quitRequestedDuringOpeningPlayback = false;
 static std::atomic<bool> quitRequestedDuringOpeningPlayback = false;
+static std::atomic<bool> headlessQuit = false;
 
 
 #ifndef VCMI_IOS
 #ifndef VCMI_IOS
 void processCommand(const std::string &message);
 void processCommand(const std::string &message);
@@ -371,8 +372,10 @@ int main(int argc, char * argv[])
 	}
 	}
 	else
 	else
 	{
 	{
-		while(true)
+		while(!headlessQuit)
 			boost::this_thread::sleep_for(boost::chrono::milliseconds(200));
 			boost::this_thread::sleep_for(boost::chrono::milliseconds(200));
+
+		quitApplication();
 	}
 	}
 
 
 	return 0;
 	return 0;
@@ -481,7 +484,15 @@ void handleQuit(bool ask)
 	// proper solution would be to abort init thread (or wait for it to finish)
 	// proper solution would be to abort init thread (or wait for it to finish)
 	if(!ask)
 	if(!ask)
 	{
 	{
-		quitApplication();
+		if(settings["session"]["headless"].Bool())
+		{
+			headlessQuit = true;
+		}
+		else
+		{
+			quitApplication();
+		}
+
 		return;
 		return;
 	}
 	}
 
 

+ 1 - 1
client/Client.cpp

@@ -7,8 +7,8 @@
  * Full text of license available in license.txt file, in main folder
  * Full text of license available in license.txt file, in main folder
  *
  *
  */
  */
-#include "Global.h"
 #include "StdInc.h"
 #include "StdInc.h"
+#include "Global.h"
 #include "Client.h"
 #include "Client.h"
 
 
 #include "CGameInfo.h"
 #include "CGameInfo.h"

+ 4 - 0
client/NetPacksClient.cpp

@@ -410,7 +410,11 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
 
 
 	// In auto testing pack.mode we always close client if red pack.player won or lose
 	// In auto testing pack.mode we always close client if red pack.player won or lose
 	if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0))
 	if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0))
+	{
+		logAi->info("Red player %s. Ending game.", pack.victoryLossCheckResult.victory() ? "won" : "lost");
+
 		handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not
 		handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not
+	}
 }
 }
 
 
 void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface & pack)
 void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface & pack)

+ 9 - 0
client/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 // Creates the precompiled header
 // Creates the precompiled header
 #include "StdInc.h"
 #include "StdInc.h"

+ 9 - 0
client/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #pragma once
 #pragma once
 
 
 #include "../Global.h"
 #include "../Global.h"

+ 69 - 26
client/globalLobby/GlobalLobbyClient.cpp

@@ -85,14 +85,9 @@ void GlobalLobbyClient::receiveAccountCreated(const JsonNode & json)
 		throw std::runtime_error("lobby connection finished without active login window!");
 		throw std::runtime_error("lobby connection finished without active login window!");
 
 
 	{
 	{
-		Settings configID = settings.write["lobby"]["accountID"];
-		configID->String() = json["accountID"].String();
-
-		Settings configName = settings.write["lobby"]["displayName"];
-		configName->String() = json["displayName"].String();
-
-		Settings configCookie = settings.write["lobby"]["accountCookie"];
-		configCookie->String() = json["accountCookie"].String();
+		setAccountID(json["accountID"].String());
+		setAccountDisplayName(json["displayName"].String());
+		setAccountCookie(json["accountCookie"].String());
 	}
 	}
 
 
 	sendClientLogin();
 	sendClientLogin();
@@ -105,18 +100,15 @@ void GlobalLobbyClient::receiveOperationFailed(const JsonNode & json)
 	if(loginWindowPtr)
 	if(loginWindowPtr)
 		loginWindowPtr->onConnectionFailed(json["reason"].String());
 		loginWindowPtr->onConnectionFailed(json["reason"].String());
 
 
+	logGlobal->warn("Operation failed! Reason: %s", json["reason"].String());
 	// TODO: handle errors in lobby menu
 	// TODO: handle errors in lobby menu
 }
 }
 
 
 void GlobalLobbyClient::receiveClientLoginSuccess(const JsonNode & json)
 void GlobalLobbyClient::receiveClientLoginSuccess(const JsonNode & json)
 {
 {
-	{
-		Settings configCookie = settings.write["lobby"]["accountCookie"];
-		configCookie->String() = json["accountCookie"].String();
-
-		Settings configName = settings.write["lobby"]["displayName"];
-		configName->String() = json["displayName"].String();
-	}
+	accountLoggedIn = true;
+	setAccountDisplayName(json["displayName"].String());
+	setAccountCookie(json["accountCookie"].String());
 
 
 	auto loginWindowPtr = loginWindow.lock();
 	auto loginWindowPtr = loginWindow.lock();
 
 
@@ -288,8 +280,8 @@ void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json)
 		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, {});
 		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, {});
 		CSH->loadMode = ELoadMode::MULTI;
 		CSH->loadMode = ELoadMode::MULTI;
 
 
-		std::string hostname = settings["lobby"]["hostname"].String();
-		int16_t port = settings["lobby"]["port"].Integer();
+		std::string hostname = getServerHost();
+		uint16_t port = getServerPort();
 		CSH->connectToServer(hostname, port);
 		CSH->connectToServer(hostname, port);
 	}
 	}
 
 
@@ -324,8 +316,8 @@ void GlobalLobbyClient::sendClientLogin()
 {
 {
 	JsonNode toSend;
 	JsonNode toSend;
 	toSend["type"].String() = "clientLogin";
 	toSend["type"].String() = "clientLogin";
-	toSend["accountID"] = settings["lobby"]["accountID"];
-	toSend["accountCookie"] = settings["lobby"]["accountCookie"];
+	toSend["accountID"].String() = getAccountID();
+	toSend["accountCookie"].String() = getAccountCookie();
 	toSend["language"].String() = CGI->generaltexth->getPreferredLanguage();
 	toSend["language"].String() = CGI->generaltexth->getPreferredLanguage();
 	toSend["version"].String() = VCMI_VERSION_STRING;
 	toSend["version"].String() = VCMI_VERSION_STRING;
 	sendMessage(toSend);
 	sendMessage(toSend);
@@ -350,6 +342,7 @@ void GlobalLobbyClient::onDisconnected(const std::shared_ptr<INetworkConnection>
 
 
 	assert(connection == networkConnection);
 	assert(connection == networkConnection);
 	networkConnection.reset();
 	networkConnection.reset();
+	accountLoggedIn = false;
 
 
 	while (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
 	while (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
 	{
 	{
@@ -370,7 +363,7 @@ void GlobalLobbyClient::sendOpenRoom(const std::string & mode, int playerLimit)
 {
 {
 	JsonNode toSend;
 	JsonNode toSend;
 	toSend["type"].String() = "activateGameRoom";
 	toSend["type"].String() = "activateGameRoom";
-	toSend["hostAccountID"] = settings["lobby"]["accountID"];
+	toSend["hostAccountID"].String() = getAccountID();
 	toSend["roomType"].String() = mode;
 	toSend["roomType"].String() = mode;
 	toSend["playerLimit"].Integer() = playerLimit;
 	toSend["playerLimit"].Integer() = playerLimit;
 	sendMessage(toSend);
 	sendMessage(toSend);
@@ -378,11 +371,16 @@ void GlobalLobbyClient::sendOpenRoom(const std::string & mode, int playerLimit)
 
 
 void GlobalLobbyClient::connect()
 void GlobalLobbyClient::connect()
 {
 {
-	std::string hostname = settings["lobby"]["hostname"].String();
-	int16_t port = settings["lobby"]["port"].Integer();
+	std::string hostname = getServerHost();
+	uint16_t port = getServerPort();
 	CSH->getNetworkHandler().connectToRemote(*this, hostname, port);
 	CSH->getNetworkHandler().connectToRemote(*this, hostname, port);
 }
 }
 
 
+bool GlobalLobbyClient::isLoggedIn() const
+{
+	return networkConnection != nullptr && accountLoggedIn;
+}
+
 bool GlobalLobbyClient::isConnected() const
 bool GlobalLobbyClient::isConnected() const
 {
 {
 	return networkConnection != nullptr;
 	return networkConnection != nullptr;
@@ -468,7 +466,7 @@ void GlobalLobbyClient::activateInterface()
 	if (!GH.windows().findWindows<GlobalLobbyLoginWindow>().empty())
 	if (!GH.windows().findWindows<GlobalLobbyLoginWindow>().empty())
 		return;
 		return;
 
 
-	if (isConnected())
+	if (isLoggedIn())
 		GH.windows().pushWindow(createLobbyWindow());
 		GH.windows().pushWindow(createLobbyWindow());
 	else
 	else
 		GH.windows().pushWindow(createLoginWindow());
 		GH.windows().pushWindow(createLoginWindow());
@@ -479,12 +477,55 @@ void GlobalLobbyClient::activateRoomInviteInterface()
 	GH.windows().createAndPushWindow<GlobalLobbyInviteWindow>();
 	GH.windows().createAndPushWindow<GlobalLobbyInviteWindow>();
 }
 }
 
 
+void GlobalLobbyClient::setAccountID(const std::string & accountID)
+{
+	Settings configID = persistentStorage.write["lobby"][getServerHost()]["accountID"];
+	configID->String() = accountID;
+}
+
+void GlobalLobbyClient::setAccountCookie(const std::string & accountCookie)
+{
+	Settings configCookie = persistentStorage.write["lobby"][getServerHost()]["accountCookie"];
+	configCookie->String() = accountCookie;
+}
+
+void GlobalLobbyClient::setAccountDisplayName(const std::string & accountDisplayName)
+{
+	Settings configName = persistentStorage.write["lobby"][getServerHost()]["displayName"];
+	configName->String() = accountDisplayName;
+}
+
+const std::string & GlobalLobbyClient::getAccountID() const
+{
+	return persistentStorage["lobby"][getServerHost()]["accountID"].String();
+}
+
+const std::string & GlobalLobbyClient::getAccountCookie() const
+{
+	return persistentStorage["lobby"][getServerHost()]["accountCookie"].String();
+}
+
+const std::string & GlobalLobbyClient::getAccountDisplayName() const
+{
+	return persistentStorage["lobby"][getServerHost()]["displayName"].String();
+}
+
+const std::string & GlobalLobbyClient::getServerHost() const
+{
+	return settings["lobby"]["hostname"].String();
+}
+
+uint16_t GlobalLobbyClient::getServerPort() const
+{
+	return settings["lobby"]["port"].Integer();
+}
+
 void GlobalLobbyClient::sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection)
 void GlobalLobbyClient::sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection)
 {
 {
 	JsonNode toSend;
 	JsonNode toSend;
 	toSend["type"].String() = "clientProxyLogin";
 	toSend["type"].String() = "clientProxyLogin";
-	toSend["accountID"] = settings["lobby"]["accountID"];
-	toSend["accountCookie"] = settings["lobby"]["accountCookie"];
+	toSend["accountID"].String() = getAccountID();
+	toSend["accountCookie"].String() = getAccountCookie();
 	toSend["gameRoomID"].String() = currentGameRoomUUID;
 	toSend["gameRoomID"].String() = currentGameRoomUUID;
 
 
 	assert(JsonUtils::validate(toSend, "vcmi:lobbyProtocol/" + toSend["type"].String(), toSend["type"].String() + " pack"));
 	assert(JsonUtils::validate(toSend, "vcmi:lobbyProtocol/" + toSend["type"].String(), toSend["type"].String() + " pack"));
@@ -498,7 +539,7 @@ void GlobalLobbyClient::resetMatchState()
 
 
 void GlobalLobbyClient::sendMatchChatMessage(const std::string & messageText)
 void GlobalLobbyClient::sendMatchChatMessage(const std::string & messageText)
 {
 {
-	if (!isConnected())
+	if (!isLoggedIn())
 		return; // we are not playing with lobby
 		return; // we are not playing with lobby
 
 
 	if (currentGameRoomUUID.empty())
 	if (currentGameRoomUUID.empty())
@@ -510,6 +551,8 @@ void GlobalLobbyClient::sendMatchChatMessage(const std::string & messageText)
 	toSend["channelName"].String() = currentGameRoomUUID;
 	toSend["channelName"].String() = currentGameRoomUUID;
 	toSend["messageText"].String() = messageText;
 	toSend["messageText"].String() = messageText;
 
 
+	assert(TextOperations::isValidUnicodeString(messageText));
+
 	CSH->getGlobalLobby().sendMessage(toSend);
 	CSH->getGlobalLobby().sendMessage(toSend);
 }
 }
 
 

+ 12 - 0
client/globalLobby/GlobalLobbyClient.h

@@ -34,6 +34,7 @@ class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyabl
 
 
 	std::shared_ptr<INetworkConnection> networkConnection;
 	std::shared_ptr<INetworkConnection> networkConnection;
 	std::string currentGameRoomUUID;
 	std::string currentGameRoomUUID;
+	bool accountLoggedIn = false;
 
 
 	std::weak_ptr<GlobalLobbyLoginWindow> loginWindow;
 	std::weak_ptr<GlobalLobbyLoginWindow> loginWindow;
 	std::weak_ptr<GlobalLobbyWindow> lobbyWindow;
 	std::weak_ptr<GlobalLobbyWindow> lobbyWindow;
@@ -58,6 +59,10 @@ class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyabl
 	std::shared_ptr<GlobalLobbyLoginWindow> createLoginWindow();
 	std::shared_ptr<GlobalLobbyLoginWindow> createLoginWindow();
 	std::shared_ptr<GlobalLobbyWindow> createLobbyWindow();
 	std::shared_ptr<GlobalLobbyWindow> createLobbyWindow();
 
 
+	void setAccountID(const std::string & accountID);
+	void setAccountCookie(const std::string & accountCookie);
+	void setAccountDisplayName(const std::string & accountDisplayName);
+
 public:
 public:
 	explicit GlobalLobbyClient();
 	explicit GlobalLobbyClient();
 	~GlobalLobbyClient();
 	~GlobalLobbyClient();
@@ -68,6 +73,12 @@ public:
 	const std::vector<GlobalLobbyRoom> & getMatchesHistory() const;
 	const std::vector<GlobalLobbyRoom> & getMatchesHistory() const;
 	const std::vector<GlobalLobbyChannelMessage> & getChannelHistory(const std::string & channelType, const std::string & channelName) const;
 	const std::vector<GlobalLobbyChannelMessage> & getChannelHistory(const std::string & channelType, const std::string & channelName) const;
 
 
+	const std::string & getAccountID() const;
+	const std::string & getAccountCookie() const;
+	const std::string & getAccountDisplayName() const;
+	const std::string & getServerHost() const;
+	uint16_t getServerPort() const;
+
 	/// Activate interface and pushes lobby UI as top window
 	/// Activate interface and pushes lobby UI as top window
 	void activateInterface();
 	void activateInterface();
 	void activateRoomInviteInterface();
 	void activateRoomInviteInterface();
@@ -83,5 +94,6 @@ public:
 
 
 	void connect();
 	void connect();
 	bool isConnected() const;
 	bool isConnected() const;
+	bool isLoggedIn() const;
 	bool isInvitedToRoom(const std::string & gameRoomID);
 	bool isInvitedToRoom(const std::string & gameRoomID);
 };
 };

+ 17 - 5
client/globalLobby/GlobalLobbyLoginWindow.cpp

@@ -36,9 +36,14 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
 	pos.w = 284;
 	pos.w = 284;
 	pos.h = 220;
 	pos.h = 220;
 
 
+	MetaString loginAs;
+	loginAs.appendTextID("vcmi.lobby.login.as");
+	loginAs.replaceTextID(CSH->getGlobalLobby().getAccountDisplayName());
+
 	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
 	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
 	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title"));
 	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title"));
-	labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username"));
+	labelUsernameTitle = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username"));
+	labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString());
 	backgroundUsername = std::make_shared<TransparentFilledRectangle>(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
 	backgroundUsername = std::make_shared<TransparentFilledRectangle>(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
 	inputUsername = std::make_shared<CTextInput>(Rect(15, 93, 260, 16), FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true);
 	inputUsername = std::make_shared<CTextInput>(Rect(15, 93, 260, 16), FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true);
 	buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); });
 	buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); });
@@ -56,7 +61,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
 	toggleMode->setSelected(settings["lobby"]["roomType"].Integer());
 	toggleMode->setSelected(settings["lobby"]["roomType"].Integer());
 	toggleMode->addCallback([this](int index){onLoginModeChanged(index);});
 	toggleMode->addCallback([this](int index){onLoginModeChanged(index);});
 
 
-	if (settings["lobby"]["accountID"].String().empty())
+	if (CSH->getGlobalLobby().getAccountID().empty())
 	{
 	{
 		buttonLogin->block(true);
 		buttonLogin->block(true);
 		toggleMode->setSelected(0);
 		toggleMode->setSelected(0);
@@ -77,12 +82,19 @@ void GlobalLobbyLoginWindow::onLoginModeChanged(int value)
 {
 {
 	if (value == 0)
 	if (value == 0)
 	{
 	{
-		inputUsername->setText("");
+		inputUsername->enable();
+		backgroundUsername->enable();
+		labelUsernameTitle->enable();
+		labelUsername->disable();
 	}
 	}
 	else
 	else
 	{
 	{
-		inputUsername->setText(settings["lobby"]["displayName"].String());
+		inputUsername->disable();
+		backgroundUsername->disable();
+		labelUsernameTitle->disable();
+		labelUsername->enable();
 	}
 	}
+	redraw();
 }
 }
 
 
 void GlobalLobbyLoginWindow::onClose()
 void GlobalLobbyLoginWindow::onClose()
@@ -104,7 +116,7 @@ void GlobalLobbyLoginWindow::onLogin()
 
 
 void GlobalLobbyLoginWindow::onConnectionSuccess()
 void GlobalLobbyLoginWindow::onConnectionSuccess()
 {
 {
-	std::string accountID = settings["lobby"]["accountID"].String();
+	std::string accountID = CSH->getGlobalLobby().getAccountID();
 
 
 	if(toggleMode->getSelected() == 0)
 	if(toggleMode->getSelected() == 0)
 		CSH->getGlobalLobby().sendClientRegister(inputUsername->getText());
 		CSH->getGlobalLobby().sendClientRegister(inputUsername->getText());

+ 1 - 0
client/globalLobby/GlobalLobbyLoginWindow.h

@@ -23,6 +23,7 @@ class GlobalLobbyLoginWindow : public CWindowObject
 {
 {
 	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
 	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
 	std::shared_ptr<CLabel> labelTitle;
 	std::shared_ptr<CLabel> labelTitle;
+	std::shared_ptr<CLabel> labelUsernameTitle;
 	std::shared_ptr<CLabel> labelUsername;
 	std::shared_ptr<CLabel> labelUsername;
 	std::shared_ptr<CTextBox> labelStatus;
 	std::shared_ptr<CTextBox> labelStatus;
 	std::shared_ptr<TransparentFilledRectangle> backgroundUsername;
 	std::shared_ptr<TransparentFilledRectangle> backgroundUsername;

+ 1 - 1
client/globalLobby/GlobalLobbyWidget.cpp

@@ -272,7 +272,7 @@ GlobalLobbyMatchCard::GlobalLobbyMatchCard(GlobalLobbyWindow * window, const Glo
 
 
 	if (matchDescription.participants.size() == 2)
 	if (matchDescription.participants.size() == 2)
 	{
 	{
-		std::string ourAccountID = settings["lobby"]["accountID"].String();
+		std::string ourAccountID = CSH->getGlobalLobby().getAccountID();
 
 
 		opponentDescription.appendTextID("vcmi.lobby.match.duel");
 		opponentDescription.appendTextID("vcmi.lobby.match.duel");
 		// Find display name of our one and only opponent in this game
 		// Find display name of our one and only opponent in this game

+ 5 - 2
client/globalLobby/GlobalLobbyWindow.cpp

@@ -24,6 +24,7 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/Languages.h"
 #include "../../lib/Languages.h"
 #include "../../lib/MetaString.h"
 #include "../../lib/MetaString.h"
+#include "../../lib/TextOperations.h"
 
 
 GlobalLobbyWindow::GlobalLobbyWindow()
 GlobalLobbyWindow::GlobalLobbyWindow()
 	: CWindowObject(BORDERED)
 	: CWindowObject(BORDERED)
@@ -33,7 +34,7 @@ GlobalLobbyWindow::GlobalLobbyWindow()
 	pos = widget->pos;
 	pos = widget->pos;
 	center();
 	center();
 
 
-	widget->getAccountNameLabel()->setText(settings["lobby"]["displayName"].String());
+	widget->getAccountNameLabel()->setText(CSH->getGlobalLobby().getAccountDisplayName());
 	doOpenChannel("global", "english", Languages::getLanguageOptions("english").nameNative);
 	doOpenChannel("global", "english", Languages::getLanguageOptions("english").nameNative);
 
 
 	widget->getChannelListHeader()->setText(MetaString::createFromTextID("vcmi.lobby.header.channels").toString());
 	widget->getChannelListHeader()->setText(MetaString::createFromTextID("vcmi.lobby.header.channels").toString());
@@ -54,7 +55,7 @@ void GlobalLobbyWindow::doOpenChannel(const std::string & channelType, const std
 
 
 	auto history = CSH->getGlobalLobby().getChannelHistory(channelType, channelName);
 	auto history = CSH->getGlobalLobby().getChannelHistory(channelType, channelName);
 
 
-	for (auto const & entry : history)
+	for(const auto & entry : history)
 		onGameChatMessage(entry.displayName, entry.messageText, entry.timeFormatted, channelType, channelName);
 		onGameChatMessage(entry.displayName, entry.messageText, entry.timeFormatted, channelType, channelName);
 
 
 	MetaString text;
 	MetaString text;
@@ -80,6 +81,8 @@ void GlobalLobbyWindow::doSendChatMessage()
 	toSend["channelName"].String() = currentChannelName;
 	toSend["channelName"].String() = currentChannelName;
 	toSend["messageText"].String() = messageText;
 	toSend["messageText"].String() = messageText;
 
 
+	assert(TextOperations::isValidUnicodeString(messageText));
+
 	CSH->getGlobalLobby().sendMessage(toSend);
 	CSH->getGlobalLobby().sendMessage(toSend);
 
 
 	widget->getMessageInput()->setText("");
 	widget->getMessageInput()->setText("");

+ 1 - 1
client/gui/FramerateManager.cpp

@@ -1,5 +1,5 @@
 /*
 /*
- * FramerateManager.h, part of VCMI engine
+ * FramerateManager.cpp, part of VCMI engine
  *
  *
  * Authors: listed in file AUTHORS in main folder
  * Authors: listed in file AUTHORS in main folder
  *
  *

+ 1 - 0
client/ios/GameChatKeyboardHandler.h

@@ -7,6 +7,7 @@
  * Full text of license available in license.txt file, in main folder
  * Full text of license available in license.txt file, in main folder
  *
  *
  */
  */
+#pragma once
 
 
 #import <UIKit/UIKit.h>
 #import <UIKit/UIKit.h>
 
 

+ 1 - 1
client/ios/startSDL.mm

@@ -7,8 +7,8 @@
  * Full text of license available in license.txt file, in main folder
  * Full text of license available in license.txt file, in main folder
  *
  *
  */
  */
-#import "startSDL.h"
 #include "StdInc.h"
 #include "StdInc.h"
+#import "startSDL.h"
 #import "GameChatKeyboardHandler.h"
 #import "GameChatKeyboardHandler.h"
 
 
 #include "../Global.h"
 #include "../Global.h"

+ 1 - 1
client/render/Colors.h

@@ -1,5 +1,5 @@
 /*
 /*
- * ICursor.h, part of VCMI engine
+ * Colors.h, part of VCMI engine
  *
  *
  * Authors: listed in file AUTHORS in main folder
  * Authors: listed in file AUTHORS in main folder
  *
  *

+ 3 - 3
client/widgets/CExchangeController.h

@@ -7,10 +7,10 @@
  * Full text of license available in license.txt file, in main folder
  * Full text of license available in license.txt file, in main folder
  *
  *
  */
  */
- #pragma once
+#pragma once
  
  
- #include "../windows/CWindowObject.h"
- #include "CWindowWithArtifacts.h"
+#include "../windows/CWindowObject.h"
+#include "CWindowWithArtifacts.h"
  
  
 class CCallback;
 class CCallback;
 
 

+ 4 - 40
config/schemas/settings.json

@@ -501,11 +501,7 @@
 				"enableInstalledMods", 
 				"enableInstalledMods", 
 				"autoCheckRepositories", 
 				"autoCheckRepositories", 
 				"updateOnStartup", 
 				"updateOnStartup", 
-				"updateConfigUrl", 
-				"lobbyUrl", 
-				"lobbyPort", 
-				"lobbyUsername", 
-				"connectionTimeout"
+				"updateConfigUrl"
 			],
 			],
 			"properties" : {
 			"properties" : {
 				"defaultRepositoryEnabled" : {
 				"defaultRepositoryEnabled" : {
@@ -543,26 +539,6 @@
 				"updateConfigUrl" : {
 				"updateConfigUrl" : {
 					"type" : "string",
 					"type" : "string",
 					"default" : "https://raw.githubusercontent.com/vcmi/vcmi-updates/master/vcmi-updates.json"
 					"default" : "https://raw.githubusercontent.com/vcmi/vcmi-updates/master/vcmi-updates.json"
-				},
-				"lobbyUrl" : {
-					"type" : "string",
-					"description" : "ip address or web link to remote proxy server",
-					"default" : "beholder.vcmi.eu"
-				},
-				"lobbyPort" : {
-					"type" : "number",
-					"description" : "connection port for remote proxy server",
-					"default" : 5002
-				},
-				"lobbyUsername" : {
-					"type" : "string",
-					"description" : "username for the client on the remote proxy server",
-					"default" : ""
-				},
-				"connectionTimeout" : {
-					"type" : "number",
-					"description" : "maximum time in ms, should be enough to establish socket connection to remote proxy server.",
-					"default" : 2000
 				}
 				}
 			}
 			}
 		},
 		},
@@ -570,31 +546,19 @@
 			"type" : "object",
 			"type" : "object",
 			"additionalProperties" : false,
 			"additionalProperties" : false,
 			"default" : {},
 			"default" : {},
-			"required" : [ "mapPreview", "accountID", "accountCookie", "displayName", "hostname", "port", "roomPlayerLimit", "roomType", "roomMode" ],
+			"required" : [ "mapPreview", "hostname", "port", "roomPlayerLimit", "roomType", "roomMode" ],
 			"properties" : {
 			"properties" : {
 				"mapPreview" : {
 				"mapPreview" : {
 					"type" : "boolean",
 					"type" : "boolean",
 					"default" : true
 					"default" : true
 				},
 				},
-				"accountID" : {
-					"type" : "string",
-					"default" : ""
-				},
-				"accountCookie" : {
-					"type" : "string",
-					"default" : ""
-				},
-				"displayName" : {
-					"type" : "string",
-					"default" : ""
-				},
 				"hostname" : {
 				"hostname" : {
 					"type" : "string",
 					"type" : "string",
-					"default" : "127.0.0.1"
+					"default" : "beholder.vcmi.eu"
 				},
 				},
 				"port" : {
 				"port" : {
 					"type" : "number",
 					"type" : "number",
-					"default" : 30303
+					"default" : 3031
 				},
 				},
 				"roomPlayerLimit" : {
 				"roomPlayerLimit" : {
 					"type" : "number",
 					"type" : "number",

+ 1 - 7
debian/control

@@ -2,7 +2,7 @@ Source: vcmi
 Section: games
 Section: games
 Priority: optional
 Priority: optional
 Maintainer: Ivan Savenko <[email protected]>
 Maintainer: Ivan Savenko <[email protected]>
-Build-Depends: debhelper (>= 8), cmake, libsdl2-dev, libsdl2-image-dev, libsdl2-ttf-dev, libsdl2-mixer-dev, zlib1g-dev, libavformat-dev, libswscale-dev, libboost-dev (>=1.48), libboost-program-options-dev (>=1.48), libboost-filesystem-dev (>=1.48), libboost-system-dev (>=1.48), libboost-locale-dev (>=1.48), libboost-thread-dev (>=1.48), qtbase5-dev, libtbb2-dev, libfuzzylite-dev, libminizip-dev, libluajit-5.1-dev, qttools5-dev
+Build-Depends: debhelper (>= 8), cmake, libsdl2-dev, libsdl2-image-dev, libsdl2-ttf-dev, libsdl2-mixer-dev, zlib1g-dev, libavformat-dev, libswscale-dev, libboost-dev (>=1.48), libboost-program-options-dev (>=1.48), libboost-filesystem-dev (>=1.48), libboost-system-dev (>=1.48), libboost-locale-dev (>=1.48), libboost-thread-dev (>=1.48), qtbase5-dev, libtbb-dev, libfuzzylite-dev, libminizip-dev, libluajit-5.1-dev, qttools5-dev
 Standards-Version: 3.9.1
 Standards-Version: 3.9.1
 Homepage: http://vcmi.eu
 Homepage: http://vcmi.eu
 Vcs-Git: git://github.com/vcmi/vcmi.git
 Vcs-Git: git://github.com/vcmi/vcmi.git
@@ -19,9 +19,3 @@ Description: Rewrite of the Heroes of Might and Magic 3 engine
  .
  .
  In its current state it already supports maps of any sizes, higher
  In its current state it already supports maps of any sizes, higher
  resolutions and extended engine limits.
  resolutions and extended engine limits.
-
-Package: vcmi-dbg
-Architecture: any
-Section: debug
-Depends: vcmi (= ${binary:Version}),    ${misc:Depends} 
-Description: Debug symbols for vcmi package

+ 1 - 3
debian/rules

@@ -8,14 +8,12 @@ override_dh_auto_configure:
 	dh_auto_configure -- \
 	dh_auto_configure -- \
 		-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON \
 		-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON \
 		-DCMAKE_INSTALL_RPATH=/usr/lib/$(DEB_HOST_MULTIARCH)/vcmi \
 		-DCMAKE_INSTALL_RPATH=/usr/lib/$(DEB_HOST_MULTIARCH)/vcmi \
-		-DCMAKE_BUILD_TYPE=RelWithDebInfo \
+		-DCMAKE_BUILD_TYPE=Release \
 		-DENABLE_GITVERSION=OFF \
 		-DENABLE_GITVERSION=OFF \
 		-DBIN_DIR=games \
 		-DBIN_DIR=games \
 		-DFORCE_BUNDLED_FL=OFF \
 		-DFORCE_BUNDLED_FL=OFF \
 		-DENABLE_TEST=0
 		-DENABLE_TEST=0
 
 
-override_dh_strip:
-	dh_strip --dbg-package=vcmi-dbg
 override_dh_auto_install:
 override_dh_auto_install:
 	dh_auto_install --destdir=debian/vcmi
 	dh_auto_install --destdir=debian/vcmi
 override_dh_installdocs:
 override_dh_installdocs:

+ 9 - 0
launcher/StdInc.cpp

@@ -1 +1,10 @@
+/*
+ * StdInc.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #include "StdInc.h"
 #include "StdInc.h"

+ 9 - 0
launcher/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #pragma once
 #pragma once
 
 
 #include "../Global.h"
 #include "../Global.h"

+ 0 - 8
launcher/modManager/cmodlist.h

@@ -9,18 +9,10 @@
  */
  */
 #pragma once
 #pragma once
 
 
-#include "StdInc.h"
-
 #include <QVariantMap>
 #include <QVariantMap>
 #include <QVariant>
 #include <QVariant>
 #include <QVector>
 #include <QVector>
 
 
-VCMI_LIB_NAMESPACE_BEGIN
-
-class JsonNode;
-
-VCMI_LIB_NAMESPACE_END
-
 namespace ModStatus
 namespace ModStatus
 {
 {
 enum EModStatus
 enum EModStatus

+ 1 - 4
launcher/modManager/imageviewer_moc.h

@@ -7,8 +7,7 @@
  * Full text of license available in license.txt file, in main folder
  * Full text of license available in license.txt file, in main folder
  *
  *
  */
  */
-#ifndef IMAGEVIEWER_H
-#define IMAGEVIEWER_H
+#pragma once
 
 
 #include <QDialog>
 #include <QDialog>
 
 
@@ -38,5 +37,3 @@ protected:
 private:
 private:
 	Ui::ImageViewer * ui;
 	Ui::ImageViewer * ui;
 };
 };
-
-#endif // IMAGEVIEWER_H

+ 98 - 80
lib/CMakeLists.txt

@@ -1,6 +1,47 @@
 set(lib_SRCS
 set(lib_SRCS
 	StdInc.cpp
 	StdInc.cpp
 
 
+	filesystem/AdapterLoaders.cpp
+	filesystem/CArchiveLoader.cpp
+	filesystem/CBinaryReader.cpp
+	filesystem/CCompressedStream.cpp
+	filesystem/CFileInputStream.cpp
+	filesystem/CFilesystemLoader.cpp
+	filesystem/CMemoryBuffer.cpp
+	filesystem/CMemoryStream.cpp
+	filesystem/CZipLoader.cpp
+	filesystem/CZipSaver.cpp
+	filesystem/FileInfo.cpp
+	filesystem/Filesystem.cpp
+	filesystem/MinizipExtensions.cpp
+	filesystem/ResourcePath.cpp
+
+	json/JsonNode.cpp
+	json/JsonParser.cpp
+	json/JsonUtils.cpp
+	json/JsonValidator.cpp
+	json/JsonWriter.cpp
+
+	logging/CBasicLogConfigurator.cpp
+	logging/CLogger.cpp
+	logging/VisualLogger.cpp
+
+	network/NetworkConnection.cpp
+	network/NetworkHandler.cpp
+	network/NetworkServer.cpp
+
+	vstd/DateUtils.cpp
+	vstd/StringUtils.cpp
+
+	CConfigHandler.cpp
+	CConsoleHandler.cpp
+	CThreadHelper.cpp
+	TextOperations.cpp
+	VCMIDirs.cpp
+)
+
+set(lib_MAIN_SRCS
+
 	battle/AccessibilityInfo.cpp
 	battle/AccessibilityInfo.cpp
 	battle/BattleAction.cpp
 	battle/BattleAction.cpp
 	battle/BattleAttackInfo.cpp
 	battle/BattleAttackInfo.cpp
@@ -46,38 +87,14 @@ set(lib_SRCS
 	events/PlayerGotTurn.cpp
 	events/PlayerGotTurn.cpp
 	events/TurnStarted.cpp
 	events/TurnStarted.cpp
 
 
-	filesystem/AdapterLoaders.cpp
-	filesystem/CArchiveLoader.cpp
-	filesystem/CBinaryReader.cpp
-	filesystem/CCompressedStream.cpp
-	filesystem/CFileInputStream.cpp
-	filesystem/CFilesystemLoader.cpp
-	filesystem/CMemoryBuffer.cpp
-	filesystem/CMemoryStream.cpp
-	filesystem/CZipLoader.cpp
-	filesystem/CZipSaver.cpp
-	filesystem/FileInfo.cpp
-	filesystem/Filesystem.cpp
-	filesystem/MinizipExtensions.cpp
-	filesystem/ResourcePath.cpp
-
 	json/JsonBonus.cpp
 	json/JsonBonus.cpp
-	json/JsonNode.cpp
-	json/JsonParser.cpp
 	json/JsonRandom.cpp
 	json/JsonRandom.cpp
-	json/JsonUtils.cpp
-	json/JsonValidator.cpp
-	json/JsonWriter.cpp
 
 
 	gameState/CGameState.cpp
 	gameState/CGameState.cpp
 	gameState/CGameStateCampaign.cpp
 	gameState/CGameStateCampaign.cpp
 	gameState/InfoAboutArmy.cpp
 	gameState/InfoAboutArmy.cpp
 	gameState/TavernHeroesPool.cpp
 	gameState/TavernHeroesPool.cpp
 
 
-	logging/CBasicLogConfigurator.cpp
-	logging/CLogger.cpp
-	logging/VisualLogger.cpp
-
 	mapObjectConstructors/AObjectTypeHandler.cpp
 	mapObjectConstructors/AObjectTypeHandler.cpp
 	mapObjectConstructors/CBankInstanceConstructor.cpp
 	mapObjectConstructors/CBankInstanceConstructor.cpp
 	mapObjectConstructors/CObjectClassesHandler.cpp
 	mapObjectConstructors/CObjectClassesHandler.cpp
@@ -128,10 +145,6 @@ set(lib_SRCS
 	modding/IdentifierStorage.cpp
 	modding/IdentifierStorage.cpp
 	modding/ModUtility.cpp
 	modding/ModUtility.cpp
 
 
-	network/NetworkConnection.cpp
-	network/NetworkHandler.cpp
-	network/NetworkServer.cpp
-
 	networkPacks/NetPacksLib.cpp
 	networkPacks/NetPacksLib.cpp
 
 
 	pathfinder/CGPathNode.cpp
 	pathfinder/CGPathNode.cpp
@@ -225,9 +238,6 @@ set(lib_SRCS
 	spells/effects/RemoveObstacle.cpp
 	spells/effects/RemoveObstacle.cpp
 	spells/effects/Sacrifice.cpp
 	spells/effects/Sacrifice.cpp
 
 
-	vstd/DateUtils.cpp
-	vstd/StringUtils.cpp
-
 	ArtifactUtils.cpp
 	ArtifactUtils.cpp
 	BasicTypes.cpp
 	BasicTypes.cpp
 	BattleFieldHandler.cpp
 	BattleFieldHandler.cpp
@@ -236,8 +246,6 @@ set(lib_SRCS
 	CArtifactInstance.cpp
 	CArtifactInstance.cpp
 	CBonusTypeHandler.cpp
 	CBonusTypeHandler.cpp
 	CBuildingHandler.cpp
 	CBuildingHandler.cpp
-	CConfigHandler.cpp
-	CConsoleHandler.cpp
 	CCreatureHandler.cpp
 	CCreatureHandler.cpp
 	CCreatureSet.cpp
 	CCreatureSet.cpp
 	CGameInfoCallback.cpp
 	CGameInfoCallback.cpp
@@ -249,7 +257,6 @@ set(lib_SRCS
 	CScriptingModule.cpp
 	CScriptingModule.cpp
 	CSkillHandler.cpp
 	CSkillHandler.cpp
 	CStack.cpp
 	CStack.cpp
-	CThreadHelper.cpp
 	CTownHandler.cpp
 	CTownHandler.cpp
 	GameSettings.cpp
 	GameSettings.cpp
 	IGameCallback.cpp
 	IGameCallback.cpp
@@ -264,12 +271,14 @@ set(lib_SRCS
 	RoadHandler.cpp
 	RoadHandler.cpp
 	ScriptHandler.cpp
 	ScriptHandler.cpp
 	TerrainHandler.cpp
 	TerrainHandler.cpp
-	TextOperations.cpp
 	TurnTimerInfo.cpp
 	TurnTimerInfo.cpp
-	VCMIDirs.cpp
 	VCMI_Lib.cpp
 	VCMI_Lib.cpp
 )
 )
 
 
+if (NOT ENABLE_MINIMAL_LIB)
+	list(APPEND lib_SRCS ${lib_MAIN_SRCS})
+endif()
+
 # Version.cpp is a generated file
 # Version.cpp is a generated file
 if(ENABLE_GITVERSION)
 if(ENABLE_GITVERSION)
 	list(APPEND lib_SRCS ${CMAKE_BINARY_DIR}/Version.cpp)
 	list(APPEND lib_SRCS ${CMAKE_BINARY_DIR}/Version.cpp)
@@ -280,14 +289,59 @@ endif()
 
 
 set(lib_HEADERS
 set(lib_HEADERS
 	../include/vstd/CLoggerBase.h
 	../include/vstd/CLoggerBase.h
+	../include/vstd/DateUtils.h
+	../include/vstd/StringUtils.h
 	../Global.h
 	../Global.h
 	../AUTHORS.h
 	../AUTHORS.h
 	StdInc.h
 	StdInc.h
 
 
+	filesystem/AdapterLoaders.h
+	filesystem/CArchiveLoader.h
+	filesystem/CBinaryReader.h
+	filesystem/CCompressedStream.h
+	filesystem/CFileInputStream.h
+	filesystem/CFilesystemLoader.h
+	filesystem/CInputOutputStream.h
+	filesystem/CInputStream.h
+	filesystem/CMemoryBuffer.h
+	filesystem/CMemoryStream.h
+	filesystem/COutputStream.h
+	filesystem/CStream.h
+	filesystem/CZipLoader.h
+	filesystem/CZipSaver.h
+	filesystem/FileInfo.h
+	filesystem/Filesystem.h
+	filesystem/ISimpleResourceLoader.h
+	filesystem/MinizipExtensions.h
+	filesystem/ResourcePath.h
+
+	json/JsonFormatException.h
+	json/JsonNode.h
+	json/JsonParser.h
+	json/JsonUtils.h
+	json/JsonValidator.h
+	json/JsonWriter.h
+
+	logging/CBasicLogConfigurator.h
+	logging/CLogger.h
+	logging/VisualLogger.h
+
+	network/NetworkConnection.h
+	network/NetworkDefines.h
+	network/NetworkHandler.h
+	network/NetworkInterface.h
+	network/NetworkServer.h
+
+	CConfigHandler.h
+	CConsoleHandler.h
+	CThreadHelper.h
+	TextOperations.h
+	VCMIDirs.h
+)
+
+set(lib_MAIN_HEADERS
 	../include/vstd/ContainerUtils.h
 	../include/vstd/ContainerUtils.h
 	../include/vstd/RNG.h
 	../include/vstd/RNG.h
-	../include/vstd/DateUtils.h
-	../include/vstd/StringUtils.h
 
 
 	../include/vcmi/events/AdventureEvents.h
 	../include/vcmi/events/AdventureEvents.h
 	../include/vcmi/events/ApplyDamage.h
 	../include/vcmi/events/ApplyDamage.h
@@ -385,34 +439,8 @@ set(lib_HEADERS
 	events/PlayerGotTurn.h
 	events/PlayerGotTurn.h
 	events/TurnStarted.h
 	events/TurnStarted.h
 
 
-	filesystem/AdapterLoaders.h
-	filesystem/CArchiveLoader.h
-	filesystem/CBinaryReader.h
-	filesystem/CCompressedStream.h
-	filesystem/CFileInputStream.h
-	filesystem/CFilesystemLoader.h
-	filesystem/CInputOutputStream.h
-	filesystem/CInputStream.h
-	filesystem/CMemoryBuffer.h
-	filesystem/CMemoryStream.h
-	filesystem/COutputStream.h
-	filesystem/CStream.h
-	filesystem/CZipLoader.h
-	filesystem/CZipSaver.h
-	filesystem/FileInfo.h
-	filesystem/Filesystem.h
-	filesystem/ISimpleResourceLoader.h
-	filesystem/MinizipExtensions.h
-	filesystem/ResourcePath.h
-
 	json/JsonBonus.h
 	json/JsonBonus.h
-	json/JsonFormatException.h
-	json/JsonNode.h
-	json/JsonParser.h
 	json/JsonRandom.h
 	json/JsonRandom.h
-	json/JsonUtils.h
-	json/JsonValidator.h
-	json/JsonWriter.h
 
 
 	gameState/CGameState.h
 	gameState/CGameState.h
 	gameState/CGameStateCampaign.h
 	gameState/CGameStateCampaign.h
@@ -423,10 +451,6 @@ set(lib_HEADERS
 	gameState/TavernSlot.h
 	gameState/TavernSlot.h
 	gameState/QuestInfo.h
 	gameState/QuestInfo.h
 
 
-	logging/CBasicLogConfigurator.h
-	logging/CLogger.h
-	logging/VisualLogger.h
-
 	mapObjectConstructors/AObjectTypeHandler.h
 	mapObjectConstructors/AObjectTypeHandler.h
 	mapObjectConstructors/CBankInstanceConstructor.h
 	mapObjectConstructors/CBankInstanceConstructor.h
 	mapObjectConstructors/CDefaultObjectTypeHandler.h
 	mapObjectConstructors/CDefaultObjectTypeHandler.h
@@ -487,12 +511,6 @@ set(lib_HEADERS
 	modding/ModUtility.h
 	modding/ModUtility.h
 	modding/ModVerificationInfo.h
 	modding/ModVerificationInfo.h
 
 
-	network/NetworkConnection.h
-	network/NetworkDefines.h
-	network/NetworkHandler.h
-	network/NetworkInterface.h
-	network/NetworkServer.h
-
 	networkPacks/ArtifactLocation.h
 	networkPacks/ArtifactLocation.h
 	networkPacks/BattleChanges.h
 	networkPacks/BattleChanges.h
 	networkPacks/Component.h
 	networkPacks/Component.h
@@ -622,8 +640,6 @@ set(lib_HEADERS
 	CArtifactInstance.h
 	CArtifactInstance.h
 	CBonusTypeHandler.h
 	CBonusTypeHandler.h
 	CBuildingHandler.h
 	CBuildingHandler.h
-	CConfigHandler.h
-	CConsoleHandler.h
 	CCreatureHandler.h
 	CCreatureHandler.h
 	CCreatureSet.h
 	CCreatureSet.h
 	CGameInfoCallback.h
 	CGameInfoCallback.h
@@ -640,7 +656,6 @@ set(lib_HEADERS
 	CSoundBase.h
 	CSoundBase.h
 	CStack.h
 	CStack.h
 	CStopWatch.h
 	CStopWatch.h
-	CThreadHelper.h
 	CTownHandler.h
 	CTownHandler.h
 	ExtraOptionsInfo.h
 	ExtraOptionsInfo.h
 	FunctionList.h
 	FunctionList.h
@@ -667,14 +682,16 @@ set(lib_HEADERS
 	ScopeGuard.h
 	ScopeGuard.h
 	StartInfo.h
 	StartInfo.h
 	TerrainHandler.h
 	TerrainHandler.h
-	TextOperations.h
 	TurnTimerInfo.h
 	TurnTimerInfo.h
 	UnlockGuard.h
 	UnlockGuard.h
-	VCMIDirs.h
 	vcmi_endian.h
 	vcmi_endian.h
 	VCMI_Lib.h
 	VCMI_Lib.h
 )
 )
 
 
+if (NOT ENABLE_MINIMAL_LIB)
+	list(APPEND lib_HEADERS ${lib_MAIN_HEADERS})
+endif()
+
 assign_source_group(${lib_SRCS} ${lib_HEADERS})
 assign_source_group(${lib_SRCS} ${lib_HEADERS})
 
 
 if(ENABLE_STATIC_LIBS)
 if(ENABLE_STATIC_LIBS)
@@ -682,13 +699,14 @@ if(ENABLE_STATIC_LIBS)
 else()
 else()
 	add_library(vcmi SHARED ${lib_SRCS} ${lib_HEADERS})
 	add_library(vcmi SHARED ${lib_SRCS} ${lib_HEADERS})
 endif()
 endif()
+
 set_target_properties(vcmi PROPERTIES COMPILE_DEFINITIONS "VCMI_DLL=1")
 set_target_properties(vcmi PROPERTIES COMPILE_DEFINITIONS "VCMI_DLL=1")
 target_link_libraries(vcmi PUBLIC
 target_link_libraries(vcmi PUBLIC
 	minizip::minizip ZLIB::ZLIB
 	minizip::minizip ZLIB::ZLIB
 	${SYSTEM_LIBS} Boost::boost Boost::thread Boost::filesystem Boost::program_options Boost::locale Boost::date_time
 	${SYSTEM_LIBS} Boost::boost Boost::thread Boost::filesystem Boost::program_options Boost::locale Boost::date_time
 )
 )
 
 
-if(ENABLE_STATIC_LIBS)
+if(ENABLE_STATIC_LIBS AND ENABLE_CLIENT)
 	target_compile_definitions(vcmi PRIVATE STATIC_AI)
 	target_compile_definitions(vcmi PRIVATE STATIC_AI)
 	target_link_libraries(vcmi PRIVATE
 	target_link_libraries(vcmi PRIVATE
 		BattleAI
 		BattleAI

+ 9 - 0
lib/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 // Creates the precompiled header
 // Creates the precompiled header
 #include "StdInc.h"
 #include "StdInc.h"

+ 9 - 0
lib/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #pragma once
 #pragma once
 
 
 #include "../Global.h"
 #include "../Global.h"

+ 3 - 2
lib/VCMIDirs.cpp

@@ -367,8 +367,9 @@ class IVCMIDirsUNIX : public IVCMIDirs
 bool IVCMIDirsUNIX::developmentMode() const
 bool IVCMIDirsUNIX::developmentMode() const
 {
 {
 	// We want to be able to run VCMI from single directory. E.g to run from build output directory
 	// We want to be able to run VCMI from single directory. E.g to run from build output directory
-	const bool result = bfs::exists("AI") && bfs::exists("config") && bfs::exists("Mods") && bfs::exists("vcmiclient");
-	return result;
+	const bool hasConfigs = bfs::exists("config") && bfs::exists("Mods");
+	const bool hasBinaries = bfs::exists("vcmiclient") || bfs::exists("vcmiserver") || bfs::exists("vcmilobby");
+	return hasConfigs && hasBinaries;
 }
 }
 
 
 bfs::path IVCMIDirsUNIX::clientPath() const { return binaryPath() / "vcmiclient"; }
 bfs::path IVCMIDirsUNIX::clientPath() const { return binaryPath() / "vcmiclient"; }

+ 2 - 1
lib/battle/PossiblePlayerBattleAction.h

@@ -1,5 +1,5 @@
 /*
 /*
- * CBattleInfoCallback.h, part of VCMI engine
+ * PossiblePlayerBattleAction.h, part of VCMI engine
  *
  *
  * Authors: listed in file AUTHORS in main folder
  * Authors: listed in file AUTHORS in main folder
  *
  *
@@ -7,6 +7,7 @@
  * Full text of license available in license.txt file, in main folder
  * Full text of license available in license.txt file, in main folder
  *
  *
  */
  */
+#pragma once
 
 
 #include "../GameConstants.h"
 #include "../GameConstants.h"
 
 

+ 0 - 1
lib/bonuses/IBonusBearer.h

@@ -1,4 +1,3 @@
-
 /*
 /*
  * IBonusBearer.h, part of VCMI engine
  * IBonusBearer.h, part of VCMI engine
  *
  *

+ 2 - 0
lib/json/JsonValidator.cpp

@@ -422,6 +422,7 @@ static std::string additionalPropertiesCheck(JsonValidator & validator, const Js
 
 
 static bool testFilePresence(const std::string & scope, const ResourcePath & resource)
 static bool testFilePresence(const std::string & scope, const ResourcePath & resource)
 {
 {
+#ifndef ENABLE_MINIMAL_LIB
 	std::set<std::string> allowedScopes;
 	std::set<std::string> allowedScopes;
 	if(scope != ModScope::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies
 	if(scope != ModScope::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies
 	{
 	{
@@ -441,6 +442,7 @@ static bool testFilePresence(const std::string & scope, const ResourcePath & res
 		if (CResourceHandler::get(entry)->existsResource(resource))
 		if (CResourceHandler::get(entry)->existsResource(resource))
 			return true;
 			return true;
 	}
 	}
+#endif
 	return false;
 	return false;
 }
 }
 
 

+ 1 - 1
lib/modding/CModVersion.cpp

@@ -1,5 +1,5 @@
 /*
 /*
- * CModVersion.h, part of VCMI engine
+ * CModVersion.cpp, part of VCMI engine
  *
  *
  * Authors: listed in file AUTHORS in main folder
  * Authors: listed in file AUTHORS in main folder
  *
  *

+ 37 - 34
lib/pathfinder/CPathfinder.cpp

@@ -49,30 +49,27 @@ bool CPathfinderHelper::canMoveFromNode(const PathNodeInfo & source) const
 	return true;
 	return true;
 }
 }
 
 
-std::vector<int3> CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & source) const
+void CPathfinderHelper::calculateNeighbourTiles(std::vector<int3> & result, const PathNodeInfo & source) const
 {
 {
-	std::vector<int3> neighbourTiles;
+	result.clear();
 
 
 	if (!canMoveFromNode(source))
 	if (!canMoveFromNode(source))
-		return neighbourTiles;
+		return;
 
 
-	neighbourTiles.reserve(8);
 	getNeighbours(
 	getNeighbours(
 		*source.tile,
 		*source.tile,
 		source.node->coord,
 		source.node->coord,
-		neighbourTiles,
+		result,
 		boost::logic::indeterminate,
 		boost::logic::indeterminate,
 		source.node->layer == EPathfindingLayer::SAIL);
 		source.node->layer == EPathfindingLayer::SAIL);
 
 
 	if(source.isNodeObjectVisitable())
 	if(source.isNodeObjectVisitable())
 	{
 	{
-		vstd::erase_if(neighbourTiles, [&](const int3 & tile) -> bool
+		vstd::erase_if(result, [&](const int3 & tile) -> bool
 		{
 		{
 			return !canMoveBetween(tile, source.nodeObject->visitablePos());
 			return !canMoveBetween(tile, source.nodeObject->visitablePos());
 		});
 		});
 	}
 	}
-
-	return neighbourTiles;
 }
 }
 
 
 CPathfinder::CPathfinder(CGameState * _gs, std::shared_ptr<PathfinderConfig> config): 
 CPathfinder::CPathfinder(CGameState * _gs, std::shared_ptr<PathfinderConfig> config): 
@@ -129,6 +126,8 @@ void CPathfinder::calculatePaths()
 		pq.push(initialNode);
 		pq.push(initialNode);
 	}
 	}
 
 
+	std::vector<CGPathNode *> neighbourNodes;
+
 	while(!pq.empty())
 	while(!pq.empty())
 	{
 	{
 		counter++;
 		counter++;
@@ -158,43 +157,47 @@ void CPathfinder::calculatePaths()
 		source.updateInfo(hlp, gamestate);
 		source.updateInfo(hlp, gamestate);
 
 
 		//add accessible neighbouring nodes to the queue
 		//add accessible neighbouring nodes to the queue
-		auto neighbourNodes = config->nodeStorage->calculateNeighbours(source, config.get(), hlp);
-		for(CGPathNode * neighbour : neighbourNodes)
+		for(EPathfindingLayer layer = EPathfindingLayer::LAND; layer < EPathfindingLayer::NUM_LAYERS; layer.advance(1))
 		{
 		{
-			if(neighbour->locked)
+			if(!hlp->isLayerAvailable(layer))
 				continue;
 				continue;
 
 
-			if(!hlp->isLayerAvailable(neighbour->layer))
-				continue;
+			config->nodeStorage->calculateNeighbours(neighbourNodes, source, layer, config.get(), hlp);
 
 
-			destination.setNode(gamestate, neighbour);
-			hlp = config->getOrCreatePathfinderHelper(destination, gamestate);
+			for(CGPathNode * neighbour : neighbourNodes)
+			{
+				if(neighbour->locked)
+					continue;
 
 
-			if(!hlp->isPatrolMovementAllowed(neighbour->coord))
-				continue;
+				destination.setNode(gamestate, neighbour);
+				hlp = config->getOrCreatePathfinderHelper(destination, gamestate);
 
 
-			/// Check transition without tile accessability rules
-			if(source.node->layer != neighbour->layer && !isLayerTransitionPossible())
-				continue;
+				if(!hlp->isPatrolMovementAllowed(neighbour->coord))
+					continue;
 
 
-			destination.turn = turn;
-			destination.movementLeft = movement;
-			destination.cost = cost;
-			destination.updateInfo(hlp, gamestate);
-			destination.isGuardianTile = destination.guarded && isDestinationGuardian();
+				/// Check transition without tile accessability rules
+				if(source.node->layer != neighbour->layer && !isLayerTransitionPossible())
+					continue;
 
 
-			for(const auto & rule : config->rules)
-			{
-				rule->process(source, destination, config.get(), hlp);
+				destination.turn = turn;
+				destination.movementLeft = movement;
+				destination.cost = cost;
+				destination.updateInfo(hlp, gamestate);
+				destination.isGuardianTile = destination.guarded && isDestinationGuardian();
 
 
-				if(destination.blocked)
-					break;
-			}
+				for(const auto & rule : config->rules)
+				{
+					rule->process(source, destination, config.get(), hlp);
 
 
-			if(!destination.blocked)
-				push(destination.node);
+					if(destination.blocked)
+						break;
+				}
 
 
-		} //neighbours loop
+				if(!destination.blocked)
+					push(destination.node);
+
+			} //neighbours loop
+		}
 
 
 		//just add all passable teleport exits
 		//just add all passable teleport exits
 		hlp = config->getOrCreatePathfinderHelper(source, gamestate);
 		hlp = config->getOrCreatePathfinderHelper(source, gamestate);

+ 1 - 1
lib/pathfinder/CPathfinder.h

@@ -96,7 +96,7 @@ public:
 	bool addTeleportWhirlpool(const CGWhirlpool * obj) const;
 	bool addTeleportWhirlpool(const CGWhirlpool * obj) const;
 	bool canMoveBetween(const int3 & a, const int3 & b) const; //checks only for visitable objects that may make moving between tiles impossible, not other conditions (like tiles itself accessibility)
 	bool canMoveBetween(const int3 & a, const int3 & b) const; //checks only for visitable objects that may make moving between tiles impossible, not other conditions (like tiles itself accessibility)
 
 
-	std::vector<int3> getNeighbourTiles(const PathNodeInfo & source) const;
+	void calculateNeighbourTiles(std::vector<int3> & result, const PathNodeInfo & source) const;
 	std::vector<int3> getTeleportExits(const PathNodeInfo & source) const;
 	std::vector<int3> getTeleportExits(const PathNodeInfo & source) const;
 
 
 	void getNeighbours(
 	void getNeighbours(

+ 3 - 1
lib/pathfinder/INodeStorage.h

@@ -31,8 +31,10 @@ public:
 
 
 	virtual std::vector<CGPathNode *> getInitialNodes() = 0;
 	virtual std::vector<CGPathNode *> getInitialNodes() = 0;
 
 
-	virtual std::vector<CGPathNode *> calculateNeighbours(
+	virtual void calculateNeighbours(
+		std::vector<CGPathNode *> & result,
 		const PathNodeInfo & source,
 		const PathNodeInfo & source,
+		EPathfindingLayer layer,
 		const PathfinderConfig * pathfinderConfig,
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) = 0;
 		const CPathfinderHelper * pathfinderHelper) = 0;
 
 

+ 13 - 13
lib/pathfinder/NodeStorage.cpp

@@ -60,29 +60,29 @@ void NodeStorage::initialize(const PathfinderOptions & options, const CGameState
 	}
 	}
 }
 }
 
 
-std::vector<CGPathNode *> NodeStorage::calculateNeighbours(
+void NodeStorage::calculateNeighbours(
+	std::vector<CGPathNode *> & result,
 	const PathNodeInfo & source,
 	const PathNodeInfo & source,
+	EPathfindingLayer layer,
 	const PathfinderConfig * pathfinderConfig,
 	const PathfinderConfig * pathfinderConfig,
 	const CPathfinderHelper * pathfinderHelper)
 	const CPathfinderHelper * pathfinderHelper)
 {
 {
-	std::vector<CGPathNode *> neighbours;
-	neighbours.reserve(16);
-	auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
+	std::vector<int3> accessibleNeighbourTiles;
+	
+	result.clear();
+	accessibleNeighbourTiles.reserve(8);
+	
+	pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);
 
 
 	for(auto & neighbour : accessibleNeighbourTiles)
 	for(auto & neighbour : accessibleNeighbourTiles)
 	{
 	{
-		for(EPathfindingLayer i = EPathfindingLayer::LAND; i < EPathfindingLayer::NUM_LAYERS; i.advance(1))
-		{
-			auto * node = getNode(neighbour, i);
+		auto * node = getNode(neighbour, layer);
 
 
-			if(node->accessible == EPathAccessibility::NOT_SET)
-				continue;
+		if(node->accessible == EPathAccessibility::NOT_SET)
+			continue;
 
 
-			neighbours.push_back(node);
-		}
+		result.push_back(node);
 	}
 	}
-
-	return neighbours;
 }
 }
 
 
 std::vector<CGPathNode *> NodeStorage::calculateTeleportations(
 std::vector<CGPathNode *> NodeStorage::calculateTeleportations(

+ 3 - 1
lib/pathfinder/NodeStorage.h

@@ -36,8 +36,10 @@ public:
 
 
 	std::vector<CGPathNode *> getInitialNodes() override;
 	std::vector<CGPathNode *> getInitialNodes() override;
 
 
-	virtual std::vector<CGPathNode *> calculateNeighbours(
+	virtual void calculateNeighbours(
+		std::vector<CGPathNode *> & result,
 		const PathNodeInfo & source,
 		const PathNodeInfo & source,
+		EPathfindingLayer layer,
 		const PathfinderConfig * pathfinderConfig,
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) override;
 		const CPathfinderHelper * pathfinderHelper) override;
 
 

+ 1 - 0
lib/spells/ObstacleCasterProxy.h

@@ -7,6 +7,7 @@
  * Full text of license available in license.txt file, in main folder
  * Full text of license available in license.txt file, in main folder
  *
  *
  */
  */
+#pragma once
 
 
 #include "ProxyCaster.h"
 #include "ProxyCaster.h"
 #include "../battle/BattleHex.h"
 #include "../battle/BattleHex.h"

+ 3 - 3
lib/spells/ViewSpellInt.h

@@ -8,10 +8,10 @@
  *
  *
  */
  */
 
 
- #pragma once
+#pragma once
 
 
- #include "../int3.h"
- #include "../GameConstants.h"
+#include "../int3.h"
+#include "../GameConstants.h"
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 
 

+ 9 - 0
lib/vstd/DateUtils.cpp

@@ -1,3 +1,12 @@
+/*
+ * DateUtils.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #include "StdInc.h"
 #include "StdInc.h"
 #include <vstd/DateUtils.h>
 #include <vstd/DateUtils.h>
 
 

+ 9 - 0
lib/vstd/StringUtils.cpp

@@ -1,3 +1,12 @@
+/*
+ * StringUtils.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #include "StdInc.h"
 #include "StdInc.h"
 #include <vstd/StringUtils.h>
 #include <vstd/StringUtils.h>
 
 

+ 1 - 1
lobby/EntryPoint.cpp

@@ -16,7 +16,7 @@
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/VCMIDirs.h"
 #include "../lib/VCMIDirs.h"
 
 
-static const int LISTENING_PORT = 30303;
+static const int LISTENING_PORT = 3031;
 
 
 int main(int argc, const char * argv[])
 int main(int argc, const char * argv[])
 {
 {

+ 4 - 1
lobby/LobbyServer.cpp

@@ -293,6 +293,7 @@ void LobbyServer::onDisconnected(const NetworkConnectionPtr & connection, const
 {
 {
 	if(activeAccounts.count(connection))
 	if(activeAccounts.count(connection))
 	{
 	{
+		logGlobal->info("Account %s disconnecting. Accounts online: %d", activeAccounts.at(connection), activeAccounts.size() - 1);
 		database->setAccountOnline(activeAccounts.at(connection), false);
 		database->setAccountOnline(activeAccounts.at(connection), false);
 		activeAccounts.erase(connection);
 		activeAccounts.erase(connection);
 	}
 	}
@@ -300,6 +301,7 @@ void LobbyServer::onDisconnected(const NetworkConnectionPtr & connection, const
 	if(activeGameRooms.count(connection))
 	if(activeGameRooms.count(connection))
 	{
 	{
 		std::string gameRoomID = activeGameRooms.at(connection);
 		std::string gameRoomID = activeGameRooms.at(connection);
+		logGlobal->info("Game room %s disconnecting. Rooms online: %d", gameRoomID, activeGameRooms.size() - 1);
 
 
 		if (database->getGameRoomStatus(gameRoomID) == LobbyRoomState::BUSY)
 		if (database->getGameRoomStatus(gameRoomID) == LobbyRoomState::BUSY)
 		{
 		{
@@ -489,7 +491,7 @@ void LobbyServer::receiveSendChatMessage(const NetworkConnectionPtr & connection
 	std::string channelName = json["channelName"].String();
 	std::string channelName = json["channelName"].String();
 	std::string displayName = database->getAccountDisplayName(senderAccountID);
 	std::string displayName = database->getAccountDisplayName(senderAccountID);
 
 
-	if(TextOperations::isValidUnicodeString(messageText))
+	if(!TextOperations::isValidUnicodeString(messageText))
 		return sendOperationFailed(connection, "String contains invalid characters!");
 		return sendOperationFailed(connection, "String contains invalid characters!");
 
 
 	std::string messageTextClean = sanitizeChatMessage(messageText);
 	std::string messageTextClean = sanitizeChatMessage(messageText);
@@ -595,6 +597,7 @@ void LobbyServer::receiveClientLogin(const NetworkConnectionPtr & connection, co
 
 
 	activeAccounts[connection] = accountID;
 	activeAccounts[connection] = accountID;
 
 
+	logGlobal->info("%s: Logged in as %s", accountID, displayName);
 	sendClientLoginSuccess(connection, accountCookie, displayName);
 	sendClientLoginSuccess(connection, accountCookie, displayName);
 	sendRecentChatHistory(connection, "global", "english");
 	sendRecentChatHistory(connection, "global", "english");
 	if (language != "english")
 	if (language != "english")

+ 9 - 0
lobby/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 // Creates the precompiled header
 // Creates the precompiled header
 #include "StdInc.h"
 #include "StdInc.h"

+ 9 - 0
mapeditor/StdInc.cpp

@@ -1 +1,10 @@
+/*
+ * StdInc.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #include "StdInc.h"
 #include "StdInc.h"

+ 9 - 0
mapeditor/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #pragma once
 #pragma once
 
 
 #include "../Global.h"
 #include "../Global.h"

+ 1 - 1
mapeditor/main.cpp

@@ -7,8 +7,8 @@
  * Full text of license available in license.txt file, in main folder
  * Full text of license available in license.txt file, in main folder
  *
  *
  */
  */
-#include <QApplication>
 #include "StdInc.h"
 #include "StdInc.h"
+#include <QApplication>
 #include "mainwindow.h"
 #include "mainwindow.h"
 
 
 int main(int argc, char * argv[])
 int main(int argc, char * argv[])

+ 1 - 1
mapeditor/mapsettings/victoryconditions.cpp

@@ -1,5 +1,5 @@
 /*
 /*
- * victoryconditions.h, part of VCMI engine
+ * victoryconditions.cpp, part of VCMI engine
  *
  *
  * Authors: listed in file AUTHORS in main folder
  * Authors: listed in file AUTHORS in main folder
  *
  *

+ 9 - 0
scripting/erm/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 // Creates the precompiled header
 // Creates the precompiled header
 #include "StdInc.h"
 #include "StdInc.h"

+ 9 - 0
scripting/erm/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #pragma once
 #pragma once
 
 
 #include "../../Global.h"
 #include "../../Global.h"

+ 9 - 0
scripting/lua/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 // Creates the precompiled header
 // Creates the precompiled header
 #include "StdInc.h"
 #include "StdInc.h"

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików