Browse Source

NKAI: optimize clusterization and pathfinding for object graph

Andrii Danylchenko 1 year ago
parent
commit
419d6a648b

+ 22 - 0
AI/Nullkiller/AIUtility.h

@@ -266,6 +266,28 @@ void pforeachTilePos(const int3 & mapSize, TFunc fn)
 	}
 }
 
+template<typename TFunc>
+void pforeachTilePaths(const int3 & mapSize, const Nullkiller * ai, TFunc fn)
+{
+	for(int z = 0; z < mapSize.z; ++z)
+	{
+		parallel_for(blocked_range<size_t>(0, mapSize.x), [&](const blocked_range<size_t> & r)
+			{
+				int3 pos(0, 0, z);
+				std::vector<AIPath> paths;
+
+				for(pos.x = r.begin(); pos.x != r.end(); ++pos.x)
+				{
+					for(pos.y = 0; pos.y < mapSize.y; ++pos.y)
+					{
+						ai->pathfinder->calculatePathInfo(paths, pos);
+						fn(pos, paths);
+					}
+				}
+			});
+	}
+}
+
 class CDistanceSorter
 {
 	const CGHeroInstance * hero;

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

@@ -82,9 +82,9 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 		boost::this_thread::interruption_point();
 
-		pforeachTilePos(mapSize, [&](const int3 & pos)
+		pforeachTilePaths(mapSize, ai, [&](const int3 & pos, const std::vector<AIPath> & paths)
 		{
-			for(AIPath & path : ai->pathfinder->getPathInfo(pos))
+			for(const AIPath & path : paths)
 			{
 				if(path.getFirstBlockedAction())
 					continue;
@@ -194,14 +194,14 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 
 	ai->pathfinder->updatePaths(townHeroes, ps);
 
-	pforeachTilePos(mapSize, [&](const int3 & pos)
+	pforeachTilePaths(mapSize, ai, [&](const int3 & pos, const std::vector<AIPath> & paths)
 		{
 			float ourDistance = std::numeric_limits<float>::max();
 			float enemyDistance = std::numeric_limits<float>::max();
 			const CGTownInstance * enemyTown = nullptr;
 			const CGTownInstance * ourTown = nullptr;
 
-			for(AIPath & path : ai->pathfinder->getPathInfo(pos))
+			for(const AIPath & path : paths)
 			{
 				if(!path.targetHero || path.getFirstBlockedAction())
 					continue;

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

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

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

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

+ 84 - 50
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

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

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

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

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

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

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

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

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

@@ -35,12 +35,14 @@ Goals::TGoalVec StayAtTownBehavior::decompose() const
 	if(!towns.size())
 		return tasks;
 
+	std::vector<AIPath> paths;
+
 	for(auto town : towns)
 	{
 		if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
 			continue;
 
-		auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos());
+		ai->nullkiller->pathfinder->calculatePathInfo(paths, town->visitablePos());
 
 		for(auto & path : paths)
 		{

+ 10 - 10
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -609,6 +609,14 @@ bool AINodeStorage::selectNextActor()
 	return false;
 }
 
+uint64_t AINodeStorage::evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const
+{
+	float fightingStrength = ai->heroManager->getFightingStrengthCached(hero);
+	double ratio = (double)danger / (armyValue * fightingStrength);
+
+	return (uint64_t)(armyValue * ratio * ratio);
+}
+
 void HeroChainCalculationTask::cleanupInefectiveChains(std::vector<ExchangeCandidate> & result) const
 {
 	vstd::erase_if(result, [&](const ExchangeCandidate & chainInfo) -> bool
@@ -1326,12 +1334,8 @@ bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, con
 	return false;
 }
 
-std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand) const
+void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 & pos, bool isOnLand) const
 {
-	std::vector<AIPath> paths;
-
-	paths.reserve(AIPathfinding::NUM_CHAINS / 4);
-
 	auto layer = isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL;
 	auto chains = nodes.get(pos);
 
@@ -1346,7 +1350,7 @@ std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand)
 			continue;
 		}
 
-		AIPath path;
+		AIPath & path = paths.emplace_back();
 
 		path.targetHero = node.actor->hero;
 		path.heroArmy = node.actor->creatureSet;
@@ -1357,11 +1361,7 @@ std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand)
 		path.exchangeCount = node.actor->actorExchangeCount;
 		
 		fillChainInfo(&node, path, -1);
-
-		paths.push_back(path);
 	}
-
-	return paths;
 }
 
 void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const

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

@@ -265,7 +265,7 @@ public:
 	bool isDistanceLimitReached(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
 
 	std::optional<AIPathNode *> getOrCreateNode(const int3 & coord, const EPathfindingLayer layer, const ChainActor * actor);
-	std::vector<AIPath> getChainInfo(const int3 & pos, bool isOnLand) const;
+	void calculateChainInfo(std::vector<AIPath> & result, const int3 & pos, bool isOnLand) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const;
 	void setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes);
 	void setScoutTurnDistanceLimit(uint8_t distanceLimit) { turnDistanceLimit[HeroRole::SCOUT] = distanceLimit; }
@@ -283,12 +283,7 @@ public:
 		return dangerEvaluator->evaluateDanger(tile, hero, checkGuards);
 	}
 
-	inline uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const
-	{
-		double ratio = (double)danger / (armyValue * hero->getFightingStrength());
-
-		return (uint64_t)(armyValue * ratio * ratio);
-	}
+	uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const;
 
 	inline EPathAccessibility getAccessibility(const int3 & tile, EPathfindingLayer layer) const
 	{

+ 25 - 9
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

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

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

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

+ 147 - 20
AI/Nullkiller/Pathfinding/ObjectGraph.cpp

@@ -32,6 +32,7 @@ class ObjectGraphCalculator
 private:
 	ObjectGraph * target;
 	const Nullkiller * ai;
+	std::mutex syncLock;
 
 	std::map<const CGHeroInstance *, HeroRole> actors;
 	std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
@@ -41,7 +42,7 @@ private:
 
 public:
 	ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
-		:ai(ai), target(target)
+		:ai(ai), target(target), syncLock()
 	{
 	}
 
@@ -65,15 +66,17 @@ public:
 	{
 		updatePaths();
 
-		foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+		std::vector<AIPath> pathCache;
+
+		foreach_tile_pos(ai->cb.get(), [this, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
 			{
-				calculateConnections(pos);
+				calculateConnections(pos, pathCache);
 			});
 
 		removeExtraConnections();
 	}
 
-	float getNeighborConnectionsCost(const int3 & pos)
+	float getNeighborConnectionsCost(const int3 & pos, std::vector<AIPath> & pathCache)
 	{
 		float neighborCost = std::numeric_limits<float>::max();
 
@@ -85,9 +88,11 @@ public:
 		foreach_neighbour(
 			ai->cb.get(),
 			pos,
-			[this, &neighborCost](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
+			[this, &neighborCost, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
 			{
-				auto costTotal = this->getConnectionsCost(neighbor);
+				ai->pathfinder->calculatePathInfo(pathCache, neighbor);
+
+				auto costTotal = this->getConnectionsCost(pathCache);
 
 				if(costTotal.connectionsCount > 2 && costTotal.avg < neighborCost)
 				{
@@ -105,7 +110,7 @@ public:
 
 	void addMinimalDistanceJunctions()
 	{
-		foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+		pforeachTilePaths(ai->cb->getMapSize(), ai, [this](const int3 & pos, std::vector<AIPath> & paths)
 			{
 				if(target->hasNodeAt(pos))
 					return;
@@ -113,12 +118,12 @@ public:
 				if(ai->cb->getGuardingCreaturePosition(pos).valid())
 					return;
 
-				ConnectionCostInfo currentCost = getConnectionsCost(pos);
+				ConnectionCostInfo currentCost = getConnectionsCost(paths);
 
 				if(currentCost.connectionsCount <= 2)
 					return;
 
-				float neighborCost = getNeighborConnectionsCost(pos);
+				float neighborCost = getNeighborConnectionsCost(pos, paths);
 
 				if(currentCost.avg < neighborCost)
 				{
@@ -139,20 +144,20 @@ private:
 		ai->pathfinder->updatePaths(actors, ps);
 	}
 
-	void calculateConnections(const int3 & pos)
+	void calculateConnections(const int3 & pos, std::vector<AIPath> & pathCache)
 	{
 		if(target->hasNodeAt(pos))
 		{
 			foreach_neighbour(
 				ai->cb.get(),
 				pos,
-				[this, &pos](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
+				[this, &pos, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
 				{
 					if(target->hasNodeAt(neighbor))
 					{
-						auto paths = ai->pathfinder->getPathInfo(neighbor);
+						ai->pathfinder->calculatePathInfo(pathCache, neighbor);
 
-						for(auto & path : paths)
+						for(auto & path : pathCache)
 						{
 							if(pos == path.targetHero->visitablePos())
 							{
@@ -166,11 +171,12 @@ private:
 		}
 
 		auto guardPos = ai->cb->getGuardingCreaturePosition(pos);
-		auto paths = ai->pathfinder->getPathInfo(pos);
+		
+		ai->pathfinder->calculatePathInfo(pathCache, pos);
 
-		for(AIPath & path1 : paths)
+		for(AIPath & path1 : pathCache)
 		{
-			for(AIPath & path2 : paths)
+			for(AIPath & path2 : pathCache)
 			{
 				if(path1.targetHero == path2.targetHero)
 					continue;
@@ -287,6 +293,8 @@ private:
 
 	void addJunctionActor(const int3 & visitablePos)
 	{
+		std::lock_guard<std::mutex> lock(syncLock);
+
 		auto internalCb = temporaryActorHeroes.front()->cb;
 		auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
 
@@ -297,7 +305,7 @@ private:
 		objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
 		objectActor->initObj(rng);
 
-		if(cb->getTile(visitablePos)->isWater())
+		if(ai->cb->getTile(visitablePos)->isWater())
 		{
 			objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
 		}
@@ -310,9 +318,8 @@ private:
 		target->registerJunction(visitablePos);
 	}
 
-	ConnectionCostInfo getConnectionsCost(const int3 & pos) const
+	ConnectionCostInfo getConnectionsCost(std::vector<AIPath> & paths) const
 	{
-		auto paths = ai->pathfinder->getPathInfo(pos);
 		std::map<int3, float> costs;
 
 		for(auto & path : paths)
@@ -466,9 +473,14 @@ bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const Graph
 	return pathNodes.at(lhs.coord)[lhs.nodeType].cost > pathNodes.at(rhs.coord)[rhs.nodeType].cost;
 }
 
+GraphPaths::GraphPaths()
+	: visualKey(""), graph(), pathNodes()
+{
+}
+
 void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai)
 {
-	graph = *ai->baseGraph;
+	graph.copyFrom(*ai->baseGraph);
 	graph.connectHeroes(ai);
 
 	visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
@@ -675,4 +687,119 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 	}
 }
 
+void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const
+{
+	auto nodes = pathNodes.find(tile);
+
+	if(nodes == pathNodes.end())
+		return;
+
+	for(auto & targetNode : nodes->second)
+	{
+		if(!targetNode.reachable())
+			continue;
+
+		std::vector<GraphPathNodePointer> tilesToPass;
+
+		uint64_t danger = targetNode.danger;
+		float cost = targetNode.cost;
+		bool allowBattle = false;
+
+		auto current = GraphPathNodePointer(nodes->first, targetNode.nodeType);
+
+		while(true)
+		{
+			auto currentTile = pathNodes.find(current.coord);
+
+			if(currentTile == pathNodes.end())
+				break;
+
+			auto currentNode = currentTile->second[current.nodeType];
+
+			if(!currentNode.previous.valid())
+				break;
+
+			allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE;
+			vstd::amax(danger, currentNode.danger);
+			vstd::amax(cost, currentNode.cost);
+
+			tilesToPass.push_back(current);
+
+			if(currentNode.cost < 2.0f)
+				break;
+
+			current = currentNode.previous;
+		}
+		
+		if(tilesToPass.empty())
+			continue;
+
+		auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
+
+		for(auto & entryPath : entryPaths)
+		{
+			if(entryPath.targetHero != hero)
+				continue;
+
+			auto & path = paths.emplace_back();
+
+			path.targetHero = entryPath.targetHero;
+			path.heroArmy = entryPath.heroArmy;
+			path.exchangeCount = entryPath.exchangeCount;
+			path.armyLoss = entryPath.armyLoss + ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger);
+			path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle);
+			path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
+
+			AIPathNodeInfo n;
+
+			n.targetHero = hero;
+			n.parentIndex = -1;
+
+			// final node
+			n.coord = tile;
+			n.cost = targetNode.cost;
+			n.danger = targetNode.danger;
+			n.parentIndex = path.nodes.size();
+			path.nodes.push_back(n);
+
+			for(auto entryNode = entryPath.nodes.rbegin(); entryNode != entryPath.nodes.rend(); entryNode++)
+			{
+				auto blocker = ai->objectClusterizer->getBlocker(*entryNode);
+
+				if(blocker)
+				{
+					// blocker node
+					path.nodes.push_back(*entryNode);
+					path.nodes.back().parentIndex = path.nodes.size() - 1;
+					break;
+				}
+			}
+			
+			if(!path.nodes.empty())
+				break;
+
+			for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
+			{
+				auto & node = getNode(*graphTile);
+
+				n.coord = graphTile->coord;
+				n.cost = node.cost;
+				n.turns = static_cast<ui8>(node.cost);
+				n.danger = node.danger;
+				n.specialAction = node.specialAction;
+				n.parentIndex = path.nodes.size();
+
+				auto blocker = ai->objectClusterizer->getBlocker(n);
+
+				if(!blocker)
+					continue;
+
+				// blocker node
+				path.nodes.push_back(n);
+				break;
+			}
+		}
+	}
+}
+
 }

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

@@ -73,6 +73,11 @@ public:
 	void removeConnection(const int3 & from, const int3 & to);
 	void dumpToLog(std::string visualKey) const;
 
+	void copyFrom(const ObjectGraph & other)
+	{
+		nodes = other.nodes;
+	}
+
 	template<typename Func>
 	void iterateConnections(const int3 & pos, Func fn)
 	{
@@ -167,8 +172,10 @@ class GraphPaths
 	std::string visualKey;
 
 public:
+	GraphPaths();
 	void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai);
 	void addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
+	void quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
 	void dumpToLog() const;
 
 private: