瀏覽代碼

AI: separate hero chain recalculation

Andrii Danylchenko 4 年之前
父節點
當前提交
a88181acd7

+ 7 - 2
AI/Nullkiller/AIUtility.cpp

@@ -199,9 +199,9 @@ bool isSafeToVisit(HeroPtr h, crint3 tile)
 	return isSafeToVisit(h, fh->evaluateDanger(tile, h.get()));
 }
 
-bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength)
+bool isSafeToVisit(HeroPtr h, const CCreatureSet * heroArmy, uint64_t dangerStrength)
 {
-	const ui64 heroStrength = h->getTotalStrength();
+	const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength();
 
 	if(dangerStrength)
 	{
@@ -218,6 +218,11 @@ bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength)
 	return true; //there's no danger
 }
 
+bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength)
+{
+	return isSafeToVisit(h, h.get(), dangerStrength);
+}
+
 bool isObjectRemovable(const CGObjectInstance * obj)
 {
 	//FIXME: move logic to object property!

+ 1 - 0
AI/Nullkiller/AIUtility.h

@@ -168,6 +168,7 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj);
 
 bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property!
 bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength);
+bool isSafeToVisit(HeroPtr h, const CCreatureSet *, uint64_t dangerStrength);
 bool isSafeToVisit(HeroPtr h, crint3 tile);
 
 bool compareHeroStrength(HeroPtr h1, HeroPtr h2);

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

@@ -94,11 +94,15 @@ namespace Goals
 		float movementCost;
 		int manaCost;
 		uint64_t danger;
+		uint64_t armyLoss;
+		uint64_t heroStrength;
 
 		EvaluationContext()
 			: movementCost(0.0),
 			manaCost(0),
-			danger(0)
+			danger(0),
+			armyLoss(0),
+			heroStrength(0)
 		{
 		}
 	};

+ 140 - 42
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -28,8 +28,10 @@ AINodeStorage::~AINodeStorage() = default;
 
 void AINodeStorage::initialize(const PathfinderOptions & options, const CGameState * gs)
 {
-	//TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline
+	if(heroChainPass)
+		return;
 
+	//TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline
 	int3 pos;
 	const PlayerColor player = ai->playerID;
 	const int3 sizes = gs->getMapSize();
@@ -73,6 +75,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
 void AINodeStorage::clear()
 {
 	actors.clear();
+	heroChainPass = false;
 }
 
 const AIPathNode * AINodeStorage::getAINode(const CGPathNode * node) const
@@ -114,6 +117,9 @@ boost::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 
 std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 {
+	if(heroChainPass)
+		return heroChain;
+
 	std::vector<CGPathNode *> initialNodes;
 
 	for(auto actorPtr : actors)
@@ -152,6 +158,7 @@ void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, CGPat
 		heroNode.manaCost = 0;
 		heroNode.specialAction.reset();
 		heroNode.armyLoss = 0;
+		heroNode.chainOther = nullptr;
 		heroNode.update(coord, layer, accessibility);
 	}
 }
@@ -162,14 +169,7 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf
 
 	updateAINode(destination.node, [&](AIPathNode * dstNode)
 	{
-		dstNode->moveRemains = destination.movementLeft;
-		dstNode->turns = destination.turn;
-		dstNode->cost = destination.cost;
-		dstNode->danger = srcNode->danger;
-		dstNode->action = destination.action;
-		dstNode->theNodeBefore = srcNode->theNodeBefore;
-		dstNode->manaCost = srcNode->manaCost;
-		dstNode->armyLoss = srcNode->armyLoss;
+		commit(dstNode, srcNode, destination.action, destination.turn, destination.movementLeft, destination.cost);
 
 		if(dstNode->specialAction && dstNode->actor)
 		{
@@ -178,6 +178,24 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf
 	});
 }
 
+void AINodeStorage::commit(
+	AIPathNode * destination, 
+	const AIPathNode * source, 
+	CGPathNode::ENodeAction action, 
+	int turn, 
+	int movementLeft, 
+	float cost) const
+{
+	destination->action = source->action;
+	destination->cost = cost;
+	destination->moveRemains = movementLeft;
+	destination->turns = turn;
+	destination->armyLoss = source->armyLoss;
+	destination->manaCost = source->manaCost;
+	destination->danger = source->danger;
+	destination->theNodeBefore = source->theNodeBefore;
+}
+
 std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
 	const PathNodeInfo & source,
 	const PathfinderConfig * pathfinderConfig,
@@ -200,42 +218,56 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
 			neighbours.push_back(nextNode.get());
 		}
 	}
-
-	if((source.node->layer == EPathfindingLayer::LAND || source.node->layer == EPathfindingLayer::SAIL)
-		&& source.node->turns < 1)
-	{
-		addHeroChain(neighbours, srcNode);
-	}
 	
 	return neighbours;
 }
 
-void AINodeStorage::addHeroChain(std::vector<CGPathNode *> & result, const AIPathNode * srcNode)
+bool AINodeStorage::calculateHeroChain()
+{
+	heroChainPass = true;
+	heroChain.resize(0);
+
+	foreach_tile_pos([&](const int3 & pos) {
+		auto layer = EPathfindingLayer::LAND;
+		auto chains = nodes[pos.x][pos.y][pos.z][layer];
+
+		for(AIPathNode & node : chains)
+		{
+			if(node.locked && node.turns < 1)
+				addHeroChain(&node);
+		}
+	});
+
+	return heroChain.size();
+}
+
+void AINodeStorage::addHeroChain(AIPathNode * srcNode)
 {
 	auto chains = nodes[srcNode->coord.x][srcNode->coord.y][srcNode->coord.z][srcNode->layer];
 
-	for(const AIPathNode & node : chains)
+	for(AIPathNode & node : chains)
 	{
 		if(!node.locked || !node.actor || node.action == CGPathNode::ENodeAction::UNKNOWN && node.actor->hero)
 		{
 			continue;
 		}
 
-		addHeroChain(result, srcNode, &node);
-		addHeroChain(result, &node, srcNode);
+		addHeroChain(srcNode, &node);
+		addHeroChain(&node, srcNode);
 	}
 }
 
-void AINodeStorage::addHeroChain(
-	std::vector<CGPathNode *> & result, 
-	const AIPathNode * carrier, 
-	const AIPathNode * other)
+void AINodeStorage::addHeroChain(AIPathNode * carrier, AIPathNode * other)
 {
 	if(carrier->actor->canExchange(other->actor))
 	{
 		bool hasLessMp = carrier->turns > other->turns || carrier->moveRemains < other->moveRemains;
 		bool hasLessExperience = carrier->actor->hero->exp < other->actor->hero->exp;
 
+#ifdef VCMI_TRACE_PATHFINDER
+		logAi->trace("Check hero exhange at %s, %s -> %s", carrier->coord.toString(), other->actor->hero->name, carrier->actor->hero->name);
+#endif
+
 		if(hasLessMp && hasLessExperience)
 			return;
 
@@ -250,10 +282,57 @@ void AINodeStorage::addHeroChain(
 		if(chainNode->locked)
 			return;
 
-		chainNode->specialAction = newActor->getExchangeAction();
+#ifdef VCMI_TRACE_PATHFINDER
+		logAi->trace("Hero exhange at %s, %s -> %s", carrier->coord.toString(), other->actor->hero->name, carrier->actor->hero->name);
+#endif
+		
+		commitExchange(chainNode, carrier, other);
+		heroChain.push_back(chainNode);
+	}
+}
+
+void AINodeStorage::commitExchange(
+	AIPathNode * exchangeNode, 
+	AIPathNode * carrierParentNode, 
+	AIPathNode * otherParentNode) const
+{
+	auto carrierActor = carrierParentNode->actor;
+	auto exchangeActor = exchangeNode->actor;
+	auto otherActor = otherParentNode->actor;
+
+	auto armyLoss = carrierParentNode->armyLoss + otherParentNode->armyLoss;
+	auto turns = carrierParentNode->turns;
+	auto cost = carrierParentNode->cost;
+	auto movementLeft = carrierParentNode->moveRemains;
+
+	if(carrierParentNode->turns < otherParentNode->turns)
+	{
+		int moveRemains = exchangeActor->hero->maxMovePoints(exchangeNode->layer);
+		float waitingCost = otherParentNode->turns - carrierParentNode->turns - 1
+			+ carrierParentNode->moveRemains / (float)moveRemains;
 
-		result.push_back(chainNode);
+		turns = otherParentNode->turns;
+		cost = waitingCost;
+		movementLeft = moveRemains;
 	}
+		
+	if(exchangeNode->turns != 0xFF && exchangeNode->cost < cost)
+		return;
+
+#ifdef VCMI_TRACE_PATHFINDER
+	logAi->trace(
+		"Accepted hero exhange at %s, carrier %s, mp cost %f", 
+		destination.coord.toString(),
+		carrierActor->hero->name,
+		destination.cost);
+#endif
+	
+	commit(exchangeNode, carrierParentNode, carrierParentNode->action, turns, movementLeft, cost);
+
+	exchangeNode->theNodeBefore = carrierParentNode;
+	exchangeNode->chainOther = otherParentNode;
+	exchangeNode->armyLoss = armyLoss;
+	exchangeNode->manaCost = carrierParentNode->manaCost;
 }
 
 const CGHeroInstance * AINodeStorage::getHero(const CGPathNode * node) const
@@ -441,6 +520,9 @@ bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, con
 std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand) const
 {
 	std::vector<AIPath> paths;
+
+	paths.reserve(NUM_CHAINS / 4);
+
 	auto chains = nodes[pos.x][pos.y][pos.z][isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL];
 
 	for(const AIPathNode & node : chains)
@@ -451,26 +533,13 @@ std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand)
 		}
 
 		AIPath path;
