Bladeren bron

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

thiscris 1 jaar geleden
bovenliggende
commit
9bbcb4cf5c
100 gewijzigde bestanden met toevoegingen van 1813 en 849 verwijderingen
  1. 9 0
      AI/EmptyAI/StdInc.cpp
  2. 9 0
      AI/EmptyAI/StdInc.h
  3. 7 2
      AI/Nullkiller/AIGateway.cpp
  4. 5 18
      AI/Nullkiller/AIUtility.cpp
  5. 8 43
      AI/Nullkiller/AIUtility.h
  6. 5 4
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  7. 8 0
      AI/Nullkiller/Analyzers/HeroManager.cpp
  8. 14 29
      AI/Nullkiller/Analyzers/HeroManager.h
  9. 97 53
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  10. 9 4
      AI/Nullkiller/Analyzers/ObjectClusterizer.h
  11. 8 8
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  12. 1 1
      AI/Nullkiller/Behaviors/BuildingBehavior.h
  13. 8 8
      AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp
  14. 1 5
      AI/Nullkiller/Behaviors/BuyArmyBehavior.h
  15. 70 44
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  16. 7 2
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h
  17. 9 8
      AI/Nullkiller/Behaviors/ClusterBehavior.cpp
  18. 2 2
      AI/Nullkiller/Behaviors/ClusterBehavior.h
  19. 32 31
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  20. 3 4
      AI/Nullkiller/Behaviors/DefenceBehavior.h
  21. 37 37
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  22. 3 3
      AI/Nullkiller/Behaviors/GatherArmyBehavior.h
  23. 12 12
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  24. 1 1
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.h
  25. 24 24
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  26. 1 1
      AI/Nullkiller/Behaviors/StartupBehavior.h
  27. 8 3
      AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp
  28. 1 1
      AI/Nullkiller/Behaviors/StayAtTownBehavior.h
  29. 1 0
      AI/Nullkiller/CMakeLists.txt
  30. 8 3
      AI/Nullkiller/Engine/DeepDecomposer.cpp
  31. 2 0
      AI/Nullkiller/Engine/DeepDecomposer.h
  32. 28 26
      AI/Nullkiller/Engine/Nullkiller.cpp
  33. 2 1
      AI/Nullkiller/Engine/Nullkiller.h
  34. 2 2
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  35. 2 1
      AI/Nullkiller/Goals/AbstractGoal.h
  36. 2 3
      AI/Nullkiller/Goals/Build.cpp
  37. 3 3
      AI/Nullkiller/Goals/Build.h
  38. 3 3
      AI/Nullkiller/Goals/CGoal.h
  39. 2 2
      AI/Nullkiller/Goals/CaptureObject.cpp
  40. 1 1
      AI/Nullkiller/Goals/CaptureObject.h
  41. 34 34
      AI/Nullkiller/Goals/CompleteQuest.cpp
  42. 10 10
      AI/Nullkiller/Goals/CompleteQuest.h
  43. 1 1
      AI/Nullkiller/Goals/Composition.cpp
  44. 1 1
      AI/Nullkiller/Goals/Composition.h
  45. 47 23
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  46. 1 2
      AI/Nullkiller/Goals/ExecuteHeroChain.h
  47. 2 3
      AI/Nullkiller/Goals/GatherArmy.cpp
  48. 3 3
      AI/Nullkiller/Goals/GatherArmy.h
  49. 1 1
      AI/Nullkiller/Goals/Invalid.h
  50. 2 2
      AI/Nullkiller/Markers/HeroExchange.cpp
  51. 1 1
      AI/Nullkiller/Markers/HeroExchange.h
  52. 202 183
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  53. 97 30
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  54. 37 11
      AI/Nullkiller/Pathfinding/AIPathfinder.cpp
  55. 13 3
      AI/Nullkiller/Pathfinding/AIPathfinder.h
  56. 1 1
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
  57. 2 2
      AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp
  58. 2 2
      AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h
  59. 1 1
      AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp
  60. 1 1
      AI/Nullkiller/Pathfinding/Actions/BattleAction.h
  61. 23 8
      AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp
  62. 20 5
      AI/Nullkiller/Pathfinding/Actions/BoatActions.h
  63. 1 1
      AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp
  64. 2 2
      AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.h
  65. 16 6
      AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp
  66. 5 3
      AI/Nullkiller/Pathfinding/Actions/QuestAction.h
  67. 9 9
      AI/Nullkiller/Pathfinding/Actions/SpecialAction.cpp
  68. 19 6
      AI/Nullkiller/Pathfinding/Actions/SpecialAction.h
  69. 1 1
      AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp
  70. 1 1
      AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h
  71. 529 48
      AI/Nullkiller/Pathfinding/ObjectGraph.cpp
  72. 46 3
      AI/Nullkiller/Pathfinding/ObjectGraph.h
  73. 5 2
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  74. 4 1
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h
  75. 32 2
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
  76. 2 0
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.h
  77. 1 1
      AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp
  78. 9 0
      AI/Nullkiller/StdInc.cpp
  79. 9 0
      AI/Nullkiller/StdInc.h
  80. 50 0
      AI/Nullkiller/pforeach.h
  81. 9 0
      AI/StupidAI/StdInc.cpp
  82. 9 0
      AI/StupidAI/StdInc.h
  83. 1 1
      AI/VCAI/AIhelper.cpp
  84. 9 0
      AI/VCAI/MapObjectsEvaluator.cpp
  85. 11 7
      AI/VCAI/Pathfinding/AINodeStorage.cpp
  86. 3 1
      AI/VCAI/Pathfinding/AINodeStorage.h
  87. 9 0
      AI/VCAI/StdInc.cpp
  88. 9 0
      AI/VCAI/StdInc.h
  89. 8 0
      CCallback.cpp
  90. 2 0
      CCallback.h
  91. 1 1
      CI/linux-qt6/before_install.sh
  92. 1 1
      CI/linux/before_install.sh
  93. 41 29
      CMakeLists.txt
  94. 2 2
      Global.h
  95. BIN
      Mods/vcmi/Data/lobby/iconEnter.png
  96. BIN
      Mods/vcmi/Data/lobby/iconFolder.png
  97. BIN
      Mods/vcmi/Data/lobby/iconPlayer.png
  98. BIN
      Mods/vcmi/Data/lobby/iconSend.png
  99. BIN
      Mods/vcmi/Data/lobby/selectionTabSortDate.png
  100. BIN
      Mods/vcmi/Data/lobby/townBorderBig.png

+ 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
 #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
 
 #include "../../Global.h"

+ 7 - 2
AI/Nullkiller/AIGateway.cpp

@@ -566,7 +566,7 @@ void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_
 	myCb->waitTillRealize = true;
 	myCb->unlockGsWhenWaiting = true;
 
-	nullkiller->init(CB, playerID);
+	nullkiller->init(CB, this);
 	
 	retrieveVisitableObjs();
 }
@@ -828,7 +828,7 @@ void AIGateway::makeTurn()
 	{
 		for(const CGObjectInstance * obj : nullkiller->memory->visitableObjs)
 		{
-			if(isWeeklyRevisitable(obj))
+			if(isWeeklyRevisitable(nullkiller.get(), obj))
 			{
 				nullkiller->memory->markObjectUnvisited(obj);
 			}
@@ -1152,6 +1152,11 @@ void AIGateway::retrieveVisitableObjs()
 	{
 		for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
 		{
+			if(!obj->appearance)
+			{
+				logAi->error("Bad!");
+			}
+
 			addVisitableObj(obj);
 		}
 	});

+ 5 - 18
AI/Nullkiller/AIUtility.cpp

@@ -163,15 +163,7 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
 	return h == rhs.get(true);
 }
 
-bool CDistanceSorter::operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const
-{
-	const CGPathNode * ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos());
-	const CGPathNode * rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos());
-
-	return ln->getCost() < rn->getCost();
-}
-
-bool isSafeToVisit(HeroPtr h, const CCreatureSet * heroArmy, uint64_t dangerStrength)
+bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength)
 {
 	const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength();
 
@@ -183,9 +175,9 @@ bool isSafeToVisit(HeroPtr h, const CCreatureSet * heroArmy, uint64_t dangerStre
 	return true; //there's no danger
 }
 
-bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength)
+bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength)
 {
-	return isSafeToVisit(h, h.get(), dangerStrength);
+	return isSafeToVisit(h, h, dangerStrength);
 }
 
 bool isObjectRemovable(const CGObjectInstance * obj)
@@ -235,11 +227,6 @@ bool isObjectPassable(const Nullkiller * ai, const CGObjectInstance * obj)
 	return isObjectPassable(obj, ai->playerID, ai->cb->getPlayerRelations(obj->tempOwner, ai->playerID));
 }
 
-bool isObjectPassable(const CGObjectInstance * obj)
-{
-	return isObjectPassable(obj, ai->playerID, ai->myCb->getPlayerRelations(obj->tempOwner, ai->playerID));
-}
-
 // Pathfinder internal helper
 bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations objectRelations)
 {
@@ -285,7 +272,7 @@ creInfo infoFromDC(const dwellingContent & dc)
 	return ci;
 }
 
-bool compareHeroStrength(HeroPtr h1, HeroPtr h2)
+bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2)
 {
 	return h1->getTotalStrength() < h2->getTotalStrength();
 }
@@ -306,7 +293,7 @@ bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2
 		return art1->getPrice() > art2->getPrice();
 }
 
-bool isWeeklyRevisitable(const CGObjectInstance * obj)
+bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj)
 {
 	if(!obj)
 		return false;

+ 8 - 43
AI/Nullkiller/AIUtility.h

@@ -50,7 +50,6 @@
 
 #include <chrono>
 
-using namespace tbb;
 
 using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
@@ -71,7 +70,6 @@ extern const float SAFE_ATTACK_CONSTANT;
 extern const int GOLD_RESERVE;
 
 extern thread_local CCallback * cb;
-extern thread_local AIGateway * ai;
 
 enum HeroRole
 {
@@ -115,11 +113,11 @@ public:
 	bool validAndSet() const;
 
 
-	template<typename Handler> void serialize(Handler & h)
+	template<typename Handler> void serialize(Handler & handler)
 	{
-		h & this->h;
-		h & hid;
-		h & name;
+		handler & h;
+		handler & hid;
+		handler & name;
 	}
 };
 
@@ -225,18 +223,17 @@ void foreach_neighbour(CCallback * cbp, const int3 & pos, const Func & foo) // a
 }
 
 bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater);
-bool isObjectPassable(const CGObjectInstance * obj);
 bool isObjectPassable(const Nullkiller * ai, const CGObjectInstance * obj);
 bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations objectRelations);
 bool isBlockVisitObj(const int3 & pos);
 
-bool isWeeklyRevisitable(const CGObjectInstance * obj);
+bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj);
 
 bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property!
-bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength);
-bool isSafeToVisit(HeroPtr h, const CCreatureSet *, uint64_t dangerStrength);
+bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength);
+bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength);
 
-bool compareHeroStrength(HeroPtr h1, HeroPtr h2);
+bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2);
 bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2);
 bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2);
 bool townHasFreeTavern(const CGTownInstance * town);
@@ -246,38 +243,6 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
 // todo: move to obj manager
 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
-{
-	const CGHeroInstance * hero;
-
-public:
-	CDistanceSorter(const CGHeroInstance * hero)
-		: hero(hero)
-	{
-	}
-	bool operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const;
-};
-
 template <class T>
 class SharedPool
 {

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

@@ -11,6 +11,7 @@
 #include "DangerHitMapAnalyzer.h"
 
 #include "../Engine/Nullkiller.h"
+#include "../pforeach.h"
 #include "../../../lib/CRandomGenerator.h"
 
 namespace NKAI
@@ -82,9 +83,9 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 		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())
 					continue;
@@ -194,14 +195,14 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 
 	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 enemyDistance = std::numeric_limits<float>::max();
 			const CGTownInstance * enemyTown = nullptr;
 			const CGTownInstance * ourTown = nullptr;
 
-			for(AIPath & path : ai->pathfinder->getPathInfo(pos))
+			for(const AIPath & path : paths)
 			{
 				if(!path.targetHero || path.getFirstBlockedAction())
 					continue;

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

@@ -109,6 +109,7 @@ void HeroManager::update()
 	for(auto & hero : myHeroes)
 	{
 		scores[hero] = evaluateFightingStrength(hero);
+		knownFightingStrength[hero->id] = hero->getFightingStrength();
 	}
 
 	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);
 }
 
+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
 {
 	auto hasFly = hero->spellbookContainsSpell(SpellID::FLY);

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

@@ -20,23 +20,6 @@
 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
 {
 public:
@@ -55,7 +38,7 @@ public:
 	float evaluateSecSkill(const CGHeroInstance * hero, SecondarySkill skill) const;
 };
 
-class DLL_EXPORT HeroManager : public IHeroManager
+class DLL_EXPORT HeroManager
 {
 private:
 	static const SecondarySkillEvaluator wariorSkillsScores;
@@ -64,20 +47,22 @@ private:
 	CCallback * cb; //this is enough, but we downcast from CCallback
 	const Nullkiller * ai;
 	std::map<HeroPtr, HeroRole> heroRoles;
+	std::map<ObjectInstanceID, float> knownFightingStrength;
 
 public:
 	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:
 	float evaluateFightingStrength(const CGHeroInstance * hero) const;

+ 97 - 53
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -90,64 +90,74 @@ std::vector<std::shared_ptr<ObjectCluster>> ObjectClusterizer::getLockedClusters
 	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;
-		}
+	}
+
+	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;
@@ -170,7 +180,7 @@ bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const
 
 	auto playerRelations = ai->cb->getPlayerRelations(ai->playerID, obj->tempOwner);
 
-	if(playerRelations != PlayerRelations::ENEMIES && !isWeeklyRevisitable(obj))
+	if(playerRelations != PlayerRelations::ENEMIES && !isWeeklyRevisitable(ai, obj))
 	{
 		return false;
 	}
@@ -225,15 +235,17 @@ void ObjectClusterizer::clusterize()
 		ai->memory->visitableObjs.end());
 
 #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
-	blocked_range<size_t> r(0, objs.size());
+	tbb::blocked_range<size_t> r(0, objs.size());
 #endif
 		auto priorityEvaluator = ai->priorityEvaluators->acquire();
+		auto heroes = ai->cb->getHeroesInfo();
+		std::vector<AIPath> pathCache;
 
 		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
 	});
@@ -257,7 +269,11 @@ void ObjectClusterizer::clusterize()
 	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))
 	{
@@ -271,9 +287,14 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
 	logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
 #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
 		logAi->trace("No paths found.");
@@ -281,17 +302,17 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
 		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();
 		});
 
 	if(vstd::contains(IgnoredObjectTypes, obj->ID))
 	{
-		farObjects.addObject(obj, paths.front(), 0);
+		farObjects.addObject(obj, pathCache.front(), 0);
 
 #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
 
 		return;
@@ -299,12 +320,35 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
 
 	std::set<const CGHeroInstance *> heroesProcessed;
 
-	for(auto & path : paths)
+	for(auto & path : pathCache)
 	{
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace("Checking path %s", path.toString());
 #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 NKAI_TRACE_LEVEL >= 2

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

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "../Pathfinding/AINodeStorage.h"
+#include "../Engine/PriorityEvaluator.h"
 
 namespace NKAI
 {
@@ -49,8 +50,6 @@ public:
 
 using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
 
-class PriorityEvaluator;
-
 class ObjectClusterizer
 {
 private:
@@ -60,6 +59,7 @@ private:
 	ObjectCluster farObjects;
 	ClusterMap blockedObjects;
 	const Nullkiller * ai;
+	RewardEvaluator valueEvaluator;
 
 public:
 	void clusterize();
@@ -67,12 +67,17 @@ public:
 	std::vector<const CGObjectInstance *> getFarObjects() const;
 	std::vector<std::shared_ptr<ObjectCluster>> getLockedClusters() 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:
 	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);
 };
 
 }

+ 8 - 8
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

@@ -27,14 +27,14 @@ std::string BuildingBehavior::toString() const
 	return "Build";
 }
 
-Goals::TGoalVec BuildingBehavior::decompose() const
+Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
 
-	TResources resourcesRequired = ai->nullkiller->buildAnalyzer->getResourcesRequiredNow();
-	TResources totalDevelopmentCost = ai->nullkiller->buildAnalyzer->getTotalResourcesRequired();
-	TResources availableResources = ai->nullkiller->getFreeResources();
-	TResources dailyIncome = ai->nullkiller->buildAnalyzer->getDailyIncome();
+	TResources resourcesRequired = ai->buildAnalyzer->getResourcesRequiredNow();
+	TResources totalDevelopmentCost = ai->buildAnalyzer->getTotalResourcesRequired();
+	TResources availableResources = ai->getFreeResources();
+	TResources dailyIncome = ai->buildAnalyzer->getDailyIncome();
 
 	logAi->trace("Free resources amount: %s", availableResources.toString());
 
@@ -46,8 +46,8 @@ Goals::TGoalVec BuildingBehavior::decompose() const
 		resourcesRequired.toString(),
 		totalDevelopmentCost.toString());
 
