Fix branched hero chain reconstruction to preserve order and parent links

Fix reconstruction of branched hero chains so branch nodes keep their
original ordering and parentIndex links remain valid.
Previously, reconstructed paths could connect nodes in the wrong sequence,
which produced invalid handoff paths during chain execution.
This commit is contained in:
Michał Zaremba
2026-05-17 22:42:11 +02:00
parent 61e41b1243
commit 01cddcecca
2 changed files with 83 additions and 26 deletions
+72 -25
View File
@@ -1442,8 +1442,18 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
path.targetHero = node.actor->hero;
path.heroArmy = node.actor->creatureSet;
path.armyLoss = node.armyLoss;
path.chainMask = node.actor->chainMask;
fillChainInfo(&node, path, -1);
RealMoveMasksByHero realMoveMasks;
int parentIndex = -1;
if(!tryReconstructChainInfo(&node, path, parentIndex, realMoveMasks))
{
#if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace("AINodeStorage::calculateChainInfo Skip conflicting reconstructed chain path %s", path.toString());
#endif
paths.pop_back();
continue;
}
path.targetObjectDanger = aiNk->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle);
for(const auto & pathNode : path.nodes)
{
@@ -1487,46 +1497,83 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
path.targetHero,
getHeroArmyStrengthWithCommander(path.targetHero, path.heroArmy, fortLevel),
path.targetObjectDanger);
path.chainMask = node.actor->chainMask;
path.exchangeCount = node.actor->actorExchangeCount;
}
}
void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const
bool AINodeStorage::tryReconstructChainInfo(const AIPathNode * node, AIPath & path, int & parentIndex, RealMoveMasksByHero & realMoveMasks) const
{
while(node != nullptr)
{
if(!node->actor->hero)
return;
return true;
const auto tryAppendCurrentNode = [this, &node](AIPath & candidatePath, int candidateParentIndex,
RealMoveMasksByHero & candidateMasks) -> std::optional<int>
{
if(isRealMovementNode(node))
{
auto existingMask = candidateMasks.find(node->actor->hero);
if(existingMask != candidateMasks.end() && existingMask->second != node->actor->chainMask)
return std::nullopt;
}
AIPathNodeInfo pathNode;
pathNode.cost = node->getCost();
pathNode.targetHero = node->actor->hero;
pathNode.chainMask = node->actor->chainMask;
pathNode.specialAction = node->specialAction;
pathNode.turns = node->turns;
pathNode.danger = node->danger;
pathNode.coord = node->coord;
pathNode.parentIndex = candidateParentIndex;
pathNode.actionIsBlocked = false;
pathNode.layer = node->layer;
if(pathNode.specialAction)
{
auto targetNode = node->theNodeBefore ? getAINode(node->theNodeBefore) : node;
pathNode.actionIsBlocked = !pathNode.specialAction->canAct(aiNk, targetNode);
}
const int nextParentIndex = static_cast<int>(candidatePath.nodes.size());
candidatePath.nodes.push_back(pathNode);
if(isRealMovementNode(node))
candidateMasks[node->actor->hero] = node->actor->chainMask;
return nextParentIndex;
};
if(node->chainOther)
fillChainInfo(node->chainOther, path, parentIndex);
AIPathNodeInfo pathNode;
pathNode.cost = node->getCost();
pathNode.targetHero = node->actor->hero;
pathNode.chainMask = node->actor->chainMask;
pathNode.specialAction = node->specialAction;
pathNode.turns = node->turns;
pathNode.danger = node->danger;
pathNode.coord = node->coord;
pathNode.parentIndex = parentIndex;
pathNode.actionIsBlocked = false;
pathNode.layer = node->layer;
if(pathNode.specialAction)
{
auto targetNode =node->theNodeBefore ? getAINode(node->theNodeBefore) : node;
AIPath pathWithBranch = path;
auto masksWithBranch = realMoveMasks;
int parentIndexWithBranch = parentIndex;
pathNode.actionIsBlocked = !pathNode.specialAction->canAct(aiNk, targetNode);
if(!tryReconstructChainInfo(node->chainOther, pathWithBranch, parentIndexWithBranch, masksWithBranch))
return false; // Reject path because chainOther branch is conflicting for hero
auto nextParentIndex = tryAppendCurrentNode(pathWithBranch, parentIndexWithBranch, masksWithBranch);
if(!nextParentIndex)
return false; // Reject path because current node conflicts after chainOther for hero
path = std::move(pathWithBranch);
realMoveMasks = std::move(masksWithBranch);
parentIndex = *nextParentIndex;
node = getAINode(node->theNodeBefore);
continue;
}
parentIndex = path.nodes.size();
path.nodes.push_back(pathNode);
auto nextParentIndex = tryAppendCurrentNode(path, parentIndex, realMoveMasks);
if(!nextParentIndex)
return false;
parentIndex = *nextParentIndex;
node = getAINode(node->theNodeBefore);
}
return true;
}
AIPath::AIPath()
+11 -1
View File
@@ -288,7 +288,17 @@ public:
}
void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);
void fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const;
using RealMoveMasksByHero = std::map<const CGHeroInstance *, uint64_t>;
STRONG_INLINE bool isRealMovementNode(const AIPathNode * node) const
{
return node && node->actor && node->actor->hero && node->coord != node->actor->hero->visitablePos();
}
// Reconstructs an AIPath by walking theNodeBefore / chainOther, appending branch nodes first and linking them via parentIndex
// Returns false when reconstruction would assign conflicting real-move chainMasks to the same hero
bool tryReconstructChainInfo(const AIPathNode * node, AIPath & path, int & parentIndex, RealMoveMasksByHero & realMoveMasks) const;
template<typename Fn>
void iterateValidNodes(const int3 & pos, EPathfindingLayer layer, Fn fn)