Browse Source

Nullkiller: clusterization fixes, heroes clusterization for additional mains in case of locked heroes

Andrii Danylchenko 4 years ago
parent
commit
1fdf0de75d

+ 122 - 8
AI/Nullkiller/AIUtility.cpp

@@ -18,6 +18,8 @@
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapping/CMapDefines.h"
 
+#include "../../lib/CModHandler.h"
+
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<VCAI> ai;
 
@@ -272,14 +274,6 @@ bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, Pla
 	return false;
 }
 
-bool isBlockedBorderGate(int3 tileToHit) //TODO: is that function needed? should be handled by pathfinder
-{
-	if(cb->getTile(tileToHit)->topVisitableId() != Obj::BORDER_GATE)
-		return false;
-	auto gate = dynamic_cast<const CGKeys *>(cb->getTile(tileToHit)->topVisitableObj());
-	return !gate->passableFor(ai->playerID);
-}
-
 bool isBlockVisitObj(const int3 & pos)
 {
 	if(auto obj = cb->getTopObj(pos))
@@ -358,4 +352,124 @@ uint64_t timeElapsed(boost::chrono::time_point<boost::chrono::steady_clock> star
 	auto end = boost::chrono::high_resolution_clock::now();
 
 	return boost::chrono::duration_cast<boost::chrono::milliseconds>(end - start).count();
+}
+
+// todo: move to obj manager
+bool shouldVisit(const CGHeroInstance * h, const CGObjectInstance * obj)
+{
+	switch(obj->ID)
+	{
+	case Obj::TOWN:
+	case Obj::HERO: //never visit our heroes at random
+		return obj->tempOwner != h->tempOwner; //do not visit our towns at random
+	case Obj::BORDER_GATE:
+	{
+		for(auto q : ai->myCb->getMyQuests())
+		{
+			if(q.obj == obj)
+			{
+				return false; // do not visit guards or gates when wandering
+			}
+		}
+		return true; //we don't have this quest yet
+	}
+	case Obj::BORDERGUARD: //open borderguard if possible
+		return (dynamic_cast<const CGKeys *>(obj))->wasMyColorVisited(ai->playerID);
+	case Obj::SEER_HUT:
+	case Obj::QUEST_GUARD:
+	{
+		for(auto q : ai->myCb->getMyQuests())
+		{
+			if(q.obj == obj)
+			{
+				if(q.quest->checkQuest(h))
+					return true; //we completed the quest
+				else
+					return false; //we can't complete this quest
+			}
+		}
+		return true; //we don't have this quest yet
+	}
+	case Obj::CREATURE_GENERATOR1:
+	{
+		if(obj->tempOwner != h->tempOwner)
+			return true; //flag just in case
+
+		const CGDwelling * d = dynamic_cast<const CGDwelling *>(obj);
+
+		for(auto level : d->creatures)
+		{
+			for(auto c : level.second)
+			{
+				if(level.first
+					&& h->getSlotFor(CreatureID(c)) != SlotID()
+					&& cb->getResourceAmount().canAfford(c.toCreature()->cost))
+				{
+					return true;
+				}
+			}
+		}
+
+		return false;
+	}
+	case Obj::HILL_FORT:
+	{
+		for(auto slot : h->Slots())
+		{
+			if(slot.second->type->upgrades.size())
+				return true; //TODO: check price?
+		}
+		return false;
+	}
+	case Obj::MONOLITH_ONE_WAY_ENTRANCE:
+	case Obj::MONOLITH_ONE_WAY_EXIT:
+	case Obj::MONOLITH_TWO_WAY:
+	case Obj::WHIRLPOOL:
+		return false;
+	case Obj::SCHOOL_OF_MAGIC:
+	case Obj::SCHOOL_OF_WAR:
+	{
+		if(cb->getResourceAmount(Res::GOLD) < 1000)
+			return false;
+		break;
+	}
+	case Obj::LIBRARY_OF_ENLIGHTENMENT:
+		if(h->level < 12)
+			return false;
+		break;
+	case Obj::TREE_OF_KNOWLEDGE:
+	{
+		if(ai->nullkiller->heroManager->getHeroRole(h) == HeroRole::SCOUT)
+			return false;
+
+		TResources myRes = cb->getResourceAmount();
+		if(myRes[Res::GOLD] < 2000 || myRes[Res::GEMS] < 10)
+			return false;
+		break;
+	}
+	case Obj::MAGIC_WELL:
+		return h->mana < h->manaLimit();
+	case Obj::PRISON:
+		return ai->myCb->getHeroesInfo().size() < VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER;
+	case Obj::TAVERN:
+	{
+		//TODO: make AI actually recruit heroes
+		//TODO: only on request
+		if(ai->myCb->getHeroesInfo().size() >= VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER)
+			return false;
+		else if(cb->getResourceAmount(Res::GOLD) < GameConstants::HERO_GOLD_COST)
+			return false;
+		break;
+	}
+	case Obj::BOAT:
+		return false;
+		//Boats are handled by pathfinder
+	case Obj::EYE_OF_MAGI:
+		return false; //this object is useless to visit, but could be visited indefinitely
+	}
+
+	if(obj->wasVisited(h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player);
+		return false;
+
+	return true;
 }

+ 3 - 1
AI/Nullkiller/AIUtility.h

@@ -167,7 +167,6 @@ void foreach_neighbour(const int3 & pos, std::function<void(const int3 & pos)> f
 void foreach_neighbour(CCallback * cbp, const int3 & pos, std::function<void(CCallback * cbp, const int3 & pos)> foo); // avoid costly retrieval of thread-specific pointer
 
 bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater);
-//bool isBlockedBorderGate(int3 tileToHit);
 bool isObjectPassable(const CGObjectInstance * obj);
 bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations::PlayerRelations objectRelations);
 bool isBlockVisitObj(const int3 & pos);
@@ -184,6 +183,9 @@ bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2
 
 uint64_t timeElapsed(boost::chrono::time_point<boost::chrono::steady_clock> start);
 
+// todo: move to obj manager
+bool shouldVisit(const CGHeroInstance * h, const CGObjectInstance * obj);
+
 class CDistanceSorter
 {
 	const CGHeroInstance * hero;

+ 63 - 5
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -89,11 +89,42 @@ float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
 	return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
 }
 
+std::vector<std::vector<const CGHeroInstance *>> clusterizeHeroes(CCallback * cb, std::vector<const CGHeroInstance *> heroes)
+{
+	std::vector<std::vector<const CGHeroInstance *>> clusters;
+
+	for(auto hero : heroes)
+	{
+		auto paths = cb->getPathsInfo(hero);
+		std::vector<const CGHeroInstance *> newCluster = {hero};
+
+		for(auto cluster = clusters.begin(); cluster != clusters.end();)
+		{
+			auto hero = std::find_if(cluster->begin(), cluster->end(), [&](const CGHeroInstance * h) -> bool
+			{
+				return paths->getNode(h->visitablePos())->turns <= SCOUT_TURN_DISTANCE_LIMIT;
+			});
+
+			if(hero != cluster->end())
+			{
+				vstd::concatenate(newCluster, *cluster);
+				clusters.erase(cluster);
+			}
+			else
+				cluster++;
+		}
+
+		clusters.push_back(newCluster);
+	}
+
+	return clusters;
+}
+
 void HeroManager::update()
 {
 	logAi->trace("Start analysing our heroes");
 
-	std::map<HeroPtr, float> scores;
+	std::map<const CGHeroInstance *, float> scores;
 	auto myHeroes = cb->getHeroesInfo();
 
 	for(auto & hero : myHeroes)
@@ -101,16 +132,43 @@ void HeroManager::update()
 		scores[hero] = evaluateFightingStrength(hero);
 	}
 
-	std::sort(myHeroes.begin(), myHeroes.end(), [&](const HeroPtr & h1, const HeroPtr & h2) -> bool
+	auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
 	{
 		return scores.at(h1) > scores.at(h2);
-	});
+	};
+
+	int globalMainCount = std::min(((int)myHeroes.size() + 2) / 3, cb->getMapSize().x / 100 + 1);
+
+	std::sort(myHeroes.begin(), myHeroes.end(), scoreSort);
+
+	for(auto hero : myHeroes)
+	{
+		heroRoles[hero] = (globalMainCount--) > 0 ? HeroRole::MAIN : HeroRole::SCOUT;
+	}
 