-	auto & developmentInfos = ai->nullkiller->buildAnalyzer->getDevelopmentInfo();
-	auto isGoldPreasureLow = !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh();
+	auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
+	auto isGoldPreasureLow = !ai->buildAnalyzer->isGoldPreasureHigh();
 
 	for(auto & developmentInfo : developmentInfos)
 	{
@@ -57,7 +57,7 @@ Goals::TGoalVec BuildingBehavior::decompose() const
 			{
 				if(buildingInfo.notEnoughRes)
 				{
-					if(ai->nullkiller->getLockedResources().canAfford(buildingInfo.buildCost))
+					if(ai->getLockedResources().canAfford(buildingInfo.buildCost))
 						continue;
 
 					Composition composition;

+ 1 - 1
AI/Nullkiller/Behaviors/BuildingBehavior.h

@@ -25,7 +25,7 @@ namespace Goals
 		{
 		}
 
-		Goals::TGoalVec decompose() const override;
+		Goals::TGoalVec decompose(const Nullkiller * ai) const override;
 		std::string toString() const override;
 		bool operator==(const BuildingBehavior & other) const override
 		{

+ 8 - 8
AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp

@@ -24,11 +24,11 @@ std::string BuyArmyBehavior::toString() const
 	return "Buy army";
 }
 
-Goals::TGoalVec BuyArmyBehavior::decompose() const
+Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
 
-	if(cb->getDate(Date::DAY) == 1)
+	if(ai->cb->getDate(Date::DAY) == 1)
 		return tasks;
 		
 	auto heroes = cb->getHeroesInfo();
@@ -40,26 +40,26 @@ Goals::TGoalVec BuyArmyBehavior::decompose() const
 
 	for(auto town : cb->getTownsInfo())
 	{
-		auto townArmyAvailableToBuy = ai->nullkiller->armyManager->getArmyAvailableToBuyAsCCreatureSet(
+		auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet(
 			town,
-			ai->nullkiller->getFreeResources());
+			ai->getFreeResources());
 
 		for(const CGHeroInstance * targetHero : heroes)
 		{
-			if(ai->nullkiller->buildAnalyzer->isGoldPreasureHigh()	&& !town->hasBuilt(BuildingID::CITY_HALL))
+			if(ai->buildAnalyzer->isGoldPreasureHigh()	&& !town->hasBuilt(BuildingID::CITY_HALL))
 			{
 				continue;
 			}
 
-			if(ai->nullkiller->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
+			if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
 			{
-				auto reinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanGet(
+				auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(
 					targetHero,
 					targetHero,
 					&*townArmyAvailableToBuy);
 
 				if(reinforcement)
-					vstd::amin(reinforcement, ai->nullkiller->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town));
+					vstd::amin(reinforcement, ai->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town));
 
 				if(reinforcement)
 				{

+ 1 - 5
AI/Nullkiller/Behaviors/BuyArmyBehavior.h

@@ -20,11 +20,7 @@ namespace Goals
 	class BuyArmyBehavior : public CGoal<BuyArmyBehavior>
 	{
 	public:
-		BuyArmyBehavior()
-		{
-		}
-
-		Goals::TGoalVec decompose() const override;
+		Goals::TGoalVec decompose(const Nullkiller * ai) const override;
 		std::string toString() const override;
 		bool operator==(const BuyArmyBehavior & other) const override
 		{

+ 70 - 44
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -47,7 +47,7 @@ bool CaptureObjectsBehavior::operator==(const CaptureObjectsBehavior & other) co
 		&& vectorEquals(objectSubTypes, other.objectSubTypes);
 }
 
-Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath> & paths, const CGObjectInstance * objToVisit)
+Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath> & paths, const Nullkiller * nullkiller, const CGObjectInstance * objToVisit)
 {
 	Goals::TGoalVec tasks;
 
@@ -64,7 +64,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 		logAi->trace("Path found %s", path.toString());
 #endif
 
-		if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
+		if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength());
@@ -72,19 +72,31 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 			continue;
 		}
 
-		if(objToVisit && !shouldVisit(ai->nullkiller.get(), path.targetHero, objToVisit))
+		if(objToVisit && !shouldVisit(nullkiller, path.targetHero, objToVisit))
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Ignore path. Hero %s should not visit obj %s", path.targetHero->getNameTranslated(), objToVisit->getObjectName());
+#endif
 			continue;
+		}
 
 		auto hero = path.targetHero;
 		auto danger = path.getTotalDanger();
 
-		if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && path.exchangeCount > 1)
+		if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
+			&& (path.getTotalDanger() == 0 || path.turn() > 0)
+			&& path.exchangeCount > 1)
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Ignore path. Hero %s is SCOUT, chain used and no danger", path.targetHero->getNameTranslated());
+#endif
 			continue;
+		}
 
 		auto firstBlockedAction = path.getFirstBlockedAction();
 		if(firstBlockedAction)
 		{
-			auto subGoal = firstBlockedAction->decompose(path.targetHero);
+			auto subGoal = firstBlockedAction->decompose(nullkiller, path.targetHero);
 
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Decomposing special action %s returns %s", firstBlockedAction->toString(), subGoal->toString());
@@ -123,7 +135,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 
 			sharedPtr.reset(newWay);
 
-			auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
+			auto heroRole = nullkiller->heroManager->getHeroRole(path.targetHero);
 
 			auto & closestWay = closestWaysByRole[heroRole];
 
@@ -132,7 +144,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 				closestWay = &path;
 			}
 
-			if(!ai->nullkiller->arePathHeroesLocked(path))
+			if(!nullkiller->arePathHeroesLocked(path))
 			{
 				waysToVisitObj.push_back(newWay);
 				tasks[tasks.size() - 1] = sharedPtr;
@@ -142,7 +154,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 
 	for(auto way : waysToVisitObj)
 	{
-		auto heroRole = ai->nullkiller->heroManager->getHeroRole(way->getPath().targetHero);
+		auto heroRole = nullkiller->heroManager->getHeroRole(way->getPath().targetHero);
 		auto closestWay = closestWaysByRole[heroRole];
 
 		if(closestWay)
@@ -155,66 +167,80 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 	return tasks;
 }
 
-Goals::TGoalVec CaptureObjectsBehavior::decompose() const
+void CaptureObjectsBehavior::decomposeObjects(
+	Goals::TGoalVec & result,
+	const std::vector<const CGObjectInstance *> & objs,
+	const Nullkiller * nullkiller) const
 {
-	Goals::TGoalVec tasks;
-
-	auto captureObjects = [&](const std::vector<const CGObjectInstance*> & objs) -> void
+	if(objs.empty())
 	{
-		if(objs.empty())
-		{
-			return;
-		}
+		return;
+	}
+
+	std::mutex sync;
 
-		logAi->debug("Scanning objects, count %d", objs.size());
+	logAi->debug("Scanning objects, count %d", objs.size());
 
-		for(auto objToVisit : objs)
+	// tbb::blocked_range<size_t> r(0, objs.size());
+	tbb::parallel_for(
+		tbb::blocked_range<size_t>(0, objs.size()),
+		[this, &objs, &sync, &result, nullkiller](const tbb::blocked_range<size_t> & r)
 		{
-			if(!objectMatchesFilter(objToVisit))
-				continue;
-	
+			std::vector<AIPath> paths;
+			Goals::TGoalVec tasksLocal;
+
+			for(auto i = r.begin(); i != r.end(); i++)
+			{
+				auto objToVisit = objs[i];
+
+				if(!objectMatchesFilter(objToVisit))
+					continue;
+
 #if NKAI_TRACE_LEVEL >= 1
-			logAi->trace("Checking object %s, %s", objToVisit->getObjectName(), objToVisit->visitablePos().toString());
+				logAi->trace("Checking object %s, %s", objToVisit->getObjectName(), objToVisit->visitablePos().toString());
 #endif
 
-			const int3 pos = objToVisit->visitablePos();
-			bool useObjectGraph = ai->nullkiller->settings->isObjectGraphAllowed()
-				&& ai->nullkiller->getScanDepth() != ScanDepth::SMALL;
-
-			auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, useObjectGraph);
+				nullkiller->pathfinder->calculatePathInfo(paths, objToVisit->visitablePos(), nullkiller->settings->isObjectGraphAllowed());
 
-			std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
-			std::shared_ptr<ExecuteHeroChain> closestWay;
-					
 #if NKAI_TRACE_LEVEL >= 1
-			logAi->trace("Found %d paths", paths.size());
+				logAi->trace("Found %d paths", paths.size());
 #endif
-			vstd::concatenate(tasks, getVisitGoals(paths, objToVisit));
-		}
+				vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit));
+			}
 
-		vstd::erase_if(tasks, [](TSubgoal task) -> bool
-		{
-			return task->invalid();
+			std::lock_guard<std::mutex> lock(sync);
+			vstd::concatenate(result, tasksLocal);
 		});
-	};
+}
+
+Goals::TGoalVec CaptureObjectsBehavior::decompose(const Nullkiller * ai) const
+{
+	Goals::TGoalVec tasks;
+
+	vstd::erase_if(tasks, [](TSubgoal task) -> bool
+	{
+		return task->invalid();
+	});
 
 	if(specificObjects)
 	{
-		captureObjects(objectsToCapture);
+		decomposeObjects(tasks, objectsToCapture, ai);
 	}
 	else if(objectTypes.size())
 	{
-		captureObjects(
+		decomposeObjects(
+			tasks,
 			std::vector<const CGObjectInstance *>(
-				ai->nullkiller->memory->visitableObjs.begin(),
-				ai->nullkiller->memory->visitableObjs.end()));
+				ai->memory->visitableObjs.begin(),
+				ai->memory->visitableObjs.end()),
+			ai);
 	}
 	else
 	{
-		captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());
+		decomposeObjects(tasks, ai->objectClusterizer->getNearbyObjects(), ai);
 
-		if(tasks.empty() || ai->nullkiller->getScanDepth() != ScanDepth::SMALL)
-			captureObjects(ai->nullkiller->objectClusterizer->getFarObjects());
+		if(tasks.empty() || ai->getScanDepth() != ScanDepth::SMALL)
+			decomposeObjects(tasks, ai->objectClusterizer->getFarObjects(), ai);
 	}
 
 	return tasks;

+ 7 - 2
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h

@@ -25,6 +25,7 @@ namespace Goals
 		std::vector<int> objectSubTypes;
 		std::vector<const CGObjectInstance *> objectsToCapture;
 		bool specificObjects;
+
 	public:
 		CaptureObjectsBehavior()
 			:CGoal(CAPTURE_OBJECTS)
@@ -48,7 +49,7 @@ namespace Goals
 			specificObjects = true;
 		}
 
-		Goals::TGoalVec decompose() const override;
+		Goals::TGoalVec decompose(const Nullkiller * ai) const override;
 		std::string toString() const override;
 
 		CaptureObjectsBehavior & ofType(int type)
@@ -67,10 +68,14 @@ namespace Goals
 
 		bool operator==(const CaptureObjectsBehavior & other) const override;
 
-		static Goals::TGoalVec getVisitGoals(const std::vector<AIPath> & paths, const CGObjectInstance * objToVisit = nullptr);
+		static Goals::TGoalVec getVisitGoals(const std::vector<AIPath> & paths, const Nullkiller * nullkiller, const CGObjectInstance * objToVisit = nullptr);
 
 	private:
 		bool objectMatchesFilter(const CGObjectInstance * obj) const;
+		void decomposeObjects(
+			Goals::TGoalVec & result,
+			const std::vector<const CGObjectInstance *> & objs,
+			const Nullkiller * nullkiller) const;
 	};
 }
 

+ 9 - 8
AI/Nullkiller/Behaviors/ClusterBehavior.cpp

@@ -26,23 +26,24 @@ std::string ClusterBehavior::toString() const
 	return "Unlock Clusters";
 }
 
-Goals::TGoalVec ClusterBehavior::decompose() const
+Goals::TGoalVec ClusterBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
-	auto clusters = ai->nullkiller->objectClusterizer->getLockedClusters();
+	auto clusters = ai->objectClusterizer->getLockedClusters();
 
 	for(auto cluster : clusters)
 	{
-		vstd::concatenate(tasks, decomposeCluster(cluster));
+		vstd::concatenate(tasks, decomposeCluster(ai, cluster));
 	}
 
 	return tasks;
 }
 
