Explorar el Código

NKAI: allow multiple tasks to be executed from one calculation

Andrii Danylchenko hace 1 año
padre
commit
ed76d8a652
Se han modificado 33 ficheros con 539 adiciones y 199 borrados
  1. 3 6
      AI/Nullkiller/AIGateway.cpp
  2. 2 25
      AI/Nullkiller/AIUtility.cpp
  3. 1 1
      AI/Nullkiller/AIUtility.h
  4. 118 24
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  5. 24 5
      AI/Nullkiller/Analyzers/ObjectClusterizer.h
  6. 1 1
      AI/Nullkiller/Behaviors/ClusterBehavior.cpp
  7. 9 1
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  8. 2 6
      AI/Nullkiller/Engine/DeepDecomposer.cpp
  9. 1 1
      AI/Nullkiller/Engine/DeepDecomposer.h
  10. 175 84
      AI/Nullkiller/Engine/Nullkiller.cpp
  11. 22 3
      AI/Nullkiller/Engine/Nullkiller.h
  12. 14 15
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  13. 5 3
      AI/Nullkiller/Goals/AbstractGoal.cpp
  14. 12 5
      AI/Nullkiller/Goals/AbstractGoal.h
  15. 3 3
      AI/Nullkiller/Goals/AdventureSpellCast.cpp
  16. 1 1
      AI/Nullkiller/Goals/AdventureSpellCast.h
  17. 29 2
      AI/Nullkiller/Goals/CGoal.h
  18. 32 0
      AI/Nullkiller/Goals/Composition.cpp
  19. 3 0
      AI/Nullkiller/Goals/Composition.h
  20. 1 1
      AI/Nullkiller/Goals/DigAtTile.cpp
  21. 4 4
      AI/Nullkiller/Goals/DismissHero.cpp
  22. 5 1
      AI/Nullkiller/Goals/DismissHero.h
  23. 20 0
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp
  24. 3 0
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h
  25. 32 0
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  26. 3 0
      AI/Nullkiller/Goals/ExecuteHeroChain.h
  27. 1 1
      AI/Nullkiller/Goals/StayAtTown.cpp
  28. 1 1
      AI/Nullkiller/Markers/ArmyUpgrade.cpp
  29. 1 1
      AI/Nullkiller/Markers/DefendTown.cpp
  30. 2 2
      AI/Nullkiller/Markers/HeroExchange.cpp
  31. 1 1
      AI/Nullkiller/Markers/UnlockCluster.h
  32. 4 0
      AI/Nullkiller/Pathfinding/AIPathfinder.cpp
  33. 4 1
      AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp

+ 3 - 6
AI/Nullkiller/AIGateway.cpp

@@ -251,6 +251,7 @@ void AIGateway::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance
 	if(start && visitedObj) //we can end visit with null object, anyway
 	{
 		nullkiller->memory->markObjectVisited(visitedObj);
+		nullkiller->objectClusterizer->invalidate(visitedObj->id);
 	}
 
 	status.heroVisit(visitedObj, start);
@@ -373,6 +374,7 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor &
 		return;
 
 	nullkiller->memory->removeFromMemory(obj);
+	nullkiller->objectClusterizer->onObjectRemoved(obj->id);
 
 	if(nullkiller->baseGraph && nullkiller->settings->isObjectGraphAllowed())
 	{
@@ -1152,11 +1154,6 @@ void AIGateway::retrieveVisitableObjs()
 	{
 		for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
 		{
-			if(!obj->appearance)
-			{
-				logAi->error("Bad!");
-			}
-
 			addVisitableObj(obj);
 		}
 	});