-		const AIPathNode * current = &node;
 
 		path.targetHero = node.actor->hero;
-		auto initialPos = path.targetHero->visitablePos();
-
-		while(current != nullptr && current->coord != initialPos)
-		{
-			AIPathNodeInfo pathNode;
-			pathNode.cost = current->cost;
-			pathNode.turns = current->turns;
-			pathNode.danger = current->danger;
-			pathNode.coord = current->coord;
-
-			path.nodes.push_back(pathNode);
-			path.specialAction = current->specialAction;
-
-			current = getAINode(current->theNodeBefore);
-		}
-
+		path.heroArmy = node.actor->creatureSet;
+		path.armyLoss = node.armyLoss;
 		path.targetObjectDanger = evaluateDanger(pos, path.targetHero);
+		
+		fillChainInfo(&node, path);
 
 		paths.push_back(path);
 	}
@@ -478,6 +547,30 @@ std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand)
 	return paths;
 }
 
+void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path) const
+{
+	while(node != nullptr)
+	{
+		if(!node->actor->hero || node->coord == node->actor->hero->visitablePos())
+			return;
+
+		AIPathNodeInfo pathNode;
+		pathNode.cost = node->cost;
+		pathNode.targetHero = node->actor->hero;
+		pathNode.turns = node->turns;
+		pathNode.danger = node->danger;
+		pathNode.coord = node->coord;
+
+		path.nodes.push_back(pathNode);
+		path.specialAction = node->specialAction;
+
+		if(node->chainOther)
+			fillChainInfo(node->chainOther, path);
+
+		node = getAINode(node->theNodeBefore);
+	}
+}
+
 AIPath::AIPath()
 	: nodes({})
 {
@@ -514,6 +607,11 @@ float AIPath::movementCost() const
 	return 0.0;
 }
 
+uint64_t AIPath::getHeroStrength() const
+{
+	return targetHero->getFightingStrength() * heroArmy->getArmyStrength();
+}
+
 uint64_t AIPath::getTotalDanger(HeroPtr hero) const
 {
 	uint64_t pathDanger = getPathDanger();

+ 26 - 8
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -23,6 +23,7 @@ struct AIPathNode : public CGPathNode
 	uint64_t danger;
 	uint64_t armyLoss;
 	uint32_t manaCost;
+	const AIPathNode * chainOther;
 	std::shared_ptr<const ISpecialAction> specialAction;
 	const ChainActor * actor;
 };
@@ -33,6 +34,7 @@ struct AIPathNodeInfo
 	int turns;
 	int3 coord;
 	uint64_t danger;
+	const CGHeroInstance * targetHero;
 };
 
 struct AIPath
@@ -40,7 +42,9 @@ struct AIPath
 	std::vector<AIPathNodeInfo> nodes;
 	std::shared_ptr<const ISpecialAction> specialAction;
 	uint64_t targetObjectDanger;
+	uint64_t armyLoss;
 	const CGHeroInstance * targetHero;
+	const CCreatureSet * heroArmy;
 
 	AIPath();
 
@@ -53,6 +57,8 @@ struct AIPath
 	int3 firstTileToGet() const;
 
 	float movementCost() const;
+
+	uint64_t getHeroStrength() const;
 };
 
 class AINodeStorage : public INodeStorage
@@ -66,11 +72,9 @@ private:
 	const VCAI * ai;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
 	std::vector<std::shared_ptr<ChainActor>> actors;
+	std::vector<CGPathNode *> heroChain;
+	bool heroChainPass; // true if we need to calculate hero chain
 
-	STRONG_INLINE
-	void resetTile(const int3 & tile, EPathfindingLayer layer, CGPathNode::EAccessibility accessibility);
-	void addHeroChain(std::vector<CGPathNode *> & result, const AIPathNode * srcNode);
-	void addHeroChain(std::vector<CGPathNode *> & result, const AIPathNode * carrier, const AIPathNode * other);
 public:
 	/// more than 1 chain layer for each hero allows us to have more than 1 path to each tile so we can chose more optimal one.
 	static const int NUM_CHAINS = 3 * GameConstants::MAX_HEROES_PER_PLAYER;
