Browse Source

NKAI: initial implementation of object graph

Andrii Danylchenko 1 year ago
parent
commit
376a17409f

+ 2 - 2
AI/Nullkiller/AIUtility.h

@@ -185,8 +185,8 @@ void foreach_tile_pos(const Func & foo)
 	}
 }
 
-template<class Func>
-void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retrieval of thread-specific pointer
+template<class Func, class TCallback>
+void foreach_tile_pos(TCallback * cbp, const Func & foo) // avoid costly retrieval of thread-specific pointer
 {
 	int3 mapSize = cbp->getMapSize();
 	for(int z = 0; z < mapSize.z; z++)

+ 10 - 6
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -75,8 +75,7 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 		PathfinderSettings ps;
 
-		ps.mainTurnDistanceLimit = 10;
-		ps.scoutTurnDistanceLimit = 10;
+		ps.scoutTurnDistanceLimit = ps.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT;
 		ps.useHeroChain = false;
 
 		ai->pathfinder->updatePaths(pair.second, ps);
@@ -160,9 +159,6 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 
 	std::map<const CGHeroInstance *, HeroRole> townHeroes;
 	std::map<const CGHeroInstance *, const CGTownInstance *> heroTownMap;
-	PathfinderSettings pathfinderSettings;
-
-	pathfinderSettings.mainTurnDistanceLimit = 5;
 
 	auto addTownHero = [&](const CGTownInstance * town)
 	{
@@ -192,7 +188,10 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 		addTownHero(town);
 	}
 
-	ai->pathfinder->updatePaths(townHeroes, PathfinderSettings());
+	PathfinderSettings ps;
+	ps.mainTurnDistanceLimit = ps.scoutTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT;
+
+	ai->pathfinder->updatePaths(townHeroes, ps);
 
 	pforeachTilePos(mapSize, [&](const int3 & pos)
 		{
@@ -239,6 +238,11 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 				hitMap[pos.x][pos.y][pos.z].closestTown = enemyTown;
 			}
 		});
+
+	for(auto h : townHeroes)
+	{
+		delete h.first;
+	}
 }
 
 const std::vector<HitMapInfo> & DangerHitMapAnalyzer::getTownThreats(const CGTownInstance * town) const

+ 1 - 1
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -244,7 +244,7 @@ void ObjectClusterizer::clusterize()
 			logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
 #endif
 
-			auto paths = ai->pathfinder->getPathInfo(obj->visitablePos());
+			auto paths = ai->pathfinder->getPathInfo(obj->visitablePos(), true);
 
 			if(paths.empty())
 			{

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

@@ -179,7 +179,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 
 			const int3 pos = objToVisit->visitablePos();
 
-			auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
+			auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, true);
 			std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
 			std::shared_ptr<ExecuteHeroChain> closestWay;
 					
@@ -210,7 +210,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 	{
 		captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());
 
-		if(tasks.empty() || ai->nullkiller->getScanDepth() != ScanDepth::SMALL)
+		if(tasks.empty())
 			captureObjects(ai->nullkiller->objectClusterizer->getFarObjects());
 	}
 

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

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

+ 2 - 0
AI/Nullkiller/CMakeLists.txt