@@ -1401,7 +1398,7 @@ void AIGateway::tryRealize(Goals::DigAtTile & g)
 	assert(g.hero->visitablePos() == g.tile); //surely we want to crash here?
 	if(g.hero->diggingStatus() == EDiggingStatus::CAN_DIG)
 	{
-		cb->dig(g.hero.get());
+		cb->dig(g.hero);
 	}
 	else
 	{

+ 2 - 25
AI/Nullkiller/AIUtility.cpp

@@ -91,33 +91,10 @@ bool HeroPtr::operator<(const HeroPtr & rhs) const
 
 const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const
 {
-	//TODO? check if these all assertions every time we get info about hero affect efficiency
-	//
-	//behave terribly when attempting unauthorized access to hero that is not ours (or was lost)
-	assert(doWeExpectNull || h);
-
-	if(h)
-	{
-		auto obj = cb->getObj(hid);
-		//const bool owned = obj && obj->tempOwner == ai->playerID;
-
-		if(doWeExpectNull && !obj)
-		{
-			return nullptr;
-		}
-		else
-		{
-			if (!obj)
-				logAi->error("Accessing no longer accessible hero %s!", h->getNameTranslated());
-			//assert(obj);
-			//assert(owned);
-		}
-	}
-
-	return h;
+	return get(cb);
 }
 
-const CGHeroInstance * HeroPtr::get(CCallback * cb, bool doWeExpectNull) const
+const CGHeroInstance * HeroPtr::get(const CPlayerSpecificInfoCallback * cb, bool doWeExpectNull) const
 {
 	//TODO? check if these all assertions every time we get info about hero affect efficiency
 	//

+ 1 - 1
AI/Nullkiller/AIUtility.h

@@ -109,7 +109,7 @@ public:
 	}
 
 	const CGHeroInstance * get(bool doWeExpectNull = false) const;
-	const CGHeroInstance * get(CCallback * cb, bool doWeExpectNull = false) const;
+	const CGHeroInstance * get(const CPlayerSpecificInfoCallback * cb, bool doWeExpectNull = false) const;
 	bool validAndSet() const;
 
 

+ 118 - 24
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -20,7 +20,7 @@ void ObjectCluster::addObject(const CGObjectInstance * obj, const AIPath & path,
 {
 	ClusterObjects::accessor info;
 
-	objects.insert(info, ClusterObjects::value_type(obj, ClusterObjectInfo()));
+	objects.insert(info, ClusterObjects::value_type(obj->id, ClusterObjectInfo()));
 
 	if(info->second.priority < priority)
 	{
@@ -31,15 +31,14 @@ void ObjectCluster::addObject(const CGObjectInstance * obj, const AIPath & path,
 	}
 }
 
-const CGObjectInstance * ObjectCluster::calculateCenter() const
+const CGObjectInstance * ObjectCluster::calculateCenter(const CPlayerSpecificInfoCallback * cb) const
 {
-	auto v = getObjects();
 	auto tile = int3(0);
 	float priority = 0;
 
-	for(auto pair : objects)
+	for(auto & pair : objects)
 	{
-		auto newPoint = pair.first->visitablePos();
+		auto newPoint = cb->getObj(pair.first)->visitablePos();
 		float newPriority = std::pow(pair.second.priority, 4); // lets make high priority targets more weghtful
 		int3 direction = newPoint - tile;
 		float priorityRatio = newPriority / (priority + newPriority);
@@ -48,21 +47,21 @@ const CGObjectInstance * ObjectCluster::calculateCenter() const
 		priority += newPriority;
 	}
 
-	auto closestPair = *vstd::minElementByFun(objects, [&](const std::pair<const CGObjectInstance *, ClusterObjectInfo> & pair) -> int
+	auto closestPair = *vstd::minElementByFun(objects, [&](const std::pair<ObjectInstanceID, ClusterObjectInfo> & pair) -> int
 	{
-		return pair.first->visitablePos().dist2dSQ(tile);
+		return cb->getObj(pair.first)->visitablePos().dist2dSQ(tile);
 	});
 
-	return closestPair.first;
+	return cb->getObj(closestPair.first);
 }
 
-std::vector<const CGObjectInstance *> ObjectCluster::getObjects() const
+std::vector<const CGObjectInstance *> ObjectCluster::getObjects(const CPlayerSpecificInfoCallback * cb) const
 {
 	std::vector<const CGObjectInstance *> result;
 
-	for(auto pair : objects)
+	for(auto & pair : objects)
 	{
-		result.push_back(pair.first);
+		result.push_back(cb->getObj(pair.first));
 	}
 
 	return result;
@@ -70,19 +69,19 @@ std::vector<const CGObjectInstance *> ObjectCluster::getObjects() const
 
 std::vector<const CGObjectInstance *> ObjectClusterizer::getNearbyObjects() const
 {
-	return nearObjects.getObjects();
+	return nearObjects.getObjects(ai->cb.get());
 }
 
 std::vector<const CGObjectInstance *> ObjectClusterizer::getFarObjects() const
 {
-	return farObjects.getObjects();
+	return farObjects.getObjects(ai->cb.get());
 }
 
 std::vector<std::shared_ptr<ObjectCluster>> ObjectClusterizer::getLockedClusters() const
 {
 	std::vector<std::shared_ptr<ObjectCluster>> result;
 
-	for(auto pair : blockedObjects)
+	for(auto & pair : blockedObjects)
 	{
 		result.push_back(pair.second);
 	}
@@ -163,6 +162,69 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons
 	return nullptr;
 }
 
+void ObjectClusterizer::invalidate(ObjectInstanceID id)
+{
+	nearObjects.objects.erase(id);
+	farObjects.objects.erase(id);
+	invalidated.push_back(id);
+
+	for(auto & c : blockedObjects)
+	{
+		c.second->objects.erase(id);
+	}
+}
+
+void ObjectClusterizer::validateObjects()
+{
+	std::vector<ObjectInstanceID> toRemove;
+
+	auto scanRemovedObjects = [this, &toRemove](const ObjectCluster & cluster)
+	{
+		for(auto & pair : cluster.objects)
+		{
+			if(!ai->cb->getObj(pair.first, false))
+				toRemove.push_back(pair.first);
+		}
+	};
+
+	scanRemovedObjects(nearObjects);
+	scanRemovedObjects(farObjects);
+
+	for(auto & pair : blockedObjects)
+	{
+		if(!ai->cb->getObj(pair.first, false) || pair.second->objects.empty())
+			toRemove.push_back(pair.first);
+		else
+			scanRemovedObjects(*pair.second);
+	}
+
+	vstd::removeDuplicates(toRemove);
+
+	for(auto id : toRemove)
+	{
+		onObjectRemoved(id);
+	}
+}
+
+void ObjectClusterizer::onObjectRemoved(ObjectInstanceID id)
+{
+	invalidate(id);
+
+	vstd::erase_if_present(invalidated, id);
+
+	NKAI::ClusterMap::accessor cluster;
+	
+	if(blockedObjects.find(cluster, id))
+	{
+		for(auto & unlocked : cluster->second->objects)
+		{
+			invalidated.push_back(unlocked.first);
+		}
+
+		blockedObjects.erase(cluster);
+	}
+}
+
 bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const
 {
 	if(isObjectRemovable(obj))
@@ -222,17 +284,45 @@ Obj ObjectClusterizer::IgnoredObjectTypes[] = {
 
 void ObjectClusterizer::clusterize()
 {
-	auto start = std::chrono::high_resolution_clock::now();
+	if(isUpToDate)
+	{
+		validateObjects();
+	}
 
-	nearObjects.reset();
-	farObjects.reset();
-	blockedObjects.clear();
+	if(isUpToDate && invalidated.empty())
+		return;
+		
+	auto start = std::chrono::high_resolution_clock::now();
 
 	logAi->debug("Begin object clusterization");
 
-	std::vector<const CGObjectInstance *> objs(
-		ai->memory->visitableObjs.begin(),
-		ai->memory->visitableObjs.end());
+	std::vector<const CGObjectInstance *> objs;
+	
+	if(isUpToDate)
+	{
+		for(auto id : invalidated)
+		{
+			auto obj = cb->getObj(id, false);
+
+			if(obj)
+			{
+				objs.push_back(obj);
+			}
+		}
+
+		invalidated.clear();
+	}
+	else
+	{
+		nearObjects.reset();
+		farObjects.reset();
+		blockedObjects.clear();
+		invalidated.clear();
+
+		objs = std::vector<const CGObjectInstance *>(
+			ai->memory->visitableObjs.begin(),
+			ai->memory->visitableObjs.end());
+	}
 
 #if NKAI_TRACE_LEVEL == 0
 	tbb::parallel_for(tbb::blocked_range<size_t>(0, objs.size()), [&](const tbb::blocked_range<size_t> & r) {
@@ -256,16 +346,20 @@ void ObjectClusterizer::clusterize()
 
 	for(auto pair : blockedObjects)
 	{
-		logAi->trace("Cluster %s %s count: %i", pair.first->getObjectName(), pair.first->visitablePos().toString(), pair.second->objects.size());
+		auto blocker = cb->getObj(pair.first);
+
+		logAi->trace("Cluster %s %s count: %i", blocker->getObjectName(), blocker->visitablePos().toString(), pair.second->objects.size());
 
 #if NKAI_TRACE_LEVEL >= 1
-		for(auto obj : pair.second->getObjects())
+		for(auto obj : pair.second->getObjects(ai->cb.get()))
 		{
 			logAi->trace("Object %s %s", obj->getObjectName(), obj->visitablePos().toString());
 		}
 #endif
 	}
 
+	isUpToDate = true;
+
 	logAi->trace("Clusterization complete in %ld", timeElapsed(start));
 }
 
@@ -381,7 +475,7 @@ void ObjectClusterizer::clusterizeObject(
 				ClusterMap::accessor cluster;
 				blockedObjects.insert(
 					cluster,
-					ClusterMap::value_type(blocker, std::make_shared<ObjectCluster>(blocker)));
+					ClusterMap::value_type(blocker->id, std::make_shared<ObjectCluster>(blocker)));
 
 				cluster->second->addObject(obj, path, priority);
 

+ 24 - 5
AI/Nullkiller/Analyzers/ObjectClusterizer.h

@@ -23,7 +23,15 @@ struct ClusterObjectInfo
 	uint8_t turn;
 };
 
-using ClusterObjects = tbb::concurrent_hash_map<const CGObjectInstance *, ClusterObjectInfo>;
+struct ObjectInstanceIDHash
+{
+	ObjectInstanceID::hash hash;
+	bool equal(ObjectInstanceID o1, ObjectInstanceID o2) const
+	{
+		return o1 == o2;
+	}
+};
+using ClusterObjects = tbb::concurrent_hash_map<ObjectInstanceID, ClusterObjectInfo, ObjectInstanceIDHash>;
 
 struct ObjectCluster
 {
@@ -44,11 +52,11 @@ public:
 	{
 	}
 
-	std::vector<const CGObjectInstance *> getObjects() const;
-	const CGObjectInstance * calculateCenter() const;
+	std::vector<const CGObjectInstance *> getObjects(const CPlayerSpecificInfoCallback * cb) const;
+	const CGObjectInstance * calculateCenter(const CPlayerSpecificInfoCallback * cb) const;
 };
 
-using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
+using ClusterMap = tbb::concurrent_hash_map<ObjectInstanceID, std::shared_ptr<ObjectCluster>, ObjectInstanceIDHash>;
 
 class ObjectClusterizer
 {
@@ -60,6 +68,8 @@ private:
 	ClusterMap blockedObjects;
 	const Nullkiller * ai;
 	RewardEvaluator valueEvaluator;
+	bool isUpToDate;
+	std::vector<ObjectInstanceID> invalidated;
 
 public:
 	void clusterize();
@@ -69,7 +79,16 @@ public:
 	const CGObjectInstance * getBlocker(const AIPath & path) const;
 	std::optional<const CGObjectInstance *> getBlocker(const AIPathNodeInfo & node) const;
 
-	ObjectClusterizer(const Nullkiller * ai): ai(ai), valueEvaluator(ai) {}
+	ObjectClusterizer(const Nullkiller * ai): ai(ai), valueEvaluator(ai), isUpToDate(false){}
+
+	void validateObjects();
+	void onObjectRemoved(ObjectInstanceID id);
+	void invalidate(ObjectInstanceID id);
+
+	void reset() {
+		isUpToDate = false;
+		invalidated.clear();
+	}
 
 private:
 	bool shouldVisitObject(const CGObjectInstance * obj) const;

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

@@ -41,7 +41,7 @@ Goals::TGoalVec ClusterBehavior::decompose(const Nullkiller * ai) const
 
 Goals::TGoalVec ClusterBehavior::decomposeCluster(const Nullkiller * ai, std::shared_ptr<ObjectCluster> cluster) const
 {
-	auto center = cluster->calculateCenter();
+	auto center = cluster->calculateCenter(ai->cb.get());
 	auto paths = ai->pathfinder->getPathInfo(center->visitablePos(), ai->settings->isObjectGraphAllowed());
 
 	auto blockerPos = cluster->blocker->visitablePos();

+ 9 - 1
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -71,7 +71,15 @@ bool needToRecruitHero(const Nullkiller * ai, const CGTownInstance * startupTown
 
 	for(auto obj : ai->objectClusterizer->getNearbyObjects())
 	{
-		if((obj->ID == Obj::RESOURCE && dynamic_cast<const CGResource *>(obj)->resourceID() == EGameResID::GOLD)
+		auto armed = dynamic_cast<const CArmedInstance *>(obj);
+
+		if(armed && armed->getArmyStrength() > 0)
+			continue;
+
+		bool isGoldPile = dynamic_cast<const CGResource *>(obj)
+			&& dynamic_cast<const CGResource *>(obj)->resourceID() == EGameResID::GOLD;
+
+		if(isGoldPile
 			|| obj->ID == Obj::TREASURE_CHEST
 			|| obj->ID == Obj::CAMPFIRE
 			|| obj->ID == Obj::WATER_WHEEL)

+ 2 - 6
AI/Nullkiller/Engine/DeepDecomposer.cpp

@@ -37,10 +37,8 @@ void DeepDecomposer::reset()
 	goals.clear();
 }
 
-Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit)
+void DeepDecomposer::decompose(TGoalVec & result, TSubgoal behavior, int depthLimit)
 {
-	TGoalVec tasks;
-
 	goals.clear();
 	goals.resize(depthLimit);
 	decompositionCache.resize(depthLimit);
@@ -79,7 +77,7 @@ Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit)
 #endif
 				if(!isCompositionLoop(subgoal))
 				{
-					tasks.push_back(task);
+					result.push_back(task);
 
 					if(!fromCache)
 					{
@@ -121,8 +119,6 @@ Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit)
 			}
 		}
 	}
-
-	return tasks;
 }
 
 Goals::TSubgoal DeepDecomposer::aggregateGoals(int startDepth, TSubgoal last)

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

@@ -35,7 +35,7 @@ private:
 public:
 	DeepDecomposer(const Nullkiller * ai);
 	void reset();
-	Goals::TGoalVec decompose(Goals::TSubgoal behavior, int depthLimit);
+	void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int depthLimit);
 
 private:
 	Goals::TSubgoal aggregateGoals(int startDepth, Goals::TSubgoal last);

+ 175 - 84
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -64,55 +64,119 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
 	armyFormation.reset(new ArmyFormation(cb, this));
 }
 
-Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
+TaskPlanItem::TaskPlanItem(TSubgoal task)
+	:task(task), affectedObjects(task->asTask()->getAffectedObjects())
 {
-	Goals::TTask bestTask = *vstd::maxElementByFun(tasks, [](Goals::TTask task) -> float{
-		return task->priority;
-	});
-
-	return bestTask;
 }
 
-Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const
+Goals::TTaskVec TaskPlan::getTasks() const
 {
-	boost::this_thread::interruption_point();
+	Goals::TTaskVec result;
 
-	logAi->debug("Checking behavior %s", behavior->toString());
+	for(auto & item : tasks)
+	{
+		result.push_back(taskptr(*item.task));
+	}
 
-	auto start = std::chrono::high_resolution_clock::now();
-	
-	Goals::TGoalVec elementarGoals = decomposer->decompose(behavior, decompositionMaxDepth);
-	Goals::TTaskVec tasks;
+	vstd::removeDuplicates(result);
 
-	boost::this_thread::interruption_point();
-	
-	for(auto goal : elementarGoals)
-	{
-		Goals::TTask task = Goals::taskptr(*goal);
+	return result;
+}
 
-		if(task->priority <= 0)
-			task->priority = priorityEvaluator->evaluate(goal);
+void TaskPlan::merge(TSubgoal task)
+{
+	TGoalVec blockers;
+
+	for(auto & item : tasks)
+	{
+		for(auto objid : item.affectedObjects)
+		{
+			if(task == item.task || task->asTask()->isObjectAffected(objid))
+			{
+				if(item.task->asTask()->priority >= task->asTask()->priority)
+					return;
 
-		tasks.push_back(task);
+				blockers.push_back(item.task);
+				break;
+			}
+		}
 	}
 
+	vstd::erase_if(tasks, [&](const TaskPlanItem & task)
+		{
+			return vstd::contains(blockers, task.task);
+		});
+
+	tasks.emplace_back(task);
+}
+
+Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
+{
 	if(tasks.empty())
 	{
-		logAi->debug("Behavior %s found no tasks. Time taken %ld", behavior->toString(), timeElapsed(start));
+		return taskptr(Invalid());
+	}
+
+	for(TSubgoal & task : tasks)
+	{
+		if(task->asTask()->priority <= 0)
+			task->asTask()->priority = priorityEvaluator->evaluate(task);
+	}
 
-		return Goals::taskptr(Goals::Invalid());
+	auto bestTask = *vstd::maxElementByFun(tasks, [](Goals::TSubgoal task) -> float
+		{
+			return task->asTask()->priority;
+		});
+
+	return taskptr(*bestTask);
+}
+
+Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const
+{
+	TaskPlan taskPlan;
+
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks](const tbb::blocked_range<size_t> & r)
+		{
+			auto evaluator = this->priorityEvaluators->acquire();
+
+			for(size_t i = r.begin(); i != r.end(); i++)
+			{
+				auto task = tasks[i];
+
+				if(task->asTask()->priority <= 0)
+					task->asTask()->priority = evaluator->evaluate(task);
+			}
+		});
+
+	std::sort(tasks.begin(), tasks.end(), [](TSubgoal g1, TSubgoal g2) -> bool
+		{
+			return g2->asTask()->priority < g1->asTask()->priority;
+		});
+
+	for(TSubgoal & task : tasks)
+	{
+		taskPlan.merge(task);
 	}
 
-	auto task = choseBestTask(tasks);
+	return taskPlan.getTasks();
+}
+
+void Nullkiller::decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const
+{
+	boost::this_thread::interruption_point();
+
+	logAi->debug("Checking behavior %s", behavior->toString());
+
+	auto start = std::chrono::high_resolution_clock::now();
+	
+	decomposer->decompose(result, behavior, decompositionMaxDepth);
+
+	boost::this_thread::interruption_point();
 
 	logAi->debug(
-		"Behavior %s returns %s, priority %f. Time taken %ld",
+		"Behavior %s. Time taken %ld",
 		behavior->toString(),
-		task->toString(),
-		task->priority,
 		timeElapsed(start));
-
-	return task;
 }
 
 void Nullkiller::resetAiState()
@@ -124,6 +188,7 @@ void Nullkiller::resetAiState()
 	lockedHeroes.clear();
 	dangerHitMap->reset();
 	useHeroChain = true;
+	objectClusterizer->reset();
 
 	if(!baseGraph && settings->isObjectGraphAllowed())
 	{
@@ -251,6 +316,8 @@ void Nullkiller::makeTurn()
 
 	resetAiState();
 
+	Goals::TGoalVec bestTasks;
+
 	for(int i = 1; i <= settings->getMaxPass(); i++)
 	{
 		auto start = std::chrono::high_resolution_clock::now();
@@ -260,16 +327,18 @@ void Nullkiller::makeTurn()
 
 		for(;i <= settings->getMaxPass(); i++)
 		{
-			Goals::TTaskVec fastTasks = {
-				choseBestTask(sptr(BuyArmyBehavior()), 1),
-				choseBestTask(sptr(BuildingBehavior()), 1)
-			};
+			bestTasks.clear();
+
+			decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
+			decompose(bestTasks, sptr(BuildingBehavior()), 1);
 
-			bestTask = choseBestTask(fastTasks);
+			bestTask = choseBestTask(bestTasks);
 
 			if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
 			{
-				executeTask(bestTask);
+				if(!executeTask(bestTask))
+					return;
+
 				updateAiState(i, true);
 			}
 			else
@@ -278,83 +347,105 @@ void Nullkiller::makeTurn()
 			}
 		}
 
-		Goals::TTaskVec bestTasks = {
-			bestTask,
-			choseBestTask(sptr(RecruitHeroBehavior()), 1),
-			choseBestTask(sptr(CaptureObjectsBehavior()), 1),
-			choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH),
-			choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH),
-			choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH),
-			choseBestTask(sptr(StayAtTownBehavior()), MAX_DEPTH)
-		};
+		decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
+		decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
+		decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
+		decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
+		decompose(bestTasks, sptr(GatherArmyBehavior()), MAX_DEPTH);
+		decompose(bestTasks, sptr(StayAtTownBehavior()), MAX_DEPTH);
 
 		if(cb->getDate(Date::DAY) == 1)
 		{
-			bestTasks.push_back(choseBestTask(sptr(StartupBehavior()), 1));
+			decompose(bestTasks, sptr(StartupBehavior()), 1);
 		}
 
-		bestTask = choseBestTask(bestTasks);
+		auto selectedTasks = buildPlan(bestTasks);
 
-		std::string taskDescription = bestTask->toString();
-		HeroPtr hero = bestTask->getHero();
-		HeroRole heroRole = HeroRole::MAIN;
+		logAi->debug("Decission madel in %ld", timeElapsed(start));
 
-		if(hero.validAndSet())
-			heroRole = heroManager->getHeroRole(hero);
+		if(selectedTasks.empty())
+		{
+			return;
+		}
 
-		if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
-			useHeroChain = false;
+		bool hasAnySuccess = false;
 
-		// TODO: better to check turn distance here instead of priority
-		if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
-			&& scanDepth == ScanDepth::MAIN_FULL)
+		for(auto bestTask : selectedTasks)
 		{
-			useHeroChain = false;
-			scanDepth = ScanDepth::SMALL;
+			std::string taskDescription = bestTask->toString();
+			HeroPtr hero = bestTask->getHero();
+			HeroRole heroRole = HeroRole::MAIN;
 
-			logAi->trace(
-				"Goal %s has low priority %f so decreasing  scan depth to gain performance.",
-				taskDescription,
-				bestTask->priority);
-		}
+			if(hero.validAndSet())
+				heroRole = heroManager->getHeroRole(hero);
 
-		if(bestTask->priority < MIN_PRIORITY)
-		{
-			auto heroes = cb->getHeroesInfo();
-			auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
-				{
-					return h->movementPointsRemaining() > 100;
-				});
+			if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
+				useHeroChain = false;
 
-			if(hasMp && scanDepth != ScanDepth::ALL_FULL)
+			// TODO: better to check turn distance here instead of priority
+			if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
+				&& scanDepth == ScanDepth::MAIN_FULL)
 			{
+				useHeroChain = false;
+				scanDepth = ScanDepth::SMALL;
+
 				logAi->trace(
-					"Goal %s has too low priority %f so increasing scan depth to full.",
+					"Goal %s has low priority %f so decreasing  scan depth to gain performance.",
 					taskDescription,
 					bestTask->priority);
+			}
+
+			if(bestTask->priority < MIN_PRIORITY)
+			{
+				auto heroes = cb->getHeroesInfo();
+				auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
+					{
+						return h->movementPointsRemaining() > 100;
+					});
+
+				if(hasMp && scanDepth != ScanDepth::ALL_FULL)
+				{
+					logAi->trace(
+						"Goal %s has too low priority %f so increasing scan depth to full.",
+						taskDescription,
+						bestTask->priority);
+
+					scanDepth = ScanDepth::ALL_FULL;
+					useHeroChain = false;
+					hasAnySuccess = true;
+					break;;
+				}
+
+				logAi->trace("Goal %s has too low priority. It is not worth doing it.", taskDescription);
 
-				scanDepth = ScanDepth::ALL_FULL;
-				useHeroChain = false;
 				continue;
 			}
 