@@ -101,14 +105,11 @@ public:
 	boost::optional<AIPathNode *> getOrCreateNode(const int3 & coord, const EPathfindingLayer layer, const ChainActor * actor);
 	std::vector<AIPath> getChainInfo(const int3 & pos, bool isOnLand) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const;
-
 	void setHeroes(std::vector<HeroPtr> heroes, const VCAI * ai);
-
 	const CGHeroInstance * getHero(const CGPathNode * node) const;
-
 	const std::set<const CGHeroInstance *> getAllHeroes() const;
-
 	void clear();
+	bool calculateHeroChain();
 
 	uint64_t evaluateDanger(const int3 &  tile, const CGHeroInstance * hero) const
 	{
@@ -116,5 +117,22 @@ public:
 	}
 
 private:
+	STRONG_INLINE
+	void resetTile(const int3 & tile, EPathfindingLayer layer, CGPathNode::EAccessibility accessibility);
+	void addHeroChain(AIPathNode * srcNode);
+	void addHeroChain(AIPathNode * carrier, AIPathNode * other);
 	void calculateTownPortalTeleportations(const PathNodeInfo & source, std::vector<CGPathNode *> & neighbours);
+	void fillChainInfo(const AIPathNode * node, AIPath & path) const;
+	void commit(
+		AIPathNode * destination, 
+		const AIPathNode * source, 
+		CGPathNode::ENodeAction action, 
+		int turn, 
+		int movementLeft, 
+		float cost) const;
+
+	void AINodeStorage::commitExchange(
+		AIPathNode * exchangeNode, 
+		AIPathNode * carrierParentNode, 
+		AIPathNode * otherParentNode) const;
 };

+ 8 - 2
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -43,7 +43,7 @@ std::vector<AIPath> AIPathfinder::getPathInfo(const HeroPtr & hero, const int3 &
 	return storage->getChainInfo(tile, !tileInfo->isWater());
 }
 
-void AIPathfinder::updatePaths(std::vector<HeroPtr> heroes)
+void AIPathfinder::updatePaths(std::vector<HeroPtr> heroes, bool useHeroChain)
 {
 	if(!storage)
 	{
@@ -55,8 +55,14 @@ void AIPathfinder::updatePaths(std::vector<HeroPtr> heroes)
 	storage->clear();
 	storage->setHeroes(heroes, ai);
 
-		auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage);
+	auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage);
+	cb->calculatePaths(config);
+
+	while(useHeroChain && storage->calculateHeroChain())
+	{
+		config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage);
 		cb->calculatePaths(config);
+	}
 }
 
 void AIPathfinder::updatePaths(const HeroPtr & hero)

+ 1 - 1
AI/Nullkiller/Pathfinding/AIPathfinder.h

@@ -25,7 +25,7 @@ public:
 	AIPathfinder(CPlayerSpecificInfoCallback * cb, VCAI * ai);
 	std::vector<AIPath> getPathInfo(const HeroPtr & hero, const int3 & tile) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const;
-	void updatePaths(std::vector<HeroPtr> heroes);
+	void updatePaths(std::vector<HeroPtr> heroes, bool useHeroChain = false);
 	void updatePaths(const HeroPtr & heroes);
 	void init();
 };

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

@@ -13,6 +13,7 @@
 #include "Rules/AIMovementAfterDestinationRule.h"
 #include "Rules/AIMovementToDestinationRule.h"
 #include "Rules/AIPreviousNodeRule.h"
+#include "Rules/AIMovementCostRule.h"
 
 namespace AIPathfinding
 {
@@ -25,7 +26,7 @@ namespace AIPathfinding
 			std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage),
 			std::make_shared<DestinationActionRule>(),
 			std::make_shared<AIMovementToDestinationRule>(nodeStorage),
-			std::make_shared<MovementCostRule>(),
+			std::make_shared<AIMovementCostRule>(nodeStorage),
 			std::make_shared<AIPreviousNodeRule>(nodeStorage),
 			std::make_shared<AIMovementAfterDestinationRule>(cb, nodeStorage)
 		};

