浏览代码

AI: replace SectorMap with new PathfinderManager

Andrii Danylchenko 7 年之前
父节点
当前提交
49c872e4ec

+ 1 - 1
AI/FuzzyLite

@@ -1 +1 @@
-Subproject commit 42c30e157e999b45aee9b53bb849bb6ec3a2be22
+Subproject commit 9751a751a17c0682ed5d02e583c6a0cda8bc88e5

+ 16 - 11
AI/VCAI/AIUtility.cpp

@@ -36,6 +36,11 @@ ObjectIdRef::operator const CGObjectInstance *() const
 	return cb->getObj(id, false);
 }
 
+ObjectIdRef::operator bool() const
+{
+	return cb->getObj(id, false);
+}
+
 ObjectIdRef::ObjectIdRef(ObjectInstanceID _id)
 	: id(_id)
 {
@@ -338,14 +343,18 @@ bool compareDanger(const CGObjectInstance * lhs, const CGObjectInstance * rhs)
 }
 
 bool isSafeToVisit(HeroPtr h, crint3 tile)
+{
+	return isSafeToVisit(h, evaluateDanger(tile));
+}
+
+bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength)
 {
 	const ui64 heroStrength = h->getTotalStrength();
-	const ui64 dangerStrength = evaluateDanger(tile, *h);
+
 	if(dangerStrength)
 	{
 		if(heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength)
 		{
-			logAi->trace("It's safe for %s to visit tile %s", h->name, tile.toString());
 			return true;
 		}
 		else
@@ -398,11 +407,11 @@ int3 whereToExplore(HeroPtr h)
 	TimeCheck tc("where to explore");
 	int radius = h->getSightRadius();
 	int3 hpos = h->visitablePos();
-
-	auto sm = ai->getCachedSectorMap(h);
-
+	
 	//look for nearby objs -> visit them if they're close enouh
 	const int DIST_LIMIT = 3;
+	const int MP_LIMIT = DIST_LIMIT * 150; // aproximate cost of diagonal movement
+
 	std::vector<const CGObjectInstance *> nearbyVisitableObjs;
 	for(int x = hpos.x - DIST_LIMIT; x <= hpos.x + DIST_LIMIT; ++x) //get only local objects instead of all possible objects on the map
 	{
@@ -410,13 +419,9 @@ int3 whereToExplore(HeroPtr h)
 		{
 			for(auto obj : cb->getVisitableObjs(int3(x, y, hpos.z), false))
 			{
-				int3 op = obj->visitablePos();
-				CGPath p;
-				ai->myCb->getPathsInfo(h.get())->getPath(p, op);
-				if(p.nodes.size() && p.endPos() == op && p.nodes.size() <= DIST_LIMIT)
+				if(ai->isGoodForVisit(obj, h, MP_LIMIT))
 				{
-					if(ai->isGoodForVisit(obj, h, *sm))
-						nearbyVisitableObjs.push_back(obj);
+					nearbyVisitableObjs.push_back(obj);
 				}
 			}
 		}

+ 7 - 1
AI/VCAI/AIUtility.h

@@ -59,6 +59,10 @@ public:
 	const CGHeroInstance * operator->() const;
 	const CGHeroInstance * operator*() const; //not that consistent with -> but all interfaces use CGHeroInstance*, so it's convenient
 	bool operator==(const HeroPtr & rhs) const;
+	bool operator!=(const HeroPtr & rhs) const
+	{
+		return !(*this == rhs);
+	}
 
 	const CGHeroInstance * get(bool doWeExpectNull = false) const;
 	bool validAndSet() const;
@@ -88,6 +92,7 @@ struct ObjectIdRef
 
 	const CGObjectInstance * operator->() const;
 	operator const CGObjectInstance *() const;
+	operator bool() const;
 
 	ObjectIdRef(ObjectInstanceID _id);
 	ObjectIdRef(const CGObjectInstance * obj);
@@ -166,8 +171,9 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj);
 
 ui64 evaluateDanger(const CGObjectInstance * obj);
 ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor);
-bool isSafeToVisit(HeroPtr h, crint3 tile);
 bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property!
+bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength);
+bool isSafeToVisit(HeroPtr h, crint3 tile);
 
 bool compareMovement(HeroPtr lhs, HeroPtr rhs);
 bool compareHeroStrength(HeroPtr h1, HeroPtr h2);

+ 33 - 0
AI/VCAI/AIhelper.cpp

@@ -17,6 +17,7 @@ AIhelper::AIhelper()
 {
 	resourceManager.reset(new ResourceManager());
 	buildingManager.reset(new BuildingManager());
+	pathfindingManager.reset(new CPathfindingManager());
 	//TODO: push to vector
 }
 
@@ -34,6 +35,7 @@ void AIhelper::setCB(CPlayerSpecificInfoCallback * CB)
 	//TODO: for
 	resourceManager->setCB(CB);
 	buildingManager->setCB(CB);
+	pathfindingManager->setCB(CB);
 }
 
 void AIhelper::setAI(VCAI * AI)
@@ -41,6 +43,7 @@ void AIhelper::setAI(VCAI * AI)
 	//TODO: for loop
 	resourceManager->setAI(AI);
 	buildingManager->setAI(AI);
+	pathfindingManager->setAI(AI);
 }
 
 bool AIhelper::getBuildingOptions(const CGTownInstance * t)
@@ -112,3 +115,33 @@ TResource AIhelper::allGold() const
 {
 	return resourceManager->allGold();
 }
+
+Goals::TGoalVec AIhelper::howToVisitTile(int3 tile)
+{
+	return pathfindingManager->howToVisitTile(tile);
+}
+
+Goals::TGoalVec AIhelper::howToVisitObj(ObjectIdRef obj)
+{
+	return pathfindingManager->howToVisitObj(obj);
+}
+
+Goals::TGoalVec AIhelper::howToVisitTile(HeroPtr hero, int3 tile, bool allowGatherArmy)
+{
+	return pathfindingManager->howToVisitTile(hero, tile, allowGatherArmy);
+}
+
+Goals::TGoalVec AIhelper::howToVisitObj(HeroPtr hero, ObjectIdRef obj, bool allowGatherArmy)
+{
+	return pathfindingManager->howToVisitObj(hero, obj, allowGatherArmy);
+}
+
+std::vector<AIPath> AIhelper::getPathsToTile(HeroPtr hero, int3 tile)
+{
+	return pathfindingManager->getPathsToTile(hero, tile);
+}
+
+void AIhelper::resetPaths()
+{
+	pathfindingManager->resetPaths();
+}

+ 16 - 10
AI/VCAI/AIhelper.h

@@ -16,25 +16,26 @@
 
 #include "ResourceManager.h"
 #include "BuildingManager.h"
+#include "Pathfinding/CPathfindingManager.h"
 
 class ResourceManager;
 class BuildingManager;
 
 
 //indirection interface for various modules
-class DLL_EXPORT AIhelper : public IResourceManager, public IBuildingManager
+class DLL_EXPORT AIhelper : public IResourceManager, public IBuildingManager, public IPathfindingManager
 {
 	friend class VCAI;
 	friend struct SetGlobalState; //mess?
 
 	std::shared_ptr<ResourceManager> resourceManager;
 	std::shared_ptr<BuildingManager> buildingManager;
+	std::shared_ptr<CPathfindingManager> pathfindingManager;
 	//TODO: vector<IAbstractManager>
 public:
 	AIhelper();
 	~AIhelper();
 
-	//from ResourceManager
 	bool canAfford(const TResources & cost) const;
 	TResources reservedResources() const override;
 	TResources freeResources() const override;
@@ -43,21 +44,26 @@ public:
 	TResource allGold() const override;
 
 	Goals::TSubgoal whatToDo(TResources &res, Goals::TSubgoal goal) override;
-	Goals::TSubgoal whatToDo() const override; //peek highest-priority goal
+	Goals::TSubgoal whatToDo() const override;
 	bool containsObjective(Goals::TSubgoal goal) const;
 	bool hasTasksLeft() const override;
 
+	bool getBuildingOptions(const CGTownInstance * t) override;
+	boost::optional<PotentialBuilding> immediateBuilding() const override;
+	boost::optional<PotentialBuilding> expensiveBuilding() const override;
+	boost::optional<BuildingID> canBuildAnyStructure(const CGTownInstance * t, const std::vector<BuildingID> & buildList, unsigned int maxDays = 7) const override;
+
+	Goals::TGoalVec howToVisitTile(HeroPtr hero, int3 tile, bool allowGatherArmy = true) override;
+	Goals::TGoalVec howToVisitObj(HeroPtr hero, ObjectIdRef obj, bool allowGatherArmy = true) override;
+	Goals::TGoalVec howToVisitTile(int3 tile) override;
+	Goals::TGoalVec howToVisitObj(ObjectIdRef obj) override;
+	std::vector<AIPath> getPathsToTile(HeroPtr hero, int3 tile) override;
+	void resetPaths() override;
+
 private:
 	bool notifyGoalCompleted(Goals::TSubgoal goal);
 
 	void setCB(CPlayerSpecificInfoCallback * CB) override;
 	void setAI(VCAI * AI) override;
-
-	//from BuildingManager
-public:
-	bool getBuildingOptions(const CGTownInstance * t) override;
-	boost::optional<PotentialBuilding> immediateBuilding() const override;
-	boost::optional<PotentialBuilding> expensiveBuilding() const override;
-	boost::optional<BuildingID> canBuildAnyStructure(const CGTownInstance * t, const std::vector<BuildingID> & buildList, unsigned int maxDays = 7) const override;
 };
 

+ 9 - 1
AI/VCAI/CMakeLists.txt

