2025-08-13 14:41:15 +02:00
/*
* ExecuteHeroChain . cpp , part of VCMI engine
*
* Authors : listed in file AUTHORS in main folder
*
* License : GNU General Public License v2 .0 or later
* Full text of license available in license . txt file , in main folder
*
*/
# include "StdInc.h"
# include "ExecuteHeroChain.h"
# include "../AIGateway.h"
2025-08-13 17:05:30 +02:00
# include "../Engine/Nullkiller.h"
2025-08-13 14:41:15 +02:00
2025-08-13 17:16:27 +02:00
namespace NK2AI
2025-08-13 14:41:15 +02:00
{
using namespace Goals ;
ExecuteHeroChain : : ExecuteHeroChain ( const AIPath & path , const CGObjectInstance * obj )
: ElementarGoal ( Goals : : EXECUTE_HERO_CHAIN ) , chainPath ( path ) , closestWayRatio ( 1 )
{
hero = path . targetHero ;
tile = path . targetTile ( ) ;
closestWayRatio = 1 ;
if ( obj )
{
objid = obj - > id . getNum ( ) ;
# if NKAI_TRACE_LEVEL >= 1
targetName = obj - > getObjectName ( ) + tile . toString ( ) ;
# else
targetName = obj - > getTypeName ( ) + tile . toString ( ) ;
# endif
}
else
{
targetName = " tile " + tile . toString ( ) ;
}
}
bool ExecuteHeroChain : : operator = = ( const ExecuteHeroChain & other ) const
{
return tile = = other . tile
& & chainPath . targetHero = = other . chainPath . targetHero
& & chainPath . nodes . size ( ) = = other . chainPath . nodes . size ( )
& & 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 . getNum ( ) )
return true ;
for ( auto & node : chainPath . nodes )
{
if ( node . targetHero & & node . targetHero - > id = = id )
return true ;
}
return false ;
}
2025-08-15 17:47:21 +02:00
void ExecuteHeroChain : : accept ( AIGateway * aiGw )
2025-08-13 14:41:15 +02:00
{
logAi - > debug ( " Executing hero chain towards %s. Path %s " , targetName , chainPath . toString ( ) ) ;
2025-08-15 17:47:21 +02:00
aiGw - > nullkiller - > setActive ( chainPath . targetHero , tile ) ;
aiGw - > nullkiller - > setTargetObject ( objid ) ;
aiGw - > nullkiller - > objectClusterizer - > reset ( ) ;
2025-08-13 14:41:15 +02:00
2025-08-16 20:17:18 +02:00
auto targetObject = aiGw - > cc - > getObj ( static_cast < ObjectInstanceID > ( objid ) , false ) ;
2025-08-13 14:41:15 +02:00
if ( chainPath . turn ( ) = = 0 & & targetObject & & targetObject - > ID = = Obj : : TOWN )
{
2025-08-16 20:17:18 +02:00
auto relations = aiGw - > cc - > getPlayerRelations ( aiGw - > playerID , targetObject - > getOwner ( ) ) ;
2025-08-13 14:41:15 +02:00
if ( relations = = PlayerRelations : : ENEMIES )
{
2025-08-15 17:47:21 +02:00
aiGw - > nullkiller - > armyFormation - > rearrangeArmyForSiege (
2025-08-13 14:41:15 +02:00
dynamic_cast < const CGTownInstance * > ( targetObject ) ,
chainPath . targetHero ) ;
}
}
std : : set < int > blockedIndexes ;
for ( int i = chainPath . nodes . size ( ) - 1 ; i > = 0 ; i - - )
{
auto * node = & chainPath . nodes [ i ] ;
const CGHeroInstance * hero = node - > targetHero ;
HeroPtr heroPtr = hero ;
if ( ! heroPtr . validAndSet ( ) )
{
logAi - > error ( " Hero %s was lost. Exit hero chain. " , heroPtr . name ( ) ) ;
return ;
}
if ( node - > parentIndex > = i )
{
logAi - > error ( " Invalid parentIndex while executing node " + node - > coord . toString ( ) ) ;
}
if ( vstd : : contains ( blockedIndexes , i ) )
{
blockedIndexes . insert ( node - > parentIndex ) ;
2025-08-15 17:47:21 +02:00
aiGw - > nullkiller - > lockHero ( hero , HeroLockedReason : : HERO_CHAIN ) ;
2025-08-13 14:41:15 +02:00
continue ;
}
logAi - > debug ( " Executing chain node %d. Moving hero %s to %s " , i , hero - > getNameTranslated ( ) , node - > coord . toString ( ) ) ;
try
{
if ( hero - > movementPointsRemaining ( ) > 0 )
{
2025-08-15 17:47:21 +02:00
aiGw - > nullkiller - > setActive ( hero , node - > coord ) ;
2025-08-13 14:41:15 +02:00
if ( node - > specialAction )
{
if ( node - > actionIsBlocked )
{
throw cannotFulfillGoalException ( " Path is nondeterministic. " ) ;
}
2025-08-15 17:47:21 +02:00
node - > specialAction - > execute ( aiGw , hero ) ;
2025-08-13 14:41:15 +02:00
if ( ! heroPtr . validAndSet ( ) )
{
logAi - > error ( " Hero %s was lost trying to execute special action. Exit hero chain. " , heroPtr . name ( ) ) ;
return ;
}
}
2025-08-15 17:47:21 +02:00
else if ( i > 0 & & aiGw - > nullkiller - > isObjectGraphAllowed ( ) )
2025-08-13 14:41:15 +02:00
{
auto chainMask = i < chainPath . nodes . size ( ) - 1 ? chainPath . nodes [ i + 1 ] . chainMask : node - > chainMask ;
for ( auto j = i - 1 ; j > = 0 ; j - - )
{
auto & nextNode = chainPath . nodes [ j ] ;
if ( nextNode . specialAction | | nextNode . chainMask ! = chainMask )
break ;
2025-08-15 17:47:21 +02:00
auto targetNode = aiGw - > nullkiller - > getPathsInfo ( hero ) - > getPathInfo ( nextNode . coord ) ;
2025-08-13 14:41:15 +02:00
if ( ! targetNode - > reachable ( )
| | targetNode - > getCost ( ) > nextNode . cost )
break ;
i = j ;
node = & nextNode ;
if ( targetNode - > action = = EPathNodeAction : : BATTLE | | targetNode - > action = = EPathNodeAction : : TELEPORT_BATTLE )
break ;
}
}
if ( node - > turns = = 0 & & node - > coord ! = hero - > visitablePos ( ) )
{
2025-08-15 17:47:21 +02:00
auto targetNode = aiGw - > nullkiller - > getPathsInfo ( hero ) - > getPathInfo ( node - > coord ) ;
2025-08-13 14:41:15 +02:00
if ( targetNode - > accessible = = EPathAccessibility : : NOT_SET
| | targetNode - > accessible = = EPathAccessibility : : BLOCKED
| | targetNode - > accessible = = EPathAccessibility : : FLYABLE
| | targetNode - > turns ! = 0 )
{
logAi - > error (
" Unable to complete chain. Expected hero %s to arrive to %s in 0 turns but he cannot do this " ,
hero - > getNameTranslated ( ) ,
node - > coord . toString ( ) ) ;
return ;
}
}
2025-08-15 17:47:21 +02:00
auto findWhirlpool = [ & aiGw ] ( const int3 & pos ) - > ObjectInstanceID
2025-08-13 14:41:15 +02:00
{
2025-08-16 20:17:18 +02:00
auto objs = aiGw - > cc - > getVisitableObjs ( pos ) ;
2025-08-13 14:41:15 +02:00
auto whirlpool = std : : find_if ( objs . begin ( ) , objs . end ( ) , [ ] ( const CGObjectInstance * o ) - > bool
{
return o - > ID = = Obj : : WHIRLPOOL ;
} ) ;
return whirlpool ! = objs . end ( ) ? dynamic_cast < const CGWhirlpool * > ( * whirlpool ) - > id : ObjectInstanceID ( - 1 ) ;
} ;
auto sourceWhirlpool = findWhirlpool ( hero - > visitablePos ( ) ) ;
auto targetWhirlpool = findWhirlpool ( node - > coord ) ;
if ( i ! = chainPath . nodes . size ( ) - 1 & & sourceWhirlpool . hasValue ( ) & & sourceWhirlpool = = targetWhirlpool )
{
logAi - > trace ( " AI exited whirlpool at %s but expected at %s " , hero - > visitablePos ( ) . toString ( ) , node - > coord . toString ( ) ) ;
continue ;
}
if ( hero - > movementPointsRemaining ( ) )
{
try
{
2025-08-15 17:47:21 +02:00
if ( moveHeroToTile ( aiGw , hero , node - > coord ) )
2025-08-13 14:41:15 +02:00
{
continue ;
}
}
catch ( const cannotFulfillGoalException & )
{
if ( ! heroPtr . validAndSet ( ) )
{
logAi - > error ( " Hero %s was lost. Exit hero chain. " , heroPtr . name ( ) ) ;
return ;
}
if ( hero - > movementPointsRemaining ( ) > 0 )
{
CGPath path ;
2025-08-15 17:47:21 +02:00
bool isOk = aiGw - > nullkiller - > getPathsInfo ( hero ) - > getPath ( path , node - > coord ) ;
2025-08-13 14:41:15 +02:00
if ( isOk & & path . nodes . back ( ) . turns > 0 )
{
logAi - > warn ( " Hero %s has %d mp which is not enough to continue his way towards %s. " , hero - > getNameTranslated ( ) , hero - > movementPointsRemaining ( ) , node - > coord . toString ( ) ) ;
2025-08-15 17:47:21 +02:00
aiGw - > nullkiller - > lockHero ( hero , HeroLockedReason : : HERO_CHAIN ) ;
2025-08-13 14:41:15 +02:00
return ;
}
}
throw ;
}
}
}
if ( node - > coord = = hero - > visitablePos ( ) )
continue ;
if ( node - > turns = = 0 )
{
logAi - > error (
" Unable to complete chain. Expected hero %s to arrive to %s but he is at %s " ,
hero - > getNameTranslated ( ) ,
node - > coord . toString ( ) ,
hero - > visitablePos ( ) . toString ( ) ) ;
return ;
}
// no exception means we were not able to reach the tile
2025-08-15 17:47:21 +02:00
aiGw - > nullkiller - > lockHero ( hero , HeroLockedReason : : HERO_CHAIN ) ;
2025-08-13 14:41:15 +02:00
blockedIndexes . insert ( node - > parentIndex ) ;
}
catch ( const goalFulfilledException & )
{
if ( ! heroPtr . validAndSet ( ) )
{
logAi - > debug ( " Hero %s was killed while attempting to reach %s " , heroPtr . name ( ) , node - > coord . toString ( ) ) ;
return ;
}
}
}
}
std : : string ExecuteHeroChain : : toString ( ) const
{
# if NKAI_TRACE_LEVEL >= 1
return " ExecuteHeroChain " + targetName + " by " + chainPath . toString ( ) ;
# else
return " ExecuteHeroChain " + targetName + " by " + chainPath . targetHero - > getNameTranslated ( ) ;
# endif
}
2025-08-15 18:30:28 +02:00
bool ExecuteHeroChain : : moveHeroToTile ( AIGateway * aiGw , const CGHeroInstance * hero , const int3 & tile )
2025-08-13 14:41:15 +02:00
{
2025-08-16 20:17:18 +02:00
if ( tile = = hero - > visitablePos ( ) & & aiGw - > cc - > getVisitableObjs ( hero - > visitablePos ( ) ) . size ( ) < 2 )
2025-08-13 14:41:15 +02:00
{
logAi - > warn ( " Why do I want to move hero %s to tile %s? Already standing on that tile! " , hero - > getNameTranslated ( ) , tile . toString ( ) ) ;
return true ;
}
2025-08-15 18:30:28 +02:00
return aiGw - > moveHeroToTile ( tile , hero ) ;
2025-08-13 14:41:15 +02:00
}
}