@@ -14,6 +14,7 @@ set(Nullkiller_SRCS
 		Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
 		Pathfinding/Rules/AIMovementToDestinationRule.cpp
 		Pathfinding/Rules/AIPreviousNodeRule.cpp
+		Pathfinding/ObjectGraph.cpp
 		AIUtility.cpp
 		Analyzers/ArmyManager.cpp
 		Analyzers/HeroManager.cpp
@@ -78,6 +79,7 @@ set(Nullkiller_HEADERS
 		Pathfinding/Rules/AIMovementAfterDestinationRule.h
 		Pathfinding/Rules/AIMovementToDestinationRule.h
 		Pathfinding/Rules/AIPreviousNodeRule.h
+		Pathfinding/ObjectGraph.h
 		AIUtility.h
 		Analyzers/ArmyManager.h
 		Analyzers/HeroManager.h

+ 10 - 0
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -27,6 +27,9 @@ namespace NKAI
 
 using namespace Goals;
 
+// while we play vcmieagles graph can be shared
+std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
+
 Nullkiller::Nullkiller()
 	:activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true)
 {
@@ -119,6 +122,12 @@ void Nullkiller::resetAiState()
 	lockedHeroes.clear();
 	dangerHitMap->reset();
 	useHeroChain = true;
+
+	if(!baseGraph)
+	{
+		baseGraph = std::make_unique<ObjectGraph>();
+		baseGraph->updateGraph(this);
+	}
 }
 
 void Nullkiller::updateAiState(int pass, bool fast)
@@ -173,6 +182,7 @@ void Nullkiller::updateAiState(int pass, bool fast)
 		boost::this_thread::interruption_point();
 
 		pathfinder->updatePaths(activeHeroes, cfg);
+		pathfinder->updateGraphs(activeHeroes);
 
 		boost::this_thread::interruption_point();
 

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

@@ -25,7 +25,6 @@ namespace NKAI
 {
 
 const float MIN_PRIORITY = 0.01f;
-const float SMALL_SCAN_MIN_PRIORITY = 0.4f;
 
 enum class HeroLockedReason
 {
@@ -38,15 +37,6 @@ enum class HeroLockedReason
 	HERO_CHAIN = 3
 };
 
-enum class ScanDepth
-{
-	MAIN_FULL = 0,
-
-	SMALL = 1,
-
-	ALL_FULL = 2
-};
-
 class Nullkiller
 {
 private:
@@ -54,11 +44,12 @@ private:
 	int3 targetTile;
 	ObjectInstanceID targetObject;
 	std::map<const CGHeroInstance *, HeroLockedReason> lockedHeroes;
-	ScanDepth scanDepth;
 	TResources lockedResources;
 	bool useHeroChain;
 
 public:
+	static std::unique_ptr<ObjectGraph> baseGraph;
+
 	std::unique_ptr<DangerHitMapAnalyzer> dangerHitMap;
 	std::unique_ptr<BuildAnalyzer> buildAnalyzer;
 	std::unique_ptr<ObjectClusterizer> objectClusterizer;
@@ -94,7 +85,6 @@ public:
 	int32_t getFreeGold() const { return getFreeResources()[EGameResID::GOLD]; }
 	void lockResources(const TResources & res);
 	const TResources & getLockedResources() const { return lockedResources; }
-	ScanDepth getScanDepth() const { return scanDepth; }
 
 private:
 	void resetAiState();

+ 2 - 6
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -26,13 +26,9 @@ namespace NKAI
 {
 namespace AIPathfinding
 {
-#ifdef ENVIRONMENT64
-	const int BUCKET_COUNT = 7;
-#else
-	const int BUCKET_COUNT = 5;
-#endif // ENVIRONMENT64
 
-	const int BUCKET_SIZE = 5;
+    const int BUCKET_COUNT = 5;
+	const int BUCKET_SIZE = 3;
 	const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
 	const int THREAD_COUNT = 8;
 	const int CHAIN_MAX_DEPTH = 4;

+ 44 - 3
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -33,7 +33,7 @@ bool AIPathfinder::isTileAccessible(const HeroPtr & hero, const int3 & tile) con
 		|| storage->isTileAccessible(hero, tile, EPathfindingLayer::SAIL);
 }
 
-std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile) const
+std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile, bool includeGraph) const
 {
 	const TerrainTile * tileInfo = cb->getTile(tile, false);
 
@@ -42,7 +42,19 @@ std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile) const
 		return std::vector<AIPath>();
 	}
 
-	return storage->getChainInfo(tile, !tileInfo->isWater());
+	auto info = storage->getChainInfo(tile, !tileInfo->isWater());
+
+	if(includeGraph)
+	{
+		for(auto hero : cb->getHeroesInfo())
+		{
+			auto & graph = heroGraphs.at(hero->id);
+
+			graph.addChainInfo(info, tile, hero, ai);
+		}
+	}
+
+	return info;
 }
 
 void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes, PathfinderSettings pathfinderSettings)