-	int mainHeroCount = (myHeroes.size() + 2) / 3;
+	for(auto cluster : clusterizeHeroes(cb, myHeroes))
+	{
+		std::sort(cluster.begin(), cluster.end(), scoreSort);
+
+		auto localMainCountMax = (cluster.size() + 2) / 3;
+
+		for(auto hero : cluster)
+		{
+			if(heroRoles[hero] != HeroRole::MAIN)
+			{
+				heroRoles[hero] = HeroRole::MAIN;
+				break;
+			}
+			
+			localMainCountMax--;
+
+			if(localMainCountMax == 0)
+				break;
+		}
+	}
 
 	for(auto hero : myHeroes)
 	{
-		heroRoles[hero] = (mainHeroCount--) > 0 ? HeroRole::MAIN : HeroRole::SCOUT;
 		logAi->trace("Hero %s has role %s", hero->name, heroRoles[hero] == HeroRole::MAIN ? "main" : "scout");
 	}
 }

+ 2 - 2
AI/Nullkiller/Analyzers/HeroManager.h

@@ -51,12 +51,12 @@ private:
 	static SecondarySkillEvaluator wariorSkillsScores;
 	static SecondarySkillEvaluator scountSkillsScores;
 
-	CPlayerSpecificInfoCallback * cb; //this is enough, but we downcast from CCallback
+	CCallback * cb; //this is enough, but we downcast from CCallback
 	const Nullkiller * ai;
 	std::map<HeroPtr, HeroRole> heroRoles;
 
 public:
-	HeroManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai) : cb(CB), ai(ai) {}
+	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB), ai(ai) {}
 	const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
 	HeroRole getHeroRole(const HeroPtr & hero) const override;
 	int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;

+ 95 - 20
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -122,13 +122,17 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons
 			|| blocker->ID == Obj::MONSTER
 			|| blocker->ID == Obj::GARRISON2
 			|| blocker->ID == Obj::BORDERGUARD
-			|| blocker->ID == Obj::QUEST_GUARD
 			|| blocker->ID == Obj::BORDER_GATE
 			|| blocker->ID == Obj::SHIPYARD)
 		{
 			if(!isObjectPassable(blocker))
 				return blocker;
 		}
+
+		if(blocker->ID == Obj::QUEST_GUARD && node->actionIsBlocked)
+		{
+			return blocker;
+		}
 	}
 
 	return nullptr;
@@ -179,13 +183,25 @@ void ObjectClusterizer::clusterize()
 	blockedObjects.clear();
 
 	Obj ignoreObjects[] = {
-		Obj::MONSTER,
-		Obj::SIGN,
-		Obj::REDWOOD_OBSERVATORY,
-		Obj::MONOLITH_TWO_WAY,
+		Obj::BOAT,
+		Obj::EYE_OF_MAGI,
 		Obj::MONOLITH_ONE_WAY_ENTRANCE,
 		Obj::MONOLITH_ONE_WAY_EXIT,
-		Obj::BUOY
+		Obj::MONOLITH_TWO_WAY,
+		Obj::SUBTERRANEAN_GATE,
+		Obj::WHIRLPOOL,
+		Obj::BUOY,
+		Obj::SIGN,
+		Obj::SIGN,
+		Obj::GARRISON,
+		Obj::MONSTER,
+		Obj::GARRISON2,
+		Obj::BORDERGUARD,
+		Obj::QUEST_GUARD,
+		Obj::BORDER_GATE,
+		Obj::REDWOOD_OBSERVATORY,
+		Obj::CARTOGRAPHER,
+		Obj::PILLAR_OF_FIRE
 	};
 
 	logAi->debug("Begin object clusterization");
@@ -195,10 +211,19 @@ void ObjectClusterizer::clusterize()
 		if(!shouldVisitObject(obj))
 			continue;
 
+#if AI_TRACE_LEVEL >= 2
+		logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
+#endif
+
 		auto paths = ai->pathfinder->getPathInfo(obj->visitablePos());
 
 		if(paths.empty())