@@ -8,6 +8,10 @@ include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_HOME_DIRECTORY} ${CMAKE_HOME_D
 set(VCAI_SRCS
 		StdInc.cpp
 
+		Pathfinding/AIPathfinderConfig.cpp
+		Pathfinding/AIPathfinder.cpp
+		Pathfinding/AINodeStorage.cpp
+		Pathfinding/CPathfindingManager.cpp
 		AIUtility.cpp
 		AIhelper.cpp
 		ResourceManager.cpp
@@ -24,7 +28,11 @@ set(VCAI_SRCS
 
 set(VCAI_HEADERS
 		StdInc.h
-
+		
+		Pathfinding/AIPathfinderConfig.h
+		Pathfinding/AIPathfinder.h
+		Pathfinding/AINodeStorage.h
+		Pathfinding/CPathfindingManager.h
 		AIUtility.h
 		AIhelper.h
 		ResourceManager.h

+ 1 - 1
AI/VCAI/FuzzyEngines.cpp

@@ -79,7 +79,7 @@ armyStructure evaluateArmyStructure(const CArmedInstance * army)
 float HeroMovementGoalEngineBase::calculateTurnDistanceInputValue(const CGHeroInstance * h, int3 tile) const
 {
 	float turns = 0.0f;
-	float distance = CPathfinderHelper::getMovementCost(h, tile);
+	float distance =  distanceToTile(h, tile);
 	if(distance)
 	{
 		if(distance < h->movement) //we can move there within one turn

+ 1 - 20
AI/VCAI/FuzzyHelper.cpp

@@ -22,8 +22,6 @@ Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec)
 	if(vec.empty()) //no possibilities found
 		return sptr(Goals::Invalid());
 
-	ai->cachedSectorMaps.clear();
-
 	//a trick to switch between heroes less often - calculatePaths is costly
 	auto sortByHeroes = [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool
 	{
@@ -96,24 +94,7 @@ float FuzzyHelper::evaluate(Goals::ClearWayTo & g)
 	if (!g.hero.h)
 		return 0; //lowest priority
 
-	int3 t = ai->getCachedSectorMap(g.hero)->firstTileToGet(g.hero, g.tile);
-
-	if(t.valid())
-	{
-		if(isSafeToVisit(g.hero, t))
-		{
-			g.setpriority(Goals::VisitTile(g.tile).sethero(g.hero).setisAbstract(g.isAbstract).accept(this));
-		}
-		else
-		{
-			g.setpriority (Goals::GatherArmy(evaluateDanger(t, g.hero.h)*SAFE_ATTACK_CONSTANT).
-				sethero(g.hero).setisAbstract(true).accept(this));
-		}
-		return g.priority;
-	}
-	else
-		return -1;
-
+	return g.whatToDoToAchieve()->accept(this);
 }
 
 float FuzzyHelper::evaluate(Goals::BuildThis & g)

+ 27 - 98
AI/VCAI/Goals.cpp

@@ -698,63 +698,10 @@ TGoalVec ClearWayTo::getAllPossibleSubgoals()
 
 		//if our hero is trapped, make sure we request clearing the way from OUR perspective
 
-		auto sm = ai->getCachedSectorMap(h);
-
-		int3 tileToHit = sm->firstTileToGet(h, tile);
-		if(!tileToHit.valid())
-			continue;
-
-		if(isBlockedBorderGate(tileToHit))
-		{
-			//FIXME: this way we'll not visit gate and activate quest :?
-			ret.push_back(sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(tileToHit)->visitableObjects.back()->subID)));
-			return ret; //only option
-		}
-
-		auto topObj = cb->getTopObj(tileToHit);
-		if(topObj)
-		{
-			if(vstd::contains(ai->reservedObjs, topObj) && !vstd::contains(ai->reservedHeroesMap[h], topObj))
-			{
-				throw goalFulfilledException(sptr(Goals::ClearWayTo(tile, h)));
-				continue; //do not capure object reserved by other hero
-			}
-
-			if(topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES)
-			{
-				if(topObj != hero.get(true)) //the hero we want to free
-					logAi->error("%s stands in the way of %s", topObj->getObjectName(), h->getObjectName());
-			}
-			if(topObj->ID == Obj::QUEST_GUARD || topObj->ID == Obj::BORDERGUARD)
-			{
-				if(shouldVisit(h, topObj))
-				{
-					//do NOT use VISIT_TILE, as tile with quets guard can't be visited
-					ret.push_back(sptr(Goals::VisitObj(topObj->id.getNum()).sethero(h))); //TODO: Recheck this code - object visit became elementar goal
-					continue; //do not try to visit tile or gather army
-				}
-				else
-				{
-					//TODO: we should be able to return apriopriate quest here
-					//ret.push_back(ai->questToGoal());
-					//however, visiting obj for firts time will give us quest
-					logAi->debug("Quest guard blocks the way to %s", tile.toString());
-					continue; //do not access quets guard if we can't complete the quest
-				}
-				return ret; //try complete quest as the only option
-			}
-		}
-		if(isSafeToVisit(h, tileToHit)) //this makes sense only if tile is guarded, but there is no quest object
-		{
-			ret.push_back(sptr(Goals::VisitTile(tileToHit).sethero(h)));
-		}
-		else
-		{
-			ret.push_back(sptr(Goals::GatherArmy(evaluateDanger(tileToHit, h) * SAFE_ATTACK_CONSTANT).
-					   sethero(h).setisAbstract(true)));
-		}
+		vstd::concatenate(ret, ai->ah->howToVisitTile(h, tile));
 	}
-	if(ai->canRecruitAnyHero())
+
+	if(ret.empty() && ai->canRecruitAnyHero())
 		ret.push_back(sptr(Goals::RecruitHero()));
 
 	if(ret.empty())
@@ -860,13 +807,11 @@ TGoalVec Explore::getAllPossibleSubgoals()
 	auto primaryHero = ai->primaryHero().h;
 	for(auto h : heroes)
 	{
-		auto sm = ai->getCachedSectorMap(h);
-
 		for(auto obj : objs) //double loop, performance risk?
 		{
-			auto t = sm->firstTileToGet(h, obj->visitablePos()); //we assume that no more than one tile on the way is guarded
-			if(ai->isTileNotReserved(h, t))
-				ret.push_back(sptr(Goals::ClearWayTo(obj->visitablePos(), h).setisAbstract(true)));
+			auto waysToVisitObj = ai->ah->howToVisitObj(h, obj);
+
+			vstd::concatenate(ret, waysToVisitObj);
 		}
 
 		int3 t = whereToExplore(h);
@@ -883,7 +828,9 @@ TGoalVec Explore::getAllPossibleSubgoals()
 				t = ai->explorationDesperate(h);  //check this only ONCE, high cost
 				if (t.valid()) //don't waste time if we are completely blocked
 				{
-					ret.push_back(sptr(Goals::ClearWayTo(t, h).setisAbstract(true)));
+					auto waysToVisitTile = ai->ah->howToVisitTile(h, t);
+
+					vstd::concatenate(ret, waysToVisitTile);
 					continue;
 				}
 			}
@@ -1131,7 +1078,6 @@ TGoalVec Goals::CollectRes::getAllPossibleSubgoals()
 	}
 	for (auto h : cb->getHeroesInfo())
 	{
-		auto sm = ai->getCachedSectorMap(h);
 		std::vector<const CGObjectInstance *> ourObjs(objs); //copy common objects
 
 		for (auto obj : ai->reservedHeroesMap[h]) //add objects reserved by this hero
@@ -1139,11 +1085,12 @@ TGoalVec Goals::CollectRes::getAllPossibleSubgoals()
 			if (givesResource(obj))
 				ourObjs.push_back(obj);
 		}
+
 		for (auto obj : ourObjs)
 		{
-			auto pos = obj->visitablePos();
-			if (ai->isTileNotReserved(h, pos)) //further decomposition and evaluation will be handled by VisitObj
-				ret.push_back(sptr(Goals::VisitObj(obj->id.getNum()).sethero(h).setisAbstract(true)));
+			auto waysToGo = ai->ah->howToVisitObj(h, ObjectIdRef(obj));
+
+			vstd::concatenate(ret, waysToGo);
 		}
 	}
 	return ret;
@@ -1384,7 +1331,6 @@ TGoalVec Conquer::getAllPossibleSubgoals()
 
 	for(auto h : cb->getHeroesInfo())
 	{
-		auto sm = ai->getCachedSectorMap(h);
 		std::vector<const CGObjectInstance *> ourObjs(objs); //copy common objects
 
 		for(auto obj : ai->reservedHeroesMap[h]) //add objects reserved by this hero
@@ -1394,32 +1340,9 @@ TGoalVec Conquer::getAllPossibleSubgoals()
 		}
 		for(auto obj : ourObjs)
 		{
-			int3 dest = obj->visitablePos();
-			auto t = sm->firstTileToGet(h, dest); //we assume that no more than one tile on the way is guarded
-			if(t.valid()) //we know any path at all
-			{
-				if(ai->isTileNotReserved(h, t)) //no other hero wants to conquer that tile
-				{
-					if(isSafeToVisit(h, dest))
-					{
-						if(dest != t) //there is something blocking our way
-							ret.push_back(sptr(Goals::ClearWayTo(dest, h).setisAbstract(true)));
-						else
-						{
-							if(obj->ID.num == Obj::HERO) //enemy hero may move to other position
-							{
-								ret.push_back(sptr(Goals::VisitHero(obj->id.getNum()).sethero(h).setisAbstract(true)));
-							}
-							else //just get that object
-							{
-								ret.push_back(sptr(Goals::VisitObj(obj->id.getNum()).sethero(h).setisAbstract(true)));
-							}
-						}
-					}
-					else //we need to get army in order to conquer that place
-						ret.push_back(sptr(Goals::GatherArmy(evaluateDanger(dest, h) * SAFE_ATTACK_CONSTANT).sethero(h).setisAbstract(true)));
-				}
-			}
+			auto waysToGo = ai->ah->howToVisitObj(h, ObjectIdRef(obj));
+
+			vstd::concatenate(ret, waysToGo);
 		}
 	}
 	if(!objs.empty() && ai->canRecruitAnyHero()) //probably no point to recruit hero if we see no objects to capture
@@ -1500,6 +1423,11 @@ TGoalVec GatherArmy::getAllPossibleSubgoals()
 	//get all possible towns, heroes and dwellings we may use
 	TGoalVec ret;
 