-			logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", taskDescription);
+			if(!executeTask(bestTask))
+			{
+				if(hasAnySuccess)
+					break;
+				else
+					return;
+			}
 
-			return;
+			hasAnySuccess = true;
 		}
 
-		logAi->debug("Decission madel in %ld", timeElapsed(start));
-
-		executeTask(bestTask);
+		if(!hasAnySuccess)
+		{
+			logAi->trace("Nothing was done this turn. Ending turn.");
+			return;
+		}
 
 		if(i == settings->getMaxPass())
 		{
-			logAi->warn("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription);
+			logAi->warn("Maxpass exceeded. Terminating AI turn.");
 		}
 	}
 }
 
-void Nullkiller::executeTask(Goals::TTask task)
+bool Nullkiller::executeTask(Goals::TTask task)
 {
 	auto start = std::chrono::high_resolution_clock::now();
 	std::string taskDescr = task->toString();
@@ -376,10 +467,10 @@ void Nullkiller::executeTask(Goals::TTask task)
 		logAi->error("Failed to realize subgoal of type %s, I will stop.", taskDescr);
 		logAi->error("The error message was: %s", e.what());
 
-#if NKAI_TRACE_LEVEL == 0
-		throw; // will be recatched and AI turn ended
-#endif
+		return false;
 	}
