From a88181acd7bc9e4bf4596392e998fef7a6cb1071 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 15 May 2021 21:01:48 +0300 Subject: [PATCH] AI: separate hero chain recalculation --- AI/Nullkiller/AIUtility.cpp | 9 +- AI/Nullkiller/AIUtility.h | 1 + AI/Nullkiller/Goals/AbstractGoal.h | 6 +- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 184 ++++++++++++++---- AI/Nullkiller/Pathfinding/AINodeStorage.h | 34 +++- AI/Nullkiller/Pathfinding/AIPathfinder.cpp | 10 +- AI/Nullkiller/Pathfinding/AIPathfinder.h | 2 +- .../Pathfinding/AIPathfinderConfig.cpp | 3 +- AI/Nullkiller/Pathfinding/Actors.cpp | 2 +- .../Pathfinding/PathfindingManager.cpp | 4 +- .../Pathfinding/Rules/AIMovementCostRule.cpp | 39 ---- .../Pathfinding/Rules/AIPreviousNodeRule.cpp | 33 +++- 12 files changed, 226 insertions(+), 101 deletions(-) diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index 5713cf83c..08add4819 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/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! diff --git a/AI/Nullkiller/AIUtility.h b/AI/Nullkiller/AIUtility.h index 7c5c3686a..954b72631 100644 --- a/AI/Nullkiller/AIUtility.h +++ b/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); diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index 5b0567da7..19bebd81c 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/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) { } }; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 63bd8ba27..d325ee939 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/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 AINodeStorage::getOrCreateNode( std::vector AINodeStorage::getInitialNodes() { + if(heroChainPass) + return heroChain; + std::vector 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 AINodeStorage::calculateNeighbours( const PathNodeInfo & source, const PathfinderConfig * pathfinderConfig, @@ -200,42 +218,56 @@ std::vector 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 & 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 & 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,12 +282,59 @@ void AINodeStorage::addHeroChain( if(chainNode->locked) return; - chainNode->specialAction = newActor->getExchangeAction(); - - result.push_back(chainNode); +#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; + + 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 { auto aiNode = getAINode(node); @@ -441,6 +520,9 @@ bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, con std::vector AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand) const { std::vector 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 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 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(); diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 2a5d0a8f5..ae86f6c2e 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/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 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 nodes; std::shared_ptr 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 dangerEvaluator; std::vector> actors; + std::vector 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 & result, const AIPathNode * srcNode); - void addHeroChain(std::vector & 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 getOrCreateNode(const int3 & coord, const EPathfindingLayer layer, const ChainActor * actor); std::vector getChainInfo(const int3 & pos, bool isOnLand) const; bool isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const; - void setHeroes(std::vector heroes, const VCAI * ai); - const CGHeroInstance * getHero(const CGPathNode * node) const; - const std::set 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 & 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; }; diff --git a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp index e0b89d93d..ead99a49b 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp @@ -43,7 +43,7 @@ std::vector AIPathfinder::getPathInfo(const HeroPtr & hero, const int3 & return storage->getChainInfo(tile, !tileInfo->isWater()); } -void AIPathfinder::updatePaths(std::vector heroes) +void AIPathfinder::updatePaths(std::vector heroes, bool useHeroChain) { if(!storage) { @@ -55,8 +55,14 @@ void AIPathfinder::updatePaths(std::vector heroes) storage->clear(); storage->setHeroes(heroes, ai); - auto config = std::make_shared(cb, ai, storage); + auto config = std::make_shared(cb, ai, storage); + cb->calculatePaths(config); + + while(useHeroChain && storage->calculateHeroChain()) + { + config = std::make_shared(cb, ai, storage); cb->calculatePaths(config); + } } void AIPathfinder::updatePaths(const HeroPtr & hero) diff --git a/AI/Nullkiller/Pathfinding/AIPathfinder.h b/AI/Nullkiller/Pathfinding/AIPathfinder.h index 30ac8b41f..98a6e0b7b 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinder.h +++ b/AI/Nullkiller/Pathfinding/AIPathfinder.h @@ -25,7 +25,7 @@ public: AIPathfinder(CPlayerSpecificInfoCallback * cb, VCAI * ai); std::vector getPathInfo(const HeroPtr & hero, const int3 & tile) const; bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const; - void updatePaths(std::vector heroes); + void updatePaths(std::vector heroes, bool useHeroChain = false); void updatePaths(const HeroPtr & heroes); void init(); }; diff --git a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp index 7547a2500..4444f44ae 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp +++ b/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(cb, ai, nodeStorage), std::make_shared(), std::make_shared(nodeStorage), - std::make_shared(), + std::make_shared(nodeStorage), std::make_shared(nodeStorage), std::make_shared(cb, nodeStorage) }; diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index a0e3b3100..15a7ba2f1 100644 --- a/AI/Nullkiller/Pathfinding/Actors.cpp +++ b/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(this); - armyValue = heroArmy->getArmyStrength(); + armyValue = hero->getFightingStrength() * heroArmy->getArmyStrength(); } HeroActor::HeroActor(const CGHeroInstance * hero, int chainMask) diff --git a/AI/Nullkiller/Pathfinding/PathfindingManager.cpp b/AI/Nullkiller/Pathfinding/PathfindingManager.cpp index b839bd71d..f3b826965 100644 --- a/AI/Nullkiller/Pathfinding/PathfindingManager.cpp +++ b/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 diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementCostRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementCostRule.cpp index d2044317a..ca0bbf32a 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementCostRule.cpp +++ b/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; - } } } diff --git a/AI/Nullkiller/Pathfinding/Rules/AIPreviousNodeRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIPreviousNodeRule.cpp index 3dbdcee6d..480808626 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIPreviousNodeRule.cpp +++ b/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 + } + }); } }