+ 1 - 1
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -48,7 +48,7 @@ ChainActor::ChainActor(const ChainActor * carrier, const ChainActor * other, con
 	carrierParent(carrier), otherParent(other), chainMask(carrier->chainMask | other->chainMask)
 {
 	baseActor = static_cast<HeroActor *>(this);
-	armyValue = heroArmy->getArmyStrength();
+	armyValue = hero->getFightingStrength() * heroArmy->getArmyStrength();
 }
 
 HeroActor::HeroActor(const CGHeroInstance * hero, int chainMask)

+ 3 - 1
AI/Nullkiller/Pathfinding/PathfindingManager.cpp

@@ -136,7 +136,7 @@ Goals::TGoalVec PathfindingManager::findPaths(
 		{
 			danger = path.getTotalDanger(hero);
 
-			if(isSafeToVisit(hero, danger))
+			if(isSafeToVisit(hero, path.heroArmy, danger))
 			{
 				Goals::TSubgoal solution;
 
@@ -158,6 +158,8 @@ Goals::TGoalVec PathfindingManager::findPaths(
 					solution->evaluationContext.danger = danger;
 
 				solution->evaluationContext.movementCost += path.movementCost();
+				solution->evaluationContext.armyLoss += path.armyLoss;
+				solution->evaluationContext.heroStrength = path.getHeroStrength();
 #ifdef VCMI_TRACE_PATHFINDER
 				logAi->trace("It's safe for %s to visit tile %s with danger %s, goal %s", hero->name, dest.toString(), std::to_string(danger), solution->name());
 #endif

+ 0 - 39
AI/Nullkiller/Pathfinding/Rules/AIMovementCostRule.cpp

@@ -23,44 +23,5 @@ namespace AIPathfinding
 		const PathfinderConfig * pathfinderConfig,
 		CPathfinderHelper * pathfinderHelper) const
 	{
-		auto srcNode = nodeStorage->getAINode(source.node);
-		auto dstNode = nodeStorage->getAINode(destination.node);
-		auto srcActor = srcNode->actor;
-		auto dstActor = dstNode->actor;
-
-		if(srcActor == dstActor)
-		{
-			MovementCostRule::process(source, destination, pathfinderConfig, pathfinderHelper);
-
-			return;
-		}
-		
-		auto carrierActor = dstActor->carrierParent;
-		auto otherActor = dstActor->otherParent;
-
-		if(source.coord == destination.coord)
-		{
-			auto carrierNode = nodeStorage->getOrCreateNode(source.coord, source.node->layer, carrierActor).get();
-			auto otherNode = nodeStorage->getOrCreateNode(source.coord, source.node->layer, otherActor).get();
-
-			if(carrierNode->turns >= otherNode->turns)
-			{
-				destination.turn = carrierNode->turns;
-				destination.cost = carrierNode->cost;
-
-				return;
-			}
-
-			double waitingCost = otherNode->turns - carrierNode->turns - 1.0
-				+ carrierNode->moveRemains / (double)pathfinderHelper->getMaxMovePoints(carrierNode->layer);
-
-			destination.turn = otherNode->turns;
-			destination.cost = waitingCost;
-		}
-		else
-		{
-			// TODO: exchange through sail->land border might be more sofisticated
-			destination.blocked = true;
-		}
 	}
 }

+ 31 - 2
AI/Nullkiller/Pathfinding/Rules/AIPreviousNodeRule.cpp

@@ -36,12 +36,41 @@ namespace AIPathfinding
 			return;
 		}
 
-		auto aiSourceNode = nodeStorage->getAINode(source.node);
+		auto srcNode = nodeStorage->getAINode(source.node);
 
-		if(aiSourceNode->specialAction)
+		if(srcNode->specialAction)
 		{
 			// there is some action on source tile which should be performed before we can bypass it
 			destination.node->theNodeBefore = source.node;
 		}
+
+		auto dstNode = nodeStorage->getAINode(destination.node);
+		auto srcActor = srcNode->actor;
+		auto dstActor = dstNode->actor;
+
+		if(srcActor == dstActor)
+		{
+			return;
+		}
+
+		auto carrierActor = dstActor->carrierParent;
+		auto otherActor = dstActor->otherParent;
+
+		nodeStorage->updateAINode(destination.node, [&](AIPathNode * dstNode) {
+			if(source.coord == destination.coord)
+			{
+				auto carrierNode = nodeStorage->getOrCreateNode(source.coord, source.node->layer, carrierActor).get();
+				auto otherNode = nodeStorage->getOrCreateNode(source.coord, source.node->layer, otherActor).get();
+
+				if(destination.coord != carrierNode->coord)
+					dstNode->theNodeBefore = carrierNode;
+
+				dstNode->chainOther = otherNode;
+
+#ifdef VCMI_TRACE_PATHFINDER
+				logAi->trace("Link Hero exhange at %s, %s -> %s", dstNode->coord.toString(), otherNode->actor->hero->name, carrierNode->actor->hero->name);
+#endif
+			}
+		});
 	}
 }