+
+	return true;
 }
 
 TResources Nullkiller::getFreeResources() const

+ 22 - 3
AI/Nullkiller/Engine/Nullkiller.h

@@ -47,6 +47,24 @@ enum class ScanDepth
 	ALL_FULL = 2
 };
 
+struct TaskPlanItem
+{
+	std::vector<ObjectInstanceID> affectedObjects;
+	Goals::TSubgoal task;
+
+	TaskPlanItem(Goals::TSubgoal goal);
+};
+
+class TaskPlan
+{
+private:
+	std::vector<TaskPlanItem> tasks;
+
+public:
+	Goals::TTaskVec getTasks() const;
+	void merge(Goals::TSubgoal task);
+};
+
 class Nullkiller
 {
 private:
@@ -102,9 +120,10 @@ public:
 private:
 	void resetAiState();
 	void updateAiState(int pass, bool fast = false);
-	Goals::TTask choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const;
-	Goals::TTask choseBestTask(Goals::TTaskVec & tasks) const;
-	void executeTask(Goals::TTask task);
+	void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
+	Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
+	Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const;
+	bool executeTask(Goals::TTask task);
 };
 
 }

+ 14 - 15
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -655,7 +655,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 	case Obj::RESOURCE:
 	{
 		auto * res = dynamic_cast<const CGResource*>(target);
-		return res->resourceID() == GameResID::GOLD ? 600 : 100;
+		return res && res->resourceID() == GameResID::GOLD ? 600 : 100;
 	}
 	case Obj::TREASURE_CHEST:
 		return 1500;
