From 01cddceccaa183ada2ee0c97d01b08bd392ccd9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zaremba?= Date: Tue, 5 May 2026 10:06:02 +0200 Subject: [PATCH] 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. --- AI/Nullkiller2/Pathfinding/AINodeStorage.cpp | 97 +++++++++++++++----- AI/Nullkiller2/Pathfinding/AINodeStorage.h | 12 ++- 2 files changed, 83 insertions(+), 26 deletions(-) diff --git a/AI/Nullkiller2/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller2/Pathfinding/AINodeStorage.cpp index 2291c2f79..0c1197056 100644 --- a/AI/Nullkiller2/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller2/Pathfinding/AINodeStorage.cpp @@ -1442,8 +1442,18 @@ void AINodeStorage::calculateChainInfo(std::vector & 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 & 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 + { + 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(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() diff --git a/AI/Nullkiller2/Pathfinding/AINodeStorage.h b/AI/Nullkiller2/Pathfinding/AINodeStorage.h index ca9842feb..2a89d6b14 100644 --- a/AI/Nullkiller2/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller2/Pathfinding/AINodeStorage.h @@ -288,7 +288,17 @@ public: } void calculateTownPortalTeleportations(std::vector & neighbours); - void fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const; + + using RealMoveMasksByHero = std::map; + + 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 void iterateValidNodes(const int3 & pos, EPathfindingLayer layer, Fn fn)