-Goals::TGoalVec ClusterBehavior::decomposeCluster(std::shared_ptr<ObjectCluster> cluster) const
+Goals::TGoalVec ClusterBehavior::decomposeCluster(const Nullkiller * ai, std::shared_ptr<ObjectCluster> cluster) const
 {
 	auto center = cluster->calculateCenter();
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(center->visitablePos(), ai->nullkiller->settings->isObjectGraphAllowed());
+	auto paths = ai->pathfinder->getPathInfo(center->visitablePos(), ai->settings->isObjectGraphAllowed());
+
 	auto blockerPos = cluster->blocker->visitablePos();
 	std::vector<AIPath> blockerPaths;
 
@@ -64,7 +65,7 @@ Goals::TGoalVec ClusterBehavior::decomposeCluster(std::shared_ptr<ObjectCluster>
 		logAi->trace("Checking path %s", path->toString());
 #endif
 
-		auto blocker = ai->nullkiller->objectClusterizer->getBlocker(*path);
+		auto blocker = ai->objectClusterizer->getBlocker(*path);
 
 		if(blocker != cluster->blocker)
 		{
@@ -82,7 +83,7 @@ Goals::TGoalVec ClusterBehavior::decomposeCluster(std::shared_ptr<ObjectCluster>
 		{
 			clonedPath.nodes.insert(clonedPath.nodes.begin(), *node);
 
-			if(node->coord == blockerPos || cb->getGuardingCreaturePosition(node->coord) == blockerPos)
+			if(node->coord == blockerPos || ai->cb->getGuardingCreaturePosition(node->coord) == blockerPos)
 				break;
 		}
 
@@ -99,7 +100,7 @@ Goals::TGoalVec ClusterBehavior::decomposeCluster(std::shared_ptr<ObjectCluster>
 	logAi->trace("Decompose unlock paths");
 #endif
 
-	auto unlockTasks = CaptureObjectsBehavior::getVisitGoals(blockerPaths);
+	auto unlockTasks = CaptureObjectsBehavior::getVisitGoals(blockerPaths, ai);
 
 	for(int i = 0; i < paths.size(); i++)
 	{

+ 2 - 2
AI/Nullkiller/Behaviors/ClusterBehavior.h

@@ -28,7 +28,7 @@ namespace Goals
 		{
 		}
 
-		TGoalVec decompose() const override;
+		TGoalVec decompose(const Nullkiller * ai) const override;
 		std::string toString() const override;
 
 		bool operator==(const ClusterBehavior & other) const override
@@ -37,7 +37,7 @@ namespace Goals
 		}
 
 	private:
-		Goals::TGoalVec decomposeCluster(std::shared_ptr<ObjectCluster> cluster) const;
+		Goals::TGoalVec decomposeCluster(const Nullkiller * ai, std::shared_ptr<ObjectCluster> cluster) const;
 	};
 }
 

+ 32 - 31
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -34,21 +34,21 @@ std::string DefenceBehavior::toString() const
 	return "Defend towns";
 }
 
-Goals::TGoalVec DefenceBehavior::decompose() const
+Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
 		
-	for(auto town : cb->getTownsInfo())
+	for(auto town : ai->cb->getTownsInfo())
 	{
-		evaluateDefence(tasks, town);
+		evaluateDefence(tasks, town, ai);
 	}
 
 	return tasks;
 }
 
-bool isThreatUnderControl(const CGTownInstance * town, const HitMapInfo & threat, const std::vector<AIPath> & paths)
+bool isThreatUnderControl(const CGTownInstance * town, const HitMapInfo & threat, const Nullkiller * ai, const std::vector<AIPath> & paths)
 {
-	int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
+	int dayOfWeek = ai->cb->getDate(Date::DAY_OF_WEEK);
 
 	for(const AIPath & path : paths)
 	{
@@ -81,14 +81,15 @@ void handleCounterAttack(
 	const CGTownInstance * town,
 	const HitMapInfo & threat,
 	const HitMapInfo & maximumDanger,
+	const Nullkiller * ai,
 	Goals::TGoalVec & tasks)
 {
 	if(threat.hero.validAndSet()
 		&& threat.turn <= 1
 		&& (threat.danger == maximumDanger.danger || threat.turn < maximumDanger.turn))
 	{
-		auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(threat.hero->visitablePos());
-		auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, threat.hero.get());
+		auto heroCapturingPaths = ai->pathfinder->getPathInfo(threat.hero->visitablePos());
+		auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, ai, threat.hero.get());
 
 		for(int i = 0; i < heroCapturingPaths.size(); i++)
 		{
@@ -106,9 +107,9 @@ void handleCounterAttack(
 	}
 }
 
-bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoalVec & tasks)
+bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoalVec & tasks, const Nullkiller * ai)
 {
-	if(ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
+	if(ai->isHeroLocked(town->garrisonHero.get()))
 	{
 		logAi->trace(
 			"Hero %s in garrison of town %s is supposed to defend the town",
@@ -120,7 +121,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
 
 	if(!town->visitingHero)
 	{
-		if(cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
+		if(ai->cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
 		{
 			logAi->trace(
 				"Extracting hero %s from garrison of town %s",
@@ -131,10 +132,10 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
 
 			return true;
 		}
-		else if(ai->nullkiller->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
+		else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
 		{
 			auto armyDismissLimit = 1000;
-			auto heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(armyDismissLimit);
+			auto heroToDismiss = ai->heroManager->findWeakHeroToDismiss(armyDismissLimit);
 
 			if(heroToDismiss)
 			{
@@ -148,16 +149,16 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
 	return false;
 }
 
-void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const
+void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town, const Nullkiller * ai) const
 {
 	logAi->trace("Evaluating defence for %s", town->getNameTranslated());
 
-	auto threatNode = ai->nullkiller->dangerHitMap->getObjectThreat(town);
-	std::vector<HitMapInfo> threats = ai->nullkiller->dangerHitMap->getTownThreats(town);
+	auto threatNode = ai->dangerHitMap->getObjectThreat(town);
+	std::vector<HitMapInfo> threats = ai->dangerHitMap->getTownThreats(town);
 	
 	threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
 
-	if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks))
+	if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
 	{
 		return;
 	}
@@ -169,7 +170,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 		return;
 	}
 	
-	uint64_t reinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town);
+	uint64_t reinforcement = ai->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town);
 
 	if(reinforcement)
 	{
@@ -177,7 +178,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 		tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(0.5f)));
 	}
 
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos());
+	auto paths = ai->pathfinder->getPathInfo(town->visitablePos());
 
 	for(auto & threat : threats)
 	{
@@ -188,14 +189,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			std::to_string(threat.turn),
 			threat.hero ? threat.hero->getNameTranslated() : std::string("<no hero>"));
 
-		handleCounterAttack(town, threat, threatNode.maximumDanger, tasks);
+		handleCounterAttack(town, threat, threatNode.maximumDanger, ai, tasks);
 
-		if(isThreatUnderControl(town, threat, paths))
+		if(isThreatUnderControl(town, threat, ai, paths))
 		{
 			continue;
 		}
 
-		evaluateRecruitingHero(tasks, threat, town);
+		evaluateRecruitingHero(tasks, threat, town, ai);
 
 		if(paths.empty())
 		{
@@ -274,7 +275,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			// main without army and visiting scout with army, very specific case
 			if(town->visitingHero && town->getUpperArmy()->stacksCount() == 0
 				&& path.targetHero != town->visitingHero.get() && path.exchangeCount == 1 && path.turn() == 0
-				&& ai->nullkiller->heroManager->evaluateHero(path.targetHero) > ai->nullkiller->heroManager->evaluateHero(town->visitingHero.get())
+				&& ai->heroManager->evaluateHero(path.targetHero) > ai->heroManager->evaluateHero(town->visitingHero.get())
 				&& 10 * path.targetHero->getTotalStrength() < town->visitingHero->getTotalStrength())
 			{
 				path.heroArmy = town->visitingHero.get();
@@ -293,7 +294,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				
 			if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger))
 			{
-				if(ai->nullkiller->arePathHeroesLocked(path))
+				if(ai->arePathHeroesLocked(path))
 				{
 #if NKAI_TRACE_LEVEL >= 1
 					logAi->trace("Can not move %s to defend town %s. Path is locked.",
@@ -344,7 +345,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			{
 				if(town->garrisonHero)
 				{
-					if(ai->nullkiller->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
+					if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
 						&& town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20)
 					{
 						if(path.turn() == 0)
@@ -378,7 +379,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			auto firstBlockedAction = path.getFirstBlockedAction();
 			if(firstBlockedAction)
 			{
-				auto subGoal = firstBlockedAction->decompose(path.targetHero);
+				auto subGoal = firstBlockedAction->decompose(ai, path.targetHero);
 
 #if NKAI_TRACE_LEVEL >= 2
 				logAi->trace("Decomposing special action %s returns %s", firstBlockedAction->toString(), subGoal->toString());
@@ -402,19 +403,19 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	logAi->debug("Found %d tasks", tasks.size());
 }
 
-void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town) const
+void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const
 {
 	if(town->hasBuilt(BuildingID::TAVERN)
-		&& cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
+		&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
 	{
-		auto heroesInTavern = cb->getAvailableHeroes(town);
+		auto heroesInTavern = ai->cb->getAvailableHeroes(town);
 
 		for(auto hero : heroesInTavern)
 		{
 			if(hero->getTotalStrength() < threat.danger)
 				continue;
 
-			auto myHeroes = cb->getHeroesInfo();
+			auto myHeroes = ai->cb->getHeroesInfo();
 
 #if NKAI_TRACE_LEVEL >= 1
 			logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
@@ -448,9 +449,9 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
 				if(heroToDismiss && heroToDismiss->getArmyStrength() + 500 > hero->getArmyStrength())
 					continue;
 			}
-			else if(ai->nullkiller->heroManager->heroCapReached())
+			else if(ai->heroManager->heroCapReached())
 			{
-				heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
+				heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
 
 				if(!heroToDismiss)
 					continue;

+ 3 - 4
AI/Nullkiller/Behaviors/DefenceBehavior.h

@@ -20,7 +20,6 @@ struct HitMapInfo;
 
 namespace Goals
 {
-
 	class DefenceBehavior : public CGoal<DefenceBehavior>
 	{
 	public:
@@ -29,7 +28,7 @@ namespace Goals
 		{
 		}
 
-		Goals::TGoalVec decompose() const override;
+		Goals::TGoalVec decompose(const Nullkiller * ai) const override;
 		std::string toString() const override;
 
 		bool operator==(const DefenceBehavior & other) const override
@@ -38,8 +37,8 @@ namespace Goals
 		}
 
 	private:
-		void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const;
-		void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town) const;
+		void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town, const Nullkiller * ai) const;
+		void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const;
 	};
 }
 

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

@@ -30,11 +30,11 @@ std::string GatherArmyBehavior::toString() const
 	return "Gather army";
 }
 
-Goals::TGoalVec GatherArmyBehavior::decompose() const
+Goals::TGoalVec GatherArmyBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
 
-	auto heroes = cb->getHeroesInfo();
+	auto heroes = ai->cb->getHeroesInfo();
 
 	if(heroes.empty())
 	{
@@ -43,33 +43,33 @@ Goals::TGoalVec GatherArmyBehavior::decompose() const
 
 	for(const CGHeroInstance * hero : heroes)
 	{
-		if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::MAIN)
+		if(ai->heroManager->getHeroRole(hero) == HeroRole::MAIN)
 		{
-			vstd::concatenate(tasks, deliverArmyToHero(hero));
+			vstd::concatenate(tasks, deliverArmyToHero(ai, hero));
 		}
 	}
 
-	auto towns = cb->getTownsInfo();
+	auto towns = ai->cb->getTownsInfo();
 
 	for(const CGTownInstance * town : towns)
 	{
-		vstd::concatenate(tasks, upgradeArmy(town));
+		vstd::concatenate(tasks, upgradeArmy(ai, town));
 	}
 
 	return tasks;
 }
 
-Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * hero) const
+Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, const CGHeroInstance * hero) const
 {
 	Goals::TGoalVec tasks;
 	const int3 pos = hero->visitablePos();
-	auto targetHeroScore = ai->nullkiller->heroManager->evaluateHero(hero);
+	auto targetHeroScore = ai->heroManager->evaluateHero(hero);
 
 #if NKAI_TRACE_LEVEL >= 1
 	logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString());
 #endif
 
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
+	auto paths = ai->pathfinder->getPathInfo(pos, ai->settings->isObjectGraphAllowed());
 
 #if NKAI_TRACE_LEVEL >= 1
 	logAi->trace("Gather army found %d paths", paths.size());
@@ -89,7 +89,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 			continue;
 		}
 
-		if(path.turn() > 0 && ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
+		if(path.turn() > 0 && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
@@ -97,7 +97,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 			continue;
 		}
 
-		if(ai->nullkiller->arePathHeroesLocked(path))
+		if(ai->arePathHeroesLocked(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path because of locked hero");
@@ -107,7 +107,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 
 		HeroExchange heroExchange(hero, path);
 
-		uint64_t armyValue = heroExchange.getReinforcementArmyStrength();
+		uint64_t armyValue = heroExchange.getReinforcementArmyStrength(ai);
 		float armyRatio = (float)armyValue / hero->getArmyStrength();
 
 		// avoid transferring very small amount of army
@@ -126,11 +126,11 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 		{
 			if(!node.targetHero) continue;
 
-			auto heroRole = ai->nullkiller->heroManager->getHeroRole(node.targetHero);
+			auto heroRole = ai->heroManager->getHeroRole(node.targetHero);
 
 			if(heroRole == HeroRole::MAIN)
 			{
-				auto score = ai->nullkiller->heroManager->evaluateHero(node.targetHero);
+				auto score = ai->heroManager->evaluateHero(node.targetHero);
 
 				if(score >= targetHeroScore)
 				{
@@ -174,7 +174,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 
 			if(hero->inTownGarrison && path.turn() == 0)
 			{
-				auto lockReason = ai->nullkiller->getHeroLockedReason(hero);
+				auto lockReason = ai->getHeroLockedReason(hero);
 
 				if(path.targetHero->visitedTown == hero->visitedTown)
 				{
@@ -201,7 +201,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 	#if NKAI_TRACE_LEVEL >= 2
 				logAi->trace("Action is blocked. Considering decomposition.");
 	#endif
-				auto subGoal = blockedAction->decompose(path.targetHero);
+				auto subGoal = blockedAction->decompose(ai, path.targetHero);
 
 				if(subGoal->invalid())
 				{
@@ -221,18 +221,18 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 	return tasks;
 }
 
-Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) const
+Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGTownInstance * upgrader) const
 {
 	Goals::TGoalVec tasks;
 	const int3 pos = upgrader->visitablePos();
-	TResources availableResources = ai->nullkiller->getFreeResources();
+	TResources availableResources = ai->getFreeResources();
 
 #if NKAI_TRACE_LEVEL >= 1
 	logAi->trace("Checking ways to upgrade army in town %s, %s", upgrader->getObjectName(), pos.toString());
 #endif
 	
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
-	auto goals = CaptureObjectsBehavior::getVisitGoals(paths);
+	auto paths = ai->pathfinder->getPathInfo(pos, ai->settings->isObjectGraphAllowed());
+	auto goals = CaptureObjectsBehavior::getVisitGoals(paths, ai);
 
 	std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
 
@@ -244,9 +244,9 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 
 	for(const AIPath & path : paths)
 	{
-		auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
+		auto heroRole = ai->heroManager->getHeroRole(path.targetHero);
 
-		if(heroRole == HeroRole::MAIN && path.turn() < ai->nullkiller->settings->getScoutHeroTurnDistanceLimit())
+		if(heroRole == HeroRole::MAIN && path.turn() < ai->settings->getScoutHeroTurnDistanceLimit())
 			hasMainAround = true;
 	}
 
@@ -275,7 +275,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 			continue;
 		}
 
-		if(ai->nullkiller->arePathHeroesLocked(path))
+		if(ai->arePathHeroesLocked(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path because of locked hero");
@@ -292,10 +292,10 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 			continue;
 		}
 
-		auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
+		auto heroRole = ai->heroManager->getHeroRole(path.targetHero);
 
 		if(heroRole == HeroRole::SCOUT
-			&& ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
+			&& ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
@@ -303,17 +303,17 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 			continue;
 		}
 
-		auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
+		auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
 
 		if(!upgrader->garrisonHero
 			&& (
 				hasMainAround
-				|| ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN))
+				|| ai->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN))
 		{
 			ArmyUpgradeInfo armyToGetOrBuy;
 
 			armyToGetOrBuy.addArmyToGet(
-				ai->nullkiller->armyManager->getBestArmy(
+				ai->armyManager->getBestArmy(
 					path.targetHero,
 					path.heroArmy,
 					upgrader->getUpperArmy()));
@@ -321,11 +321,11 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 			armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
 
 			armyToGetOrBuy.addArmyToBuy(
-				ai->nullkiller->armyManager->toSlotInfo(
-					ai->nullkiller->armyManager->getArmyAvailableToBuy(
+				ai->armyManager->toSlotInfo(
+					ai->armyManager->getArmyAvailableToBuy(
 						path.heroArmy,
 						upgrader,
-						ai->nullkiller->getFreeResources(),
+						ai->getFreeResources(),
 						path.turn())));
 
 			upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
@@ -334,17 +334,17 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 
 			if(!upgrade.upgradeValue
 				&& armyToGetOrBuy.upgradeValue > 20000
-				&& ai->nullkiller->heroManager->canRecruitHero(town)
-				&& path.turn() < ai->nullkiller->settings->getScoutHeroTurnDistanceLimit())
+				&& ai->heroManager->canRecruitHero(town)
+				&& path.turn() < ai->settings->getScoutHeroTurnDistanceLimit())
 			{
 				for(auto hero : cb->getAvailableHeroes(town))
 				{
-					auto scoutReinforcement =  ai->nullkiller->armyManager->howManyReinforcementsCanBuy(hero, town)
-						+ ai->nullkiller->armyManager->howManyReinforcementsCanGet(hero, town);
+					auto scoutReinforcement =  ai->armyManager->howManyReinforcementsCanBuy(hero, town)
+						+ ai->armyManager->howManyReinforcementsCanGet(hero, town);
 
 					if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
-						&& ai->nullkiller->getFreeGold() >20000
-						&& !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh())
+						&& ai->getFreeGold() >20000
+						&& !ai->buildAnalyzer->isGoldPreasureHigh())
 					{
 						Composition recruitHero;
 

+ 3 - 3
AI/Nullkiller/Behaviors/GatherArmyBehavior.h

@@ -25,7 +25,7 @@ namespace Goals
 		{
 		}
 
-		TGoalVec decompose() const override;
+		TGoalVec decompose(const Nullkiller * ai) const override;
 		std::string toString() const override;
 
 		bool operator==(const GatherArmyBehavior & other) const override
@@ -34,8 +34,8 @@ namespace Goals
 		}
 
 	private:
-		TGoalVec deliverArmyToHero(const CGHeroInstance * hero) const;
-		TGoalVec upgradeArmy(const CGTownInstance * upgrader) const;
+		TGoalVec deliverArmyToHero(const Nullkiller * ai, const CGHeroInstance * hero) const;
+		TGoalVec upgradeArmy(const Nullkiller * ai, const CGTownInstance * upgrader) const;
 	};
 }
 

+ 12 - 12
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -24,12 +24,12 @@ std::string RecruitHeroBehavior::toString() const
 	return "Recruit hero";
 }
 
-Goals::TGoalVec RecruitHeroBehavior::decompose() const
+Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
-	auto towns = cb->getTownsInfo();
+	auto towns = ai->cb->getTownsInfo();
 
-	auto ourHeroes = ai->nullkiller->heroManager->getHeroRoles();
+	auto ourHeroes = ai->heroManager->getHeroRoles();
 	auto minScoreToHireMain = std::numeric_limits<float>::max();
 
 	for(auto hero : ourHeroes)
@@ -37,7 +37,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
 		if(hero.second != HeroRole::MAIN)
 			continue;
 
-		auto newScore = ai->nullkiller->heroManager->evaluateHero(hero.first.get());
+		auto newScore = ai->heroManager->evaluateHero(hero.first.get());
 
 		if(minScoreToHireMain > newScore)
 		{
@@ -48,13 +48,13 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
 
 	for(auto town : towns)
 	{
-		if(ai->nullkiller->heroManager->canRecruitHero(town))
+		if(ai->heroManager->canRecruitHero(town))
 		{
-			auto availableHeroes = cb->getAvailableHeroes(town);
+			auto availableHeroes = ai->cb->getAvailableHeroes(town);
 
 			for(auto hero : availableHeroes)
 			{
-				auto score = ai->nullkiller->heroManager->evaluateHero(hero);
+				auto score = ai->heroManager->evaluateHero(hero);
 
 				if(score > minScoreToHireMain)
 				{
@@ -65,16 +65,16 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
 
 			int treasureSourcesCount = 0;
 
-			for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects())
+			for(auto obj : ai->objectClusterizer->getNearbyObjects())
 			{
 				if((obj->ID == Obj::RESOURCE)
 					|| obj->ID == Obj::TREASURE_CHEST
 					|| obj->ID == Obj::CAMPFIRE
-					|| isWeeklyRevisitable(obj)
+					|| isWeeklyRevisitable(ai, obj)
 					|| obj->ID ==Obj::ARTIFACT)
 				{
 					auto tile = obj->visitablePos();
-					auto closestTown = ai->nullkiller->dangerHitMap->getClosestTown(tile);
+					auto closestTown = ai->dangerHitMap->getClosestTown(tile);
 
 					if(town == closestTown)
 						treasureSourcesCount++;
@@ -84,8 +84,8 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
 			if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000))
 				continue;
 
-			if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1
-				|| (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh()))
+			if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1
+				|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPreasureHigh()))
 			{
 				tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3)));
 			}

+ 1 - 1
AI/Nullkiller/Behaviors/RecruitHeroBehavior.h

@@ -25,7 +25,7 @@ namespace Goals
 		{
 		}
 
-		TGoalVec decompose() const override;
+		TGoalVec decompose(const Nullkiller * ai) const override;
 		std::string toString() const override;
 
 		bool operator==(const RecruitHeroBehavior & other) const override

+ 24 - 24
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -41,9 +41,9 @@ const AIPath getShortestPath(const CGTownInstance * town, const std::vector<AIPa
 	return shortestPath;
 }
 
-const CGHeroInstance * getNearestHero(const CGTownInstance * town)
+const CGHeroInstance * getNearestHero(const Nullkiller * ai, const CGTownInstance * town)
 {
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos());
+	auto paths = ai->pathfinder->getPathInfo(town->visitablePos());
 
 	if(paths.empty())
 		return nullptr;
@@ -59,9 +59,9 @@ const CGHeroInstance * getNearestHero(const CGTownInstance * town)
 	return shortestPath.targetHero;
 }
 
-bool needToRecruitHero(const CGTownInstance * startupTown)
+bool needToRecruitHero(const Nullkiller * ai, const CGTownInstance * startupTown)
 {
-	if(!ai->nullkiller->heroManager->canRecruitHero(startupTown))
+	if(!ai->heroManager->canRecruitHero(startupTown))
 		return false;
 
 	if(!startupTown->garrisonHero && !startupTown->visitingHero)
@@ -69,7 +69,7 @@ bool needToRecruitHero(const CGTownInstance * startupTown)
 
 	int treasureSourcesCount = 0;
 
-	for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects())
+	for(auto obj : ai->objectClusterizer->getNearbyObjects())
 	{
 		if((obj->ID == Obj::RESOURCE && dynamic_cast<const CGResource *>(obj)->resourceID() == EGameResID::GOLD)
 			|| obj->ID == Obj::TREASURE_CHEST
@@ -91,10 +91,10 @@ bool needToRecruitHero(const CGTownInstance * startupTown)
 	return cb->getHeroCount(ai->playerID, true) < basicCount + boost;
 }
 
-Goals::TGoalVec StartupBehavior::decompose() const
+Goals::TGoalVec StartupBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
-	auto towns = cb->getTownsInfo();
+	auto towns = ai->cb->getTownsInfo();
 
 	if(!towns.size())
 		return tasks;
@@ -103,38 +103,38 @@ Goals::TGoalVec StartupBehavior::decompose() const
 
 	if(towns.size() > 1)
 	{
-		startupTown = *vstd::maxElementByFun(towns, [](const CGTownInstance * town) -> float
+		startupTown = *vstd::maxElementByFun(towns, [ai](const CGTownInstance * town) -> float
 		{
 			if(town->garrisonHero)
-				return ai->nullkiller->heroManager->evaluateHero(town->garrisonHero.get());
+				return ai->heroManager->evaluateHero(town->garrisonHero.get());
 
-			auto closestHero = getNearestHero(town);
+			auto closestHero = getNearestHero(ai, town);
 
 			if(closestHero)
-				return ai->nullkiller->heroManager->evaluateHero(closestHero);
+				return ai->heroManager->evaluateHero(closestHero);
 
 			return 0;
 		});
 	}
 
 	if(!startupTown->hasBuilt(BuildingID::TAVERN)
-		&& cb->canBuildStructure(startupTown, BuildingID::TAVERN) == EBuildingState::ALLOWED)
+		&& ai->cb->canBuildStructure(startupTown, BuildingID::TAVERN) == EBuildingState::ALLOWED)
 	{
 		tasks.push_back(Goals::sptr(Goals::BuildThis(BuildingID::TAVERN, startupTown).setpriority(100)));
 
 		return tasks;
 	}
 
-	bool canRecruitHero = needToRecruitHero(startupTown);
-	auto closestHero = getNearestHero(startupTown);
+	bool canRecruitHero = needToRecruitHero(ai, startupTown);
+	auto closestHero = getNearestHero(ai, startupTown);
 
 	if(closestHero)
 	{
 		if(!startupTown->visitingHero)
 		{
-			if(ai->nullkiller->armyManager->howManyReinforcementsCanGet(startupTown->getUpperArmy(), startupTown->getUpperArmy(), closestHero) > 200)
+			if(ai->armyManager->howManyReinforcementsCanGet(startupTown->getUpperArmy(), startupTown->getUpperArmy(), closestHero) > 200)
 			{
-				auto paths = ai->nullkiller->pathfinder->getPathInfo(startupTown->visitablePos());
+				auto paths = ai->pathfinder->getPathInfo(startupTown->visitablePos());
 
 				if(paths.size())
 				{
@@ -147,22 +147,22 @@ Goals::TGoalVec StartupBehavior::decompose() const
 		else
 		{
 			auto visitingHero = startupTown->visitingHero.get();
-			auto visitingHeroScore = ai->nullkiller->heroManager->evaluateHero(visitingHero);
+			auto visitingHeroScore = ai->heroManager->evaluateHero(visitingHero);
 				
 			if(startupTown->garrisonHero)
 			{
 				auto garrisonHero = startupTown->garrisonHero.get();
-				auto garrisonHeroScore = ai->nullkiller->heroManager->evaluateHero(garrisonHero);
+				auto garrisonHeroScore = ai->heroManager->evaluateHero(garrisonHero);
 
 				if(visitingHeroScore > garrisonHeroScore
-					|| (ai->nullkiller->heroManager->getHeroRole(garrisonHero) == HeroRole::SCOUT && ai->nullkiller->heroManager->getHeroRole(visitingHero) == HeroRole::MAIN))
+					|| (ai->heroManager->getHeroRole(garrisonHero) == HeroRole::SCOUT && ai->heroManager->getHeroRole(visitingHero) == HeroRole::MAIN))
 				{
-					if(canRecruitHero || ai->nullkiller->armyManager->howManyReinforcementsCanGet(visitingHero, garrisonHero) > 200)
+					if(canRecruitHero || ai->armyManager->howManyReinforcementsCanGet(visitingHero, garrisonHero) > 200)
 					{
 						tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, visitingHero, HeroLockedReason::STARTUP).setpriority(100)));
 					}
 				}
-				else if(ai->nullkiller->armyManager->howManyReinforcementsCanGet(garrisonHero, visitingHero) > 200)
+				else if(ai->armyManager->howManyReinforcementsCanGet(garrisonHero, visitingHero) > 200)
 				{
 					tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, garrisonHero, HeroLockedReason::STARTUP).setpriority(100)));
 				}
@@ -170,7 +170,7 @@ Goals::TGoalVec StartupBehavior::decompose() const
 			else if(canRecruitHero)
 			{
 				auto canPickTownArmy = startupTown->stacksCount() == 0
-					|| ai->nullkiller->armyManager->howManyReinforcementsCanGet(startupTown->visitingHero, startupTown) > 0;
+					|| ai->armyManager->howManyReinforcementsCanGet(startupTown->visitingHero, startupTown) > 0;
 
 				if(canPickTownArmy)
 				{
@@ -189,7 +189,7 @@ Goals::TGoalVec StartupBehavior::decompose() const
 	{
 		for(auto town : towns)
 		{
-			if(!town->visitingHero && needToRecruitHero(town))
+			if(!town->visitingHero && needToRecruitHero(ai, town))
 			{
 				tasks.push_back(Goals::sptr(Goals::RecruitHero(town)));
 
@@ -205,7 +205,7 @@ Goals::TGoalVec StartupBehavior::decompose() const
 			if(town->garrisonHero
 				&& town->garrisonHero->movementPointsRemaining()
 				&& !town->visitingHero
-				&& ai->nullkiller->getHeroLockedReason(town->garrisonHero) != HeroLockedReason::DEFENCE)
+				&& ai->getHeroLockedReason(town->garrisonHero) != HeroLockedReason::DEFENCE)
 			{
 				tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(town, nullptr).setpriority(MIN_PRIORITY)));
 			}

+ 1 - 1
AI/Nullkiller/Behaviors/StartupBehavior.h

@@ -25,7 +25,7 @@ namespace Goals
 		{
 		}
 
-		TGoalVec decompose() const override;
+		TGoalVec decompose(const Nullkiller * ai) const override;
 		std::string toString() const override;
 
 		bool operator==(const StartupBehavior & other) const override

+ 8 - 3
AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp

@@ -27,26 +27,31 @@ std::string StayAtTownBehavior::toString() const
 	return "StayAtTownBehavior";
 }
 
-Goals::TGoalVec StayAtTownBehavior::decompose() const
+Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
-	auto towns = cb->getTownsInfo();
+	auto towns = ai->cb->getTownsInfo();
 
 	if(!towns.size())
 		return tasks;
 
+	std::vector<AIPath> paths;
+
 	for(auto town : towns)
 	{
 		if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
 			continue;
 
-		auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos());
+		ai->pathfinder->calculatePathInfo(paths, town->visitablePos());
 
 		for(auto & path : paths)
 		{
 			if(town->visitingHero && town->visitingHero.get() != path.targetHero)
 				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.targetHero->mana == path.targetHero->manaLimit())

+ 1 - 1
AI/Nullkiller/Behaviors/StayAtTownBehavior.h

@@ -25,7 +25,7 @@ namespace Goals
 		{
 		}
 
-		TGoalVec decompose() const override;
+		TGoalVec decompose(const Nullkiller * ai) const override;
 		std::string toString() const override;
 
 		bool operator==(const StayAtTownBehavior & other) const override

+ 1 - 0
AI/Nullkiller/CMakeLists.txt

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

+ 8 - 3
AI/Nullkiller/Engine/DeepDecomposer.cpp

@@ -26,6 +26,11 @@ namespace NKAI
 
 using namespace Goals;
 
+DeepDecomposer::DeepDecomposer(const Nullkiller * ai)
+	:ai(ai), depth(0)
+{
+}
+
 void DeepDecomposer::reset()
 {
 	decompositionCache.clear();
@@ -136,7 +141,7 @@ Goals::TSubgoal DeepDecomposer::aggregateGoals(int startDepth, TSubgoal last)
 
 Goals::TSubgoal DeepDecomposer::unwrapComposition(Goals::TSubgoal goal)
 {
-	return goal->goalType == Goals::COMPOSITION ? goal->decompose().back() : goal;
+	return goal->goalType == Goals::COMPOSITION ? goal->decompose(ai).back() : goal;
 }
 
 bool isEquivalentGoals(TSubgoal goal1, TSubgoal goal2)
@@ -156,7 +161,7 @@ bool isEquivalentGoals(TSubgoal goal1, TSubgoal goal2)
 
 bool DeepDecomposer::isCompositionLoop(TSubgoal goal)
 {
-	auto goalsToTest = goal->goalType == Goals::COMPOSITION ? goal->decompose() : TGoalVec{goal};
+	auto goalsToTest = goal->goalType == Goals::COMPOSITION ? goal->decompose(ai) : TGoalVec{goal};
 
 	for(auto goalToTest : goalsToTest)
 	{
@@ -206,7 +211,7 @@ TGoalVec DeepDecomposer::decomposeCached(TSubgoal goal, bool & fromCache)
 
 	fromCache = false;
 
-	return goal->decompose();
+	return goal->decompose(ai);
 }
 
 void DeepDecomposer::addToCache(TSubgoal goal)

+ 2 - 0
AI/Nullkiller/Engine/DeepDecomposer.h

@@ -30,8 +30,10 @@ private:
 	std::vector<Goals::TGoalVec> goals;
 	std::vector<TGoalHashSet> decompositionCache;
 	int depth;
+	const Nullkiller * ai;
 
 public:
+	DeepDecomposer(const Nullkiller * ai);
 	void reset();
 	Goals::TGoalVec decompose(Goals::TSubgoal behavior, int depthLimit);
 

+ 28 - 26
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -37,10 +37,11 @@ Nullkiller::Nullkiller()
 	settings = std::make_unique<Settings>();
 }
 
-void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
+void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
 {
 	this->cb = cb;
-	this->playerID = playerID;
+	this->gateway = gateway;
+	this->playerID = gateway->playerID;
 
 	baseGraph.reset();
 
@@ -59,7 +60,7 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
 	pathfinder.reset(new AIPathfinder(cb.get(), this));
 	armyManager.reset(new ArmyManager(cb.get(), this));
 	heroManager.reset(new HeroManager(cb.get(), this));
-	decomposer.reset(new DeepDecomposer());
+	decomposer.reset(new DeepDecomposer(this));
 	armyFormation.reset(new ArmyFormation(cb, this));
 }
 
@@ -120,12 +121,11 @@ void Nullkiller::resetAiState()
 
 	lockedResources = TResources();
 	scanDepth = ScanDepth::MAIN_FULL;
-	playerID = ai->playerID;
 	lockedHeroes.clear();
 	dangerHitMap->reset();
 	useHeroChain = true;
 
-	if(!baseGraph && ai->nullkiller->settings->isObjectGraphAllowed())
+	if(!baseGraph && settings->isObjectGraphAllowed())
 	{
 		baseGraph = std::make_unique<ObjectGraph>();
 		baseGraph->updateGraph(this);
@@ -188,7 +188,10 @@ void Nullkiller::updateAiState(int pass, bool fast)
 
 		if(settings->isObjectGraphAllowed())
 		{
-			pathfinder->updateGraphs(activeHeroes);
+			pathfinder->updateGraphs(
+				activeHeroes,
+				scanDepth == ScanDepth::SMALL ? 255 : 10,
+				scanDepth == ScanDepth::ALL_FULL ? 255 : 3);
 		}
 
 		boost::this_thread::interruption_point();
@@ -250,6 +253,7 @@ void Nullkiller::makeTurn()
 
 	for(int i = 1; i <= settings->getMaxPass(); i++)
 	{
+		auto start = std::chrono::high_resolution_clock::now();
 		updateAiState(i);
 
 		Goals::TTask bestTask = taskptr(Goals::Invalid());
@@ -303,8 +307,7 @@ void Nullkiller::makeTurn()
 
 		// TODO: better to check turn distance here instead of priority
 		if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
-			&& scanDepth == ScanDepth::MAIN_FULL
-			&& !settings->isObjectGraphAllowed())
+			&& scanDepth == ScanDepth::MAIN_FULL)
 		{
 			useHeroChain = false;
 			scanDepth = ScanDepth::SMALL;
@@ -317,25 +320,22 @@ void Nullkiller::makeTurn()
 
 		if(bestTask->priority < MIN_PRIORITY)
 		{
-			if(!settings->isObjectGraphAllowed())
+			auto heroes = cb->getHeroesInfo();
+			auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
+				{
+					return h->movementPointsRemaining() > 100;
+				});
+
+			if(hasMp && scanDepth != ScanDepth::ALL_FULL)
 			{
-				auto heroes = cb->getHeroesInfo();
-				auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
-					{
-						return h->movementPointsRemaining() > 100;
-					});
+				logAi->trace(
+					"Goal %s has too low priority %f so increasing scan depth to full.",
+					taskDescription,
+					bestTask->priority);
 
-				if(hasMp && scanDepth != ScanDepth::ALL_FULL)
-				{
-					logAi->trace(
-						"Goal %s has too low priority %f so increasing scan depth to full.",
-						taskDescription,
-						bestTask->priority);
-
-					scanDepth = ScanDepth::ALL_FULL;
-					useHeroChain = false;
-					continue;
-				}
+				scanDepth = ScanDepth::ALL_FULL;
+				useHeroChain = false;
+				continue;
 			}
 
 			logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", taskDescription);
@@ -343,6 +343,8 @@ void Nullkiller::makeTurn()
 			return;
 		}
 
+		logAi->debug("Decission madel in %ld", timeElapsed(start));
+
 		executeTask(bestTask);
 
 		if(i == settings->getMaxPass())
@@ -362,7 +364,7 @@ void Nullkiller::executeTask(Goals::TTask task)
 
 	try
 	{
-		task->accept(ai);
+		task->accept(gateway);
 		logAi->trace("Task %s completed in %lld", taskDescr, timeElapsed(start));
 	}
 	catch(goalFulfilledException &)

+ 2 - 1
AI/Nullkiller/Engine/Nullkiller.h

@@ -57,6 +57,7 @@ private:
 	ScanDepth scanDepth;
 	TResources lockedResources;
 	bool useHeroChain;
+	AIGateway * gateway;
 
 public:
 	static std::unique_ptr<ObjectGraph> baseGraph;
@@ -79,7 +80,7 @@ public:
 	std::mutex aiStateMutex;
 
 	Nullkiller();
-	void init(std::shared_ptr<CCallback> cb, PlayerColor playerID);
+	void init(std::shared_ptr<CCallback> cb, AIGateway * gateway);
 	void makeTurn();
 	bool isActive(const CGHeroInstance * hero) const { return activeHero == hero; }
 	bool isHeroLocked(const CGHeroInstance * hero) const;

+ 2 - 2
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -709,7 +709,7 @@ public:
 
 		Goals::HeroExchange & heroExchange = dynamic_cast<Goals::HeroExchange &>(*task);
 
-		uint64_t armyStrength = heroExchange.getReinforcementArmyStrength();
+		uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
 
 		evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength());
 		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get());
@@ -1064,7 +1064,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
 
 	if(goal->goalType == Goals::COMPOSITION)
 	{
-		parts = goal->decompose();
+		parts = goal->decompose(ai);
 	}
 	else
 	{

+ 2 - 1
AI/Nullkiller/Goals/AbstractGoal.h

@@ -21,6 +21,7 @@ namespace NKAI
 struct HeroPtr;
 class AIGateway;
 class FuzzyHelper;
+class Nullkiller;
 
 namespace Goals
 {
@@ -128,7 +129,7 @@ namespace Goals
 			return const_cast<AbstractGoal *>(this);
 		}
 
-		virtual TGoalVec decompose() const
+		virtual TGoalVec decompose(const Nullkiller * ai) const
 		{
 			return TGoalVec();
 		}

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

@@ -1,6 +1,3 @@
-namespace Nullkiller
-{
-
 /*
 * Build.cpp, part of VCMI engine
 *
@@ -23,6 +20,8 @@ namespace Nullkiller
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/StringConstants.h"
 
+namespace Nullkiller
+{
 
 extern boost::thread_specific_ptr<CCallback> cb;
 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
 *
@@ -14,6 +11,9 @@ namespace Nullkiller
 
 #include "CGoal.h"
 
+namespace Nullkiller
+{
+
 struct HeroPtr;
 class AIGateway;
 class FuzzyHelper;

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

@@ -54,9 +54,9 @@ namespace Goals
 
 		virtual bool operator==(const T & other) const = 0;
 
-		TGoalVec decompose() const override
+		TGoalVec decompose(const Nullkiller * ai) const override
 		{
-			TSubgoal single = decomposeSingle();
+			TSubgoal single = decomposeSingle(ai);
 
 			if(!single || single->invalid())
 				return {};
@@ -65,7 +65,7 @@ namespace Goals
 		}
 
 	protected:
-		virtual TSubgoal decomposeSingle() const
+		virtual TSubgoal decomposeSingle(const Nullkiller * ai) const
 		{
 			return TSubgoal();
 		}

+ 2 - 2
AI/Nullkiller/Goals/CaptureObject.cpp

@@ -35,9 +35,9 @@ std::string CaptureObject::toString() const
 	return "Capture " + name + " at " + tile.toString();
 }
 
-TGoalVec CaptureObject::decompose() const
+TGoalVec CaptureObject::decompose(const Nullkiller * ai) const
 {
-	return CaptureObjectsBehavior(cb->getObj(ObjectInstanceID(objid))).decompose();
+	return CaptureObjectsBehavior(ai->cb->getObj(ObjectInstanceID(objid))).decompose(ai);
 }
 
 }

+ 1 - 1
AI/Nullkiller/Goals/CaptureObject.h

@@ -35,7 +35,7 @@ namespace Goals
 		}
 
 		bool operator==(const CaptureObject & other) const override;
-		Goals::TGoalVec decompose() const override;
+		Goals::TGoalVec decompose(const Nullkiller * ai) const override;
 		std::string toString() const override;
 		bool hasHash() const override { return true; }
 		uint64_t getHash() const override;

+ 34 - 34
AI/Nullkiller/Goals/CompleteQuest.cpp

@@ -29,36 +29,36 @@ std::string CompleteQuest::toString() const
 	return "Complete quest " + questToString();
 }
 
-TGoalVec CompleteQuest::decompose() const
+TGoalVec CompleteQuest::decompose(const Nullkiller * ai) const
 {
 	if(isKeyMaster(q))
 	{
-		return missionKeymaster();
+		return missionKeymaster(ai);
 	}
 
 	logAi->debug("Trying to realize quest: %s", questToString());
 	
 	if(!q.quest->mission.artifacts.empty())
-		return missionArt();
+		return missionArt(ai);
 
 	if(!q.quest->mission.heroes.empty())
-		return missionHero();
+		return missionHero(ai);
 
 	if(!q.quest->mission.creatures.empty())
-		return missionArmy();
+		return missionArmy(ai);
 
 	if(q.quest->mission.resources.nonZero())
-		return missionResources();
+		return missionResources(ai);
 
 	if(q.quest->killTarget != ObjectInstanceID::NONE)
-		return missionDestroyObj();
+		return missionDestroyObj(ai);
 
 	for(auto & s : q.quest->mission.primary)
 		if(s)
-			return missionIncreasePrimaryStat();
+			return missionIncreasePrimaryStat(ai);
 
 	if(q.quest->mission.heroLevel > 0)
-		return missionLevel();
+		return missionLevel(ai);
 
 	return TGoalVec();
 }
@@ -103,21 +103,21 @@ std::string CompleteQuest::questToString() const
 	return ms.toString();
 }
 
-TGoalVec CompleteQuest::tryCompleteQuest() const
+TGoalVec CompleteQuest::tryCompleteQuest(const Nullkiller * ai) const
 {
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(q.obj->visitablePos());
+	auto paths = ai->pathfinder->getPathInfo(q.obj->visitablePos());
 
 	vstd::erase_if(paths, [&](const AIPath & path) -> bool
 	{
 		return !q.quest->checkQuest(path.targetHero);
 	});
 	
-	return CaptureObjectsBehavior::getVisitGoals(paths, q.obj);
+	return CaptureObjectsBehavior::getVisitGoals(paths, ai, q.obj);
 }
 
-TGoalVec CompleteQuest::missionArt() const
+TGoalVec CompleteQuest::missionArt(const Nullkiller * ai) const
 {
-	TGoalVec solutions = tryCompleteQuest();
+	TGoalVec solutions = tryCompleteQuest(ai);
 
 	if(!solutions.empty())
 		return solutions;
@@ -132,9 +132,9 @@ TGoalVec CompleteQuest::missionArt() const
 	return solutions;
 }
 
-TGoalVec CompleteQuest::missionHero() const
+TGoalVec CompleteQuest::missionHero(const Nullkiller * ai) const
 {
-	TGoalVec solutions = tryCompleteQuest();
+	TGoalVec solutions = tryCompleteQuest(ai);
 
 	if(solutions.empty())
 	{
@@ -145,43 +145,43 @@ TGoalVec CompleteQuest::missionHero() const
 	return solutions;
 }
 
-TGoalVec CompleteQuest::missionArmy() const
+TGoalVec CompleteQuest::missionArmy(const Nullkiller * ai) const
 {
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(q.obj->visitablePos());
+	auto paths = ai->pathfinder->getPathInfo(q.obj->visitablePos());
 
 	vstd::erase_if(paths, [&](const AIPath & path) -> bool
 	{
 		return !CQuest::checkMissionArmy(q.quest, path.heroArmy);
 	});
 
-	return CaptureObjectsBehavior::getVisitGoals(paths, q.obj);
+	return CaptureObjectsBehavior::getVisitGoals(paths, ai, q.obj);
 }
 
-TGoalVec CompleteQuest::missionIncreasePrimaryStat() const
+TGoalVec CompleteQuest::missionIncreasePrimaryStat(const Nullkiller * ai) const
 {
-	return tryCompleteQuest();
+	return tryCompleteQuest(ai);
 }
 
-TGoalVec CompleteQuest::missionLevel() const
+TGoalVec CompleteQuest::missionLevel(const Nullkiller * ai) const
 {
-	return tryCompleteQuest();
+	return tryCompleteQuest(ai);
 }
 
-TGoalVec CompleteQuest::missionKeymaster() const
+TGoalVec CompleteQuest::missionKeymaster(const Nullkiller * ai) const
 {
-	if(isObjectPassable(q.obj))
+	if(isObjectPassable(ai, q.obj))
 	{
-		return CaptureObjectsBehavior(q.obj).decompose();
+		return CaptureObjectsBehavior(q.obj).decompose(ai);
 	}
 	else
 	{
-		return CaptureObjectsBehavior().ofType(Obj::KEYMASTER, q.obj->subID).decompose();
+		return CaptureObjectsBehavior().ofType(Obj::KEYMASTER, q.obj->subID).decompose(ai);
 	}
 }
 
-TGoalVec CompleteQuest::missionResources() const
+TGoalVec CompleteQuest::missionResources(const Nullkiller * ai) const
 {
-	TGoalVec solutions = tryCompleteQuest();
+	TGoalVec solutions = tryCompleteQuest(ai);
 
 	/*auto heroes = cb->getHeroesInfo(); //TODO: choose best / free hero from among many possibilities?
 
@@ -208,14 +208,14 @@ TGoalVec CompleteQuest::missionResources() const
 	return solutions;
 }
 
-TGoalVec CompleteQuest::missionDestroyObj() const
+TGoalVec CompleteQuest::missionDestroyObj(const Nullkiller * ai) const
 {
-	auto obj = cb->getObj(q.quest->killTarget);
+	auto obj = ai->cb->getObj(q.quest->killTarget);
 
 	if(!obj)
-		return CaptureObjectsBehavior(q.obj).decompose();
+		return CaptureObjectsBehavior(q.obj).decompose(ai);
 
-	auto relations = cb->getPlayerRelations(ai->playerID, obj->tempOwner);
+	auto relations = ai->cb->getPlayerRelations(ai->playerID, obj->tempOwner);
 
 	//if(relations == PlayerRelations::SAME_PLAYER)
 	//{
@@ -226,7 +226,7 @@ TGoalVec CompleteQuest::missionDestroyObj() const
 	//else 
 	if(relations == PlayerRelations::ENEMIES)
 	{
-		return CaptureObjectsBehavior(obj).decompose();
+		return CaptureObjectsBehavior(obj).decompose(ai);
 	}
 
 	return TGoalVec();

+ 10 - 10
AI/Nullkiller/Goals/CompleteQuest.h

@@ -29,7 +29,7 @@ namespace Goals
 		{
 		}
 
-		Goals::TGoalVec decompose() const override;
+		Goals::TGoalVec decompose(const Nullkiller * ai) const override;
 		std::string toString() const override;
 		bool hasHash() const override { return true; }
 		uint64_t getHash() const override;
@@ -37,15 +37,15 @@ namespace Goals
 		bool operator==(const CompleteQuest & other) const override;
 
 	private:
-		TGoalVec tryCompleteQuest() const;
-		TGoalVec missionArt() const;
-		TGoalVec missionHero() const;
-		TGoalVec missionArmy() const;
-		TGoalVec missionResources() const;
-		TGoalVec missionDestroyObj() const;
-		TGoalVec missionIncreasePrimaryStat() const;
-		TGoalVec missionLevel() const;
-		TGoalVec missionKeymaster() const;
+		TGoalVec tryCompleteQuest(const Nullkiller * ai) const;
+		TGoalVec missionArt(const Nullkiller * ai) const;
+		TGoalVec missionHero(const Nullkiller * ai) const;
+		TGoalVec missionArmy(const Nullkiller * ai) const;
+		TGoalVec missionResources(const Nullkiller * ai) const;
+		TGoalVec missionDestroyObj(const Nullkiller * ai) const;
+		TGoalVec missionIncreasePrimaryStat(const Nullkiller * ai) const;
+		TGoalVec missionLevel(const Nullkiller * ai) const;
+		TGoalVec missionKeymaster(const Nullkiller * ai) const;
 		std::string questToString() const;
 	};
 }

+ 1 - 1
AI/Nullkiller/Goals/Composition.cpp

@@ -59,7 +59,7 @@ void Composition::accept(AIGateway * ai)
 	}
 }
 
-TGoalVec Composition::decompose() const
+TGoalVec Composition::decompose(const Nullkiller * ai) const
 {
 	TGoalVec result;
 

+ 1 - 1
AI/Nullkiller/Goals/Composition.h

@@ -32,7 +32,7 @@ namespace Goals
 		Composition & addNext(const AbstractGoal & goal);
 		Composition & addNext(TSubgoal goal);
 		Composition & addNextSequence(const TGoalVec & taskSequence);
-		TGoalVec decompose() const override;
+		TGoalVec decompose(const Nullkiller * ai) const override;
 		bool isElementar() const override;
 		int getHeroExchangeCount() const override;
 	};

+ 47 - 23
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -67,40 +67,40 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 
 	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;
 
-		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))
 		{
-			blockedIndexes.insert(node.parentIndex);
+			blockedIndexes.insert(node->parentIndex);
 			ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 
 			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
 		{
 			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.");
 					}
 					
-					node.specialAction->execute(hero);
+					node->specialAction->execute(ai, hero);
 					
 					if(!heroPtr.validAndSet())
 					{
@@ -109,10 +109,34 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						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
 						|| targetNode->accessible == EPathAccessibility::BLOCKED
@@ -122,7 +146,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						logAi->error(
 							"Unable to complete chain. Expected hero %s to arrive to %s in 0 turns but he cannot do this",
 							hero->getNameTranslated(),
-							node.coord.toString());
+							node->coord.toString());
 
 						return;
 					}
@@ -132,7 +156,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 				{
 					try
 					{
-						if(moveHeroToTile(hero, node.coord))
+						if(moveHeroToTile(ai, hero, node->coord))
 						{
 							continue;
 						}
@@ -149,11 +173,11 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						if(hero->movementPointsRemaining() > 0)
 						{
 							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)
 							{
-								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);
 								return;
@@ -165,15 +189,15 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 				}
 			}
 
-			if(node.coord == hero->visitablePos())
+			if(node->coord == hero->visitablePos())
 				continue;
 
-			if(node.turns == 0)
+			if(node->turns == 0)
 			{
 				logAi->error(
 					"Unable to complete chain. Expected hero %s to arive to %s but he is at %s",
 					hero->getNameTranslated(),
-					node.coord.toString(),
+					node->coord.toString(),
 					hero->visitablePos().toString());
 
 				return;
@@ -181,13 +205,13 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 			
 			// no exception means we were not able to reach the tile
 			ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
-			blockedIndexes.insert(node.parentIndex);
+			blockedIndexes.insert(node->parentIndex);
 		}
 		catch(const goalFulfilledException &)
 		{
 			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;
 			}
@@ -200,9 +224,9 @@ std::string ExecuteHeroChain::toString() const
 	return "ExecuteHeroChain " + targetName + " by " + chainPath.targetHero->getNameTranslated();
 }
 
-bool ExecuteHeroChain::moveHeroToTile(const CGHeroInstance * hero, const int3 & tile)
+bool ExecuteHeroChain::moveHeroToTile(AIGateway * ai, const CGHeroInstance * hero, const int3 & tile)
 {
-	if(tile == hero->visitablePos() && cb->getVisitableObjs(hero->visitablePos()).size() < 2)
+	if(tile == hero->visitablePos() && ai->myCb->getVisitableObjs(hero->visitablePos()).size() < 2)
 	{
 		logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", hero->getNameTranslated(), tile.toString());
 

+ 1 - 2
AI/Nullkiller/Goals/ExecuteHeroChain.h

@@ -27,7 +27,6 @@ namespace Goals
 
 		ExecuteHeroChain(const AIPath & path, const CGObjectInstance * obj = nullptr);
 
-		
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
 		bool operator==(const ExecuteHeroChain & other) const override;
@@ -36,7 +35,7 @@ namespace Goals
 		int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
 
 	private:
-		bool moveHeroToTile(const CGHeroInstance * hero, const int3 & tile);
+		bool moveHeroToTile(AIGateway * ai, const CGHeroInstance * hero, const int3 & tile);
 	};
 }
 

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

@@ -1,6 +1,3 @@
-namespace Nullkiller
-{
-
 /*
 * GatherArmy.cpp, part of VCMI engine
 *
@@ -22,6 +19,8 @@ namespace Nullkiller
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/StringConstants.h"
 
+namespace Nullkiller
+{
 
 extern boost::thread_specific_ptr<CCallback> cb;
 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
 *
@@ -14,6 +11,9 @@ namespace Nullkiller
 
 #include "CGoal.h"
 
+namespace Nullkiller
+{
+
 struct HeroPtr;
 class AIGateway;
 class FuzzyHelper;

+ 1 - 1
AI/Nullkiller/Goals/Invalid.h

@@ -27,7 +27,7 @@ namespace Goals
 		{
 			priority = -1;
 		}
-		TGoalVec decompose() const override
+		TGoalVec decompose(const Nullkiller * ai) const override
 		{
 			return TGoalVec();
 		}

+ 2 - 2
AI/Nullkiller/Markers/HeroExchange.cpp

@@ -29,9 +29,9 @@ std::string HeroExchange::toString() const
 	return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString();
 }
 
-uint64_t HeroExchange::getReinforcementArmyStrength() const
+uint64_t HeroExchange::getReinforcementArmyStrength(const Nullkiller * ai) const
 {
-	uint64_t armyValue = ai->nullkiller->armyManager->howManyReinforcementsCanGet(hero.get(), exchangePath.heroArmy);
+	uint64_t armyValue = ai->armyManager->howManyReinforcementsCanGet(hero.get(), exchangePath.heroArmy);
 
 	return armyValue;
 }

+ 1 - 1
AI/Nullkiller/Markers/HeroExchange.h

@@ -31,7 +31,7 @@ namespace Goals
 		bool operator==(const HeroExchange & other) const override;
 		std::string toString() const override;
 
-		uint64_t getReinforcementArmyStrength() const;
+		uint64_t getReinforcementArmyStrength(const Nullkiller * ai) const;
 	};
 }
 

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

@@ -24,7 +24,8 @@
 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;
 std::set<int3> commitedTiles;
 std::set<int3> commitedTilesInitial;
@@ -40,11 +41,24 @@ const bool DO_NOT_SAVE_TO_COMMITED_TILES = false;
 AISharedStorage::AISharedStorage(int3 sizes)
 {
 	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()
@@ -80,6 +94,9 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
 AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & 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));
 }
 
@@ -90,6 +107,8 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
 	if(heroChainPass)
 		return;
 
+	AISharedStorage::version++;
+
 	//TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline
 	const PlayerColor fowPlayer = ai->playerID;
 	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
 
-	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;
 
@@ -152,9 +171,9 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 {
 	int bucketIndex = ((uintptr_t)actor) % AIPathfinding::BUCKET_COUNT;
 	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;
 	}
@@ -163,15 +182,17 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 	{
 		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;
 		}
 
-		if(!node.actor)
+		if(node.actor == actor && node.layer == layer)
 		{
-			node.actor = actor;
-
 			return &node;
 		}
 	}
@@ -226,21 +247,6 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 	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)
 {
 	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,
+	EPathfindingLayer layer,
 	const PathfinderConfig * pathfinderConfig,
 	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);
-	auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
 
 	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};
@@ -346,19 +353,16 @@ bool AINodeStorage::increaseHeroChainTurnLimit()
 	{
 		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)
 					{
 						commitedTiles.insert(pos);
-						break;
+						return true;
 					}
-				}
-			}
+
+					return false;
+				});
 		});
 	}
 
@@ -374,22 +378,17 @@ bool AINodeStorage::calculateHeroChainFinal()
 	{
 		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
 						&& !node.locked
 						&& node.action != EPathNodeAction::UNKNOWN
 						&& node.actor->actorExchangeCount > 1
-						&& !hasBetterChain(&node, &node, chains))
+						&& !hasBetterChain(&node, node))
 					{
 						heroChain.push_back(&node);
 					}
-				}
-			}
+				});
 		});
 	}
 
@@ -413,7 +412,6 @@ struct DelayedWork
 class HeroChainCalculationTask
 {
 private:
-	AISharedStorage & nodes;
 	AINodeStorage & storage;
 	std::vector<AIPathNode *> existingChains;
 	std::vector<ExchangeCandidate> newChains;
@@ -425,14 +423,14 @@ private:
 
 public:
 	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);
 		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::mt19937 randomEngine(randomDevice());
@@ -443,21 +441,19 @@ public:
 
 			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;
 
-				existingChains.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);
 
 				for(AIPathNode * node : existingChains)
@@ -530,10 +526,10 @@ bool AINodeStorage::calculateHeroChain()
 
 		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());
-			HeroChainCalculationTask task(*this, nodes, data, chainMask, heroChainTurn);
+			HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
 
 			task.execute(r);
 
@@ -546,8 +542,8 @@ bool AINodeStorage::calculateHeroChain()
 	}
 	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.flushResult(heroChain);
@@ -612,14 +608,20 @@ bool AINodeStorage::selectNextActor()
 	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
 {
 	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(isNotEffective)
@@ -645,7 +647,7 @@ void HeroChainCalculationTask::calculateHeroChain(
 {
 	for(AIPathNode * node : variants)
 	{
-		if(node == srcNode || !node->actor)
+		if(node == srcNode || !node->actor || node->version != AISharedStorage::version)
 			continue;
 
 		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)
 	{
-		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++)
 				{
@@ -1195,95 +1197,116 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
 
 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(
-	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
 			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(),
-				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
 			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
-				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
-				return true;
-			}
+			return true;
 		}
 	}
 
@@ -1292,12 +1315,15 @@ bool AINodeStorage::hasBetterChain(
 
 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)
 	{
-		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;
 		}
@@ -1306,22 +1332,23 @@ bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, con
 	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)
 	{
-		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;
 		}
 
-		AIPath path;
+		AIPath & path = paths.emplace_back();
 
 		path.targetHero = node.actor->hero;
 		path.heroArmy = node.actor->creatureSet;
@@ -1332,11 +1359,7 @@ std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand)
 		path.exchangeCount = node.actor->actorExchangeCount;
 		
 		fillChainInfo(&node, path, -1);
-
-		paths.push_back(path);
 	}
-
-	return paths;
 }
 
 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)
 			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(ai, targetNode);
 		}
-		
+
+		parentIndex = path.nodes.size();
+
+		path.nodes.push_back(pathNode);
 		node = getAINode(node->theNodeBefore);
 	}
 }

+ 97 - 30
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -27,8 +27,8 @@ namespace NKAI
 {
 namespace AIPathfinding
 {
-	const int BUCKET_COUNT = 5;
-	const int BUCKET_SIZE = 3;
+	const int BUCKET_COUNT = 3;
+	const int BUCKET_SIZE = 7;
 	const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
 	const int CHAIN_MAX_DEPTH = 4;
 }
@@ -49,15 +49,24 @@ struct AIPathNode : public CGPathNode
 	const AIPathNode * chainOther;
 	std::shared_ptr<const SpecialAction> specialAction;
 	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
@@ -133,21 +142,21 @@ enum EHeroChainPass
 
 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:
 	static boost::mutex locker;
+	static uint64_t version;
 
 	AISharedStorage(int3 mapSize);
 	~AISharedStorage();
 
 	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:
 	int3 sizes;
 
+	std::unique_ptr<boost::multi_array<EPathAccessibility, 4>> accesibility;
+
 	const CPlayerSpecificInfoCallback * cb;
 	const Nullkiller * ai;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
@@ -182,8 +193,10 @@ public:
 
 	std::vector<CGPathNode *> getInitialNodes() override;
 
-	virtual std::vector<CGPathNode *> calculateNeighbours(
+	virtual void calculateNeighbours(
+		std::vector<CGPathNode *> & result,
 		const PathNodeInfo & source,
+		EPathfindingLayer layer,
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) override;
 
@@ -222,23 +235,37 @@ public:
 		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>
 	bool hasBetterChain(
 		const CGPathNode * source, 
-		const AIPathNode * destinationNode,
+		const AIPathNode & destinationNode,
 		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::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;
 	void setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes);
 	void setScoutTurnDistanceLimit(uint8_t distanceLimit) { turnDistanceLimit[HeroRole::SCOUT] = distanceLimit; }
@@ -256,17 +283,19 @@ public:
 		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;
 	}
@@ -274,6 +303,44 @@ public:
 	void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);
 	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:
 	template<class TVector>
 	void calculateTownPortal(

+ 37 - 11
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -17,6 +17,8 @@
 namespace NKAI
 {
 
+std::map<ObjectInstanceID, std::unique_ptr<GraphPaths>>  AIPathfinder::heroGraphs;
+
 AIPathfinder::AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * ai)
 	:cb(cb), ai(ai)
 {
@@ -33,16 +35,31 @@ bool AIPathfinder::isTileAccessible(const HeroPtr & hero, const int3 & tile) con
 		|| 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);
 
+	result.clear();
+
 	if(!tileInfo)
 	{
-		return std::vector<AIPath>();
+		return;
 	}
 
-	auto info = storage->getChainInfo(tile, !tileInfo->isWater());
+	storage->calculateChainInfo(result, tile, !tileInfo->isWater());
 
 	if(includeGraph)
 	{
@@ -51,11 +68,9 @@ std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile, bool includeGra
 			auto graph = heroGraphs.find(hero->id);
 
 			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)
@@ -125,7 +140,10 @@ void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole>
 	logAi->trace("Recalculated paths in %ld", timeElapsed(start));
 }
 
-void AIPathfinder::updateGraphs(const std::map<const CGHeroInstance *, HeroRole> & heroes)
+void AIPathfinder::updateGraphs(
+	const std::map<const CGHeroInstance *, HeroRole> & heroes,
+	uint8_t mainScanDepth,
+	uint8_t scoutScanDepth)
 {
 	auto start = std::chrono::high_resolution_clock::now();
 	std::vector<const CGHeroInstance *> heroesVector;
@@ -134,21 +152,29 @@ void AIPathfinder::updateGraphs(const std::map<const CGHeroInstance *, HeroRole>
 
 	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);
+		}
 	}
 
-	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, &heroes, mainScanDepth, scoutScanDepth](const tbb::blocked_range<size_t> & r)
 		{
 			for(auto i = r.begin(); i != r.end(); i++)
-				heroGraphs.at(heroesVector[i]->id).calculatePaths(heroesVector[i], ai);
+			{
+				auto role = heroes.at(heroesVector[i]);
+				auto scanLimit = role == HeroRole::MAIN ? mainScanDepth : scoutScanDepth;
+
+				heroGraphs.at(heroesVector[i]->id)->calculatePaths(heroesVector[i], ai, scanLimit);
+			}
 		});
 
 	if(NKAI_GRAPH_TRACE_LEVEL >= 1)
 	{
 		for(auto hero : heroes)
 		{
-			heroGraphs[hero.first->id].dumpToLog();
+			heroGraphs[hero.first->id]->dumpToLog();
 		}
 	}
 

+ 13 - 3
AI/Nullkiller/Pathfinding/AIPathfinder.h

@@ -40,20 +40,30 @@ private:
 	std::shared_ptr<AINodeStorage> storage;
 	CPlayerSpecificInfoCallback * cb;
 	Nullkiller * ai;
-	std::map<ObjectInstanceID, GraphPaths>  heroGraphs;
+	static std::map<ObjectInstanceID, std::unique_ptr<GraphPaths>>  heroGraphs;
 
 public:
 	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;
 	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, uint8_t mainScanDepth, uint8_t scoutScanDepth);
+	void calculateQuickPathsWithBlocker(std::vector<AIPath> & result, const std::vector<const CGHeroInstance *> & heroes, const int3 & tile);
 	void init();
 
 	std::shared_ptr<AINodeStorage>getStorage()
 	{
 		return storage;
 	}
+
+	std::vector<AIPath> getPathInfo(const int3 & tile, bool includeGraph = false)
+	{
+		std::vector<AIPath> result;
+
+		calculatePathInfo(result, tile, includeGraph);
+
+		return result;
+	}
 };
 
 }

+ 1 - 1
AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp

@@ -33,7 +33,7 @@ namespace AIPathfinding
 				std::make_shared<AIMovementToDestinationRule>(nodeStorage, allowBypassObjects),
 				std::make_shared<MovementCostRule>(),
 				std::make_shared<AIPreviousNodeRule>(nodeStorage),
-				std::make_shared<AIMovementAfterDestinationRule>(cb, nodeStorage, allowBypassObjects)
+				std::make_shared<AIMovementAfterDestinationRule>(ai, cb, nodeStorage, allowBypassObjects)
 			};
 
 		return rules;

+ 2 - 2
AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp

@@ -49,14 +49,14 @@ namespace AIPathfinding
 		dstNode->dayFlags = static_cast<DayFlags>(dstNode->dayFlags | flagsToAdd);
 	}
 
-	void AdventureCastAction::execute(const CGHeroInstance * hero) const
+	void AdventureCastAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
 	{
 		assert(hero == this->hero);
 
 		Goals::AdventureSpellCast(hero, spellToCast).accept(ai);
 	}
 
-	bool AdventureCastAction::canAct(const AIPathNode * source) const
+	bool AdventureCastAction::canAct(const Nullkiller * ai, const AIPathNode * source) const
 	{
 		assert(hero == this->hero);
 

+ 2 - 2
AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h

@@ -29,7 +29,7 @@ namespace AIPathfinding
 	public:
 		AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero, DayFlags flagsToAdd = DayFlags::NONE);
 
-		void execute(const CGHeroInstance * hero) const override;
+		void execute(AIGateway * ai, const CGHeroInstance * hero) const override;
 
 		virtual void applyOnDestination(
 			const CGHeroInstance * hero,
@@ -38,7 +38,7 @@ namespace AIPathfinding
 			AIPathNode * dstMode,
 			const AIPathNode * srcNode) const override;
 
-		bool canAct(const AIPathNode * source) const override;
+		bool canAct(const Nullkiller * ai, const AIPathNode * source) const override;
 
 		std::string toString() const override;
 	};

+ 1 - 1
AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp

@@ -18,7 +18,7 @@ namespace NKAI
 
 namespace AIPathfinding
 {
-	void BattleAction::execute(const CGHeroInstance * hero) const
+	void BattleAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
 	{
 		ai->moveHeroToTile(targetTile, hero);
 	}

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

@@ -28,7 +28,7 @@ namespace AIPathfinding
 		{
 		}
 
-		void execute(const CGHeroInstance * hero) const override;
+		void execute(AIGateway * ai, const CGHeroInstance * hero) const override;
 
 		std::string toString() const override;
 	};

+ 23 - 8
AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp

@@ -22,12 +22,12 @@ namespace NKAI
 
 namespace AIPathfinding
 {
-	void BuildBoatAction::execute(const CGHeroInstance * hero) const
+	void BuildBoatAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
 	{
 		return Goals::BuildBoat(shipyard).accept(ai);
 	}
 
-	Goals::TSubgoal BuildBoatAction::decompose(const CGHeroInstance * hero) const
+	Goals::TSubgoal BuildBoatAction::decompose(const Nullkiller * ai, const CGHeroInstance * hero) const
 	{
 		if(cb->getPlayerRelations(ai->playerID, shipyard->getObject()->getOwner()) == PlayerRelations::ENEMIES)
 		{
@@ -37,10 +37,8 @@ namespace AIPathfinding
 		return Goals::sptr(Goals::Invalid());
 	}
 
-	bool BuildBoatAction::canAct(const AIPathNode * source) const
+	bool BuildBoatAction::canAct(const Nullkiller * ai, const CGHeroInstance * hero, const TResources & reservedResources) const
 	{
-		auto hero = source->actor->hero;
-
 		if(cb->getPlayerRelations(hero->tempOwner, shipyard->getObject()->getOwner()) == PlayerRelations::ENEMIES)
 		{
 #if NKAI_TRACE_LEVEL > 1
@@ -53,7 +51,7 @@ namespace AIPathfinding
 
 		shipyard->getBoatCost(boatCost);
 
-		if(!cb->getResourceAmount().canAfford(source->actor->armyCost + boatCost))
+		if(!cb->getResourceAmount().canAfford(reservedResources + boatCost))
 		{
 #if NKAI_TRACE_LEVEL > 1
 			logAi->trace("Can not build a boat. Not enough resources.");
@@ -65,6 +63,18 @@ namespace AIPathfinding
 		return true;
 	}
 
+	bool BuildBoatAction::canAct(const Nullkiller * ai, const AIPathNode * source) const
+	{
+		return canAct(ai, source->actor->hero, source->actor->armyCost);
+	}
+
+	bool BuildBoatAction::canAct(const Nullkiller * ai, const AIPathNodeInfo & source) const
+	{
+		TResources res;
+
+		return canAct(ai, source.targetHero, res);
+	}
+
 	const CGObjectInstance * BuildBoatAction::targetObject() const
 	{
 		return dynamic_cast<const CGObjectInstance*>(shipyard);
@@ -75,7 +85,12 @@ namespace AIPathfinding
 		return sourceActor->resourceActor;
 	}
 
-	void SummonBoatAction::execute(const CGHeroInstance * hero) const
+	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(AIGateway * ai, const CGHeroInstance * hero) const
 	{
 		Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai);
 	}
@@ -101,7 +116,7 @@ namespace AIPathfinding
 		return "Build Boat at " + shipyard->getObject()->visitablePos().toString();
 	}
 
-	bool SummonBoatAction::canAct(const AIPathNode * source) const
+	bool SummonBoatAction::canAct(const Nullkiller * ai, const AIPathNode * source) const
 	{
 		auto hero = source->actor->hero;
 

+ 20 - 5
AI/Nullkiller/Pathfinding/Actions/BoatActions.h

@@ -25,7 +25,7 @@ namespace AIPathfinding
 	class SummonBoatAction : public VirtualBoatAction
 	{
 	public:
-		void execute(const CGHeroInstance * hero) const override;
+		void execute(AIGateway * ai, const CGHeroInstance * hero) const override;
 
 		virtual void applyOnDestination(
 			const CGHeroInstance * hero,
@@ -34,7 +34,7 @@ namespace AIPathfinding
 			AIPathNode * dstMode,
 			const AIPathNode * srcNode) const override;
 
-		bool canAct(const AIPathNode * source) const override;
+		bool canAct(const Nullkiller * ai, const AIPathNode * source) const override;
 
 		const ChainActor * getActor(const ChainActor * sourceActor) const override;
 
@@ -56,11 +56,13 @@ namespace AIPathfinding
 		{
 		}
 
-		bool canAct(const AIPathNode * source) const override;
+		bool canAct(const Nullkiller * ai, const AIPathNode * source) const override;
+		bool canAct(const Nullkiller * ai, const AIPathNodeInfo & source) const override;
+		bool canAct(const Nullkiller * ai, const CGHeroInstance * hero, const TResources & reservedResources) const;
 
-		void execute(const CGHeroInstance * hero) const override;
+		void execute(AIGateway * ai, const CGHeroInstance * hero) const override;
 
-		Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
+		Goals::TSubgoal decompose(const Nullkiller * ai, const CGHeroInstance * hero) const override;
 
 		const ChainActor * getActor(const ChainActor * sourceActor) const override;
 
@@ -68,6 +70,19 @@ namespace AIPathfinding
 
 		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;
+	};
 }
 
 }

+ 1 - 1
AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp

@@ -18,7 +18,7 @@ namespace NKAI
 
 namespace AIPathfinding
 {
-	void BuyArmyAction::execute(const CGHeroInstance * hero) const
+	void BuyArmyAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
 	{
 		if(!hero->visitedTown)
 		{

+ 2 - 2
AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.h

@@ -21,12 +21,12 @@ namespace AIPathfinding
 	private:
 
 	public:
-		bool canAct(const AIPathNode * source) const override
+		bool canAct(const Nullkiller * ai, const AIPathNode * source) const override
 		{
 			return true;
 		}
 
-		void execute(const CGHeroInstance * hero) const override;
+		void execute(AIGateway * ai, const CGHeroInstance * hero) const override;
 		std::string toString() const override;
 	};
 }

+ 16 - 6
AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp

@@ -18,23 +18,33 @@ namespace NKAI
 
 namespace AIPathfinding
 {
-	bool QuestAction::canAct(const AIPathNode * node) const
+	bool QuestAction::canAct(const Nullkiller * ai, const AIPathNode * node) const
+	{
+		return canAct(ai, node->actor->hero);
+	}
+
+	bool QuestAction::canAct(const Nullkiller * ai, const AIPathNodeInfo & node) const
+	{
+		return canAct(ai, node.targetHero);
+	}
+
+	bool QuestAction::canAct(const Nullkiller * ai, const CGHeroInstance * hero) const
 	{
 		if(questInfo.obj->ID == Obj::BORDER_GATE || questInfo.obj->ID == Obj::BORDERGUARD)
 		{
-			return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(node->actor->hero);
+			return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(hero);
 		}
 
-		return questInfo.quest->activeForPlayers.count(node->actor->hero->getOwner())
-			|| questInfo.quest->checkQuest(node->actor->hero);
+		return questInfo.quest->activeForPlayers.count(hero->getOwner())
+			|| questInfo.quest->checkQuest(hero);
 	}
 
-	Goals::TSubgoal QuestAction::decompose(const CGHeroInstance * hero) const
+	Goals::TSubgoal QuestAction::decompose(const Nullkiller * ai, const CGHeroInstance * hero) const
 	{
 		return Goals::sptr(Goals::CompleteQuest(questInfo));
 	}
 
-	void QuestAction::execute(const CGHeroInstance * hero) const
+	void QuestAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
 	{
 		ai->moveHeroToTile(questInfo.obj->visitablePos(), hero);
 	}

+ 5 - 3
AI/Nullkiller/Pathfinding/Actions/QuestAction.h

@@ -28,11 +28,13 @@ namespace AIPathfinding
 		{
 		}
 
-		bool canAct(const AIPathNode * node) const override;
+		bool canAct(const Nullkiller * ai, const AIPathNode * node) const override;
+		bool canAct(const Nullkiller * ai, const AIPathNodeInfo & node) const override;
+		bool canAct(const Nullkiller * ai, const CGHeroInstance * hero) const;
 
-		Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
+		Goals::TSubgoal decompose(const Nullkiller * ai, const CGHeroInstance * hero) const override;
 
-		void execute(const CGHeroInstance * hero) const override;
+		void execute(AIGateway * ai, const CGHeroInstance * hero) const override;
 
 		std::string toString() const override;
 	};

+ 9 - 9
AI/Nullkiller/Pathfinding/Actions/SpecialAction.cpp

@@ -17,43 +17,43 @@
 namespace NKAI
 {
 
-Goals::TSubgoal SpecialAction::decompose(const CGHeroInstance * hero) const
+Goals::TSubgoal SpecialAction::decompose(const Nullkiller * ai, const CGHeroInstance * hero) const
 {
 	return Goals::sptr(Goals::Invalid());
 }
 
-void SpecialAction::execute(const CGHeroInstance * hero) const
+void SpecialAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
 {
 	throw cannotFulfillGoalException("Can not execute " + toString());
 }
 
-bool CompositeAction::canAct(const AIPathNode * source) const
+bool CompositeAction::canAct(const Nullkiller * ai, const AIPathNode * source) const
 {
 	for(auto part : parts)
 	{
-		if(!part->canAct(source)) return false;
+		if(!part->canAct(ai, source)) return false;
 	}
 
 	return true;
 }
 
-Goals::TSubgoal CompositeAction::decompose(const CGHeroInstance * hero) const
+Goals::TSubgoal CompositeAction::decompose(const Nullkiller * ai, const CGHeroInstance * hero) const
 {
 	for(auto part : parts)
 	{
-		auto goal = part->decompose(hero);
+		auto goal = part->decompose(ai, hero);
 
 		if(!goal->invalid()) return goal;
 	}
 
-	return SpecialAction::decompose(hero);
+	return SpecialAction::decompose(ai, hero);
 }
 
-void CompositeAction::execute(const CGHeroInstance * hero) const
+void CompositeAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
 {
 	for(auto part : parts)
 	{
-		part->execute(hero);
+		part->execute(ai, hero);
 	}
 }
 

+ 19 - 6
AI/Nullkiller/Pathfinding/Actions/SpecialAction.h

@@ -22,6 +22,7 @@ namespace NKAI
 {
 
 struct AIPathNode;
+struct AIPathNodeInfo;
 class ChainActor;
 
 class SpecialAction
@@ -29,14 +30,19 @@ class SpecialAction
 public:
 	virtual ~SpecialAction() = default;
 
-	virtual bool canAct(const AIPathNode * source) const
+	virtual bool canAct(const Nullkiller * ai, const AIPathNode * source) const
 	{
 		return true;
 	}
 
-	virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const;
+	virtual bool canAct(const Nullkiller * ai, const AIPathNodeInfo & source) const
+	{
+		return true;
+	}
+
+	virtual Goals::TSubgoal decompose(const Nullkiller * ai, const CGHeroInstance * hero) const;
 
-	virtual void execute(const CGHeroInstance * hero) const;
+	virtual void execute(AIGateway * ai, const CGHeroInstance * hero) const;
 
 	virtual void applyOnDestination(
 		const CGHeroInstance * hero,
@@ -70,11 +76,11 @@ private:
 public:
 	CompositeAction(std::vector<std::shared_ptr<const SpecialAction>> parts) : parts(parts) {}
 
-	bool canAct(const AIPathNode * source) const override;
-	void execute(const CGHeroInstance * hero) const override;
+	bool canAct(const Nullkiller * ai, const AIPathNode * source) const override;
+	void execute(AIGateway * ai, const CGHeroInstance * hero) const override;
 	std::string toString() const override;
 	const CGObjectInstance * targetObject() const override;
-	Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
+	Goals::TSubgoal decompose(const Nullkiller * ai, const CGHeroInstance * hero) const override;
 
 	std::vector<std::shared_ptr<const SpecialAction>> getParts() const override
 	{
@@ -89,4 +95,11 @@ public:
 		const AIPathNode * srcNode) const override;
 };
 
+class ISpecialActionFactory
+{
+public:
+	virtual std::shared_ptr<SpecialAction> create(const Nullkiller * ai) = 0;
+	virtual ~ISpecialActionFactory() = default;
+};
+
 }

+ 1 - 1
AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp

@@ -18,7 +18,7 @@ namespace NKAI
 
 using namespace AIPathfinding;
 
-void TownPortalAction::execute(const CGHeroInstance * hero) const
+void TownPortalAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
 {
 	auto goal = Goals::AdventureSpellCast(hero, SpellID::TOWN_PORTAL);
 	

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

@@ -29,7 +29,7 @@ namespace AIPathfinding
 		{
 		}
 
-		void execute(const CGHeroInstance * hero) const override;
+		void execute(AIGateway * ai, const CGHeroInstance * hero) const override;
 
 		std::string toString() const override;
 	};

+ 529 - 48
AI/Nullkiller/Pathfinding/ObjectGraph.cpp

@@ -15,15 +15,26 @@
 #include "../../../lib/mapping/CMap.h"
 #include "../Engine/Nullkiller.h"
 #include "../../../lib/logging/VisualLogger.h"
+#include "Actions/QuestAction.h"
+#include "../pforeach.h"
+#include "Actions/BoatActions.h"
 
 namespace NKAI
 {
 
+struct ConnectionCostInfo
+{
+	float totalCost = 0;
+	float avg = 0;
+	int connectionsCount = 0;
+};
+
 class ObjectGraphCalculator
 {
 private:
 	ObjectGraph * target;
 	const Nullkiller * ai;
+	std::mutex syncLock;
 
 	std::map<const CGHeroInstance *, HeroRole> actors;
 	std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
@@ -33,11 +44,15 @@ private:
 
 public:
 	ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
-		:ai(ai), target(target)
+		:ai(ai), target(target), syncLock()
+	{
+	}
+
+	void setGraphObjects()
 	{
 		for(auto obj : ai->memory->visitableObjs)
 		{
-			if(obj && obj->isVisitable() && obj->ID != Obj::HERO)
+			if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT)
 			{
 				addObjectActor(obj);
 			}
@@ -47,7 +62,81 @@ public:
 		{
 			addObjectActor(town);
 		}
+	}
+
+	void calculateConnections()
+	{
+		updatePaths();
+
+		std::vector<AIPath> pathCache;
+
+		foreach_tile_pos(ai->cb.get(), [this, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+			{
+				calculateConnections(pos, pathCache);
+			});
+
+		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()
+	{
+		pforeachTilePaths(ai->cb->getMapSize(), ai, [this](const int3 & pos, std::vector<AIPath> & paths)
+			{
+				if(target->hasNodeAt(pos))
+					return;
+
+				if(ai->cb->getGuardingCreaturePosition(pos).valid())
+					return;
+
+				ConnectionCostInfo currentCost = getConnectionsCost(paths);
+
+				if(currentCost.connectionsCount <= 2)
+					return;
+
+				float neighborCost = getNeighborConnectionsCost(pos, paths);
+
+				if(currentCost.avg < neighborCost)
+				{
+					addJunctionActor(pos);
+				}
+			});
+	}
+
+private:
+	void updatePaths()
+	{
 		PathfinderSettings ps;
 
 		ps.mainTurnDistanceLimit = 5;
@@ -57,41 +146,101 @@ public:
 		ai->pathfinder->updatePaths(actors, ps);
 	}
 
-	void calculateConnections(const int3 & pos)
+	void calculateConnections(const int3 & pos, std::vector<AIPath> & pathCache)
 	{
-		auto guarded = ai->cb->getGuardingCreaturePosition(pos).valid();
+		if(target->hasNodeAt(pos))
+		{
+			foreach_neighbour(
+				ai->cb.get(),
+				pos,
+				[this, &pos, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
+				{
+					if(target->hasNodeAt(neighbor))
+					{
+						ai->pathfinder->calculatePathInfo(pathCache, neighbor);
+
+						for(auto & path : pathCache)
+						{
+							if(pos == path.targetHero->visitablePos())
+							{
+								target->tryAddConnection(pos, neighbor, path.movementCost(), path.getTotalDanger());
+							}
+						}
+					}
+				});
+
+			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());
+					}
+				}
+			}
 
-		if(guarded)
 			return;
+		}
 
-		auto paths = ai->pathfinder->getPathInfo(pos);
+		auto guardPos = ai->cb->getGuardingCreaturePosition(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)
 					continue;
 
+				auto pos1 = path1.targetHero->visitablePos();
+				auto pos2 = path2.targetHero->visitablePos();
+
+				if(guardPos.valid() && guardPos != pos1 && guardPos != pos2)
+					continue;
+
 				auto obj1 = actorObjectMap[path1.targetHero];
 				auto obj2 = actorObjectMap[path2.targetHero];
 
-				auto tile1 = cb->getTile(obj1->visitablePos());
-				auto tile2 = cb->getTile(obj2->visitablePos());
+				auto tile1 = cb->getTile(pos1);
+				auto tile2 = cb->getTile(pos2);
 
 				if(tile2->isWater() && !tile1->isWater())
 				{
-					auto linkTile = cb->getTile(pos);
+					if(!cb->getTile(pos)->isWater())
+						continue;
 
-					if(!linkTile->isWater() || obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD)
+					auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1);
+
+					if(!startingObjIsBoat)
 						continue;
 				}
 
-				auto danger = ai->pathfinder->getStorage()->evaluateDanger(obj2->visitablePos(), path1.targetHero, true);
+				auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true);
 
 				auto updated = target->tryAddConnection(
-					obj1->visitablePos(),
-					obj2->visitablePos(), 
+					pos1,
+					pos2,
 					path1.movementCost() + path2.movementCost(),
 					danger);
 
@@ -99,8 +248,8 @@ public:
 				{
 					logAi->trace(
 						"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
-						obj1->getObjectName(), obj1->visitablePos().toString(),
-						obj2->getObjectName(), obj2->visitablePos().toString(),
+						obj1 ? obj1->getObjectName() : "J", pos1.toString(),
+						obj2 ? obj2->getObjectName() : "J", pos2.toString(),
 						pos.toString(),
 						path1.movementCost() + path2.movementCost());
 				}
@@ -108,7 +257,49 @@ public:
 		}
 	}
 
-private:
+	bool isExtraConnection(float direct, float side1, float side2) const
+	{
+		float sideRatio = (side1 + side2) / direct;
+
+		return sideRatio < 1.25f && direct > side1 && direct > side2;
+	}
+
+	void removeExtraConnections()
+	{
+		std::vector<std::pair<int3, int3>> connectionsToRemove;
+
+		for(auto & actor : temporaryActorHeroes)
+		{
+			auto pos = actor->visitablePos();
+			auto & currentNode = target->getNode(pos);
+
+			target->iterateConnections(pos, [this, &pos, &connectionsToRemove, &currentNode](int3 n1, ObjectLink o1)
+				{
+					target->iterateConnections(n1, [&pos, &o1, &currentNode, &connectionsToRemove, this](int3 n2, ObjectLink o2)
+						{
+							auto direct = currentNode.connections.find(n2);
+
+							if(direct != currentNode.connections.end() && isExtraConnection(direct->second.cost, o1.cost, o2.cost))
+							{
+								connectionsToRemove.push_back({pos, n2});
+							}
+						});
+				});
+		}
+
+		vstd::removeDuplicates(connectionsToRemove);
+
+		for(auto & c : connectionsToRemove)
+		{
+			target->removeConnection(c.first, c.second);
+
+			if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+			{
+				logAi->trace("Remove ineffective connection %s->%s", c.first.toString(), c.second.toString());
+			}
+		}
+	}
+
 	void addObjectActor(const CGObjectInstance * obj)
 	{
 		auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(obj->cb)).get();
@@ -126,11 +317,88 @@ private:
 			objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
 		}
 
+		assert(objectActor->visitablePos() == visitablePos);
+
 		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);
-	};
+
+		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, bool isVirtualBoat = false)
+	{
+		std::lock_guard<std::mutex> lock(syncLock);
+
+		auto internalCb = temporaryActorHeroes.front()->cb;
+		auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
+
+		CRandomGenerator rng;
+
+		objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
+		objectActor->initHero(rng, static_cast<HeroTypeID>(0));
+		objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
+		objectActor->initObj(rng);
+
+		if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater())
+		{
+			objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
+		}
+
+		assert(objectActor->visitablePos() == visitablePos);
+
+		actorObjectMap[objectActor] = nullptr;
+		actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT;
+
+		target->registerJunction(visitablePos);
+	}
+
+	ConnectionCostInfo getConnectionsCost(std::vector<AIPath> & paths) const
+	{
+		std::map<int3, float> costs;
+
+		for(auto & path : paths)
+		{
+			auto fromPos = path.targetHero->visitablePos();
+			auto cost = costs.find(fromPos);
+			
+			if(cost == costs.end())
+			{
+				costs.emplace(fromPos, path.movementCost());
+			}
+			else
+			{
+				if(path.movementCost() < cost->second)
+				{
+					costs[fromPos] = path.movementCost();
+				}
+			}
+		}
+
+		ConnectionCostInfo result;
+
+		for(auto & cost : costs)
+		{
+			result.totalCost += cost.second;
+			result.connectionsCount++;
+		}
+
+		if(result.connectionsCount)
+		{
+			result.avg = result.totalCost / result.connectionsCount;
+		}
+
+		return result;
+	}
 };
 
 bool ObjectGraph::tryAddConnection(
@@ -139,7 +407,20 @@ bool ObjectGraph::tryAddConnection(
 	float cost,
 	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)
+{
+	nodes[from].connections.erase(to);
 }
 
 void ObjectGraph::updateGraph(const Nullkiller * ai)
@@ -148,29 +429,41 @@ void ObjectGraph::updateGraph(const Nullkiller * ai)
 
 	ObjectGraphCalculator calculator(this, ai);
 
-	foreach_tile_pos(cb.get(), [this, &calculator](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
-		{
-			if(nodes.find(pos) != nodes.end())
-				return;
-
-			calculator.calculateConnections(pos);
-		});
+	calculator.setGraphObjects();
+	calculator.calculateConnections();
+	calculator.addMinimalDistanceJunctions();
+	calculator.calculateConnections();
 
 	if(NKAI_GRAPH_TRACE_LEVEL >= 1)
 		dumpToLog("graph");
-
 }
 
 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)
+{
+	if(!hasNodeAt(pos))
+		nodes[pos].initJunction();
+
 }
 
 void ObjectGraph::removeObject(const CGObjectInstance * obj)
 {
 	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
 			{
@@ -198,6 +491,9 @@ void ObjectGraph::connectHeroes(const Nullkiller * ai)
 
 		for(AIPath & path : paths)
 		{
+			if(path.getFirstBlockedAction())
+				continue;
+
 			auto heroPos = path.targetHero->visitablePos();
 
 			nodes[pos].connections[heroPos].update(
@@ -240,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;
 }
 
-void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai)
+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, uint8_t scanDepth)
 {
-	graph = *ai->baseGraph;
+	graph.copyFrom(*ai->baseGraph);
 	graph.connectHeroes(ai);
 
 	visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
@@ -259,20 +581,61 @@ void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkil
 		GraphPathNodePointer pos = pq.top();
 		pq.pop();
 
-		auto & node = getNode(pos);
+		auto & node = getOrCreateNode(pos);
+		std::shared_ptr<SpecialAction> transitionAction;
+
+		if(node.obj)
+		{
+			if(node.obj->ID == Obj::QUEST_GUARD
+				|| node.obj->ID == Obj::BORDERGUARD
+				|| node.obj->ID == Obj::BORDER_GATE)
+			{
+				auto questObj = dynamic_cast<const IQuestObject *>(node.obj);
+				auto questInfo = QuestInfo(questObj->quest, node.obj, pos.coord);
+
+				if(node.obj->ID == Obj::QUEST_GUARD
+					&& questObj->quest->mission == Rewardable::Limiter{}
+					&& questObj->quest->killTarget == ObjectInstanceID::NONE)
+				{
+					continue;
+				}
+
+				auto questAction = std::make_shared<AIPathfinding::QuestAction>(questInfo);
+
+				if(!questAction->canAct(ai, targetHero))
+				{
+					transitionAction = questAction;
+				}
+			}
+		}
 
 		node.isInQueue = false;
 
-		graph.iterateConnections(pos.coord, [&](int3 target, ObjectLink o)
+		graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq, scanDepth](int3 target, const ObjectLink & o)
 			{
-				auto targetNodeType = o.danger ? 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 & targetNode = getNode(targetPointer);
+				auto & targetNode = getOrCreateNode(targetPointer);
 
 				if(targetNode.tryUpdate(pos, node, o))
 				{
-					if(graph.getNode(target).objTypeID == Obj::HERO)
+					if(targetNode.cost > scanDepth)
+					{
 						return;
+					}
+
+					targetNode.specialAction = compositeAction;
+
+					auto targetGraphNode = graph.getNode(target);
+
+					if(targetGraphNode.objID.hasValue())
+					{
+						targetNode.obj = ai->cb->getObj(targetGraphNode.objID, false);
+
+						if(targetNode.obj && targetNode.obj->ID == Obj::HERO)
+							return;
+					}
 
 					if(targetNode.isInQueue)
 					{
@@ -321,11 +684,6 @@ bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathN
 
 	if(newCost < cost)
 	{
-		if(nodeType < pos.nodeType)
-		{
-			logAi->error("Linking error");
-		}
-
 		previous = pos;
 		danger = prev.danger + link.danger;
 		cost = newCost;
@@ -348,7 +706,7 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 		if(!node.reachable())
 			continue;
 
-		std::vector<int3> tilesToPass;
+		std::vector<GraphPathNodePointer> tilesToPass;
 
 		uint64_t danger = node.danger;
 		float cost = node.cost;
@@ -372,7 +730,7 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 			vstd::amax(danger, currentNode.danger);
 			vstd::amax(cost, currentNode.cost);
 
-			tilesToPass.push_back(current.coord);
+			tilesToPass.push_back(current);
 
 			if(currentNode.cost < 2.0f)
 				break;
@@ -383,7 +741,7 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 		if(tilesToPass.empty())
 			continue;
 
-		auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back());
+		auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
 
 		for(auto & path : entryPaths)
 		{
@@ -394,12 +752,18 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 			{
 				AIPathNodeInfo n;
 
-				n.coord = *graphTile;
+				n.coord = graphTile->coord;
 				n.cost = cost;
 				n.turns = static_cast<ui8>(cost) + 1; // just in case lets select worst scenario
 				n.danger = danger;
 				n.targetHero = hero;
-				n.parentIndex = 0;
+				n.parentIndex = -1;
+				n.specialAction = getNode(*graphTile).specialAction;
+
+				if(n.specialAction)
+				{
+					n.actionIsBlocked = !n.specialAction->canAct(ai, n);
+				}
 
 				for(auto & node : path.nodes)
 				{
@@ -418,4 +782,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(ai, n);
+				}
+
+				auto blocker = ai->objectClusterizer->getBlocker(n);
+
+				if(!blocker)
+					continue;
+
+				// blocker node
+				path.nodes.push_back(n);
+				break;
+			}
+		}
+	}
+}
+
 }

+ 46 - 3
AI/Nullkiller/Pathfinding/ObjectGraph.h

@@ -22,6 +22,7 @@ struct ObjectLink
 {
 	float cost = 100000; // some big number
 	uint64_t danger = 0;
+	std::shared_ptr<ISpecialActionFactory> specialAction;
 
 	bool update(float newCost, uint64_t newDanger)
 	{
@@ -50,19 +51,47 @@ struct ObjectNode
 		objID = obj->id;
 		objTypeID = obj->ID;
 	}
+
+	void initJunction()
+	{
+		objectExists = false;
+		objID = ObjectInstanceID();
+		objTypeID = Obj();
+	}
 };
 
 class ObjectGraph
 {
 	std::unordered_map<int3, ObjectNode> nodes;
+	std::unordered_map<int3, ObjectInstanceID> virtualBoats;
 
 public:
+	ObjectGraph()
+		:nodes(), virtualBoats()
+	{
+	}
+
 	void updateGraph(const Nullkiller * ai);
 	void addObject(const CGObjectInstance * obj);
+	void registerJunction(const int3 & pos);
+	void addVirtualBoat(const int3 & pos, const CGObjectInstance * shipyard);
 	void connectHeroes(const Nullkiller * ai);
 	void removeObject(const CGObjectInstance * obj);
+	bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger);
+	void removeConnection(const int3 & from, const int3 & to);
 	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>
 	void iterateConnections(const int3 & pos, Func fn)
 	{
@@ -77,7 +106,10 @@ public:
 		return nodes.at(tile);
 	}
 
-	bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger);
+	bool hasNodeAt(const int3 & tile) const
+	{
+		return vstd::contains(nodes, tile);
+	}
 };
 
 struct GraphPathNode;
@@ -131,6 +163,8 @@ struct GraphPathNode
 	GraphPathNodePointer previous;
 	float cost = BAD_COST;
 	uint64_t danger = 0;
+	const CGObjectInstance * obj = nullptr;
+	std::shared_ptr<SpecialAction> specialAction;
 
 	using TFibHeap = boost::heap::fibonacci_heap<GraphPathNodePointer, boost::heap::compare<GraphNodeComparer>>;
 
@@ -152,12 +186,14 @@ class GraphPaths
 	std::string visualKey;
 
 public:
-	void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai);
+	GraphPaths();
+	void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai, uint8_t scanDepth);
 	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;
 
 private:
-	GraphPathNode & getNode(const GraphPathNodePointer & pos)
+	GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos)
 	{
 		auto & node = pathNodes[pos.coord][pos.nodeType];
 
@@ -165,6 +201,13 @@ private:
 
 		return node;
 	}
+
+	const GraphPathNode & getNode(const GraphPathNodePointer & pos) const
+	{
+		auto & node = pathNodes.at(pos.coord)[pos.nodeType];
+
+		return node;
+	}
 };
 
 }

+ 5 - 2
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -17,7 +17,10 @@ namespace NKAI
 {
 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)
 	{
 		setup();
@@ -174,7 +177,7 @@ namespace AIPathfinding
 			const CGHeroInstance * hero = nodeStorage->getHero(source.node);
 
 			if(vstd::contains(summonableVirtualBoats, hero)
-				&& summonableVirtualBoats.at(hero)->canAct(nodeStorage->getAINode(source.node)))
+				&& summonableVirtualBoats.at(hero)->canAct(ai, nodeStorage->getAINode(source.node)))
 			{
 				virtualBoat = summonableVirtualBoats.at(hero);
 			}

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

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

+ 32 - 2
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -20,10 +20,11 @@ namespace NKAI
 namespace AIPathfinding
 {
 	AIMovementAfterDestinationRule::AIMovementAfterDestinationRule(
+		const Nullkiller * ai,
 		CPlayerSpecificInfoCallback * cb, 
 		std::shared_ptr<AINodeStorage> nodeStorage,
 		bool allowBypassObjects)
-		:cb(cb), nodeStorage(nodeStorage), allowBypassObjects(allowBypassObjects)
+		:ai(ai), cb(cb), nodeStorage(nodeStorage), allowBypassObjects(allowBypassObjects)
 	{
 	}
 
@@ -40,17 +41,46 @@ namespace AIPathfinding
 
 			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);
+
 		if(blocker == BlockingReason::NONE)
 		{
 			destination.blocked = nodeStorage->isDistanceLimitReached(source, destination);
 
+			if(destination.nodeObject
+				&& !destination.blocked
+				&& !allowBypassObjects
+				&& !dynamic_cast<const CGTeleport *>(destination.nodeObject)
+				&& destination.nodeObject->ID != Obj::EVENT)
+			{
+				destination.blocked = true;
+				destination.node->locked = true;
+			}
+
 			return;
 		}
 		
 		if(!allowBypassObjects)
+		{
+			if(destination.nodeObject)
+			{
+				destination.blocked = true;
+				destination.node->locked = true;
+			}
+
 			return;
+		}
 
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 		logAi->trace(
@@ -142,7 +172,7 @@ namespace AIPathfinding
 			return false;
 		}
 
-		if(!questAction.canAct(destinationNode))
+		if(!questAction.canAct(ai, destinationNode))
 		{
 			if(!destinationNode->actor->allowUseResources)
 			{

+ 2 - 0
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.h

@@ -24,11 +24,13 @@ namespace AIPathfinding
 	{
 	private:
 		CPlayerSpecificInfoCallback * cb;
+		const Nullkiller * ai;
 		std::shared_ptr<AINodeStorage> nodeStorage;
 		bool allowBypassObjects;
 
 	public:
 		AIMovementAfterDestinationRule(
+			const Nullkiller * ai,
 			CPlayerSpecificInfoCallback * cb,
 			std::shared_ptr<AINodeStorage> nodeStorage,
 			bool allowBypassObjects);

+ 1 - 1
AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp

@@ -49,7 +49,7 @@ namespace AIPathfinding
 					return;
 
 				// when actor represents moster graph node, we need to let him escape monster
-				if(!destination.guarded && cb->getGuardingCreaturePosition(source.coord) == actor->initialPosition)
+				if(cb->getGuardingCreaturePosition(source.coord) == actor->initialPosition)
 					return;
 			}
 

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

+ 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
 #include "../../Global.h"
 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
 #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
 
 #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
 *

+ 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 "MapObjectsEvaluator.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,
+	EPathfindingLayer layer,
 	const PathfinderConfig * pathfinderConfig,
 	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);
-	auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
 
 	for(auto & neighbour : accessibleNeighbourTiles)
 	{
@@ -174,11 +180,9 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
 			if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
 				continue;
 
-			neighbours.push_back(nextNode.value());
+			result.push_back(nextNode.value());
 		}
 	}
-
-	return neighbours;
 }
 
 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;
 
-	virtual std::vector<CGPathNode *> calculateNeighbours(
+	virtual void calculateNeighbours(
+		std::vector<CGPathNode *> & result,
 		const PathNodeInfo & source,
+		EPathfindingLayer layer,
 		const PathfinderConfig * pathfinderConfig,
 		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"

+ 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
 #include "../../Global.h"
 

+ 8 - 0
CCallback.cpp

@@ -180,6 +180,14 @@ void CCallback::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dst
 	sendRequest(&bma);
 }
 
+void CCallback::scrollBackpackArtifacts(ObjectInstanceID hero, bool left)
+{
+	ManageBackpackArtifacts mba(hero, ManageBackpackArtifacts::ManageCmd::SCROLL_RIGHT);
+	if(left)
+		mba.cmd = ManageBackpackArtifacts::ManageCmd::SCROLL_LEFT;
+	sendRequest(&mba);
+}
+
 void CCallback::eraseArtifactByClient(const ArtifactLocation & al)
 {
 	EraseArtifactByClient ea(al);

+ 2 - 0
CCallback.h

@@ -90,6 +90,7 @@ public:
 	virtual int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)=0;//split creatures from the first stack
 	//virtual bool swapArtifacts(const CGHeroInstance * hero1, ui16 pos1, const CGHeroInstance * hero2, ui16 pos2)=0; //swaps artifacts between two given heroes
 	virtual bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2)=0;
+	virtual void scrollBackpackArtifacts(ObjectInstanceID hero, bool left) = 0;
 	virtual void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=0;
 	virtual void eraseArtifactByClient(const ArtifactLocation & al)=0;
 	virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0;
@@ -174,6 +175,7 @@ public:
 	bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override;
 	void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override;
 	void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped = true, bool backpack = true) override;
+	void scrollBackpackArtifacts(ObjectInstanceID hero, bool left) override;
 	void eraseArtifactByClient(const ArtifactLocation & al) override;
 	bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override;
 	void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override;

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

@@ -3,7 +3,7 @@
 sudo apt-get update
 
 # 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 \
 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 \

+ 1 - 1
CI/linux/before_install.sh

@@ -3,7 +3,7 @@
 sudo apt-get update
 
 # 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 \
 qtbase5-dev \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \

+ 41 - 29
CMakeLists.txt

@@ -5,11 +5,6 @@ cmake_minimum_required(VERSION 3.16.0)
 
 project(VCMI)
 # TODO
-# macOS:
-# - There is problem with running fixup_bundle in main project after subdirectories.
-# Cmake put them after all install code of main CMakelists in cmake_install.cmake
-# Currently I just added extra add_subdirectory and CMakeLists.txt in osx directory to bypass that.
-#
 # Vckpg:
 # - Improve install code once there is better way to deploy DLLs and Qt plugins
 #
@@ -23,7 +18,7 @@ project(VCMI)
 # - Make FindFuzzyLite check for the right version and disable FORCE_BUNDLED_FL by default
 
 if(APPLE)
-	if(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+	if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
 		set(APPLE_MACOS 1)
 	else()
 		set(APPLE_IOS 1)
@@ -43,11 +38,13 @@ endif()
 
 # Platform-independent options
 
+option(ENABLE_CLIENT "Enable compilation of game client" ON)
 option(ENABLE_ERM "Enable compilation of ERM 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_NULLKILLER_AI "Enable compilation of Nullkiller AI library" 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
 
@@ -252,6 +249,10 @@ if(ENABLE_SINGLE_APP_BUILD)
 	add_definitions(-DENABLE_SINGLE_APP_BUILD)
 endif()
 
+if(ENABLE_MINIMAL_LIB)
+	add_definitions(-DENABLE_MINIMAL_LIB)
+endif()
+
 if(APPLE_IOS)
 	set(CMAKE_MACOSX_RPATH 1)
 	set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0)
@@ -455,12 +456,6 @@ if(TARGET zlib::zlib)
 	add_library(ZLIB::ZLIB ALIAS zlib::zlib)
 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)
 if(NOT FORCE_BUNDLED_MINIZIP)
 	find_package(minizip)
@@ -469,18 +464,26 @@ if(NOT FORCE_BUNDLED_MINIZIP)
 	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()
 
 if(ENABLE_LOBBY)
@@ -498,7 +501,7 @@ if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
 	endif()
 endif()
 
-if(ENABLE_NULLKILLER_AI)
+if(ENABLE_NULLKILLER_AI AND ENABLE_CLIENT)
 	find_package(TBB REQUIRED)
 endif()
 
@@ -608,10 +611,15 @@ if(APPLE_IOS)
 	add_subdirectory(ios)
 endif()
 
-add_subdirectory_with_folder("AI" AI)
+if (ENABLE_CLIENT)
+	add_subdirectory_with_folder("AI" AI)
+endif()
 
 add_subdirectory(lib)
-add_subdirectory(server)
+
+if (ENABLE_CLIENT OR ENABLE_SERVER)
+	add_subdirectory(server)
+endif()
 
 if(ENABLE_ERM)
 	add_subdirectory(scripting/erm)
@@ -638,7 +646,9 @@ if(ENABLE_LOBBY)
 	add_subdirectory(lobby)
 endif()
 
-add_subdirectory(client)
+if (ENABLE_CLIENT)
+	add_subdirectory(client)
+endif()
 
 if(ENABLE_SERVER)
 	add_subdirectory(serverapp)
@@ -682,7 +692,9 @@ if(ANDROID)
 	")
 else()
 	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()
 if(ENABLE_LUA)
 	install(DIRECTORY scripts DESTINATION ${DATA_DIR})

+ 2 - 2
Global.h

@@ -215,10 +215,10 @@ using TLockGuardRec = std::lock_guard<std::recursive_mutex>;
 /* ---------------------------------------------------------------------------- */
 // Import + Export macro declarations
 #ifdef VCMI_WINDOWS
-#ifdef VCMI_DLL_STATIC
+#  ifdef VCMI_DLL_STATIC
 #    define DLL_IMPORT
 #    define DLL_EXPORT
-#elif defined(__GNUC__)
+#  elif defined(__GNUC__)
 #    define DLL_IMPORT __attribute__((dllimport))
 #    define DLL_EXPORT __attribute__((dllexport))
 #  else

BIN
Mods/vcmi/Data/lobby/iconEnter.png


BIN
Mods/vcmi/Data/lobby/iconFolder.png


BIN
Mods/vcmi/Data/lobby/iconPlayer.png


BIN
Mods/vcmi/Data/lobby/iconSend.png


BIN
Mods/vcmi/Data/lobby/selectionTabSortDate.png


BIN
Mods/vcmi/Data/lobby/townBorderBig.png


Some files were not shown because too many files changed in this diff