@@ -711,8 +711,8 @@ public:
 
 		uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
 
-		evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength());
-		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get());
+		evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
+		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
 	}
 };
 
@@ -743,7 +743,7 @@ public:
 
 		Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
 
-		evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero().get());
+		evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
 		evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
 		evaluationContext.movementCost += stayAtTown.getMovementWasted();
 	}
@@ -792,7 +792,7 @@ public:
 		if(defendTown.getTurn() > 0 && defendTown.isCounterAttack())
 		{
 			auto ourSpeed = defendTown.hero->movementPointsLimit(true);
-			auto enemySpeed = treat.hero->movementPointsLimit(true);
+			auto enemySpeed = treat.hero.get(evaluationContext.evaluator.ai->cb.get())->movementPointsLimit(true);
 
 			if(enemySpeed > ourSpeed) multiplier *= 0.7f;
 		}
@@ -847,13 +847,12 @@ public:
 			evaluationContext.movementCostByRole[role] += pair.second;
 		}
 
-		auto heroPtr = task->hero;
-		auto hero = heroPtr.get(ai->cb.get());
+		auto hero = task->hero;
 		bool checkGold = evaluationContext.danger == 0;
 		auto army = path.heroArmy;
 
 		const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false);
-		auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
+		auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(hero);
 
 		if(heroRole == HeroRole::MAIN)
 			evaluationContext.heroRole = heroRole;