+	if(!hero.validAndSet())
+	{
+		return ret;
+	}
+
 	//TODO: include evaluation of monsters gather in calculation
 	for(auto t : cb->getTownsInfo())
 	{
@@ -1554,7 +1482,8 @@ TGoalVec GatherArmy::getAllPossibleSubgoals()
 	for(auto h : otherHeroes)
 	{
 		// Go to the other hero if we are faster
-		if (!vstd::contains(ai->visitedHeroes[hero], h)) //visit only once each turn //FIXME: this is only bug workaround
+		if (!vstd::contains(ai->visitedHeroes[hero], h)
+			&& ai->isAccessibleForHero(h->visitablePos(), hero, true)) //visit only once each turn //FIXME: this is only bug workaround
 			ret.push_back(sptr(Goals::VisitHero(h->id.getNum()).setisAbstract(true).sethero(hero)));
 		// Let the other hero come to us
 		if (!vstd::contains(ai->visitedHeroes[h], hero))
@@ -1589,13 +1518,13 @@ TGoalVec GatherArmy::getAllPossibleSubgoals()
 	}
 	for(auto h : cb->getHeroesInfo())
 	{
-		auto sm = ai->getCachedSectorMap(h);
 		for(auto obj : objs)
 		{
 			//find safe dwelling
-			auto pos = obj->visitablePos();
-			if(ai->isGoodForVisit(obj, h, *sm))
-				ret.push_back(sptr(Goals::VisitTile(pos).sethero(h)));
+			if(ai->isGoodForVisit(obj, h))
+			{
+				vstd::concatenate(ret, ai->ah->howToVisitObj(h, obj));
+			}
 		}
 	}
 

+ 234 - 0
AI/VCAI/Pathfinding/AINodeStorage.cpp

@@ -0,0 +1,234 @@
+/*
+* AIhelper.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
+*
+*/
+#include "StdInc.h"
+#include "AINodeStorage.h"
+
+
+AINodeStorage::AINodeStorage(const int3 & Sizes)
+	: sizes(Sizes)
+{
+	nodes.resize(boost::extents[sizes.x][sizes.y][sizes.z][EPathfindingLayer::NUM_LAYERS][NUM_CHAINS]);
+}
+
+AINodeStorage::~AINodeStorage()
+{
+}
+
+AIPathNode * AINodeStorage::getAINode(CPathNodeInfo & nodeInfo) const
+{
+	return static_cast<AIPathNode *>(nodeInfo.node);
+}
+
+AIPathNode * AINodeStorage::getAINode(CGPathNode * node) const
+{
+	return static_cast<AIPathNode *>(node);
+}
+
+bool AINodeStorage::isBattleNode(CGPathNode * node) const
+{
+	return getAINode(node)->chainMask & BATTLE_CHAIN > 0;
+}
+
+AIPathNode * AINodeStorage::getNode(const int3 & coord, const EPathfindingLayer layer, int chainNumber)
+{
+	return &nodes[coord.x][coord.y][coord.z][layer][chainNumber];
+}
+
+CGPathNode * AINodeStorage::getInitialNode()
+{
+	auto hpos = hero->getPosition(false);
+	auto initialNode = getNode(hpos, hero->boat ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND, 0);
+
+	initialNode->turns = 0;
+	initialNode->moveRemains = hero->movement;
+	initialNode->danger = 0;
+
+	return initialNode;
+}
+
+void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, CGPathNode::EAccessibility accessibility)
+{
+	for(int i = 0; i < NUM_CHAINS; i++)
+	{
+		AIPathNode & heroNode = nodes[coord.x][coord.y][coord.z][layer][i];
+
+		heroNode.chainMask = i;
+		heroNode.update(coord, layer, accessibility);
+	}
+}
+
+void AINodeStorage::commit(CDestinationNodeInfo & destination, CPathNodeInfo & source)
+{
+	auto dstNode = getAINode(destination);
+	auto srcNode = getAINode(source);
+
+	dstNode->moveRemains = destination.movementLeft;
+	dstNode->turns = destination.turn;
+	dstNode->danger = srcNode->danger;
+	dstNode->action = destination.action;
+	dstNode->theNodeBefore = srcNode->theNodeBefore;
+}
+
+std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
+	CPathNodeInfo & source,
+	CPathfinderConfig * pathfinderConfig,
+	CPathfinderHelper * pathfinderHelper)
+{
+	std::vector<CGPathNode *> neighbours;
+	auto srcNode = getAINode(source);
+	auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
+
+	for(auto & neighbour : accessibleNeighbourTiles)
+	{
+		for(EPathfindingLayer i = EPathfindingLayer::LAND; i <= EPathfindingLayer::AIR; i.advance(1))
+		{
+			auto nextNode = getNode(neighbour, i, srcNode->chainMask);
+
+			if(nextNode->accessible == CGPathNode::NOT_SET)
+				continue;
+
+			neighbours.push_back(nextNode);
+		}
+	}
+
+	return neighbours;
+}
+
+std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
+	CPathNodeInfo & source,
+	CPathfinderConfig * pathfinderConfig,
+	CPathfinderHelper * pathfinderHelper)
+{
+	std::vector<CGPathNode *> neighbours;
+	auto accessibleExits = pathfinderHelper->getTeleportExits(source);
+	auto srcNode = getAINode(source);
+
+	for(auto & neighbour : accessibleExits)
+	{
+		auto node = getNode(neighbour, source.node->layer, srcNode->chainMask);
+
+		neighbours.push_back(node);
+	}
+
+	return neighbours;
+}
+
+bool AINodeStorage::hasBetterChain(CPathNodeInfo & source, CDestinationNodeInfo & destination) const
+{
+	auto pos = destination.coord;
+	auto chains = nodes[pos.x][pos.y][pos.z][EPathfindingLayer::LAND];
+	auto destinationNode = getAINode(destination);
+
+	for(const AIPathNode & node : chains)
+	{
+		auto sameNode = node.chainMask == destinationNode->chainMask;
+		if(sameNode	|| node.action == CGPathNode::ENodeAction::UNKNOWN)
+		{
+			continue;
+		}
+
+		if(node.danger <= destinationNode->danger && destinationNode->chainMask == 1 && node.chainMask == 0)
+		{
+			if(node.turns < destinationNode->turns
+				|| node.turns == destinationNode->turns && node.moveRemains >= destinationNode->moveRemains)
+			{
+				logAi->trace(
+					"Block ineficient move %s:->%s, mask=%i, mp diff: %i",
+					source.coord.toString(),
+					destination.coord.toString(), 
+					destinationNode->chainMask,
+					node.moveRemains - destinationNode->moveRemains);
+
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+std::vector<AIPath> AINodeStorage::getChainInfo(int3 pos)
+{
+	std::vector<AIPath> paths;
+	auto chains = nodes[pos.x][pos.y][pos.z][EPathfindingLayer::LAND];
+
+	for(const AIPathNode & node : chains)
+	{
+		if(node.action == CGPathNode::ENodeAction::UNKNOWN)
+		{
+			continue;
+		}
+
+		AIPath path;
+		const AIPathNode * current = &node;
+
+		while(current != nullptr)
+		{
+			AIPathNodeInfo pathNode;
+
+			pathNode.movementPointsLeft = current->moveRemains;
+			pathNode.movementPointsUsed = (int)(current->turns * hero->maxMovePoints(true) + hero->movement) - (int)current->moveRemains;
+			pathNode.turns = current->turns;
+			pathNode.danger = current->danger;
+			pathNode.coord = current->coord;
+
+			path.nodes.push_back(pathNode);
+			current = getAINode(current->theNodeBefore);
+		}
+		paths.push_back(path);
+	}
+
+	return paths;
+}
+
+AIPath::AIPath()
+	: nodes({})
+{
+}
+
+int3 AIPath::firstTileToGet() const
+{
+	if(nodes.size())
+	{
+		return nodes.back().coord;
+	}
+
+	return int3(-1, -1, -1);
+}
+
+uint64_t AIPath::getPathDanger() const
+{
+	if(nodes.size())
+	{
+		return nodes.front().danger;
+	}
+
+	return 0;
+}
+
+uint32_t AIPath::movementCost() const
+{
+	if(nodes.size())
+	{
+		return nodes.front().movementPointsUsed;
+	}
+
+	// TODO: boost:optional?
+	return 0;
+}
+
+uint64_t AIPath::getTotalDanger(HeroPtr hero) const
+{
+	uint64_t pathDanger = getPathDanger();
+	uint64_t objDanger = evaluateDanger(nodes.front().coord, hero.get()); // bank danger is not checked by pathfinder
+	uint64_t danger = pathDanger > objDanger ? pathDanger : objDanger;
+
+	return danger;
+}

+ 104 - 0
AI/VCAI/Pathfinding/AINodeStorage.h

@@ -0,0 +1,104 @@
+/*
+* AIhelper.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 "../../../lib/CPathfinder.h"
+#include "../../../lib/mapObjects/CGHeroInstance.h"
+#include "AIUtility.h"
+
+class IVirtualObject
+{
+public:
+	virtual void materialize();
+};
+
+struct AIPathNode : public CGPathNode
+{
+	uint32_t chainMask;
+	uint64_t danger;
+};
+
+struct AIPathNodeInfo
+{
+	uint32_t movementPointsLeft;
+	uint32_t movementPointsUsed;
+	int turns;
+	int3 coord;
+	uint64_t danger;
+};
+
+struct AIPath
+{
+	std::vector<AIPathNodeInfo> nodes;
+
+	AIPath();
+
+	/// Gets danger of path excluding danger of visiting the target object like creature bank
+	uint64_t getPathDanger() const;
+
+	/// Gets danger of path including danger of visiting the target object like creature bank
+	uint64_t getTotalDanger(HeroPtr hero) const;
+
+	int3 firstTileToGet() const;
+
+	uint32_t movementCost() const;
+};
+
+class AINodeStorage : public INodeStorage
+{
+private:
+	int3 sizes;
+
+	/// 1-3 - position on map, 4 - layer (air, water, land), 5 - chain (normal, battle, spellcast and combinations)
+	boost::multi_array<AIPathNode, 5> nodes;
+	const CGHeroInstance * hero;
+
+public:
+	/// more than 1 chain layer allows us to have more than 1 path to each tile so we can chose more optimal one.
+	static const int NUM_CHAINS = 2;
+	static const int NORMAL_CHAIN = 0;
+	static const int BATTLE_CHAIN = 1;
+
+	AINodeStorage(const int3 & sizes);
+	~AINodeStorage();
+
+	virtual CGPathNode * getInitialNode() override;
+	virtual void resetTile(const int3 & tile, EPathfindingLayer layer, CGPathNode::EAccessibility accessibility) override;
+
+	virtual std::vector<CGPathNode *> calculateNeighbours(
+		CPathNodeInfo & source,
+		CPathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) override;
+
+	virtual std::vector<CGPathNode *> calculateTeleportations(
+		CPathNodeInfo & source,
+		CPathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) override;
+
+	virtual void commit(CDestinationNodeInfo & destination, CPathNodeInfo & source) override;
+
+	AIPathNode * getAINode(CPathNodeInfo & nodeInfo) const;
+	AIPathNode * getAINode(CGPathNode * node) const;
+	bool isBattleNode(CGPathNode * node) const;
+	bool hasBetterChain(CPathNodeInfo & source, CDestinationNodeInfo & destination) const;
+	AIPathNode * getNode(const int3 & coord, const EPathfindingLayer layer, int chainNumber);
+	std::vector<AIPath> getChainInfo(int3 pos);
+
+	void setHero(HeroPtr heroPtr)
+	{
+		hero = heroPtr.get();
+	}
+
+	const CGHeroInstance * getHero() const
+	{
+		return hero;
+	}
+};

+ 62 - 0
AI/VCAI/Pathfinding/AIPathfinder.cpp

@@ -0,0 +1,62 @@
+/*
+* AIhelper.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
+*
+*/
+#include "StdInc.h"
+#include "AIPathfinder.h"
+#include "AIPathfinderConfig.h"
+#include "../../../CCallback.h"
+
+std::vector<std::shared_ptr<AINodeStorage>> AIPathfinder::storagePool;
+std::map<HeroPtr, std::shared_ptr<AINodeStorage>> AIPathfinder::storageMap;
+boost::mutex AIPathfinder::storageMutex;
+
+AIPathfinder::AIPathfinder(CPlayerSpecificInfoCallback * cb)
+	:cb(cb)
+{
+}
+
+void AIPathfinder::clear()
+{
+	boost::unique_lock<boost::mutex> storageLock(storageMutex);
+	storageMap.clear();
+}
+
+std::vector<AIPath> AIPathfinder::getPathInfo(HeroPtr hero, int3 tile)
+{
+	boost::unique_lock<boost::mutex> storageLock(storageMutex);
+	std::shared_ptr<AINodeStorage> nodeStorage;
+
+	if(!vstd::contains(storageMap, hero))
+	{
+		logAi->debug("Recalculate paths for %s", hero->name);
+
+		if(storageMap.size() < storagePool.size())
+		{
+			nodeStorage = storagePool.at(storageMap.size());
+		}
+		else
+		{
+			nodeStorage = std::make_shared<AINodeStorage>(cb->getMapSize());
+			storagePool.push_back(nodeStorage);
+		}
+
+		storageMap[hero] = nodeStorage;
+		
+		auto config = std::make_shared<AIPathfinderConfig>(cb, nodeStorage);
+
+		nodeStorage->setHero(hero.get());
+		cb->calculatePaths(config, hero.get());
+	}
+	else
+	{
+		nodeStorage = storageMap.at(hero);
+	}
+
+	return nodeStorage->getChainInfo(tile);
+}

+ 28 - 0
AI/VCAI/Pathfinding/AIPathfinder.h

@@ -0,0 +1,28 @@
+/*
+* AIhelper.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 "AIUtility.h"
+#include "AINodeStorage.h"
+
+class AIPathfinder
+{
+private:
+	static std::vector<std::shared_ptr<AINodeStorage>> storagePool;
+	static std::map<HeroPtr, std::shared_ptr<AINodeStorage>> storageMap;
+	static boost::mutex storageMutex;
+	CPlayerSpecificInfoCallback * cb;
+
+public:
+	AIPathfinder(CPlayerSpecificInfoCallback * cb);
+	std::vector<AIPath> getPathInfo(HeroPtr hero, int3 tile);
+	void clear();
+};

+ 280 - 0
AI/VCAI/Pathfinding/AIPathfinderConfig.cpp

@@ -0,0 +1,280 @@
+/*
+* AIhelper.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
+*
+*/
+#include "StdInc.h"
+#include "AIPathfinderConfig.h"
+#include "../../../CCallback.h"
+
+class AILayerTransitionRule : public CLayerTransitionRule
+{
+public:
+	virtual void process(
+		CPathNodeInfo & source,
+		CDestinationNodeInfo & destination,
+		CPathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) const override
+	{
+		CLayerTransitionRule::process(source, destination, pathfinderConfig, pathfinderHelper);
+
+		if(!destination.blocked)
+		{
+			return;
+		}
+
+		if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL)
+		{
+			logAi->debug("Check virtual boat!");
+		}
+	}
+};
+
+class AIMovementAfterDestinationRule : public CMovementAfterDestinationRule
+{
+private:
+	CPlayerSpecificInfoCallback * cb;
+	std::shared_ptr<AINodeStorage> nodeStorage;
+
+public:
+	AIMovementAfterDestinationRule(CPlayerSpecificInfoCallback * cb, std::shared_ptr<AINodeStorage> nodeStorage)
+		:cb(cb), nodeStorage(nodeStorage)
+	{
+	}
+
+	virtual void process(
+		CPathNodeInfo & source,
+		CDestinationNodeInfo & destination,
+		CPathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) const override
+	{
+		if(nodeStorage->hasBetterChain(source, destination))
+		{
+			destination.blocked = true;
+
+			return;
+		}
+
+		auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
+
+		if(blocker == BlockingReason::NONE)
+			return;
+
+		auto srcNode = nodeStorage->getAINode(source);
+
+		if(blocker == BlockingReason::DESTINATION_BLOCKVIS && destination.nodeObject)
+		{
+			auto objID = destination.nodeObject->ID;
+			if(objID == Obj::HERO && destination.objectRelations != PlayerRelations::ENEMIES
+				|| objID == Obj::SUBTERRANEAN_GATE || objID == Obj::MONOLITH_TWO_WAY 
+				|| objID == Obj::MONOLITH_ONE_WAY_ENTRANCE || objID == Obj::MONOLITH_ONE_WAY_EXIT
+				|| objID == Obj::WHIRLPOOL)
+			{
+				destination.blocked = true;
+			}
+
+			return;
+		}
+
+		if(blocker == BlockingReason::DESTINATION_VISIT)
+		{
+			return;
+		}
+
+		if(blocker == BlockingReason::DESTINATION_GUARDED)
+		{
+			auto srcGuardians = cb->getGuardingCreatures(source.coord);
+			auto destGuardians = cb->getGuardingCreatures(destination.coord);
+
+			if(destGuardians.empty())
+			{
+				destination.blocked = true;
+
+				return;
+			}
+
+			vstd::erase_if(destGuardians, [&](const CGObjectInstance * destGuard) -> bool
+			{
+				return vstd::contains(srcGuardians, destGuard);
+			});
+
+			auto guardsAlreadyBypassed = destGuardians.empty() && srcGuardians.size();
+			if(guardsAlreadyBypassed && nodeStorage->isBattleNode(source.node))
+			{
+				logAi->trace(
+					"Bypass guard at destination while moving %s -> %s",
+					source.coord.toString(),
+					destination.coord.toString());
+
+				return;
+			}
+
+			auto destNode = nodeStorage->getAINode(destination);
+			auto battleNode = nodeStorage->getNode(destination.coord, destination.node->layer, destNode->chainMask | AINodeStorage::BATTLE_CHAIN);
+
+			if(battleNode->locked)
+			{
+				logAi->trace(
+					"Block bypass guard at destination while moving %s -> %s",
+					source.coord.toString(),
+					destination.coord.toString());
+
+				destination.blocked = true;
+
+				return;
+			}
+
+			auto hero = nodeStorage->getHero();
+			auto danger = evaluateDanger(destination.coord, hero);
+
+			destination.node = battleNode;
+			nodeStorage->commit(destination, source);
+
+			if(battleNode->danger < danger)
+			{
+				battleNode->danger = danger;
+			}
+
+			logAi->trace(
+				"Begin bypass guard at destination with danger %s while moving %s -> %s",
+				std::to_string(danger),
+				source.coord.toString(),
+				destination.coord.toString());
+
+			return;
+		}
+
+		destination.blocked = true;
+	}
+};
+
+class AIMovementToDestinationRule : public CMovementToDestinationRule
+{
+private:
+	CPlayerSpecificInfoCallback * cb;
+	std::shared_ptr<AINodeStorage> nodeStorage;
+
+public:
+	AIMovementToDestinationRule(CPlayerSpecificInfoCallback * cb, std::shared_ptr<AINodeStorage> nodeStorage)
+		:cb(cb), nodeStorage(nodeStorage)
+	{
+	}
+
+	virtual void process(
+		CPathNodeInfo & source,
+		CDestinationNodeInfo & destination,
+		CPathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) const override
+	{
+		auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
+
+		if(blocker == BlockingReason::NONE)
+			return;
+
+		if(blocker == BlockingReason::SOURCE_GUARDED && nodeStorage->isBattleNode(source.node))
+		{
+			auto srcGuardians = cb->getGuardingCreatures(source.coord);
+			auto destGuardians = cb->getGuardingCreatures(destination.coord);
+
+			for(auto srcGuard : srcGuardians)
+			{
+				if(!vstd::contains(destGuardians, srcGuard))
+					continue;
+
+				auto guardPos = srcGuard->visitablePos();
+				if(guardPos != source.coord && guardPos != destination.coord)
+				{
+					destination.blocked = true; // allow to pass monster only through guard tile
+				}
+			}
+
+			if(!destination.blocked)
+			{
+				logAi->trace(
+					"Bypass src guard while moving from %s to %s",
+					source.coord.toString(),
+					destination.coord.toString());
+			}
+
+			return;
+		}
+
+		destination.blocked = true;
+	}
+};
+
+class AIPreviousNodeRule : public CMovementToDestinationRule
+{
+private:
+	CPlayerSpecificInfoCallback * cb;
+	std::shared_ptr<AINodeStorage> nodeStorage;
+
+public:
+	AIPreviousNodeRule(CPlayerSpecificInfoCallback * cb, std::shared_ptr<AINodeStorage> nodeStorage)
+		:cb(cb), nodeStorage(nodeStorage)
+	{
+	}
+
+	virtual void process(
+		CPathNodeInfo & source,
+		CDestinationNodeInfo & destination,
+		CPathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) const override
+	{
+		auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
+
+		if(source.guarded)
+		{
+			auto srcGuardian = cb->guardingCreaturePosition(source.node->coord);
+
+			if(srcGuardian == source.node->coord)
+			{
+				// guardian tile is used as chain junction
+				destination.node->theNodeBefore = source.node;
+
+				logAi->trace(
+					"Link src node %s to destination node %s while bypassing guard",
+					source.coord.toString(),
+					destination.coord.toString());
+			}
+		}
+
+		if(source.node->action == CGPathNode::ENodeAction::BLOCKING_VISIT || source.node->action == CGPathNode::ENodeAction::VISIT)
+		{
+			// we can not directly bypass objects, we need to interact with them first
+			destination.node->theNodeBefore = source.node;
+
+			logAi->trace(
+				"Link src node %s to destination node %s while bypassing visitable obj",
+				source.coord.toString(),
+				destination.coord.toString());
+		}
+	}
+};
+
+std::vector<std::shared_ptr<IPathfindingRule>> makeRuleset(
+	CPlayerSpecificInfoCallback * cb, 
+	std::shared_ptr<AINodeStorage> nodeStorage)
+{
+	std::vector<std::shared_ptr<IPathfindingRule>> rules = {
+		std::make_shared<AILayerTransitionRule>(),
+		std::make_shared<CDestinationActionRule>(),
+		std::make_shared<AIMovementToDestinationRule>(cb, nodeStorage),
+		std::make_shared<CMovementCostRule>(),
+		std::make_shared<AIPreviousNodeRule>(cb, nodeStorage),
+		std::make_shared<AIMovementAfterDestinationRule>(cb, nodeStorage)
+	};
+
+	return rules;
+}
+
+AIPathfinderConfig::AIPathfinderConfig(
+	CPlayerSpecificInfoCallback * cb, 
+	std::shared_ptr<AINodeStorage> nodeStorage)
+	:CPathfinderConfig(nodeStorage, makeRuleset(cb, nodeStorage))
+{
+}

+ 19 - 0
AI/VCAI/Pathfinding/AIPathfinderConfig.h

@@ -0,0 +1,19 @@
+/*
+* AIhelper.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 "AINodeStorage.h"
+
+class AIPathfinderConfig : public CPathfinderConfig
+{
+public:
+	AIPathfinderConfig(CPlayerSpecificInfoCallback * cb, std::shared_ptr<AINodeStorage> nodeStorage);
+};

+ 214 - 0
AI/VCAI/Pathfinding/CPathfindingManager.cpp

@@ -0,0 +1,214 @@
+/*
+* AIhelper.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
+*
+*/
+#include "StdInc.h"
+#include "CPathfindingManager.h"
+#include "AIPathfinder.h"
+#include "AIPathfinderConfig.h"
+#include "../../../lib/CGameInfoCallback.h"
+#include "../../../lib/mapping/CMap.h"
+
+CPathfindingManager::CPathfindingManager(CPlayerSpecificInfoCallback * CB, VCAI * AI)
+	: ai(AI), cb(CB)
+{
+}
+
+void CPathfindingManager::setCB(CPlayerSpecificInfoCallback * CB)
+{
+	cb = CB;
+	pathfinder.reset(new AIPathfinder(cb));
+}
+
+void CPathfindingManager::setAI(VCAI * AI)
+{
+	ai = AI;
+}
+
+Goals::TGoalVec CPathfindingManager::howToVisitTile(int3 tile)
+{
+	Goals::TGoalVec result;
+
+	auto heroes = cb->getHeroesInfo();
+
+	for(auto hero : heroes)
+	{
+		vstd::concatenate(result, howToVisitTile(hero, tile));
+	}
+
+	return result;
+}
+
+Goals::TGoalVec CPathfindingManager::howToVisitObj(ObjectIdRef obj)
+{
+	Goals::TGoalVec result;
+
+	auto heroes = cb->getHeroesInfo();
+
+	for(auto hero : heroes)
+	{
+		vstd::concatenate(result, howToVisitObj(hero, obj));
+	}
+
+	return result;
+}
+
+Goals::TGoalVec CPathfindingManager::howToVisitTile(HeroPtr hero, int3 tile, bool allowGatherArmy)
+{
+	return findPath(hero, tile, allowGatherArmy, [&](int3 firstTileToGet) -> Goals::TSubgoal
+	{
+		return sptr(Goals::VisitTile(firstTileToGet).sethero(hero).setisAbstract(true));
+	});
+}
+
+Goals::TGoalVec CPathfindingManager::howToVisitObj(HeroPtr hero, ObjectIdRef obj, bool allowGatherArmy)
+{
+	if(!obj)
+	{
+		return Goals::TGoalVec();
+	}
+
+	int3 dest = obj->visitablePos();
+
+	return findPath(hero, dest, allowGatherArmy, [&](int3 firstTileToGet) -> Goals::TSubgoal
+	{
+		return selectVisitingGoal(hero, obj);
+	});
+}
+
+std::vector<AIPath> CPathfindingManager::getPathsToTile(HeroPtr hero, int3 tile)
+{
+	return pathfinder->getPathInfo(hero, tile);
+}
+
+Goals::TGoalVec CPathfindingManager::findPath(
+	HeroPtr hero,
+	crint3 dest,
+	bool allowGatherArmy,
+	const std::function<Goals::TSubgoal(int3)> doVisitTile)
+{
+	Goals::TGoalVec result;
+	boost::optional<uint64_t> armyValueRequired;
+	uint64_t danger;
+
+	std::vector<AIPath> chainInfo = pathfinder->getPathInfo(hero, dest);
+
+	logAi->trace("Trying to find a way for %s to visit tile %s", hero->name, dest.toString());
+
+	for(auto path : chainInfo)
+	{
+		int3 firstTileToGet = path.firstTileToGet();
+
+		logAi->trace("Path found size=%i, first tile=%s", path.nodes.size(), firstTileToGet.toString());
+
+		if(firstTileToGet.valid() && ai->isTileNotReserved(hero.get(), firstTileToGet))
+		{
+			danger = path.getTotalDanger(hero);
+
+			if(isSafeToVisit(hero, danger))
+			{
+				logAi->trace("It's safe for %s to visit tile %s with danger %s", hero->name, dest.toString(), std::to_string(danger));
+
+				auto solution = dest == firstTileToGet
+					? doVisitTile(firstTileToGet)
+					: clearWayTo(hero, firstTileToGet);
+				result.push_back(solution);
+
+				continue;
+			}
+
+			if(!armyValueRequired || armyValueRequired > danger)
+			{
+				armyValueRequired = boost::make_optional(danger);
+			}
+		}
+	}
+
+	danger = armyValueRequired.get_value_or(0);
+
+	if(allowGatherArmy && danger > 0)
+	{
+		//we need to get army in order to conquer that place
+		logAi->trace("Gather army for %s, value=%s", hero->name, std::to_string(danger));
+		result.push_back(sptr(Goals::GatherArmy(danger * SAFE_ATTACK_CONSTANT).sethero(hero).setisAbstract(true)));
+	}
+
+	return result;
+}
+
+Goals::TSubgoal CPathfindingManager::selectVisitingGoal(HeroPtr hero, ObjectIdRef obj) const
+{
+	int3 dest = obj->visitablePos();
+
+	if(obj->ID.num == Obj::HERO) //enemy hero may move to other position
+	{
+		return sptr(Goals::VisitHero(obj->id.getNum()).sethero(hero).setisAbstract(true));
+	}
+	else //just visit that tile
+	{
+		//if target is town, fuzzy system will use additional "estimatedReward" variable to increase priority a bit
+		//TODO: change to getObj eventually and and move appropiate logic there
+		return obj->ID.num == Obj::TOWN
+			? sptr(Goals::VisitTile(dest).sethero(hero).setobjid(obj->ID.num).setisAbstract(true))
+			: sptr(Goals::VisitTile(dest).sethero(hero).setisAbstract(true));
+	}
+
+	return sptr(Goals::VisitTile(dest).sethero(hero).setisAbstract(true));
+}
+
+Goals::TSubgoal CPathfindingManager::clearWayTo(HeroPtr hero, int3 firstTileToGet)
+{
+	if(isBlockedBorderGate(firstTileToGet))
+	{
+		//FIXME: this way we'll not visit gate and activate quest :?
+		return sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(firstTileToGet)->visitableObjects.back()->subID));
+	}
+
+	auto topObj = cb->getTopObj(firstTileToGet);
+	if(topObj)
+	{
+
+		if(vstd::contains(ai->reservedObjs, topObj) && !vstd::contains(ai->reservedHeroesMap[hero], topObj))
+		{
+			return sptr(Goals::Invalid());
+		}
+
+		if(topObj->ID == Obj::HERO && cb->getPlayerRelations(hero->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES)
+		{
+			if(topObj != hero.get(true)) //the hero we want to free
+			{
+				logAi->error("%s stands in the way of %s", topObj->getObjectName(), hero->getObjectName());
+
+				return sptr(Goals::Invalid());
+			}
+		}
+
+		if(topObj->ID == Obj::QUEST_GUARD || topObj->ID == Obj::BORDERGUARD)
+		{
+			if(shouldVisit(hero, topObj))
+			{
+				//do NOT use VISIT_TILE, as tile with quets guard can't be visited
+				return sptr(Goals::VisitObj(topObj->id.getNum()).sethero(hero));
+			}
+
+			//TODO: we should be able to return apriopriate quest here
+			//ret.push_back(ai->questToGoal());
+			//however, visiting obj for firts time will give us quest
+			//do not access quets guard if we can't complete the quest
+			return sptr(Goals::Invalid());
+		}
+	}
+
+	return sptr(Goals::VisitTile(firstTileToGet).sethero(hero).setisAbstract(true));
+}
+
+void CPathfindingManager::resetPaths()
+{
+	logAi->debug("AIPathfinder has been reseted.");
+	pathfinder->clear();
+}

+ 64 - 0
AI/VCAI/Pathfinding/CPathfindingManager.h

@@ -0,0 +1,64 @@
+/*
+* AIhelper.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 "VCAI.h"
+#include "AINodeStorage.h"
+
+class IPathfindingManager // : pulbic IAbstractManager
+{
+public:
+	virtual ~IPathfindingManager() = default;
+	virtual void setCB(CPlayerSpecificInfoCallback * CB) = 0;
+	virtual void setAI(VCAI * AI) = 0;
+
+	virtual void resetPaths() = 0;
+	virtual Goals::TGoalVec howToVisitTile(HeroPtr hero, int3 tile, bool allowGatherArmy = true) = 0;
+	virtual Goals::TGoalVec howToVisitObj(HeroPtr hero, ObjectIdRef obj, bool allowGatherArmy = true) = 0;
+	virtual Goals::TGoalVec howToVisitTile(int3 tile) = 0;
+	virtual Goals::TGoalVec howToVisitObj(ObjectIdRef obj) = 0;
+	virtual std::vector<AIPath> getPathsToTile(HeroPtr hero, int3 tile) = 0;
+};
+	
+class CPathfindingManager : public IPathfindingManager
+{
+	friend class AIhelper;
+
+private:
+	CPlayerSpecificInfoCallback * cb; //this is enough, but we downcast from CCallback
+	VCAI * ai;
+	std::unique_ptr<AIPathfinder> pathfinder;
+
+public:
+	CPathfindingManager() = default;
+	CPathfindingManager(CPlayerSpecificInfoCallback * CB, VCAI * AI = nullptr); //for tests only
+
+	Goals::TGoalVec howToVisitTile(HeroPtr hero, int3 tile, bool allowGatherArmy = true) override;
+	Goals::TGoalVec howToVisitObj(HeroPtr hero, ObjectIdRef obj, bool allowGatherArmy = true) override;
+	Goals::TGoalVec howToVisitTile(int3 tile) override;
+	Goals::TGoalVec howToVisitObj(ObjectIdRef obj) override;
+	std::vector<AIPath> getPathsToTile(HeroPtr hero, int3 tile) override;
+	void resetPaths() override;
+
+private:
+	void setCB(CPlayerSpecificInfoCallback * CB) override;
+	void setAI(VCAI * AI) override;
+
+	Goals::TGoalVec findPath(
+		HeroPtr hero, 
+		crint3 dest,
+		bool allowGatherArmy, 
+		const std::function<Goals::TSubgoal(int3)> goalFactory);
+
+	Goals::TSubgoal clearWayTo(HeroPtr hero, int3 firstTileToGet);
+
+	Goals::TSubgoal selectVisitingGoal(HeroPtr hero, ObjectIdRef obj) const;
+};

+ 89 - 77
AI/VCAI/VCAI.cpp

@@ -120,7 +120,7 @@ void VCAI::heroMoved(const TryMoveHero & details)
 
 	validateObject(details.id); //enemy hero may have left visible area
 	auto hero = cb->getHero(details.id);
-	cachedSectorMaps.clear();
+	ah->resetPaths();
 
 	const int3 from = CGHeroInstance::convertPosition(details.start, false);
 	const int3 to = CGHeroInstance::convertPosition(details.end, false);
@@ -401,7 +401,7 @@ void VCAI::newObject(const CGObjectInstance * obj)
 	if(obj->isVisitable())
 		addVisitableObj(obj);
 
-	cachedSectorMaps.clear();
+	ah->resetPaths();
 }
 
 void VCAI::objectRemoved(const CGObjectInstance * obj)
@@ -460,7 +460,7 @@ void VCAI::objectRemoved(const CGObjectInstance * obj)
 		}
 	}
 
-	cachedSectorMaps.clear(); //invalidate all paths
+	ah->resetPaths();
 
 	//TODO
 	//there are other places where CGObjectinstance ptrs are stored...
@@ -761,7 +761,7 @@ void VCAI::saveGame(BinarySerializer & h, const int version)
 void VCAI::loadGame(BinaryDeserializer & h, const int version)
 {
 	LOG_TRACE_PARAMS(logAi, "version '%i'", version);
-	NET_EVENT_HANDLER;
+	//NET_EVENT_HANDLER;
 
 	#if 0
 	//disabled due to issue 2890
@@ -792,9 +792,11 @@ void makePossibleUpgrades(const CArmedInstance * obj)
 
 void VCAI::makeTurn()
 {
-	logGlobal->info("Player %d (%s) starting turn", playerID, playerID.getStr());
-
 	MAKING_TURN;
+
+	auto day = cb->getDate(Date::EDateType::DAY);
+	logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.getStr(), day);
+
 	boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
 	setThreadName("VCAI::makeTurn");
 
@@ -882,8 +884,12 @@ void VCAI::mainLoop()
 		elementarGoals.clear();
 		ultimateGoalsFromBasic.clear();
 
+		logAi->debug("Main loop: decomposing %i basic goals", basicGoals.size());
+
 		for (auto basicGoal : basicGoals)
 		{
+			logAi->debug("Main loop: decomposing basic goal %s", basicGoal->name());
+
 			auto goalToDecompose = basicGoal;
 			Goals::TSubgoal elementarGoal = sptr(Goals::Invalid());
 			int maxAbstractGoals = 10;
@@ -975,6 +981,9 @@ void VCAI::mainLoop()
 				completeGoal(e.goal);
 				//local goal was also completed?
 				completeGoal(goalToRealize);
+
+				// remove abstract visit tile if we completed the elementar one
+				vstd::erase_if_present(goalsToAdd, goalToRealize); 
 			}
 			catch (std::exception & e)
 			{
@@ -1013,17 +1022,6 @@ void VCAI::mainLoop()
 	}
 }
 
-bool VCAI::goVisitObj(const CGObjectInstance * obj, HeroPtr h)
-{
-	int3 dst = obj->visitablePos();
-	auto sm = getCachedSectorMap(h);
-	logAi->debug("%s will try to visit %s at (%s)", h->name, obj->getObjectName(), dst.toString());
-	int3 pos = sm->firstTileToGet(h, dst);
-	if(!pos.valid()) //rare case when we are already standing on one of potential objects
-		return false;
-	return moveHeroToTile(pos, h);
-}
-
 void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
 {
 	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->name % obj->getObjectName() % obj->pos.toString());
@@ -1289,10 +1287,27 @@ void VCAI::recruitCreatures(const CGDwelling * d, const CArmedInstance * recruit
 	}
 }
 
-bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, SectorMap & sm)
+bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, boost::optional<uint32_t> movementCostLimit)
+{
+	int3 op = obj->visitablePos();
+	auto paths = ah->getPathsToTile(h, op);
+
+	for(auto path : paths)
+	{
+		if(movementCostLimit && movementCostLimit.get() < path.movementCost())
+			return false;
+
+		if(ai->isGoodForVisit(obj, h, path))
+			return true;
+	}
+
+	return false;
+}
+
+bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const
 {
 	const int3 pos = obj->visitablePos();
-	const int3 targetPos = sm.firstTileToGet(h, pos);
+	const int3 targetPos = path.firstTileToGet();
 	if (!targetPos.valid())
 		return false;
 	if (!isTileNotReserved(h.get(), targetPos))
@@ -1309,8 +1324,11 @@ bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, SectorMap & s
 		return false;
 	if (vstd::contains(reservedObjs, obj))
 		return false;
-	if (!isAccessibleForHero(targetPos, h))
-		return false;
+	
+	// TODO: looks extra if we already have AIPath
+	//if (!isAccessibleForHero(targetPos, h)) 
+	//	return false;
+
 	const CGObjectInstance * topObj = cb->getVisitableObjs(obj->visitablePos()).back(); //it may be hero visiting this obj
 																						//we don't try visiting object on which allied or owned hero stands
 																						// -> it will just trigger exchange windows and AI will be confused that obj behind doesn't get visited
@@ -1320,12 +1338,14 @@ bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, SectorMap & s
 		return true; //all of the following is met
 }
 
-bool VCAI::isTileNotReserved(const CGHeroInstance * h, int3 t)
+bool VCAI::isTileNotReserved(const CGHeroInstance * h, int3 t) const
 {
 	if(t.valid())
 	{
 		auto obj = cb->getTopObj(t);
-		if(obj && vstd::contains(ai->reservedObjs, obj) && !vstd::contains(reservedHeroesMap[h], obj))
+		if(obj && vstd::contains(ai->reservedObjs, obj) 
+			&& vstd::contains(reservedHeroesMap, h)
+			&& !vstd::contains(reservedHeroesMap.at(h), obj))
 			return false; //do not capture object reserved by another hero
 		else
 			return true;
@@ -1380,47 +1400,38 @@ void VCAI::wander(HeroPtr h)
 	{
 		validateVisitableObjs();
 		std::vector<ObjectIdRef> dests;
-
-		auto sm = getCachedSectorMap(h);
-
+		
 		//also visit our reserved objects - but they are not prioritized to avoid running back and forth
 		vstd::copy_if(reservedHeroesMap[h], std::back_inserter(dests), [&](ObjectIdRef obj) -> bool
 		{
-			int3 pos = sm->firstTileToGet(h, obj->visitablePos());
-			if(pos.valid() && isAccessibleForHero(pos, h)) //even nearby objects could be blocked by other heroes :(
-				return true;
-
-			return false;
+			return ah->getPathsToTile(h, obj->visitablePos()).size();
 		});
 
 		int pass = 0;
-		while(!dests.size() && pass < 3)
+		std::vector<boost::optional<ui32>> distanceLimits = {
+			h->movement,
+			h->movement + h->maxMovePoints(true),
+			boost::none
+		};
+
+		while(!dests.size() && pass < distanceLimits.size())
 		{
-			if(pass < 2) // optimization - first check objects in current sector; then in sectors around
-			{
-				auto objs = sm->getNearbyObjs(h, pass);
-				vstd::copy_if(objs, std::back_inserter(dests), [&](ObjectIdRef obj) -> bool
-				{
-					return isGoodForVisit(obj, h, *sm);
-				});
-			}
-			else // we only check full objects list if for some reason there are no objects in closest sectors
+			boost::optional<ui32> distanceLimit = distanceLimits[pass];
+
+			logAi->debug("Looking for wander destination pass=%i, distance limit=%i", pass, distanceLimit.get_value_or(-1));
+
+			vstd::copy_if(visitableObjs, std::back_inserter(dests), [&](ObjectIdRef obj) -> bool
 			{
-				vstd::copy_if(visitableObjs, std::back_inserter(dests), [&](ObjectIdRef obj) -> bool
-				{
-					return isGoodForVisit(obj, h, *sm);
-				});
-			}
+				return isGoodForVisit(obj, h, distanceLimit);
+			});
+
 			pass++;
 		}
 
-		vstd::erase_if(dests, [&](ObjectIdRef obj) -> bool
-		{
-			return !isSafeToVisit(h, sm->firstTileToGet(h, obj->visitablePos()));
-		});
-
 		if(!dests.size())
 		{
+			logAi->debug("Looking for town destination");
+
 			if(cb->getVisitableObjs(h->visitablePos()).size() > 1)
 				moveHeroToTile(h->visitablePos(), h); //just in case we're standing on blocked subterranean gate
 
@@ -1497,10 +1508,10 @@ void VCAI::wander(HeroPtr h)
 			Goals::TGoalVec targetObjectGoals;
 			for(auto destination : dests)
 			{
-				targetObjectGoals.push_back(sptr(Goals::VisitObj(destination.id.getNum()).sethero(h).setisAbstract(true)));
+				vstd::concatenate(targetObjectGoals, ah->howToVisitObj(h, destination, false));
 			}
+
 			auto bestObjectGoal = fh->chooseSolution(targetObjectGoals);
-			decomposeGoal(bestObjectGoal)->accept(this);
 
 			//wander should not cause heroes to be reserved - they are always considered free
 			if(bestObjectGoal->goalType == Goals::VISIT_OBJ)
@@ -1510,7 +1521,19 @@ void VCAI::wander(HeroPtr h)
 					logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->pos.toString());
 			}
 			else
-				logAi->debug("Trying to realize goal of type %d as part of wandering.", bestObjectGoal->goalType);
+				logAi->debug("Trying to realize goal of type %s as part of wandering.", bestObjectGoal->name());
+
+			try
+			{
+				decomposeGoal(bestObjectGoal)->accept(this);
+			}
+			catch(goalFulfilledException e)
+			{
+				if(e.goal->goalType == Goals::EGoals::VISIT_TILE || e.goal->goalType == Goals::EGoals::VISIT_OBJ)
+					continue;
+
+				throw e;
+			}
 
 			visitTownIfAny(h);
 		}
@@ -1541,7 +1564,7 @@ void VCAI::completeGoal(Goals::TSubgoal goal)
 	if (goal->goalType == Goals::WIN) //we can never complete this goal - unless we already won
 		return;
 
-	logAi->trace("Completing goal: %s", goal->name());
+	logAi->debug("Completing goal: %s", goal->name());
 
 	//notify Managers
 	ah->notifyGoalCompleted(goal);
@@ -1649,7 +1672,7 @@ bool VCAI::isAbleToExplore(HeroPtr h)
 void VCAI::clearPathsInfo()
 {
 	heroesUnableToExplore.clear();
-	cachedSectorMaps.clear();
+	ah->resetPaths();
 }
 
 void VCAI::validateVisitableObjs()
@@ -1745,7 +1768,7 @@ const CGObjectInstance * VCAI::lookForArt(int aid) const
 	//TODO what if more than one artifact is available? return them all or some slection criteria
 }
 
-bool VCAI::isAccessible(const int3 & pos)
+bool VCAI::isAccessible(const int3 & pos) const
 {
 	//TODO precalculate for speed
 
@@ -2529,7 +2552,7 @@ void VCAI::performTypicalActions()
 		if(!h) //hero might be lost. getUnblockedHeroes() called once on start of turn
 			continue;
 
-		logAi->debug("Looking into %s, MP=%d", h->name.c_str(), h->movement);
+		logAi->debug("Hero %s started wandering, MP=%d", h->name.c_str(), h->movement);
 		makePossibleUpgrades(*h);
 		pickBestArtifacts(*h);
 		try
@@ -2644,7 +2667,6 @@ int3 VCAI::explorationNewPoint(HeroPtr h)
 
 int3 VCAI::explorationDesperate(HeroPtr h)
 {
-	auto sm = getCachedSectorMap(h);
 	int radius = h->getSightRadius();
 
 	std::vector<std::vector<int3>> tiles; //tiles[distance_to_fow]
@@ -2673,15 +2695,20 @@ int3 VCAI::explorationDesperate(HeroPtr h)
 			if(!howManyTilesWillBeDiscovered(tile, radius, cbp, h)) //avoid costly checks of tiles that don't reveal much
 				continue;
 
-			auto t = sm->firstTileToGet(h, tile);
-			if(t.valid())
+			auto paths = ah->getPathsToTile(h, tile);
+			for(auto path : paths)
 			{
+				auto t = path.firstTileToGet();
+
+				if(t == bestTile)
+					continue;
+
 				auto obj = cb->getTopObj(t);
 				if (obj)
 					if (obj->blockVisit && !isObjectRemovable(obj)) //we can't stand on object or remove it
 						continue;
 
-				ui64 ourDanger = evaluateDanger(t, h.h);
+				ui64 ourDanger = path.getTotalDanger(h);
 				if(ourDanger < lowestDanger)
 				{
 					if(!ourDanger) //at least one safe place found
@@ -2759,7 +2786,6 @@ void VCAI::lostHero(HeroPtr h)
 		vstd::erase_if_present(reservedObjs, obj); //unreserve all objects for that hero
 	}
 	vstd::erase_if_present(reservedHeroesMap, h);
-	vstd::erase_if_present(cachedSectorMaps, h);
 	vstd::erase_if_present(visitedHeroes, h);
 	for (auto heroVec : visitedHeroes)
 	{
@@ -2820,20 +2846,6 @@ void VCAI::validateObject(ObjectIdRef obj)
 	}
 }
 
-std::shared_ptr<SectorMap> VCAI::getCachedSectorMap(HeroPtr h)
-{
-	auto it = cachedSectorMaps.find(h);
-	if(it != cachedSectorMaps.end())
-	{
-		return it->second;
-	}
-	else
-	{
-		cachedSectorMaps[h] = std::make_shared<SectorMap>(h);
-		return cachedSectorMaps[h];
-	}
-}
-
 AIStatus::AIStatus()
 {
 	battle = NO_BATTLE;

+ 5 - 8
AI/VCAI/VCAI.h

@@ -25,6 +25,7 @@
 #include "../../lib/mapObjects/MiscObjects.h"
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/CondSh.h"
+#include "Pathfinding/AIPathfinder.h"
 
 struct QuestInfo;
 
@@ -106,9 +107,6 @@ public:
 	std::set<const CGObjectInstance *> reservedObjs; //to be visited by specific hero
 	std::map<HeroPtr, std::set<HeroPtr>> visitedHeroes; //visited this turn //FIXME: this is just bug workaround
 
-	//TODO: move to separate PathHandler class?
-	std::map<HeroPtr, std::shared_ptr<SectorMap>> cachedSectorMaps; //TODO: serialize? not necessary
-
 	AIStatus status;
 	std::string battlename;
 
@@ -137,7 +135,7 @@ public:
 	int3 explorationBestNeighbour(int3 hpos, int radius, HeroPtr h);
 	int3 explorationNewPoint(HeroPtr h);
 	int3 explorationDesperate(HeroPtr h);
-	bool isTileNotReserved(const CGHeroInstance * h, int3 t); //the tile is not occupied by allied hero and the object is not reserved
+	bool isTileNotReserved(const CGHeroInstance * h, int3 t) const; //the tile is not occupied by allied hero and the object is not reserved
 
 	std::string getBattleAIName() const override;
 
@@ -216,14 +214,14 @@ public:
 	Goals::TSubgoal questToGoal(const QuestInfo & q);
 
 	void recruitHero(const CGTownInstance * t, bool throwing = false);
-	bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, SectorMap & sm);
+	bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, boost::optional<uint32_t> movementCostLimit = boost::none);
+	bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const;
 	//void recruitCreatures(const CGTownInstance * t);
 	void recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter);
 	bool canGetArmy(const CGHeroInstance * h, const CGHeroInstance * source); //can we get any better stacks from other hero?
 	void pickBestCreatures(const CArmedInstance * army, const CArmedInstance * source); //called when we can't find a slot for new stack
 	void pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other = nullptr);
 	void moveCreaturesToHero(const CGTownInstance * t);
-	bool goVisitObj(const CGObjectInstance * obj, HeroPtr h);
 	void performObjectInteraction(const CGObjectInstance * obj, HeroPtr h);
 
 	bool moveHeroToTile(int3 dst, HeroPtr h);
@@ -250,13 +248,12 @@ public:
 	virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const;
 
 	const CGObjectInstance * lookForArt(int aid) const;
-	bool isAccessible(const int3 & pos);
+	bool isAccessible(const int3 & pos) const;
 	HeroPtr getHeroWithGrail() const;
 
 	const CGObjectInstance * getUnvisitedObj(const std::function<bool(const CGObjectInstance *)> & predicate);
 	bool isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies = false) const;
 	//optimization - use one SM for every hero call
-	std::shared_ptr<SectorMap> getCachedSectorMap(HeroPtr h);
 
 	const CGTownInstance * findTownWithTavern() const;
 	bool canRecruitAnyHero(const CGTownInstance * t = NULL) const;

+ 3 - 0
AUTHORS

@@ -72,3 +72,6 @@ Henning Koehler, <[email protected]>
 
 Andrzej Żak aka godric3
    * minor bug fixes and modding features
+   
+Andrii Danylchenko
+   * VCAI improvements

+ 0 - 5
CCallback.cpp

@@ -291,11 +291,6 @@ const CPathsInfo * CCallback::getPathsInfo(const CGHeroInstance *h)
 	return cl->getPathsInfo(h);
 }
 
-void CCallback::calculatePaths(std::shared_ptr<CPathfinderConfig> config, const CGHeroInstance * hero)
-{
-	cl->calculatePaths(config, hero);
-}
-
 int3 CCallback::getGuardingCreaturePosition(int3 tile)
 {
 	if (!gs->map->isInTheMap(tile))

+ 0 - 1
CCallback.h

@@ -107,7 +107,6 @@ public:
 	virtual bool canMoveBetween(const int3 &a, const int3 &b);
 	virtual int3 getGuardingCreaturePosition(int3 tile);
 	virtual const CPathsInfo * getPathsInfo(const CGHeroInstance *h);
-	virtual void calculatePaths(std::shared_ptr<CPathfinderConfig> config, const CGHeroInstance * hero);
 
 	virtual void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out);
 

+ 0 - 7
client/Client.cpp

@@ -628,13 +628,6 @@ const CPathsInfo * CClient::getPathsInfo(const CGHeroInstance * h)
 	return pathInfo.get();
 }
 
-void CClient::calculatePaths(std::shared_ptr<CPathfinderConfig> config, const CGHeroInstance * hero)
-{
-	boost::unique_lock<boost::mutex> pathLock(config->nodeStorage->getMutex());
-	
-	gs->calculatePaths(config, hero);
-}
-
 PlayerColor CClient::getLocalPlayer() const
 {
 	if(LOCPLINT)

+ 0 - 1
client/Client.h

@@ -151,7 +151,6 @@ public:
 
 	void invalidatePaths();
 	const CPathsInfo * getPathsInfo(const CGHeroInstance * h);
-	void calculatePaths(std::shared_ptr<CPathfinderConfig> config, const CGHeroInstance * hero);
 	virtual PlayerColor getLocalPlayer() const override;
 
 	friend class CCallback; //handling players actions

+ 5 - 0
lib/CGameInfoCallback.cpp

@@ -919,6 +919,11 @@ void CGameInfoCallback::getVisibleTilesInRange(std::unordered_set<int3, ShashInt
 	gs->getTilesInRange(tiles, pos, radious, getLocalPlayer(), -1, distanceFormula);
 }
 
+void CGameInfoCallback::calculatePaths(std::shared_ptr<CPathfinderConfig> config, const CGHeroInstance * hero)
+{
+	gs->calculatePaths(config, hero);
+}
+
 const CArtifactInstance * CGameInfoCallback::getArtInstance( ArtifactInstanceID aid ) const
 {
 	return gs->map->artInstances[aid.num];

+ 2 - 0
lib/CGameInfoCallback.h

@@ -31,6 +31,7 @@ struct TeamState;
 struct QuestInfo;
 struct ShashInt3;
 class CGameState;
+class CPathfinderConfig;
 
 
 class DLL_LINKAGE CGameInfoCallback : public virtual CCallbackBase
@@ -98,6 +99,7 @@ public:
 	virtual std::shared_ptr<boost::multi_array<TerrainTile*, 3>> getAllVisibleTiles() const;
 	virtual bool isInTheMap(const int3 &pos) const;
 	virtual void getVisibleTilesInRange(std::unordered_set<int3, ShashInt3> &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula = int3::DIST_2D) const;
+	virtual void calculatePaths(std::shared_ptr<CPathfinderConfig> config, const CGHeroInstance * hero);
 
 	//town
 	virtual const CGTownInstance* getTown(ObjectInstanceID objid) const;

+ 139 - 126
lib/CPathfinder.cpp

@@ -25,23 +25,19 @@ bool canSeeObj(const CGObjectInstance * obj)
 	return obj != nullptr && obj->ID != Obj::EVENT;
 }
 
-CNeighbourFinder::CNeighbourFinder()
-{
-}
-
-std::vector<CGPathNode *> CNeighbourFinder::calculateNeighbours(
+std::vector<CGPathNode *> CNodeStorage::calculateNeighbours(
 	CPathNodeInfo & source,
 	CPathfinderConfig * pathfinderConfig,
-	CPathfinderHelper * pathfinderHelper) const
+	CPathfinderHelper * pathfinderHelper)
 {
 	std::vector<CGPathNode *> neighbours;
-	auto accessibleNeighbourTiles = getNeighbourTiles(source, pathfinderConfig, pathfinderHelper);
+	auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
 
 	for(auto & neighbour : accessibleNeighbourTiles)
 	{
 		for(EPathfindingLayer i = EPathfindingLayer::LAND; i <= EPathfindingLayer::AIR; i.advance(1))
 		{
-			auto node = pathfinderConfig->nodeStorage->getNode(neighbour, i);
+			auto node = getNode(neighbour, i);
 
 			if(node->accessible == CGPathNode::NOT_SET)
 				continue;
@@ -53,17 +49,17 @@ std::vector<CGPathNode *> CNeighbourFinder::calculateNeighbours(
 	return neighbours;
 }
 
-std::vector<CGPathNode *> CNeighbourFinder::calculateTeleportations(
+std::vector<CGPathNode *> CNodeStorage::calculateTeleportations(
 	CPathNodeInfo & source,
 	CPathfinderConfig * pathfinderConfig,
-	CPathfinderHelper * pathfinderHelper) const
+	CPathfinderHelper * pathfinderHelper)
 {
 	std::vector<CGPathNode *> neighbours;
-	auto accessibleExits = getTeleportExits(source, pathfinderConfig, pathfinderHelper);
+	auto accessibleExits = pathfinderHelper->getTeleportExits(source);
 
 	for(auto & neighbour : accessibleExits)
 	{
-		auto node = pathfinderConfig->nodeStorage->getNode(neighbour, source.node->layer);
+		auto node = getNode(neighbour, source.node->layer);
 
 		neighbours.push_back(node);
 	}
@@ -71,14 +67,11 @@ std::vector<CGPathNode *> CNeighbourFinder::calculateTeleportations(
 	return neighbours;
 }
 
-std::vector<int3> CNeighbourFinder::getNeighbourTiles(
-	CPathNodeInfo & source,
-	CPathfinderConfig * pathfinderConfig,
-	CPathfinderHelper * pathfinderHelper) const
+std::vector<int3> CPathfinderHelper::getNeighbourTiles(CPathNodeInfo & source) const
 {
 	std::vector<int3> neighbourTiles;
 
-	pathfinderHelper->getNeighbours(
+	getNeighbours(
 		*source.tile,
 		source.node->coord,
 		neighbourTiles,
@@ -89,46 +82,51 @@ std::vector<int3> CNeighbourFinder::getNeighbourTiles(
 	{
 		vstd::erase_if(neighbourTiles, [&](int3 tile) -> bool
 		{
-			return !pathfinderHelper->canMoveBetween(tile, source.nodeObject->visitablePos());
+			return !canMoveBetween(tile, source.nodeObject->visitablePos());
 		});
 	}
 	
 	return neighbourTiles;
 }
 
-class CPathfinderNodeStorage : public CNodeStorage
+CNodeStorage::CNodeStorage(CPathsInfo & pathsInfo, const CGHeroInstance * hero)
+	:out(pathsInfo)
 {
-private:
-	CPathsInfo & out;
+	out.hero = hero;
+	out.hpos = hero->getPosition(false);
+}
 
-public:
-	CPathfinderNodeStorage(CPathsInfo & pathsInfo, const CGHeroInstance * hero)
-		:out(pathsInfo)
-	{
-		out.hero = hero;
-		out.hpos = hero->getPosition(false);
-	}
+CGPathNode * CNodeStorage::getNode(const int3 & coord, const EPathfindingLayer layer)
+{
+	return out.getNode(coord, layer);
+}
 
-	virtual boost::mutex & getMutex()
-	{
-		return out.pathMx;
-	}
+void CNodeStorage::resetTile(
+	const int3 & tile,
+	EPathfindingLayer layer,
+	CGPathNode::EAccessibility accessibility)
+{
+	getNode(tile, layer)->update(tile, layer, accessibility);
+}
 
-	virtual CGPathNode * getNode(const int3 & coord, const EPathfindingLayer layer)
-	{
-		return out.getNode(coord, layer);
-	}
+CGPathNode * CNodeStorage::getInitialNode()
+{
+	auto initialNode =  getNode(out.hpos, out.hero->boat ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND);
 
-	virtual CGPathNode * getInitialNode()
-	{
-		auto initialNode =  getNode(out.hpos, out.hero->boat ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND);
+	initialNode->turns = 0;
+	initialNode->moveRemains = out.hero->movement;
 
-		initialNode->turns = 0;
-		initialNode->moveRemains = out.hero->movement;
+	return initialNode;
+}
 
-		return initialNode;
-	}
-};
+void CNodeStorage::commit(CDestinationNodeInfo & destination, CPathNodeInfo & source)
+{
+	assert(destination.node != source.node->theNodeBefore); //two tiles can't point to each other
+	destination.node->moveRemains = destination.movementLeft;
+	destination.node->turns = destination.turn;
+	destination.node->theNodeBefore = source.node;
+	destination.node->action = destination.action;
+}
 
 PathfinderOptions::PathfinderOptions()
 {
@@ -151,7 +149,7 @@ void CMovementCostRule::process(
 	CPathNodeInfo & source,
 	CDestinationNodeInfo & destination,
 	CPathfinderConfig * pathfinderConfig,
-	CPathfinderHelper * pathfinderHelper)
+	CPathfinderHelper * pathfinderHelper) const
 {
 	int turnAtNextTile = destination.turn, moveAtNextTile = destination.movementLeft;
 	int cost = pathfinderHelper->getMovementCost(source, destination, moveAtNextTile);
@@ -177,11 +175,7 @@ void CMovementCostRule::process(
 	if(destination.isBetterWay() &&
 		((source.node->turns == turnAtNextTile && remains) || pathfinderHelper->passOneTurnLimitCheck(source)))
 	{
-		assert(destination.node != source.node->theNodeBefore); //two tiles can't point to each other
-		destination.node->moveRemains = remains;
-		destination.node->turns = turnAtNextTile;
-		destination.node->theNodeBefore = source.node;
-		destination.node->action = destination.action;
+		pathfinderConfig->nodeStorage->commit(destination, source);
 
 		return;
 	}
@@ -190,10 +184,9 @@ void CMovementCostRule::process(
 }
 
 CPathfinderConfig::CPathfinderConfig(
-	std::shared_ptr<CNodeStorage> nodeStorage,
-	std::shared_ptr<CNeighbourFinder> neighbourFinder,
+	std::shared_ptr<INodeStorage> nodeStorage,
 	std::vector<std::shared_ptr<IPathfindingRule>> rules)
-	: nodeStorage(nodeStorage), neighbourFinder(neighbourFinder), rules(rules), options()
+	: nodeStorage(nodeStorage), rules(rules), options()
 {
 }
 
@@ -205,9 +198,10 @@ CPathfinder::CPathfinder(
 		_gs,
 		_hero,
 		std::make_shared<CPathfinderConfig>(
-			std::make_shared<CPathfinderNodeStorage>(_out, _hero),
-			std::make_shared<CNeighbourFinder>(),
+			std::make_shared<CNodeStorage>(_out, _hero),
 			std::vector<std::shared_ptr<IPathfindingRule>>{
+				std::make_shared<CLayerTransitionRule>(),
+				std::make_shared<CDestinationActionRule>(),
 				std::make_shared<CMovementToDestinationRule>(),
 				std::make_shared<CMovementCostRule>(),
 				std::make_shared<CMovementAfterDestinationRule>()
@@ -276,35 +270,31 @@ void CPathfinder::calculatePaths()
 			source.objectRelations = gs->getPlayerRelations(hero->tempOwner, source.nodeObject->tempOwner);
 
 		//add accessible neighbouring nodes to the queue
-		auto neighbourNodes = config->neighbourFinder->calculateNeighbours(source, config.get(), hlp.get());
+		auto neighbourNodes = config->nodeStorage->calculateNeighbours(source, config.get(), hlp.get());
 		for(CGPathNode * neighbour : neighbourNodes)
 		{
-			destination.setNode(gs, neighbour);
-
-			if(destination.node->locked)
+			if(neighbour->locked)
 				continue;
 
-			if(!isPatrolMovementAllowed(destination.node->coord))
+			if(!isPatrolMovementAllowed(neighbour->coord))
 				continue;
 
-			if(!hlp->isLayerAvailable(destination.node->layer))
+			if(!hlp->isLayerAvailable(neighbour->layer))
 				continue;
 
 			/// Check transition without tile accessability rules
-			if(source.node->layer != destination.node->layer && !isLayerTransitionPossible(destination.node->layer))
-				continue;
-			
-			/// Check transition using tile accessability rules
-			if(source.node->layer != destination.node->layer && !isLayerTransitionPossible())
+			if(source.node->layer != neighbour->layer && !isLayerTransitionPossible(neighbour->layer))
 				continue;
 
-			destination.guarded = isDestinationGuarded();
-			if(destination.nodeObject)
-				destination.objectRelations = gs->getPlayerRelations(hero->tempOwner, destination.nodeObject->tempOwner);
+			destination.setNode(gs, neighbour);
 
-			destination.action = getDestAction();
 			destination.turn = turn;
 			destination.movementLeft = movement;
+
+			destination.guarded = isDestinationGuarded();
+			destination.isGuardianTile = destination.guarded && isDestinationGuardian();
+			if(destination.nodeObject)
+				destination.objectRelations = gs->getPlayerRelations(hero->tempOwner, destination.nodeObject->tempOwner);
 				
 			for(auto rule : config->rules)
 			{
@@ -326,7 +316,7 @@ void CPathfinder::calculatePaths()
 		if(!source.isNodeObjectVisitable() || patrolState == PATROL_RADIUS)
 			continue;
 
-		auto teleportationNodes = config->neighbourFinder->calculateTeleportations(source, config.get(), hlp.get());
+		auto teleportationNodes = config->nodeStorage->calculateTeleportations(source, config.get(), hlp.get());
 		for(CGPathNode * teleportNode : teleportationNodes)
 		{
 			if(teleportNode->locked)
@@ -345,10 +335,9 @@ void CPathfinder::calculatePaths()
 
 			if(destination.isBetterWay())
 			{
-				destination.node->moveRemains = destination.movementLeft;
-				destination.node->turns = destination.turn;
-				destination.node->theNodeBefore = source.node;
-				destination.node->action = getTeleportDestAction();
+				destination.action = getTeleportDestAction();
+				config->nodeStorage->commit(destination, source);
+
 				if(destination.node->action == CGPathNode::TELEPORT_NORMAL)
 					pq.push(destination.node);
 			}
@@ -396,28 +385,25 @@ std::vector<int3> CPathfinderHelper::getCastleGates(CPathNodeInfo & source) cons
 	return allowedExits;
 }
 
-std::vector<int3> CNeighbourFinder::getTeleportExits(
-	CPathNodeInfo & source,
-	CPathfinderConfig * pathfinderConfig,
-	CPathfinderHelper * pathfinderHelper) const
+std::vector<int3> CPathfinderHelper::getTeleportExits(CPathNodeInfo & source) const
 {
 	std::vector<int3> teleportationExits;
 
 	const CGTeleport * objTeleport = dynamic_cast<const CGTeleport *>(source.nodeObject);
-	if(pathfinderHelper->isAllowedTeleportEntrance(objTeleport))
+	if(isAllowedTeleportEntrance(objTeleport))
 	{
-		for(auto exit : pathfinderHelper->getAllowedTeleportChannelExits(objTeleport->channel))
+		for(auto exit : getAllowedTeleportChannelExits(objTeleport->channel))
 		{
 			teleportationExits.push_back(exit);
 		}
 	}
-	else if(pathfinderHelper->options.useCastleGate
+	else if(options.useCastleGate
 		&& (source.nodeObject->ID == Obj::TOWN && source.nodeObject->subID == ETownType::INFERNO
 		&& source.objectRelations != PlayerRelations::ENEMIES))
 	{
 		/// TODO: Find way to reuse CPlayerSpecificInfoCallback::getTownsInfo
 		/// This may be handy if we allow to use teleportation to friendly towns
-		for(auto exit : pathfinderHelper->getCastleGates(source))
+		for(auto exit : getCastleGates(source))
 		{
 			teleportationExits.push_back(exit);
 		}
@@ -488,68 +474,73 @@ bool CPathfinder::isLayerTransitionPossible(const ELayer destLayer) const
 	return false;
 }
 
-bool CPathfinder::isLayerTransitionPossible() const
+void CLayerTransitionRule::process(
+	CPathNodeInfo & source,
+	CDestinationNodeInfo & destination,
+	CPathfinderConfig * pathfinderConfig,
+	CPathfinderHelper * pathfinderHelper) const
 {
+	if(source.node->layer == destination.node->layer)
+		return;
+
 	switch(source.node->layer)
 	{
-	case ELayer::LAND:
-		if(destination.node->layer == ELayer::SAIL)
+	case EPathfindingLayer::LAND:
+		if(destination.node->layer == EPathfindingLayer::SAIL)
 		{
 			/// Cannot enter empty water tile from land -> it has to be visitable
 			if(destination.node->accessible == CGPathNode::ACCESSIBLE)
-				return false;
+				destination.blocked = true;
 		}
 
 		break;
 
-	case ELayer::SAIL:
+	case EPathfindingLayer::SAIL:
 		//tile must be accessible -> exception: unblocked blockvis tiles -> clear but guarded by nearby monster coast
 		if((destination.node->accessible != CGPathNode::ACCESSIBLE && (destination.node->accessible != CGPathNode::BLOCKVIS || destination.tile->blocked))
 			|| destination.tile->visitable)  //TODO: passableness problem -> town says it's passable (thus accessible) but we obviously can't disembark onto town gate
 		{
-			return false;
+			destination.blocked = true;
 		}
 
 		break;
 
-	case ELayer::AIR:
-		if(config->options.originalMovementRules)
+	case EPathfindingLayer::AIR:
+		if(pathfinderConfig->options.originalMovementRules)
 		{
 			if((source.node->accessible != CGPathNode::ACCESSIBLE &&
 				source.node->accessible != CGPathNode::VISITABLE) &&
 				(destination.node->accessible != CGPathNode::VISITABLE &&
 				 destination.node->accessible != CGPathNode::ACCESSIBLE))
 			{
-				return false;
+				destination.blocked = true;
 			}
 		}
 		else if(source.node->accessible != CGPathNode::ACCESSIBLE &&	destination.node->accessible != CGPathNode::ACCESSIBLE)
 		{
 			/// Hero that fly can only land on accessible tiles
-			return false;
+			destination.blocked = true;
 		}
 
 		break;
 
-	case ELayer::WATER:
+	case EPathfindingLayer::WATER:
 		if(destination.node->accessible != CGPathNode::ACCESSIBLE && destination.node->accessible != CGPathNode::VISITABLE)
 		{
 			/// Hero that walking on water can transit to accessible and visitable tiles
 			/// Though hero can't interact with blocking visit objects while standing on water
-			return false;
+			destination.blocked = true;
 		}
 
 		break;
 	}
-
-	return true;
 }
 
 CPathfinderBlockingRule::BlockingReason CMovementToDestinationRule::getBlockingReason(
 	CPathNodeInfo & source,
 	CDestinationNodeInfo & destination,
 	CPathfinderConfig * pathfinderConfig,
-	CPathfinderHelper * pathfinderHelper)
+	CPathfinderHelper * pathfinderHelper) const
 {
 
 	if(destination.node->accessible == CGPathNode::BLOCKED)
@@ -564,7 +555,7 @@ CPathfinderBlockingRule::BlockingReason CMovementToDestinationRule::getBlockingR
 		if(source.guarded)
 		{
 			if(!(pathfinderConfig->options.originalMovementRules && source.node->layer == EPathfindingLayer::AIR) &&
-				!destination.guarded) // Can step into tile of guard
+				!destination.isGuardianTile) // Can step into tile of guard
 			{
 				return BlockingReason::SOURCE_GUARDED;
 			}
@@ -579,7 +570,7 @@ CPathfinderBlockingRule::BlockingReason CMovementToDestinationRule::getBlockingR
 		if(source.guarded)
 		{
 			// Hero embarked a boat standing on a guarded tile -> we must allow to move away from that tile
-			if(source.node->action != CGPathNode::EMBARK && !destination.guarded)
+			if(source.node->action != CGPathNode::EMBARK && !destination.isGuardianTile)
 				return BlockingReason::SOURCE_GUARDED;
 		}
 
@@ -607,7 +598,7 @@ CPathfinderBlockingRule::BlockingReason CMovementToDestinationRule::getBlockingR
 		}
 
 		if(destination.guarded)
-			return BlockingReason::DESTINATION_GUARDED;
+			return BlockingReason::DESTINATION_BLOCKED;
 
 		break;
 	}
@@ -620,7 +611,7 @@ void CMovementAfterDestinationRule::process(
 	CPathNodeInfo & source,
 	CDestinationNodeInfo & destination,
 	CPathfinderConfig * config,
-	CPathfinderHelper * pathfinderHelper)
+	CPathfinderHelper * pathfinderHelper) const
 {
 	auto blocker = getBlockingReason(source, destination, config, pathfinderHelper);
 
@@ -636,7 +627,7 @@ CPathfinderBlockingRule::BlockingReason CMovementAfterDestinationRule::getBlocki
 	CPathNodeInfo & source, 
 	CDestinationNodeInfo & destination,
 	CPathfinderConfig * config,
-	CPathfinderHelper * pathfinderHelper)
+	CPathfinderHelper * pathfinderHelper) const
 {
 	switch(destination.action)
 	{
@@ -679,15 +670,15 @@ CPathfinderBlockingRule::BlockingReason CMovementAfterDestinationRule::getBlocki
 		return BlockingReason::DESTINATION_BLOCKED;
 
 	case CGPathNode::DISEMBARK:
-		if(pathfinderHelper->options.useEmbarkAndDisembark && !destination.guarded)
-			return BlockingReason::NONE;
+		if(pathfinderHelper->options.useEmbarkAndDisembark)
+			return destination.guarded ? BlockingReason::DESTINATION_GUARDED : BlockingReason::NONE;
 
 		return BlockingReason::DESTINATION_BLOCKED;
 
 	case CGPathNode::BATTLE:
 		/// Movement after BATTLE action only possible from guarded tile to guardian tile
 		if(destination.guarded)
-			return BlockingReason::NONE;
+			return BlockingReason::DESTINATION_GUARDED;
 
 		break;
 	}
@@ -695,13 +686,24 @@ CPathfinderBlockingRule::BlockingReason CMovementAfterDestinationRule::getBlocki
 	return BlockingReason::DESTINATION_BLOCKED;
 }
 
-CGPathNode::ENodeAction CPathfinder::getDestAction() const
+void CDestinationActionRule::process(
+	CPathNodeInfo & source,
+	CDestinationNodeInfo & destination,
+	CPathfinderConfig * pathfinderConfig,
+	CPathfinderHelper * pathfinderHelper) const
 {
+	if(destination.action != CGPathNode::ENodeAction::UNKNOWN)
+	{
+		return;
+	}
+
 	CGPathNode::ENodeAction action = CGPathNode::NORMAL;
+	auto hero = pathfinderHelper->hero;
+
 	switch(destination.node->layer)
 	{
-	case ELayer::LAND:
-		if(source.node->layer == ELayer::SAIL)
+	case EPathfindingLayer::LAND:
+		if(source.node->layer == EPathfindingLayer::SAIL)
 		{
 			// TODO: Handle dismebark into guarded areaa
 			action = CGPathNode::DISEMBARK;
@@ -711,10 +713,10 @@ CGPathNode::ENodeAction CPathfinder::getDestAction() const
 		/// don't break - next case shared for both land and sail layers
 		FALLTHROUGH
 
-	case ELayer::SAIL:
+	case EPathfindingLayer::SAIL:
 		if(destination.isNodeObjectVisitable())
 		{
-			auto objRel = getPlayerRelations(destination.nodeObject->tempOwner, hero->tempOwner);
+			auto objRel = destination.objectRelations;
 
 			if(destination.nodeObject->ID == Obj::BOAT)
 				action = CGPathNode::EMBARK;
@@ -736,7 +738,7 @@ CGPathNode::ENodeAction CPathfinder::getDestAction() const
 			{
 				if(destination.nodeObject->passableFor(hero->tempOwner))
 				{
-					if(isDestinationGuarded())
+					if(destination.guarded)
 						action = CGPathNode::BATTLE;
 				}
 				else if(objRel == PlayerRelations::ENEMIES)
@@ -746,32 +748,32 @@ CGPathNode::ENodeAction CPathfinder::getDestAction() const
 			{
 				if(destination.nodeObject->passableFor(hero->tempOwner))
 				{
-					if(isDestinationGuarded())
+					if(destination.guarded)
 						action = CGPathNode::BATTLE;
 				}
 				else
 					action = CGPathNode::BLOCKING_VISIT;
 			}
-			else if(isDestinationGuardian())
+			else if(destination.isGuardianTile)
 				action = CGPathNode::BATTLE;
-			else if(destination.nodeObject->blockVisit && !(config->options.useCastleGate && destination.nodeObject->ID == Obj::TOWN))
+			else if(destination.nodeObject->blockVisit && !(pathfinderConfig->options.useCastleGate && destination.nodeObject->ID == Obj::TOWN))
 				action = CGPathNode::BLOCKING_VISIT;
 
 			if(action == CGPathNode::NORMAL)
 			{
-				if(config->options.originalMovementRules && isDestinationGuarded())
+				if(pathfinderConfig->options.originalMovementRules && destination.guarded)
 					action = CGPathNode::BATTLE;
 				else
 					action = CGPathNode::VISIT;
 			}
 		}
-		else if(isDestinationGuarded())
+		else if(destination.guarded)
 			action = CGPathNode::BATTLE;
 
 		break;
 	}
 
-	return action;
+	destination.action = action;
 }
 
 CGPathNode::ENodeAction CPathfinder::getTeleportDestAction() const
@@ -842,9 +844,9 @@ void CPathfinder::initializeGraph()
 {
 	auto updateNode = [&](int3 pos, ELayer layer, const TerrainTile * tinfo)
 	{
-		auto node = config->nodeStorage->getNode(pos, layer);
 		auto accessibility = evaluateAccessibility(pos, tinfo, layer);
-		node->update(pos, layer, accessibility);
+		
+		config->nodeStorage->resetTile(pos, layer, accessibility);
 	};
 
 	int3 pos;
@@ -996,17 +998,17 @@ bool CPathfinderHelper::addTeleportWhirlpool(const CGWhirlpool * obj) const
 	return options.useTeleportWhirlpool && hasBonusOfType(Bonus::WHIRLPOOL_PROTECTION) && obj;
 }
 
-int CPathfinderHelper::getHeroMaxMovementPoints(EPathfindingLayer layer)
+int CPathfinderHelper::getHeroMaxMovementPoints(EPathfindingLayer layer) const
 {
 	return hero->maxMovePoints(layer);
 }
 
-int CPathfinderHelper::movementPointsAfterEmbark(int movement, int turn, int action)
+int CPathfinderHelper::movementPointsAfterEmbark(int movement, int turn, int action) const
 {
 	return hero->movementPointsAfterEmbark(movement, turn, action, getTurnInfo());
 }
 
-bool CPathfinderHelper::passOneTurnLimitCheck(CPathNodeInfo & source)
+bool CPathfinderHelper::passOneTurnLimitCheck(CPathNodeInfo & source) const
 {
 
 	if(!options.oneTurnSpecialLayersLimit)
@@ -1174,7 +1176,12 @@ int CPathfinderHelper::getMaxMovePoints(const EPathfindingLayer layer) const
 	return turnsInfo[turn]->getMaxMovePoints(layer);
 }
 
-void CPathfinderHelper::getNeighbours(const TerrainTile & srct, const int3 & tile, std::vector<int3> & vec, const boost::logic::tribool & onLand, const bool limitCoastSailing)
+void CPathfinderHelper::getNeighbours(
+	const TerrainTile & srct, 
+	const int3 & tile,
+	std::vector<int3> & vec,
+	const boost::logic::tribool & onLand, 
+	const bool limitCoastSailing) const
 {
 	CMap * map = gs->map;
 
@@ -1219,7 +1226,13 @@ void CPathfinderHelper::getNeighbours(const TerrainTile & srct, const int3 & til
 	}
 }
 
-int CPathfinderHelper::getMovementCost(const int3 & src, const int3 & dst, const TerrainTile * ct, const TerrainTile * dt, const int remainingMovePoints, const bool checkLast)
+int CPathfinderHelper::getMovementCost(
+	const int3 & src,
+	const int3 & dst, 
+	const TerrainTile * ct,
+	const TerrainTile * dt,
+	const int remainingMovePoints, 
+	const bool checkLast) const
 {
 	if(src == dst) //same tile
 		return 0;
@@ -1440,7 +1453,7 @@ void CDestinationNodeInfo::setNode(CGameState * gs, CGPathNode * n, bool exclude
 	action = CGPathNode::ENodeAction::UNKNOWN;
 }
 
-bool CDestinationNodeInfo::isBetterWay()
+bool CDestinationNodeInfo::isBetterWay() const
 {
 	if(node->turns == 0xff) //we haven't been here before
 		return true;

+ 86 - 46
lib/CPathfinder.h

@@ -22,6 +22,9 @@ struct TerrainTile;
 class CPathfinderHelper;
 class CMap;
 class CGWhirlpool;
+class CPathfinderHelper;
+class CPathfinder;
+class CPathfinderConfig;
 
 struct DLL_LINKAGE CGPathNode
 {
@@ -118,54 +121,63 @@ struct DLL_LINKAGE CDestinationNodeInfo : public CPathNodeInfo
 	int turn;
 	int movementLeft;
 	bool blocked;
+	bool isGuardianTile;
 
 	CDestinationNodeInfo();
 
 	virtual void setNode(CGameState * gs, CGPathNode * n, bool excludeTopObject = false) override;
 
-	virtual bool isBetterWay();
+	virtual bool isBetterWay() const;
 };
 
-class CNodeStorage
+class IPathfindingRule
 {
 public:
-	virtual CGPathNode * getNode(const int3 & coord, const EPathfindingLayer layer) = 0;
-	virtual CGPathNode * getInitialNode() = 0;
-	virtual boost::mutex & getMutex() = 0;
+	virtual void process(
+		CPathNodeInfo & source,
+		CDestinationNodeInfo & destination,
+		CPathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) const = 0;
 };
 
-class CPathfinderHelper;
-class CPathfinder;
-class CPathfinderConfig;
+class DLL_LINKAGE CMovementCostRule : public IPathfindingRule
+{
+public:
+	virtual void process(
+		CPathNodeInfo & source,
+		CDestinationNodeInfo & destination,
+		CPathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) const override;
+};
 
-class IPathfindingRule
+class DLL_LINKAGE CLayerTransitionRule : public IPathfindingRule
 {
 public:
 	virtual void process(
 		CPathNodeInfo & source,
 		CDestinationNodeInfo & destination,
 		CPathfinderConfig * pathfinderConfig,
-		CPathfinderHelper * pathfinderHelper) = 0;
+		CPathfinderHelper * pathfinderHelper) const override;
 };
 
-class CMovementCostRule : public IPathfindingRule
+class DLL_LINKAGE CDestinationActionRule : public IPathfindingRule
 {
 public:
 	virtual void process(
 		CPathNodeInfo & source,
 		CDestinationNodeInfo & destination,
 		CPathfinderConfig * pathfinderConfig,
-		CPathfinderHelper * pathfinderHelper) override;
+		CPathfinderHelper * pathfinderHelper) const override;
 };
 
-class CPathfinderBlockingRule : public IPathfindingRule
+class DLL_LINKAGE CPathfinderBlockingRule : public IPathfindingRule
 {
 public:
 	virtual void process(
 		CPathNodeInfo & source,
 		CDestinationNodeInfo & destination,
 		CPathfinderConfig * pathfinderConfig,
-		CPathfinderHelper * pathfinderHelper) override
+		CPathfinderHelper * pathfinderHelper) const override
 	{
 		auto blockingReason = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
 
@@ -188,59 +200,83 @@ protected:
 		CPathNodeInfo & source,
 		CDestinationNodeInfo & destination,
 		CPathfinderConfig * pathfinderConfig,
-		CPathfinderHelper * pathfinderHelper) = 0;
+		CPathfinderHelper * pathfinderHelper) const = 0;
 };
 
-class CMovementAfterDestinationRule : public CPathfinderBlockingRule
+class DLL_LINKAGE CMovementAfterDestinationRule : public CPathfinderBlockingRule
 {
 public:
 	virtual void process(
 		CPathNodeInfo & source,
 		CDestinationNodeInfo & destination,
 		CPathfinderConfig * pathfinderConfig,
-		CPathfinderHelper * pathfinderHelper) override;
+		CPathfinderHelper * pathfinderHelper) const override;
 
 protected:
 	virtual BlockingReason getBlockingReason(
 		CPathNodeInfo & source,
 		CDestinationNodeInfo & destination,
 		CPathfinderConfig * pathfinderConfig,
-		CPathfinderHelper * pathfinderHelper) override;
+		CPathfinderHelper * pathfinderHelper) const override;
 };
 
-class CMovementToDestinationRule : public CPathfinderBlockingRule
+class DLL_LINKAGE CMovementToDestinationRule : public CPathfinderBlockingRule
 {
 protected:
 	virtual BlockingReason getBlockingReason(
 		CPathNodeInfo & source,
 		CDestinationNodeInfo & destination,
 		CPathfinderConfig * pathfinderConfig,
-		CPathfinderHelper * pathfinderHelper) override;
+		CPathfinderHelper * pathfinderHelper) const override;
 };
 
-class CNeighbourFinder
+
+class INodeStorage
 {
 public:
-	CNeighbourFinder();
+	virtual CGPathNode * getInitialNode() = 0;
+	virtual void resetTile(const int3 & tile, EPathfindingLayer layer, CGPathNode::EAccessibility accessibility) = 0;
+
 	virtual std::vector<CGPathNode *> calculateNeighbours(
 		CPathNodeInfo & source, 
 		CPathfinderConfig * pathfinderConfig,
-		CPathfinderHelper * pathfinderHelper) const;
+		CPathfinderHelper * pathfinderHelper) = 0;
 
 	virtual std::vector<CGPathNode *> calculateTeleportations(
 		CPathNodeInfo & source, 
 		CPathfinderConfig * pathfinderConfig,
-		CPathfinderHelper * pathfinderHelper) const;
+		CPathfinderHelper * pathfinderHelper) = 0;
 
-protected:
-	std::vector<int3> getNeighbourTiles(
-		CPathNodeInfo & source, 
-		CPathfinderConfig * pathfinderConfig, 
-		CPathfinderHelper * pathfinderHelper) const;
+	virtual void commit(CDestinationNodeInfo & destination, CPathNodeInfo & source) = 0;
+};
+
+class DLL_LINKAGE CNodeStorage : public INodeStorage
+{
+private:
+	CPathsInfo & out;
+
+public:
+	CNodeStorage(CPathsInfo & pathsInfo, const CGHeroInstance * hero);
 
-	std::vector<int3> getTeleportExits(CPathNodeInfo & source,
-		CPathfinderConfig * pathfinderConfig, 
-		CPathfinderHelper * pathfinderHelper) const;
+	CGPathNode * getNode(const int3 & coord, const EPathfindingLayer layer);
+	virtual CGPathNode * getInitialNode() override;
+
+	virtual std::vector<CGPathNode *> calculateNeighbours(
+		CPathNodeInfo & source,
+		CPathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) override;
+
+	virtual std::vector<CGPathNode *> calculateTeleportations(
+		CPathNodeInfo & source,
+		CPathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) override;
+
+	virtual void resetTile(
+		const int3 & tile, 
+		EPathfindingLayer layer, 
+		CGPathNode::EAccessibility accessibility) override;
+
+	virtual void commit(CDestinationNodeInfo & destination, CPathNodeInfo & source) override;
 };
 
 struct DLL_LINKAGE PathfinderOptions
@@ -294,17 +330,15 @@ struct DLL_LINKAGE PathfinderOptions
 	PathfinderOptions();
 };
 
-class CPathfinderConfig
+class DLL_LINKAGE CPathfinderConfig
 {
 public:
-	std::shared_ptr<CNodeStorage> nodeStorage;
-	std::shared_ptr<CNeighbourFinder> neighbourFinder;
+	std::shared_ptr<INodeStorage> nodeStorage;
 	std::vector<std::shared_ptr<IPathfindingRule>> rules;
 	PathfinderOptions options;
 
 	CPathfinderConfig(
-		std::shared_ptr<CNodeStorage> nodeStorage,
-		std::shared_ptr<CNeighbourFinder> neighbourFinder,
+		std::shared_ptr<INodeStorage> nodeStorage,
 		std::vector<std::shared_ptr<IPathfindingRule>> rules);
 };
 
@@ -357,8 +391,6 @@ private:
 	bool isPatrolMovementAllowed(const int3 & dst) const;
 
 	bool isLayerTransitionPossible(const ELayer dstLayer) const;
-	bool isLayerTransitionPossible() const;
-	CGPathNode::ENodeAction getDestAction() const;
 	CGPathNode::ENodeAction getTeleportDestAction() const;
 
 	bool isSourceInitialPosition() const;
@@ -417,7 +449,7 @@ public:
 	bool hasBonusOfType(const Bonus::BonusType type, const int subtype = -1) const;
 	int getMaxMovePoints(const EPathfindingLayer layer) const;
 
-	std::vector<int3> CPathfinderHelper::getCastleGates(CPathNodeInfo & source) const;
+	std::vector<int3> getCastleGates(CPathNodeInfo & source) const;
 	bool isAllowedTeleportEntrance(const CGTeleport * obj) const;
 	std::vector<int3> getAllowedTeleportChannelExits(TeleportChannelID channelID) const;
 	bool addTeleportTwoWay(const CGTeleport * obj) const;
@@ -426,7 +458,15 @@ public:
 	bool addTeleportWhirlpool(const CGWhirlpool * obj) const;
 	bool canMoveBetween(const int3 & a, const int3 & b) const; //checks only for visitable objects that may make moving between tiles impossible, not other conditions (like tiles itself accessibility)
 
-	void getNeighbours(const TerrainTile & srct, const int3 & tile, std::vector<int3> & vec, const boost::logic::tribool & onLand, const bool limitCoastSailing);
+	std::vector<int3> getNeighbourTiles(CPathNodeInfo & source) const;
+	std::vector<int3> getTeleportExits(CPathNodeInfo & source) const;
+
+	void getNeighbours(
+		const TerrainTile & srct,
+		const int3 & tile, 
+		std::vector<int3> & vec, 
+		const boost::logic::tribool & onLand, 
+		const bool limitCoastSailing) const;
 	
 	int getMovementCost(
 		const int3 & src, 
@@ -434,13 +474,13 @@ public:
 		const TerrainTile * ct,
 		const TerrainTile * dt,
 		const int remainingMovePoints =- 1, 
-		const bool checkLast = true);
+		const bool checkLast = true) const;
 
 	int getMovementCost(
 		const CPathNodeInfo & src,
 		const CPathNodeInfo & dst,
 		const int remainingMovePoints = -1,
-		const bool checkLast = true)
+		const bool checkLast = true) const
 	{
 		return getMovementCost(
 			src.coord,
@@ -452,7 +492,7 @@ public:
 		);
 	}
 
-	int getHeroMaxMovementPoints(EPathfindingLayer layer);
-	int movementPointsAfterEmbark(int movement, int cost, int action);
-	bool passOneTurnLimitCheck(CPathNodeInfo & source);
+	int getHeroMaxMovementPoints(EPathfindingLayer layer) const;
+	int movementPointsAfterEmbark(int movement, int cost, int action) const;
+	bool passOneTurnLimitCheck(CPathNodeInfo & source) const;
 };