+		{
+#if AI_TRACE_LEVEL >= 2
+			logAi->trace("No paths found.");
+#endif
 			continue;
+		}
 
 		std::sort(paths.begin(), paths.end(), [](const AIPath & p1, const AIPath & p2) -> bool
 		{
@@ -209,19 +234,29 @@ void ObjectClusterizer::clusterize()
 		{
 			farObjects.addObject(obj, paths.front(), 0);
 
+#if AI_TRACE_LEVEL >= 2
+			logAi->trace("Object ignored. Moved to far objects with path %s", paths.front().toString());
+#endif
+
 			continue;
 		}
 		
-		bool added = false;
 		bool directlyAccessible = false;
 		std::set<const CGHeroInstance *> heroesProcessed;
 
 		for(auto & path : paths)
 		{
-			if(vstd::contains(heroesProcessed, path.targetHero))
-				continue;
+#if AI_TRACE_LEVEL >= 2
+			logAi->trace("Checking path %s", path.toString());
+#endif
 
-			heroesProcessed.insert(path.targetHero);
+			if(!shouldVisit(path.targetHero, obj))
+			{
+#if AI_TRACE_LEVEL >= 2
+				logAi->trace("Hero %s does not need to visit %s", path.targetHero->name, obj->getObjectName());
+#endif
+				continue;
+			}
 
 			if(path.nodes.size() > 1)
 			{
@@ -229,6 +264,16 @@ void ObjectClusterizer::clusterize()
 
 				if(blocker)
 				{
+					if(vstd::contains(heroesProcessed, path.targetHero))
+					{
+	#if AI_TRACE_LEVEL >= 2
+						logAi->trace("Hero %s is already processed.", path.targetHero->name);
+	#endif
+						continue;
+					}
+
+					heroesProcessed.insert(path.targetHero);
+
 					auto cluster = blockedObjects[blocker];
 
 					if(!cluster)
@@ -237,34 +282,64 @@ void ObjectClusterizer::clusterize()
 						blockedObjects[blocker] = cluster;
 					}
 
-					if(!vstd::contains(cluster->objects, obj))
-					{
-						float priority = ai->priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
+					float priority = ai->priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
 
-						cluster->addObject(obj, path, priority);
+					if(priority < MIN_PRIORITY)
+						continue;
 
-						added = true;
-					}
+					cluster->addObject(obj, path, priority);
+
+#if AI_TRACE_LEVEL >= 2
+					logAi->trace("Path added to cluster %s%s", blocker->getObjectName(), blocker->visitablePos().toString());
+#endif
+				}
+				else
+				{
+					directlyAccessible = true;
 				}
 			}
 			else
 			{
 				directlyAccessible = true;
 			}
+			
+			heroesProcessed.insert(path.targetHero);
 		}
 
-		if(!added || directlyAccessible)
+		if(directlyAccessible)
 		{
 			AIPath & shortestPath = paths.front();
 			float priority = ai->priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(shortestPath, obj)));
 
-			if(shortestPath.turn() <= 2 || priority > 0.6f)
-				nearObjects.addObject(obj, shortestPath, 0);
+			if(priority < MIN_PRIORITY)
+				continue;
+
+			bool interestingObject = shortestPath.turn() <= 2 || priority > 0.5f;
+
+			if(interestingObject)
+			{
+				nearObjects.addObject(obj, shortestPath, priority);
+			}
 			else
-				farObjects.addObject(obj, shortestPath, 0);
+			{
+				farObjects.addObject(obj, shortestPath, priority);
+			}
+
+#if AI_TRACE_LEVEL >= 2
+			logAi->trace("Path %s added to %s objects. Turn: %d, priority: %f",
+				shortestPath.toString(),
+				interestingObject ? "near" : "far",
+				shortestPath.turn(),
+				priority);
+#endif
 		}
 	}
 
+	vstd::erase_if(blockedObjects, [](std::pair<const CGObjectInstance *, std::shared_ptr<ObjectCluster>> pair) -> bool
+	{
+		return pair.second->objects.empty();
+	});
+
 	logAi->trace("Near objects count: %i", nearObjects.objects.size());
 	logAi->trace("Far objects count: %i", farObjects.objects.size());
 	for(auto pair : blockedObjects)

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

@@ -14,9 +14,6 @@
 #include "../Goals/ExecuteHeroChain.h"
 #include "CaptureObjectsBehavior.h"
 #include "../AIUtility.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
-#include "../../../lib/CModHandler.h"
 
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<VCAI> ai;
@@ -163,7 +160,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 			logAi->trace("Checking object %s, %s", objToVisit->getObjectName(), objToVisit->visitablePos().toString());
 #endif
 
-			if(!shouldVisitObject(objToVisit))
+			if(!objectMatchesFilter(objToVisit))
 				continue;
 
 			const int3 pos = objToVisit->visitablePos();
@@ -199,7 +196,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 	return tasks;
 }
 
-bool CaptureObjectsBehavior::shouldVisitObject(const CGObjectInstance * obj) const
+bool CaptureObjectsBehavior::objectMatchesFilter(const CGObjectInstance * obj) const
 {
 	if(objectTypes.size() && !vstd::contains(objectTypes, obj->ID.num))
 	{
@@ -213,122 +210,3 @@ bool CaptureObjectsBehavior::shouldVisitObject(const CGObjectInstance * obj) con
 
 	return true;
 }
-
-bool CaptureObjectsBehavior::shouldVisit(HeroPtr h, const CGObjectInstance * obj)
-{
-	switch(obj->ID)
-	{
-	case Obj::TOWN:
-	case Obj::HERO: //never visit our heroes at random
-		return obj->tempOwner != h->tempOwner; //do not visit our towns at random
-	case Obj::BORDER_GATE:
-	{
-		for(auto q : ai->myCb->getMyQuests())
-		{
-			if(q.obj == obj)
-			{
-				return false; // do not visit guards or gates when wandering
-			}
-		}
-		return true; //we don't have this quest yet
-	}
-	case Obj::BORDERGUARD: //open borderguard if possible
-		return (dynamic_cast<const CGKeys *>(obj))->wasMyColorVisited(ai->playerID);
-	case Obj::SEER_HUT:
-	case Obj::QUEST_GUARD:
-	{
-		for(auto q : ai->myCb->getMyQuests())
-		{
-			if(q.obj == obj)
-			{
-				if(q.quest->checkQuest(h.h))
-					return true; //we completed the quest
-				else
-					return false; //we can't complete this quest
-			}
-		}
-		return true; //we don't have this quest yet
-	}
-	case Obj::CREATURE_GENERATOR1:
-	{
-		if(obj->tempOwner != h->tempOwner)
-			return true; //flag just in case
-
-		const CGDwelling * d = dynamic_cast<const CGDwelling *>(obj);
-
-		for(auto level : d->creatures)
-		{
-			for(auto c : level.second)
-			{
-				if(level.first
-					&& h->getSlotFor(CreatureID(c)) != SlotID()
-					&& cb->getResourceAmount().canAfford(c.toCreature()->cost))
-				{
-					return true;
-				}
-			}
-		}
-
-		return false;
-	}
-	case Obj::HILL_FORT:
-	{
-		for(auto slot : h->Slots())
-		{
-			if(slot.second->type->upgrades.size())
-				return true; //TODO: check price?
-		}
-		return false;
-	}
-	case Obj::MONOLITH_ONE_WAY_ENTRANCE:
-	case Obj::MONOLITH_ONE_WAY_EXIT:
-	case Obj::MONOLITH_TWO_WAY:
-	case Obj::WHIRLPOOL:
-		return false;
-	case Obj::SCHOOL_OF_MAGIC:
-	case Obj::SCHOOL_OF_WAR:
-	{
-		if(cb->getResourceAmount(Res::GOLD) < 1000)
-			return false;
-		break;
-	}
-	case Obj::LIBRARY_OF_ENLIGHTENMENT:
-		if(h->level < 12)
-			return false;
-		break;
-	case Obj::TREE_OF_KNOWLEDGE:
-	{
-		if(ai->nullkiller->heroManager->getHeroRole(h) == HeroRole::SCOUT)
-			return false;
-
-		TResources myRes = cb->getResourceAmount();
-		if(myRes[Res::GOLD] < 2000 || myRes[Res::GEMS] < 10)
-			return false;
-		break;
-	}
-	case Obj::MAGIC_WELL:
-		return h->mana < h->manaLimit();
-	case Obj::PRISON:
-		return ai->myCb->getHeroesInfo().size() < VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER;
-	case Obj::TAVERN:
-	{
-		//TODO: make AI actually recruit heroes
-		//TODO: only on request
-		if(ai->myCb->getHeroesInfo().size() >= VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER)
-			return false;
-		else if(cb->getResourceAmount(Res::GOLD) < GameConstants::HERO_GOLD_COST)
-			return false;
-		break;
-	}
-	case Obj::BOAT:
-		return false;
-		//Boats are handled by pathfinder
-	case Obj::EYE_OF_MAGI:
-		return false; //this object is useless to visit, but could be visited indefinitely
-	}
-
-	if(obj->wasVisited(*h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player);
-		return false;
-
-	return true;
-}

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

@@ -68,8 +68,7 @@ namespace Goals
 		static Goals::TGoalVec getVisitGoals(const std::vector<AIPath> & paths, const CGObjectInstance * objToVisit = nullptr);
 
 	private:
-		bool shouldVisitObject(const CGObjectInstance * obj) const;
-		static bool shouldVisit(HeroPtr h, const CGObjectInstance * obj);
+		bool objectMatchesFilter(const CGObjectInstance * obj) const;
 	};
 }
 

+ 8 - 7
AI/Nullkiller/Behaviors/CompleteQuestBehavior.cpp

@@ -42,13 +42,14 @@ TGoalVec CompleteQuest::decompose() const
 	}
 
 	return solutions;*/
-	logAi->debug("Trying to realize quest: %s", questToString());
 
 	if(q.obj && (q.obj->ID == Obj::BORDER_GATE || q.obj->ID == Obj::BORDERGUARD))
 	{
 		return missionKeymaster();
 	}
 