@@ -887,21 +886,21 @@ public:
 		Goals::UnlockCluster & clusterGoal = dynamic_cast<Goals::UnlockCluster &>(*task);
 		std::shared_ptr<ObjectCluster> cluster = clusterGoal.getCluster();
 
-		auto hero = clusterGoal.hero.get();
-		auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(clusterGoal.hero);
+		auto hero = clusterGoal.hero;
+		auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(hero);
 
-		std::vector<std::pair<const CGObjectInstance *, ClusterObjectInfo>> objects(cluster->objects.begin(), cluster->objects.end());
+		std::vector<std::pair<ObjectInstanceID, ClusterObjectInfo>> objects(cluster->objects.begin(), cluster->objects.end());
 
-		std::sort(objects.begin(), objects.end(), [](std::pair<const CGObjectInstance *, ClusterObjectInfo> o1, std::pair<const CGObjectInstance *, ClusterObjectInfo> o2) -> bool
+		std::sort(objects.begin(), objects.end(), [](std::pair<ObjectInstanceID, ClusterObjectInfo> o1, std::pair<ObjectInstanceID, ClusterObjectInfo> o2) -> bool
 		{
 			return o1.second.priority > o2.second.priority;
 		});
 
 		int boost = 1;
 
-		for(auto objInfo : objects)
+		for(auto & objInfo : objects)
 		{
-			auto target = objInfo.first;
+			auto target = evaluationContext.evaluator.ai->cb->getObj(objInfo.first);
 			bool checkGold = objInfo.second.danger == 0;
 			auto army = hero;
 
@@ -960,7 +959,7 @@ public:
 			return;
 
 		Goals::DismissHero & dismissCommand = dynamic_cast<Goals::DismissHero &>(*task);
-		const CGHeroInstance * dismissedHero = dismissCommand.getHero().get();
+		const CGHeroInstance * dismissedHero = dismissCommand.getHero();
 
 		auto role = ai->heroManager->getHeroRole(dismissedHero);
 		auto mpLeft = dismissedHero->movementPointsRemaining();

+ 5 - 3
AI/Nullkiller/Goals/AbstractGoal.cpp

@@ -31,12 +31,12 @@ TTask Goals::taskptr(const AbstractGoal & tmp)
 	if(!tmp.isElementar())
 		throw cannotFulfillGoalException(tmp.toString() + " is not elementar");
 
-	ptr.reset(dynamic_cast<ITask *>(tmp.clone()));
+	ptr.reset(tmp.clone()->asTask());
 
 	return ptr;
 }
 
-std::string AbstractGoal::toString() const //TODO: virtualize
+std::string AbstractGoal::toString() const
 {
 	std::string desc;
 	switch(goalType)
@@ -63,8 +63,10 @@ std::string AbstractGoal::toString() const //TODO: virtualize
 	default:
 		return std::to_string(goalType);
 	}
-	if(hero.get(true)) //FIXME: used to crash when we lost hero and failed goal
+
+	if(hero)
 		desc += " (" + hero->getNameTranslated() + ")";
+
 	return desc;
 }
 

+ 12 - 5
AI/Nullkiller/Goals/AbstractGoal.h

@@ -10,9 +10,8 @@
 #pragma once
 
 #include "../../../lib/VCMI_Lib.h"
-#include "../../../lib/CBuildingHandler.h"
-#include "../../../lib/CCreatureHandler.h"
-#include "../../../lib/CTownHandler.h"
+#include "../../../lib/mapObjects/CGTownInstance.h"
+#include "../../../lib/mapObjects/CGHeroInstance.h"
 #include "../AIUtility.h"
 
 namespace NKAI
@@ -106,7 +105,7 @@ namespace Goals
 		int objid; SETTER(int, objid)
 		int aid; SETTER(int, aid)
 		int3 tile; SETTER(int3, tile)
-		HeroPtr hero; SETTER(HeroPtr, hero)
+		const CGHeroInstance * hero; SETTER(CGHeroInstance *, hero)
 		const CGTownInstance *town; SETTER(CGTownInstance *, town)
 		int bid; SETTER(int, bid)
 
@@ -119,6 +118,7 @@ namespace Goals
 			objid = -1;
 			tile = int3(-1, -1, -1);
 			town = nullptr;
+			hero = nullptr;
 			bid = -1;
 			goldCost = 0;
 		}