@@ -71,7 +83,7 @@ void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes
 		storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs);
 	}
 
-	auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage);
+	auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage, pathfinderSettings.allowBypassObjects);
 
 	logAi->trace("Recalculate paths pass %d", pass++);
 	cb->calculatePaths(config);
@@ -112,4 +124,33 @@ void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes
 	logAi->trace("Recalculated paths in %ld", timeElapsed(start));
 }
 
+void AIPathfinder::updateGraphs(std::map<const CGHeroInstance *, HeroRole> heroes)
+{
+	auto start = std::chrono::high_resolution_clock::now();
+	std::vector<const CGHeroInstance *> heroesVector;
+
+	heroGraphs.clear();
+
+	for(auto hero : heroes)
+	{
+		heroGraphs.emplace(hero.first->id, GraphPaths());
+		heroesVector.push_back(hero.first);
+	}
+
+	parallel_for(blocked_range<size_t>(0, heroesVector.size()), [&](const blocked_range<size_t> & r)
+		{
+			for(auto i = r.begin(); i != r.end(); i++)
+				heroGraphs[heroesVector[i]->id].calculatePaths(heroesVector[i], ai);
+		});
+
+#if NKAI_TRACE_LEVEL >= 2
+	for(auto hero : heroes)
+	{
+		heroGraphs[hero.first->id].dumpToLog();
+	}
+#endif
+
+	logAi->trace("Graph paths updated in %lld", timeElapsed(start));
+}
+
 }

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

@@ -11,6 +11,7 @@
 #pragma once
 
 #include "AINodeStorage.h"
+#include "ObjectGraph.h"
 #include "../AIUtility.h"
 
 namespace NKAI
@@ -23,11 +24,13 @@ struct PathfinderSettings
 	bool useHeroChain;
 	uint8_t scoutTurnDistanceLimit;
 	uint8_t mainTurnDistanceLimit;
+	bool allowBypassObjects;
 
 	PathfinderSettings()
 		:useHeroChain(false),
 		scoutTurnDistanceLimit(255),
-		mainTurnDistanceLimit(255)
+		mainTurnDistanceLimit(255),
+		allowBypassObjects(true)
 	{ }
 };
 
@@ -37,12 +40,14 @@ private:
 	std::shared_ptr<AINodeStorage> storage;
 	CPlayerSpecificInfoCallback * cb;
 	Nullkiller * ai;
+	std::map<ObjectInstanceID, GraphPaths>  heroGraphs;
 
 public:
 	AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * ai);
-	std::vector<AIPath> getPathInfo(const int3 & tile) const;
+	std::vector<AIPath> getPathInfo(const int3 & tile, bool includeGraph = false) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const;
 	void updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes, PathfinderSettings pathfinderSettings);
+	void updateGraphs(std::map<const CGHeroInstance *, HeroRole> heroes);
 	void init();
 };
 

+ 13 - 11
AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp

@@ -24,16 +24,17 @@ namespace AIPathfinding
 	std::vector<std::shared_ptr<IPathfindingRule>> makeRuleset(
 		CPlayerSpecificInfoCallback * cb,
 		Nullkiller * ai,
-		std::shared_ptr<AINodeStorage> nodeStorage)
+		std::shared_ptr<AINodeStorage> nodeStorage,
+		bool allowBypassObjects)
 	{
-		std::vector<std::shared_ptr<IPathfindingRule>> rules = {
-			std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage),
-			std::make_shared<DestinationActionRule>(),
-			std::make_shared<AIMovementToDestinationRule>(nodeStorage),
-			std::make_shared<MovementCostRule>(),
-			std::make_shared<AIPreviousNodeRule>(nodeStorage),
-			std::make_shared<AIMovementAfterDestinationRule>(cb, nodeStorage)
-		};
+			std::vector<std::shared_ptr<IPathfindingRule>> rules = {
+				std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage),
+				std::make_shared<DestinationActionRule>(),
+				std::make_shared<AIMovementToDestinationRule>(nodeStorage),
+				std::make_shared<MovementCostRule>(),
+				std::make_shared<AIPreviousNodeRule>(nodeStorage),
+				std::make_shared<AIMovementAfterDestinationRule>(cb, nodeStorage, allowBypassObjects)
+			};
 
 		return rules;
 	}
@@ -41,8 +42,9 @@ namespace AIPathfinding
 	AIPathfinderConfig::AIPathfinderConfig(
 		CPlayerSpecificInfoCallback * cb,
 		Nullkiller * ai,
-		std::shared_ptr<AINodeStorage> nodeStorage)
-		:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), aiNodeStorage(nodeStorage)
+		std::shared_ptr<AINodeStorage> nodeStorage,
+		bool allowBypassObjects)
+		:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage, allowBypassObjects)), aiNodeStorage(nodeStorage)
 	{
 		options.canUseCast = true;
 	}

+ 2 - 1
AI/Nullkiller/Pathfinding/AIPathfinderConfig.h

@@ -30,7 +30,8 @@ namespace AIPathfinding
 		AIPathfinderConfig(
 			CPlayerSpecificInfoCallback * cb,
 			Nullkiller * ai,
-			std::shared_ptr<AINodeStorage> nodeStorage);
+			std::shared_ptr<AINodeStorage> nodeStorage,
+			bool allowBypassObjects);
 
 		~AIPathfinderConfig();
 

+ 237 - 0
AI/Nullkiller/Pathfinding/ObjectGraph.cpp

@@ -0,0 +1,237 @@
+/*
+* ObjectGraph.cpp, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#include "StdInc.h"
+#include "ObjectGraph.h"
+#include "AIPathfinderConfig.h"
+#include "../../../CCallback.h"
+#include "../../../lib/mapping/CMap.h"
+#include "../Engine/Nullkiller.h"
+
+namespace NKAI
+{
+
+void ObjectGraph::updateGraph(const Nullkiller * ai)
+{
+	auto cb = ai->cb;
+	auto mapSize = cb->getMapSize();
+
+	std::map<const CGHeroInstance *, HeroRole> actors;
+	std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
+
+	auto addObjectActor = [&](const CGObjectInstance * obj)
+	{
+		auto objectActor = new CGHeroInstance(obj->cb);
+		CRandomGenerator rng;
+		auto visitablePos = obj->visitablePos();
+
+		objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
+		objectActor->initHero(rng, static_cast<HeroTypeID>(0));
+		objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
+		objectActor->initObj(rng);
+
+		actorObjectMap[objectActor] = obj;
+		actors[objectActor] = obj->ID == Obj::TOWN ?  HeroRole::MAIN : HeroRole::SCOUT;
+		addObject(obj);
+	};
+
+	for(auto obj : ai->memory->visitableObjs)
+	{
+		if(obj && obj->isVisitable() && obj->ID != Obj::HERO)
+		{
+			addObjectActor(obj);
+		}
+	}
+
+	for(auto town : cb->getTownsInfo())
+	{
+		addObjectActor(town);
+	}
+
+	PathfinderSettings ps;
+	
+	ps.mainTurnDistanceLimit = 5;
+	ps.scoutTurnDistanceLimit = 1;
+	ps.allowBypassObjects = false;
+
+	ai->pathfinder->updatePaths(actors, ps);
+
+	foreach_tile_pos(cb.get(), [&](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+		{
+			auto paths = ai->pathfinder->getPathInfo(pos);
+
+			for(AIPath & path1 : paths)
+			{
+				for(AIPath & path2 : paths)
+				{
+					if(path1.targetHero == path2.targetHero)
+						continue;
+
+					auto obj1 = actorObjectMap[path1.targetHero];
+					auto obj2 = actorObjectMap[path2.targetHero];
+
+					nodes[obj1->visitablePos()].connections[obj2->visitablePos()].update(
+						path1.movementCost() + path2.movementCost(),
+						path1.getPathDanger() + path2.getPathDanger());
+				}
+			}
+		});
+
+	for(auto h : actors)
+	{
+		delete h.first;
+	}
+}
+
+void ObjectGraph::addObject(const CGObjectInstance * obj)
+{
+	nodes[obj->visitablePos()].init(obj);
+}
+
+void ObjectGraph::connectHeroes(const Nullkiller * ai)
+{
+	for(auto obj : ai->memory->visitableObjs)
+	{
+		if(obj && obj->ID == Obj::HERO)
+		{
+			addObject(obj);
+		}
+	}
+
+	for(auto node : nodes)
+	{
+		auto pos = node.first;
+		auto paths = ai->pathfinder->getPathInfo(pos);
+
+		for(AIPath & path : paths)
+		{
+			if(path.turn() == 0)
+				continue;
+
+			auto heroPos = path.targetHero->visitablePos();
+
+			nodes[pos].connections[heroPos].update(
+				path.movementCost(),
+				path.getPathDanger());
+
+			nodes[heroPos].connections[pos].update(
+				path.movementCost(),
+				path.getPathDanger());
+		}
+	}
+}
+
+bool GraphNodeComparer::operator()(int3 lhs, int3 rhs) const
+{
+	return pathNodes.at(lhs).cost > pathNodes.at(rhs).cost;
+}
+
+void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai)
+{
+	graph = *ai->baseGraph;
+	graph.connectHeroes(ai);
+
+	pathNodes.clear();
+
+	GraphNodeComparer cmp(pathNodes);
+	GraphPathNode::TFibHeap pq(cmp);
+
+	pathNodes[targetHero->visitablePos()].cost = 0;
+	pq.emplace(targetHero->visitablePos());
+
+	while(!pq.empty())
+	{
+		int3 pos = pq.top();
+		pq.pop();
+
+		auto node = pathNodes[pos];
+
+		node.isInQueue = false;
+
+		graph.iterateConnections(pos, [&](int3 target, ObjectLink o)
+			{
+				auto & targetNode = pathNodes[target];
+
+				if(targetNode.tryUpdate(pos, node, o))
+				{
+					if(targetNode.isInQueue)
+					{
+						pq.increase(targetNode.handle);
+					}
+					else
+					{
+						targetNode.handle = pq.emplace(target);
+						targetNode.isInQueue = true;
+					}
+				}
+			});
+	}
+}
+
+void GraphPaths::dumpToLog() const
+{
+	for(auto & node : pathNodes)
+	{
+		logAi->trace(
+			"%s -> %s: %f !%d",
+			node.second.previous.toString(),
+			node.first.toString(),
+			node.second.cost,
+			node.second.danger);
+	}
+}
+
+void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const
+{
+	auto node = pathNodes.find(tile);
+
+	if(node == pathNodes.end() || !node->second.reachable())
+		return;
+
+	std::vector<int3> tilesToPass;
+
+	uint64_t danger = node->second.danger;
+	float cost = node->second.cost;;
+
+	while(node != pathNodes.end() && node->second.cost > 1)
+	{
+		vstd::amax(danger, node->second.danger);
+
+		tilesToPass.push_back(node->first);
+		node = pathNodes.find(node->second.previous);
+	}
+
+	if(tilesToPass.empty())
+		return;
+
+	auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back());
+
+	for(auto & path : entryPaths)
+	{
+		if(path.targetHero != hero)
+			continue;
+
+		for(auto graphTile : tilesToPass)
+		{
+			AIPathNodeInfo n;
+
+			n.coord = graphTile;
+			n.cost = cost;
+			n.turns = static_cast<ui8>(cost) + 1; // just in case lets select worst scenario
+			n.danger = danger;
+			n.targetHero = hero;
+
+			path.nodes.insert(path.nodes.begin(), n);
+		}
+
+		paths.push_back(path);
+	}
+}
+
+}

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

@@ -0,0 +1,129 @@
+/*
+* ObjectGraph.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"
+#include "../AIUtility.h"
+
+namespace NKAI
+{
+
+class Nullkiller;
+
+struct ObjectLink
+{
+	float cost = 100000; // some big number
+	uint64_t danger = 0;
+
+	void update(float newCost, uint64_t newDanger)
+	{
+		if(cost > newCost)
+		{
+			cost = newCost;
+			danger = newDanger;
+		}
+	}
+};
+
+struct ObjectNode
+{
+	ObjectInstanceID objID;
+	bool objectExists;
+	std::unordered_map<int3, ObjectLink> connections;
+
+	void init(const CGObjectInstance * obj)
+	{
+		objectExists = true;
+		objID = obj->id;
+	}
+};
+
+class ObjectGraph
+{
+	std::unordered_map<int3, ObjectNode> nodes;
+
+public:
+	void updateGraph(const Nullkiller * ai);
+	void addObject(const CGObjectInstance * obj);
+	void connectHeroes(const Nullkiller * ai);
+
+	template<typename Func>
+	void iterateConnections(const int3 & pos, Func fn)
+	{
+		for(auto connection : nodes.at(pos).connections)
+		{
+			fn(connection.first, connection.second);
+		}
+	}
+};
+
+struct GraphPathNode;
+
+class GraphNodeComparer
+{
+	const std::unordered_map<int3, GraphPathNode> & pathNodes;
+
+public:
+	GraphNodeComparer(const std::unordered_map<int3, GraphPathNode> & pathNodes)
+		:pathNodes(pathNodes)
+	{
+	}
+
+	bool operator()(int3 lhs, int3 rhs) const;
+};
+
+struct GraphPathNode
+{
+	const float BAD_COST = 100000;
+
+	int3 previous = int3(-1);
+	float cost = BAD_COST;
+	uint64_t danger = 0;
+
+	using TFibHeap = boost::heap::fibonacci_heap<int3, boost::heap::compare<GraphNodeComparer>>;
+
+	TFibHeap::handle_type handle;
+	bool isInQueue = false;
+
+	bool reachable() const
+	{
+		return cost < BAD_COST;
+	}
+
+	bool tryUpdate(const int3 & pos, const GraphPathNode & prev, const ObjectLink & link)
+	{
+		auto newCost = prev.cost + link.cost;
+
+		if(newCost < cost)
+		{
+			previous = pos;
+			danger = prev.danger + link.danger;
+			cost = newCost;
+
+			return true;
+		}
+
+		return false;
+	}
+};
+
+class GraphPaths
+{
+	ObjectGraph graph;
+	std::unordered_map<int3, GraphPathNode> pathNodes;
+
+public:
+	void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai);
+	void addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
+	void dumpToLog() const;
+};
+
+}

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

@@ -13,6 +13,7 @@
 #include "../Actions/QuestAction.h"
 #include "../../Goals/Invalid.h"
 #include "AIPreviousNodeRule.h"
+#include "../../../../lib/pathfinder/PathfinderOptions.h"
 
 namespace NKAI
 {
@@ -20,8 +21,9 @@ namespace AIPathfinding
 {
 	AIMovementAfterDestinationRule::AIMovementAfterDestinationRule(
 		CPlayerSpecificInfoCallback * cb, 
-		std::shared_ptr<AINodeStorage> nodeStorage)
-		:cb(cb), nodeStorage(nodeStorage)
+		std::shared_ptr<AINodeStorage> nodeStorage,
+		bool allowBypassObjects)
+		:cb(cb), nodeStorage(nodeStorage), allowBypassObjects(allowBypassObjects)
 	{
 	}
 
@@ -47,6 +49,9 @@ namespace AIPathfinding
 			return;
 		}
 
+		if(!allowBypassObjects)
+			return;
+
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 		logAi->trace(
 			"Movement from tile %s is blocked. Try to bypass. Action: %d, blocker: %d",

+ 5 - 1
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.h

@@ -25,9 +25,13 @@ namespace AIPathfinding
 	private:
 		CPlayerSpecificInfoCallback * cb;
 		std::shared_ptr<AINodeStorage> nodeStorage;
+		bool allowBypassObjects;
 
 	public:
-		AIMovementAfterDestinationRule(CPlayerSpecificInfoCallback * cb, std::shared_ptr<AINodeStorage> nodeStorage);
+		AIMovementAfterDestinationRule(
+			CPlayerSpecificInfoCallback * cb,
+			std::shared_ptr<AINodeStorage> nodeStorage,
+			bool allowBypassObjects);
 
 		virtual void process(
 			const PathNodeInfo & source,