+	logAi->debug("Trying to realize quest: %s", questToString());
+
 	switch(q.quest->missionType)
 	{
 	case CQuest::MISSION_ART:
@@ -107,7 +108,7 @@ TGoalVec CompleteQuest::tryCompleteQuest() const
 {
 	TGoalVec solutions;
 
-	auto tasks = CaptureObjectsBehavior(q.obj).decompose(); //TODO: choose best / free hero from among many possibilities?
+	auto tasks = CaptureObjectsBehavior(q.obj).decompose();
 
 	for(auto task : tasks)
 	{
@@ -177,14 +178,14 @@ TGoalVec CompleteQuest::missionLevel() const
 
 TGoalVec CompleteQuest::missionKeymaster() const
 {
-	TGoalVec solutions = tryCompleteQuest();
-
-	if(solutions.empty())
+	if(isObjectPassable(q.obj))
+	{
+		return CaptureObjectsBehavior(q.obj).decompose();
+	}
+	else
 	{
 		return CaptureObjectsBehavior().ofType(Obj::KEYMASTER, q.obj->subID).decompose();
 	}
-
-	return solutions;
 }
 
 TGoalVec CompleteQuest::missionResources() const

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

@@ -182,6 +182,7 @@ void Nullkiller::updateAiState(int pass)
 
 	PathfinderSettings cfg;
 	cfg.useHeroChain = true;
+	cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
 
 	pathfinder->updatePaths(activeHeroes, cfg);
 

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

@@ -20,7 +20,7 @@ extern boost::thread_specific_ptr<VCAI> ai;
 using namespace Goals;
 
 ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance * obj)
-	:ElementarGoal(Goals::EXECUTE_HERO_CHAIN), chainPath(path)
+	:ElementarGoal(Goals::EXECUTE_HERO_CHAIN), chainPath(path), closestWayRatio(1)
 {
 	hero = path.targetHero;
 	tile = path.targetTile();

+ 34 - 18
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -19,13 +19,30 @@
 #include "../../../lib/PathfinderUtil.h"
 #include "../../../lib/CPlayerState.h"
 
-/// 1-3 - position on map, 4 - layer (air, water, land), 5 - chain (normal, battle, spellcast and combinations)
-boost::multi_array<AIPathNode, 5> nodes;
+std::shared_ptr<boost::multi_array<AIPathNode, 5>> AISharedStorage::shared;
+
+AISharedStorage::AISharedStorage(int3 sizes)
+{
+	if(!shared){
+		shared.reset(new boost::multi_array<AIPathNode, 5>(
+			boost::extents[sizes.x][sizes.y][sizes.z][EPathfindingLayer::NUM_LAYERS][AINodeStorage::NUM_CHAINS]));
+	}
+
+	nodes = shared;
+}
+
+AISharedStorage::~AISharedStorage()
+{
+	nodes.reset();
+	if(shared && shared.use_count() == 1)
+	{
+		shared.reset();
+	}
+}
 
 AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
-	: sizes(Sizes), ai(ai), cb(ai->cb.get())
+	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
 {
-	nodes.resize(boost::extents[sizes.x][sizes.y][sizes.z][EPathfindingLayer::NUM_LAYERS][NUM_CHAINS]);
 	dangerEvaluator.reset(new FuzzyHelper(ai));
 }
 
@@ -104,9 +121,7 @@ boost::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 	const EPathfindingLayer layer, 
 	const ChainActor * actor)
 {
-	auto chains = nodes[pos.x][pos.y][pos.z][layer];
-
-	for(AIPathNode & node : chains)
+	for(AIPathNode & node : nodes.get(pos, layer))
 	{
 		if(node.actor == actor)
 		{
@@ -165,10 +180,8 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 
 void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, CGPathNode::EAccessibility accessibility)
 {
-	for(int i = 0; i < NUM_CHAINS; i++)
+	for(AIPathNode & heroNode : nodes.get(coord, layer))
 	{
-		AIPathNode & heroNode = nodes[coord.x][coord.y][coord.z][layer][i];
-
 		heroNode.actor = nullptr;
 		heroNode.danger = 0;
 		heroNode.manaCost = 0;
@@ -279,7 +292,7 @@ bool AINodeStorage::calculateHeroChainFinal()
 	{
 		foreach_tile_pos([&](const int3 & pos)
 		{
-			auto chains = nodes[pos.x][pos.y][pos.z][layer];
+			auto chains = nodes.get(pos, layer);
 
 			for(AIPathNode & node : chains)
 			{
@@ -313,7 +326,7 @@ bool AINodeStorage::calculateHeroChain()
 	{
 		foreach_tile_pos([&](const int3 & pos)
 		{
-			auto chains = nodes[pos.x][pos.y][pos.z][layer];
+			auto chains = nodes.get(pos, layer);
 
 			existingChains.resize(0);
 			newChains.resize(0);
@@ -394,7 +407,7 @@ void AINodeStorage::cleanupInefectiveChains(std::vector<ExchangeCandidate> & res
 	vstd::erase_if(result, [&](const ExchangeCandidate & chainInfo) -> bool
 	{
 		auto pos = chainInfo.coord;
-		auto chains = nodes[pos.x][pos.y][pos.z][EPathfindingLayer::LAND];
+		auto chains = nodes.get(pos, EPathfindingLayer::LAND);
 
 		return hasBetterChain(chainInfo.carrierParent, &chainInfo, chains)
 			|| hasBetterChain(chainInfo.carrierParent, &chainInfo, result);
@@ -910,7 +923,7 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
 bool AINodeStorage::hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
 {
 	auto pos = destination.coord;
-	auto chains = nodes[pos.x][pos.y][pos.z][EPathfindingLayer::LAND];
+	auto chains = nodes.get(pos, EPathfindingLayer::LAND);
 
 	return hasBetterChain(source.node, getAINode(destination.node), chains);
 }
@@ -950,7 +963,7 @@ bool AINodeStorage::hasBetterChain(
 			}
 		}
 
-		if(candidateActor->chainMask != node.actor->chainMask && heroChainPass == EHeroChainPass::CHAIN)
+		if(candidateActor->chainMask != node.actor->chainMask && heroChainPass != EHeroChainPass::FINAL)
 			continue;
 
 		auto nodeActor = node.actor;
@@ -1006,7 +1019,7 @@ bool AINodeStorage::hasBetterChain(
 
 bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const
 {
-	auto chains = nodes[pos.x][pos.y][pos.z][layer];
+	auto chains = nodes.get(pos, layer);
 
 	for(const AIPathNode & node : chains)
 	{
@@ -1026,7 +1039,7 @@ std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand)
 
 	paths.reserve(NUM_CHAINS / 4);
 
-	auto chains = nodes[pos.x][pos.y][pos.z][isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL];
+	auto chains = nodes.get(pos, isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL);
 
 	for(const AIPathNode & node : chains)
 	{
@@ -1074,10 +1087,13 @@ void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int pa
 			pathNode.danger = node->danger;
 			pathNode.coord = node->coord;
 			pathNode.parentIndex = parentIndex;
+			pathNode.actionIsBlocked = false;
 
 			if(pathNode.specialAction)
 			{
-				pathNode.actionIsBlocked = !pathNode.specialAction->canAct(node);
+				auto targetNode =node->theNodeBefore ?  getAINode(node->theNodeBefore) : node;
+
+				pathNode.actionIsBlocked = !pathNode.specialAction->canAct(targetNode);
 			}
 
 			parentIndex = path.nodes.size();

+ 24 - 0
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -12,6 +12,7 @@
 
 #define PATHFINDER_TRACE_LEVEL 0
 #define AI_TRACE_LEVEL 0
+#define SCOUT_TURN_DISTANCE_LIMIT 3
 
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/mapObjects/CGHeroInstance.h"
@@ -101,6 +102,28 @@ enum EHeroChainPass
 	FINAL // same as SINGLE but for heroes from CHAIN pass
 };
 
+class AISharedStorage
+{
+	/// 1-3 - position on map, 4 - layer (air, water, land), 5 - chain (normal, battle, spellcast and combinations)
+	static std::shared_ptr<boost::multi_array<AIPathNode, 5>> shared;
+	std::shared_ptr<boost::multi_array<AIPathNode, 5>> nodes;
+public:
+	AISharedStorage(int3 mapSize);
+	~AISharedStorage();
+
+	/*STRONG_INLINE
+	boost::detail::multi_array::sub_array<AIPathNode, 1> get(int3 tile, EPathfindingLayer layer)
+	{
+		return (*nodes)[tile.x][tile.y][tile.z][layer];
+	}*/
+
+	STRONG_INLINE
+	boost::detail::multi_array::sub_array<AIPathNode, 1> get(int3 tile, EPathfindingLayer layer) const
+	{
+		return (*nodes)[tile.x][tile.y][tile.z][layer];
+	}
+};
+
 class AINodeStorage : public INodeStorage
 {
 private:
@@ -109,6 +132,7 @@ private:
 	const CPlayerSpecificInfoCallback * cb;
 	const Nullkiller * ai;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
+	AISharedStorage nodes;
 	std::vector<std::shared_ptr<ChainActor>> actors;
 	std::vector<CGPathNode *> heroChain;
 	EHeroChainPass heroChainPass; // true if we need to calculate hero chain

+ 47 - 17
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -93,36 +93,66 @@ namespace AIPathfinding
 		return false;
 	}
 
-	bool AIMovementAfterDestinationRule::bypassRemovableObject(
+	bool AIMovementAfterDestinationRule::bypassQuest(
 		const PathNodeInfo & source,
 		CDestinationNodeInfo & destination,
 		const PathfinderConfig * pathfinderConfig,
 		CPathfinderHelper * pathfinderHelper) const
 	{
-		if(destination.nodeObject->ID == Obj::QUEST_GUARD
-			|| destination.nodeObject->ID == Obj::BORDERGUARD
-			|| destination.nodeObject->ID == Obj::BORDER_GATE)
+		const AIPathNode * destinationNode = nodeStorage->getAINode(destination.node);
+		auto questObj = dynamic_cast<const IQuestObject *>(destination.nodeObject);
+		auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord);
+		auto nodeHero = pathfinderHelper->hero;
+		QuestAction questAction(questInfo);
+
+		if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->missionType == CQuest::MISSION_NONE)
 		{
-			auto questObj = dynamic_cast<const IQuestObject *>(destination.nodeObject);
-			auto nodeHero = pathfinderHelper->hero;
+			return false;
+		}
 
-			if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->missionType == CQuest::MISSION_NONE)
+		if(!questAction.canAct(destinationNode))
+		{
+			if(!destinationNode->actor->allowUseResources)
 			{
-				return false;
-			}
+				boost::optional<AIPathNode *> questNode = nodeStorage->getOrCreateNode(
+					destination.coord,
+					destination.node->layer,
+					destinationNode->actor->resourceActor);
 
-			if(!destination.nodeObject->wasVisited(nodeHero->tempOwner)
-				|| !questObj->checkQuest(nodeHero))
-			{
-				nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
+				if(!questNode || questNode.get()->cost < destination.cost)
 				{
-					auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord);
+					return false;
+				}
 
-					node->specialAction.reset(new QuestAction(questInfo));
-				});
+				destinationNode = questNode.get();
+				destination.node = questNode.get();
+
+				nodeStorage->commit(destination, source);
+				AIPreviousNodeRule(nodeStorage).process(source, destination, pathfinderConfig, pathfinderHelper);
 			}
 
-			return true;
+			nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
+			{
+				auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord);
+
+				node->specialAction.reset(new QuestAction(questAction));
+			});
+		}
+
+		return true;
+	}
+
+	bool AIMovementAfterDestinationRule::bypassRemovableObject(
+		const PathNodeInfo & source,
+		CDestinationNodeInfo & destination,
+		const PathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) const
+	{
+		if(destination.nodeObject->ID == Obj::QUEST_GUARD
+			|| destination.nodeObject->ID == Obj::BORDERGUARD
+			|| destination.nodeObject->ID == Obj::BORDER_GATE)
+		{
+			return bypassQuest(source, destination, pathfinderConfig, pathfinderHelper);
 		}
 
 		auto enemyHero = destination.nodeHero && destination.heroRelations == PlayerRelations::ENEMIES;

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

@@ -58,5 +58,11 @@ namespace AIPathfinding
 			CDestinationNodeInfo & destination,
 			const PathfinderConfig * pathfinderConfig,
 			CPathfinderHelper * pathfinderHelper) const;
+
+		bool bypassQuest(
+			const PathNodeInfo & source,
+			CDestinationNodeInfo & destination,
+			const PathfinderConfig * pathfinderConfig,
+			CPathfinderHelper * pathfinderHelper) const;
 	};
 }

+ 8 - 0
AI/Nullkiller/VCAI.cpp

@@ -343,6 +343,9 @@ void VCAI::objectRemoved(const CGObjectInstance * obj)
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 
+	if(!nullkiller) // crash protection
+		return;
+
 	nullkiller->memory->removeFromMemory(obj);
 
 	if(obj->ID == Obj::HERO && obj->tempOwner == playerID)
@@ -441,6 +444,9 @@ void VCAI::objectPropertyChanged(const SetObjectProperty * sop)
 		auto relations = myCb->getPlayerRelations(playerID, (PlayerColor)sop->val);
 		auto obj = myCb->getObj(sop->id, false);
 
+		if(!nullkiller) // crash protection
+			return;
+
 		if(obj)
 		{
 			if(relations == PlayerRelations::ENEMIES)
@@ -1446,6 +1452,8 @@ void VCAI::finish()
 		makingTurn->join();
 		makingTurn.reset();
 	}
+
+	nullkiller.reset();
 }
 
 void VCAI::requestActionASAP(std::function<void()> whatToDo)