@@ -147,6 +147,11 @@ namespace Goals
 		virtual bool hasHash() const { return false; }
 
 		virtual uint64_t getHash() const { return 0; }
+
+		virtual ITask * asTask()
+		{
+			throw std::runtime_error("Abstract goal is not a task");
+		}
 		
 		bool operator!=(const AbstractGoal & g) const
 		{
@@ -165,9 +170,11 @@ namespace Goals
 		//TODO: make accept work for std::shared_ptr... somehow
 		virtual void accept(AIGateway * ai) = 0; //unhandled goal will report standard error
 		virtual std::string toString() const = 0;
-		virtual HeroPtr getHero() const = 0;
+		virtual const CGHeroInstance * getHero() const = 0;
 		virtual ~ITask() {}
 		virtual int getHeroExchangeCount() const = 0;
+		virtual bool isObjectAffected(ObjectInstanceID h) const = 0;
+		virtual std::vector<ObjectInstanceID> getAffectedObjects() const = 0;
 	};
 }
 

+ 3 - 3
AI/Nullkiller/Goals/AdventureSpellCast.cpp

@@ -18,12 +18,12 @@ using namespace Goals;
 
 bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const
 {
-	return hero.h == other.hero.h;
+	return hero == other.hero;
 }
 
 void AdventureSpellCast::accept(AIGateway * ai)
 {
-	if(!hero.validAndSet())
+	if(!hero)
 		throw cannotFulfillGoalException("Invalid hero!");
 
 	auto spell = getSpell();
@@ -56,7 +56,7 @@ void AdventureSpellCast::accept(AIGateway * ai)
 	auto wait = cb->waitTillRealize;
 
 	cb->waitTillRealize = true;
-	cb->castSpell(hero.h, spellID, tile);
+	cb->castSpell(hero, spellID, tile);
 
 	if(town && spellID == SpellID::TOWN_PORTAL)
 	{

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

@@ -22,7 +22,7 @@ namespace Goals
 		SpellID spellID;
 
 	public:
-		AdventureSpellCast(HeroPtr hero, SpellID spellID)
+		AdventureSpellCast(const CGHeroInstance * hero, SpellID spellID)
 			: ElementarGoal(Goals::ADVENTURE_SPELL_CAST), spellID(spellID)
 		{
 			sethero(hero);

+ 29 - 2
AI/Nullkiller/Goals/CGoal.h

@@ -14,7 +14,6 @@
 namespace NKAI
 {
 
-struct HeroPtr;
 class AIGateway;
 
 namespace Goals
@@ -92,9 +91,37 @@ namespace Goals
 
 		bool isElementar() const override { return true; }
 
-		HeroPtr getHero() const override { return AbstractGoal::hero; }
+		const CGHeroInstance * getHero() const override { return AbstractGoal::hero; }
 
 		int getHeroExchangeCount() const override { return 0; }
+
+		bool isObjectAffected(ObjectInstanceID id) const override
+		{
+			return (AbstractGoal::hero && AbstractGoal::hero->id == id)
+				|| AbstractGoal::objid == id
+				|| (AbstractGoal::town && AbstractGoal::town->id == id);
+		}
+
+		std::vector<ObjectInstanceID> getAffectedObjects() const override
+		{
+			auto result = std::vector<ObjectInstanceID>();
+
+			if(AbstractGoal::hero)
+				result.push_back(AbstractGoal::hero->id);
+
+			if(AbstractGoal::objid != -1)
+				result.push_back(ObjectInstanceID(AbstractGoal::objid));
+
+			if(AbstractGoal::town)
+				result.push_back(AbstractGoal::town->id);
+
+			return result;
+		}
+
+		ITask * asTask() override
+		{
+			return this;
+		}
 	};
 }
 

+ 32 - 0
AI/Nullkiller/Goals/Composition.cpp

@@ -117,4 +117,36 @@ int Composition::getHeroExchangeCount() const
 	return result;
 }
 
+std::vector<ObjectInstanceID> Composition::getAffectedObjects() const
+{
+	std::vector<ObjectInstanceID> affectedObjects;
+
+	for(auto sequence : subtasks)
+	{
+		for(auto task : sequence)
+		{
+			if(task->isElementar())
+				vstd::concatenate(affectedObjects, task->asTask()->getAffectedObjects());
+		}
+	}
+
+	vstd::removeDuplicates(affectedObjects);
+
+	return affectedObjects;
+}
+
+bool Composition::isObjectAffected(ObjectInstanceID id) const
+{
+	for(auto sequence : subtasks)
+	{
+		for(auto task : sequence)
+		{
+			if(task->isElementar() && task->asTask()->isObjectAffected(id))
+				return true;
+		}
+	}
+
+	return false;
+}
+
 }

+ 3 - 0
AI/Nullkiller/Goals/Composition.h

@@ -35,6 +35,9 @@ namespace Goals
 		TGoalVec decompose(const Nullkiller * ai) const override;
 		bool isElementar() const override;
 		int getHeroExchangeCount() const override;
+
+		std::vector<ObjectInstanceID> getAffectedObjects() const override;
+		bool isObjectAffected(ObjectInstanceID id) const override;
 	};
 }
 

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

@@ -20,7 +20,7 @@ using namespace Goals;
 
 bool DigAtTile::operator==(const DigAtTile & other) const
 {
-	return other.hero.h == hero.h && other.tile == tile;
+	return other.hero == hero && other.tile == tile;
 }
 //
 //TSubgoal DigAtTile::decomposeSingle() const

+ 4 - 4
AI/Nullkiller/Goals/DismissHero.cpp

@@ -18,22 +18,22 @@ using namespace Goals;
 
 bool DismissHero::operator==(const DismissHero & other) const
 {
-	return hero.h == other.hero.h;
+	return hero == other.hero;
 }
 
 void DismissHero::accept(AIGateway * ai)
 {
-	if(!hero.validAndSet())
+	if(!hero)
 		throw cannotFulfillGoalException("Invalid hero!");
 
-	cb->dismissHero(hero.h);
+	cb->dismissHero(hero);
 
 	throw goalFulfilledException(sptr(*this));
 }
 
 std::string DismissHero::toString() const
 {
-	return "DismissHero " + hero.name;
+	return "DismissHero " + heroName;
 }
 
 }

+ 5 - 1
AI/Nullkiller/Goals/DismissHero.h

@@ -17,11 +17,15 @@ namespace Goals
 {
 	class DLL_EXPORT DismissHero : public ElementarGoal<DismissHero>
 	{
+	private:
+		std::string heroName;
+
 	public:
-		DismissHero(HeroPtr hero)
+		DismissHero(const CGHeroInstance * hero)
 			: ElementarGoal(Goals::DISMISS_HERO)
 		{
 			sethero(hero);
+			heroName = hero->getNameTranslated();
 		}
 
 		void accept(AIGateway * ai) override;

+ 20 - 0
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp

@@ -26,6 +26,26 @@ ExchangeSwapTownHeroes::ExchangeSwapTownHeroes(
 {
 }
 
+std::vector<ObjectInstanceID> ExchangeSwapTownHeroes::getAffectedObjects() const
+{
+	std::vector<ObjectInstanceID> affectedObjects = { town->id };
+
+	if(town->garrisonHero)
+		affectedObjects.push_back(town->garrisonHero->id);
+
+	if(town->visitingHero)
+		affectedObjects.push_back(town->visitingHero->id);
+
+	return affectedObjects;
+}
+
+bool ExchangeSwapTownHeroes::isObjectAffected(ObjectInstanceID id) const
+{
+	return town->id == id
+		|| (town->visitingHero && town->visitingHero->id == id)
+		|| (town->garrisonHero && town->garrisonHero->id == id);
+}
+
 std::string ExchangeSwapTownHeroes::toString() const
 {
 	return "Exchange and swap heroes of " + town->getNameTranslated();

+ 3 - 0
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h

@@ -35,6 +35,9 @@ namespace Goals
 
 		const CGHeroInstance * getGarrisonHero() const { return garrisonHero; }
 		HeroLockedReason getLockingReason() const { return lockingReason; }
+
+		std::vector<ObjectInstanceID> getAffectedObjects() const override;
+		bool isObjectAffected(ObjectInstanceID id) const override;
 	};
 }
 

+ 32 - 0
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -42,6 +42,38 @@ bool ExecuteHeroChain::operator==(const ExecuteHeroChain & other) const
 		&& chainPath.chainMask == other.chainPath.chainMask;
 }
 
+std::vector<ObjectInstanceID> ExecuteHeroChain::getAffectedObjects() const
+{
+	std::vector<ObjectInstanceID> affectedObjects = { chainPath.targetHero->id };
+
+	if(objid != -1)
+		affectedObjects.push_back(ObjectInstanceID(objid));
+
+	for(auto & node : chainPath.nodes)
+	{
+		if(node.targetHero)
+			affectedObjects.push_back(node.targetHero->id);
+	}
+
+	vstd::removeDuplicates(affectedObjects);
+
+	return affectedObjects;
+}
+
+bool ExecuteHeroChain::isObjectAffected(ObjectInstanceID id) const
+{
+	if(chainPath.targetHero->id == id || objid == id)
+		return true;
+
+	for(auto & node : chainPath.nodes)
+	{
+		if(node.targetHero && node.targetHero->id == id)
+			return true;
+	}
+
+	return false;
+}
+
 void ExecuteHeroChain::accept(AIGateway * ai)
 {
 	logAi->debug("Executing hero chain towards %s. Path %s", targetName, chainPath.toString());

+ 3 - 0
AI/Nullkiller/Goals/ExecuteHeroChain.h

@@ -34,6 +34,9 @@ namespace Goals
 
 		int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
 
+		std::vector<ObjectInstanceID> getAffectedObjects() const override;
+		bool isObjectAffected(ObjectInstanceID id) const override;
+
 	private:
 		bool moveHeroToTile(AIGateway * ai, const CGHeroInstance * hero, const int3 & tile);
 	};

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

@@ -46,7 +46,7 @@ void StayAtTown::accept(AIGateway * ai)
 		logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
 	}
 
-	ai->nullkiller->lockHero(hero.get(), HeroLockedReason::DEFENCE);
+	ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
 }
 
 }

+ 1 - 1
AI/Nullkiller/Markers/ArmyUpgrade.cpp

@@ -22,7 +22,7 @@ ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * up
 	: CGoal(Goals::ARMY_UPGRADE), upgrader(upgrader), upgradeValue(upgrade.upgradeValue),
 	initialValue(upgradePath.heroArmy->getArmyStrength()), goldCost(upgrade.upgradeCost[EGameResID::GOLD])
 {
-	sethero(upgradePath.targetHero);
+	hero = upgradePath.targetHero;
 }
 
 ArmyUpgrade::ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade)

+ 1 - 1
AI/Nullkiller/Markers/DefendTown.cpp

@@ -22,7 +22,7 @@ DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, co
 	: CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), counterattack(isCounterAttack)
 {
 	settown(town);
-	sethero(defencePath.targetHero);
+	hero = defencePath.targetHero;
 }
 
 DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender)

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

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

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

@@ -33,7 +33,7 @@ namespace Goals
 			: CGoal(Goals::UNLOCK_CLUSTER), cluster(cluster), pathToCenter(pathToCenter)
 		{
 			tile = cluster->blocker->visitablePos();
-			sethero(pathToCenter.targetHero);
+			hero = pathToCenter.targetHero;
 		}
 
 		bool operator==(const UnlockCluster & other) const override;

+ 4 - 0
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -105,7 +105,11 @@ void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole>
 	cb->calculatePaths(config);
 
 	if(!pathfinderSettings.useHeroChain)
+	{
+		logAi->trace("Recalculated paths in %ld", timeElapsed(start));
+
 		return;
+	}
 
 	do
 	{

+ 4 - 1
AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp

@@ -35,7 +35,10 @@ namespace AIPathfinding
 			return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(hero);
 		}
 
-		return questInfo.quest->activeForPlayers.count(hero->getOwner())
+		auto notActivated = !questInfo.obj->wasVisited(ai->playerID)
+			&& !questInfo.quest->activeForPlayers.count(hero->getOwner());
+		
+		return notActivated
 			|| questInfo.quest->checkQuest(hero);
 	}