Browse Source

Merge remote-tracking branch 'origin/develop' into dimension-door-changes

Dydzio 1 year ago
parent
commit
048fb1867d
100 changed files with 1601 additions and 503 deletions
  1. 4 4
      .github/workflows/github.yml
  2. 3 7
      AI/BattleAI/AttackPossibility.cpp
  3. 0 1
      AI/BattleAI/AttackPossibility.h
  4. 0 2
      AI/BattleAI/BattleAI.cpp
  5. 1 1
      AI/BattleAI/BattleExchangeVariant.cpp
  6. 3 5
      AI/BattleAI/CMakeLists.txt
  7. 0 23
      AI/BattleAI/common.cpp
  8. 0 26
      AI/BattleAI/common.h
  9. 1 1
      AI/BattleAI/main.cpp
  10. 2 2
      AI/EmptyAI/CEmptyAI.h
  11. 3 3
      AI/EmptyAI/CMakeLists.txt
  12. 0 1
      AI/EmptyAI/main.cpp
  13. 11 5
      AI/Nullkiller/AIGateway.cpp
  14. 1 1
      AI/Nullkiller/AIUtility.cpp
  15. 2 2
      AI/Nullkiller/AIUtility.h
  16. 5 0
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  17. 1 0
      AI/Nullkiller/Analyzers/BuildAnalyzer.h
  18. 10 12
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  19. 1 1
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h
  20. 4 3
      AI/Nullkiller/Analyzers/HeroManager.cpp
  21. 3 3
      AI/Nullkiller/Analyzers/HeroManager.h
  22. 107 99
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  23. 5 0
      AI/Nullkiller/Analyzers/ObjectClusterizer.h
  24. 2 2
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  25. 3 3
      AI/Nullkiller/Behaviors/BuildingBehavior.h
  26. 1 2
      AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp
  27. 3 3
      AI/Nullkiller/Behaviors/BuyArmyBehavior.h
  28. 17 2
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  29. 3 3
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h
  30. 1 1
      AI/Nullkiller/Behaviors/ClusterBehavior.cpp
  31. 3 3
      AI/Nullkiller/Behaviors/ClusterBehavior.h
  32. 4 0
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  33. 3 3
      AI/Nullkiller/Behaviors/DefenceBehavior.h
  34. 3 3
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  35. 3 3
      AI/Nullkiller/Behaviors/GatherArmyBehavior.h
  36. 1 2
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  37. 3 3
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.h
  38. 3 3
      AI/Nullkiller/Behaviors/StartupBehavior.h
  39. 3 3
      AI/Nullkiller/Behaviors/StayAtTownBehavior.h
  40. 7 3
      AI/Nullkiller/CMakeLists.txt
  41. 46 29
      AI/Nullkiller/Engine/Nullkiller.cpp
  42. 4 1
      AI/Nullkiller/Engine/Nullkiller.h
  43. 10 10
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  44. 84 0
      AI/Nullkiller/Engine/Settings.cpp
  45. 44 0
      AI/Nullkiller/Engine/Settings.h
  46. 2 10
      AI/Nullkiller/Goals/AbstractGoal.h
  47. 8 2
      AI/Nullkiller/Goals/AdventureSpellCast.cpp
  48. 1 1
      AI/Nullkiller/Goals/AdventureSpellCast.h
  49. 1 1
      AI/Nullkiller/Goals/Build.h
  50. 1 1
      AI/Nullkiller/Goals/BuildBoat.h
  51. 2 2
      AI/Nullkiller/Goals/BuildThis.h
  52. 3 3
      AI/Nullkiller/Goals/BuyArmy.h
  53. 5 5
      AI/Nullkiller/Goals/CGoal.h
  54. 5 5
      AI/Nullkiller/Goals/CaptureObject.h
  55. 5 5
      AI/Nullkiller/Goals/CompleteQuest.h
  56. 5 5
      AI/Nullkiller/Goals/Composition.h
  57. 1 1
      AI/Nullkiller/Goals/DigAtTile.h
  58. 1 1
      AI/Nullkiller/Goals/DismissHero.h
  59. 1 1
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h
  60. 2 2
      AI/Nullkiller/Goals/ExecuteHeroChain.h
  61. 1 1
      AI/Nullkiller/Goals/GatherArmy.h
  62. 3 3
      AI/Nullkiller/Goals/Invalid.h
  63. 2 2
      AI/Nullkiller/Goals/RecruitHero.h
  64. 1 1
      AI/Nullkiller/Goals/SaveResources.h
  65. 2 2
      AI/Nullkiller/Goals/StayAtTown.h
  66. 1 1
      AI/Nullkiller/Goals/Trade.h
  67. 2 2
      AI/Nullkiller/Markers/ArmyUpgrade.h
  68. 2 2
      AI/Nullkiller/Markers/DefendTown.h
  69. 2 2
      AI/Nullkiller/Markers/HeroExchange.h
  70. 2 2
      AI/Nullkiller/Markers/UnlockCluster.h
  71. 8 7
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  72. 4 12
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  73. 47 4
      AI/Nullkiller/Pathfinding/AIPathfinder.cpp
  74. 13 3
      AI/Nullkiller/Pathfinding/AIPathfinder.h
  75. 13 11
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
  76. 3 2
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.h
  77. 3 3
      AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h
  78. 2 2
      AI/Nullkiller/Pathfinding/Actions/BattleAction.h
  79. 10 10
      AI/Nullkiller/Pathfinding/Actions/BoatActions.h
  80. 8 3
      AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp
  81. 6 4
      AI/Nullkiller/Pathfinding/Actions/QuestAction.h
  82. 2 2
      AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h
  83. 1 1
      AI/Nullkiller/Pathfinding/Actors.cpp
  84. 18 18
      AI/Nullkiller/Pathfinding/Actors.h
  85. 671 0
      AI/Nullkiller/Pathfinding/ObjectGraph.cpp
  86. 192 0
      AI/Nullkiller/Pathfinding/ObjectGraph.h
  87. 1 1
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  88. 26 2
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
  89. 5 1
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.h
  90. 25 8
      AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp
  91. 2 1
      AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.h
  92. 1 1
      AI/Nullkiller/main.cpp
  93. 3 3
      AI/StupidAI/CMakeLists.txt
  94. 1 1
      AI/StupidAI/main.cpp
  95. 1 1
      AI/VCAI/BuildingManager.cpp
  96. 3 3
      AI/VCAI/CMakeLists.txt
  97. 46 45
      AI/VCAI/Goals/AbstractGoal.cpp
  98. 5 5
      AI/VCAI/Goals/AbstractGoal.h
  99. 1 1
      AI/VCAI/Goals/AdventureSpellCast.h
  100. 1 1
      AI/VCAI/Goals/Build.h

+ 4 - 4
.github/workflows/github.yml

@@ -124,7 +124,7 @@ jobs:
       # also, running it on multiple presets is redundant and slightly increases already long CI built times
       if: ${{ startsWith(matrix.preset, 'linux-clang-test') }}
       run: |
-        pip3 install json5 jstyleson
+        pip3 install jstyleson
         python3 CI/linux-qt6/validate_json.py
 
     - name: Dependencies
@@ -134,7 +134,7 @@ jobs:
 
     # ensure the ccache for each PR is separate so they don't interfere with each other
     # fall back to ccache of the vcmi/vcmi repo if no PR-specific ccache is found
-    - name: Ccache for PRs
+    - name: ccache for PRs
       uses: hendrikmuhs/[email protected]
       if: ${{ github.event.number != '' }}
       with:
@@ -146,9 +146,9 @@ jobs:
         max-size: "5G"
         verbose: 2
 
-    - name: Ccache for vcmi/vcmi's develop branch
+    - name: ccache for everything but PRs
       uses: hendrikmuhs/[email protected]
-      if: ${{ github.event.number == '' && github.ref == 'refs/heads/develop' }}
+      if: ${{ (github.repository == 'vcmi/vcmi' && github.event.number == '' && github.ref == 'refs/heads/develop') ||  github.repository != 'vcmi/vcmi' }}
       with:
         key: ${{ matrix.preset }}-no-PR
         restore-keys: |

+ 3 - 7
AI/BattleAI/AttackPossibility.cpp

@@ -62,16 +62,12 @@ void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int sid
 
 int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)
 {
-	auto damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
+	bool wasComputedBefore = damageCache[attacker->unitId()].count(defender->unitId());
 
-	if(damage == 0)
-	{
+	if (!wasComputedBefore)
 		cacheDamage(attacker, defender, hb);
 
-		damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
-	}
-
-	return static_cast<int64_t>(damage);
+	return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
 }
 
 int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)

+ 0 - 1
AI/BattleAI/AttackPossibility.h

@@ -10,7 +10,6 @@
 #pragma once
 #include "../../lib/battle/CUnitState.h"
 #include "../../CCallback.h"
-#include "common.h"
 #include "StackWithBonuses.h"
 
 #define BATTLE_TRACE_LEVEL 0

+ 0 - 2
AI/BattleAI/BattleAI.cpp

@@ -49,7 +49,6 @@ CBattleAI::~CBattleAI()
 
 void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
 {
-	setCbc(CB);
 	env = ENV;
 	cb = CB;
 	playerID = *CB->getPlayerID();
@@ -121,7 +120,6 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
 	};
 
 	BattleAction result = BattleAction::makeDefend(stack);
-	setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
 
 	auto start = std::chrono::high_resolution_clock::now();
 

+ 1 - 1
AI/BattleAI/BattleExchangeVariant.cpp

@@ -270,7 +270,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 	{
 		float score = evaluateExchange(ap, 0, targets, damageCache, hb);
 
-		if(score > result.score || (score == result.score && result.wait))
+		if(score > result.score || (vstd::isAlmostEqual(score, result.score) && result.wait))
 		{
 			result.score = score;
 			result.bestAttack = ap;

+ 3 - 5
AI/BattleAI/CMakeLists.txt

@@ -2,7 +2,6 @@ set(battleAI_SRCS
 		AttackPossibility.cpp
 		BattleAI.cpp
 		BattleEvaluator.cpp
-		common.cpp
 		EnemyInfo.cpp
 		PossibleSpellcast.cpp
 		PotentialTargets.cpp
@@ -17,7 +16,6 @@ set(battleAI_HEADERS
 		AttackPossibility.h
 		BattleAI.h
 		BattleEvaluator.h
-		common.h
 		EnemyInfo.h
 		PotentialTargets.h
 		PossibleSpellcast.h
@@ -26,12 +24,12 @@ set(battleAI_HEADERS
 		BattleExchangeVariant.h
 )
 
-if(NOT ENABLE_STATIC_AI_LIBS)
+if(NOT ENABLE_STATIC_LIBS)
 	list(APPEND battleAI_SRCS main.cpp StdInc.cpp)
 endif()
 assign_source_group(${battleAI_SRCS} ${battleAI_HEADERS})
 
-if(ENABLE_STATIC_AI_LIBS)
+if(ENABLE_STATIC_LIBS)
 	add_library(BattleAI STATIC ${battleAI_SRCS} ${battleAI_HEADERS})
 else()
 	add_library(BattleAI SHARED ${battleAI_SRCS} ${battleAI_HEADERS})
@@ -39,7 +37,7 @@ else()
 endif()
 
 target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET} TBB::tbb)
+target_link_libraries(BattleAI PRIVATE vcmi TBB::tbb)
 
 vcmi_set_output_dir(BattleAI "AI")
 enable_pch(BattleAI)

+ 0 - 23
AI/BattleAI/common.cpp

@@ -1,23 +0,0 @@
-/*
- * common.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 "common.h"
-
-std::shared_ptr<CBattleCallback> cbc;
-
-void setCbc(std::shared_ptr<CBattleCallback> cb)
-{
-	cbc = cb;
-}
-
-std::shared_ptr<CBattleCallback> getCbc()
-{
-	return cbc;
-}

+ 0 - 26
AI/BattleAI/common.h

@@ -1,26 +0,0 @@
-/*
- * common.h, 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
- *
- */
-#pragma once
-
-class CBattleCallback;
-
-template<typename Key, typename Val, typename Val2>
-const Val getValOr(const std::map<Key, Val> &Map, const Key &key, const Val2 defaultValue)
-{
-	//returning references here won't work: defaultValue must be converted into Val, creating temporary
-	auto i = Map.find(key);
-	if(i != Map.end())
-		return i->second;
-	else
-		return defaultValue;
-}
-
-void setCbc(std::shared_ptr<CBattleCallback> cb);
-std::shared_ptr<CBattleCallback> getCbc();

+ 1 - 1
AI/BattleAI/main.cpp

@@ -15,7 +15,7 @@
 #define strcpy_s(a, b, c) strncpy(a, c, b)
 #endif
 
-static const char *g_cszAiName = "Battle AI";
+static const char * const g_cszAiName = "Battle AI";
 
 extern "C" DLL_EXPORT int GetGlobalAiVersion()
 {

+ 2 - 2
AI/EmptyAI/CEmptyAI.h

@@ -19,8 +19,8 @@ class CEmptyAI : public CGlobalAI
 	std::shared_ptr<CCallback> cb;
 
 public:
-	virtual void saveGame(BinarySerializer & h) override;
-	virtual void loadGame(BinaryDeserializer & h) override;
+	void saveGame(BinarySerializer & h) override;
+	void loadGame(BinaryDeserializer & h) override;
 
 	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
 	void yourTurn(QueryID queryID) override;

+ 3 - 3
AI/EmptyAI/CMakeLists.txt

@@ -8,12 +8,12 @@ set(emptyAI_HEADERS
 		CEmptyAI.h
 )
 
-if(NOT ENABLE_STATIC_AI_LIBS)
+if(NOT ENABLE_STATIC_LIBS)
 	list(APPEND emptyAI_SRCS main.cpp StdInc.cpp)
 endif()
 assign_source_group(${emptyAI_SRCS} ${emptyAI_HEADERS})
 
-if(ENABLE_STATIC_AI_LIBS)
+if(ENABLE_STATIC_LIBS)
 	add_library(EmptyAI STATIC ${emptyAI_SRCS} ${emptyAI_HEADERS})
 else()
 	add_library(EmptyAI SHARED ${emptyAI_SRCS} ${emptyAI_HEADERS})
@@ -21,7 +21,7 @@ else()
 endif()
 
 target_include_directories(EmptyAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(EmptyAI PRIVATE ${VCMI_LIB_TARGET})
+target_link_libraries(EmptyAI PRIVATE vcmi)
 
 vcmi_set_output_dir(EmptyAI "AI")
 enable_pch(EmptyAI)

+ 0 - 1
AI/EmptyAI/main.cpp

@@ -11,7 +11,6 @@
 
 #include "CEmptyAI.h"
 
-std::set<CGlobalAI*> ais;
 extern "C" DLL_EXPORT int GetGlobalAiVersion()
 {
 	return AI_INTERFACE_VER;

+ 11 - 5
AI/Nullkiller/AIGateway.cpp

@@ -374,6 +374,11 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor &
 
 	nullkiller->memory->removeFromMemory(obj);
 
+	if(nullkiller->baseGraph && nullkiller->settings->isObjectGraphAllowed())
+	{
+		nullkiller->baseGraph->removeObject(obj);
+	}
+
 	if(obj->ID == Obj::HERO && obj->tempOwner == playerID)
 	{
 		lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion
@@ -632,7 +637,8 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 				auto topObj = objects.front()->id == hero->id ? objects.back() : objects.front();
 				auto objType = topObj->ID; // top object should be our hero
 				auto goalObjectID = nullkiller->getTargetObject();
-				auto ratio = (float)nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()) / (float)hero->getTotalStrength();
+				auto danger = nullkiller->dangerEvaluator->evaluateDanger(target, hero.get());
+				auto ratio = static_cast<float>(danger) / hero->getTotalStrength();
 
 				answer = topObj->id == goalObjectID; // no if we do not aim to visit this object
 				logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name, ratio);
@@ -648,7 +654,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 				}
 				else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
 				{
-					bool dangerUnknown = ratio == 0;
+					bool dangerUnknown = danger == 0;
 					bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
 
 					answer = !dangerUnknown && !dangerTooHigh;
@@ -676,9 +682,9 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 					&& components.size() == 2
 					&& components.front().type == ComponentType::RESOURCE
 					&& (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN
-						|| nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE))
+						|| nullkiller->buildAnalyzer->isGoldPreasureHigh()))
 				{
-					sel = 1; // for now lets pick gold from a chest.
+					sel = 1;
 				}
 		}
 
@@ -1406,7 +1412,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
 	int accquiredResources = 0;
 	if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false))
 	{
-		if(const IMarket * m = IMarket::castFrom(obj, false))
+		if(const auto * m = dynamic_cast<const IMarket*>(obj))
 		{
 			auto freeRes = cb->getResourceAmount(); //trade only resources which are not reserved
 			for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++)

+ 1 - 1
AI/Nullkiller/AIUtility.cpp

@@ -437,7 +437,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
 	case Obj::MAGIC_WELL:
 		return h->mana < h->manaLimit();
 	case Obj::PRISON:
-		return ai->cb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
+		return !ai->heroManager->heroCapReached();
 	case Obj::TAVERN:
 	case Obj::EYE_OF_MAGI:
 	case Obj::BOAT:

+ 2 - 2
AI/Nullkiller/AIUtility.h

@@ -185,8 +185,8 @@ void foreach_tile_pos(const Func & foo)
 	}
 }
 
-template<class Func>
-void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retrieval of thread-specific pointer
+template<class Func, class TCallback>
+void foreach_tile_pos(TCallback * cbp, const Func & foo) // avoid costly retrieval of thread-specific pointer
 {
 	int3 mapSize = cbp->getMapSize();
 	for(int z = 0; z < mapSize.z; z++)

+ 5 - 0
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -120,6 +120,11 @@ TResources BuildAnalyzer::getTotalResourcesRequired() const
 	return result;
 }
 
+bool BuildAnalyzer::isGoldPreasureHigh() const
+{
+	return goldPreasure > ai->settings->getMaxGoldPreasure();
+}
+
 void BuildAnalyzer::update()
 {
 	logAi->trace("Start analysing build");

+ 1 - 0
AI/Nullkiller/Analyzers/BuildAnalyzer.h

@@ -96,6 +96,7 @@ public:
 	const std::vector<TownDevelopmentInfo> & getDevelopmentInfo() const { return developmentInfos; }
 	TResources getDailyIncome() const { return dailyIncome; }
 	float getGoldPreasure() const { return goldPreasure; }
+	bool isGoldPreasureHigh() const;
 	bool hasAnyBuilding(int32_t alignment, BuildingID bid) const;
 
 private:

+ 10 - 12
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -16,7 +16,7 @@
 namespace NKAI
 {
 
-HitMapInfo HitMapInfo::NoThreat;
+const HitMapInfo HitMapInfo::NoThreat;
 
 double HitMapInfo::value() const
 {
@@ -75,8 +75,7 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 		PathfinderSettings ps;
 
-		ps.mainTurnDistanceLimit = 10;
-		ps.scoutTurnDistanceLimit = 10;
+		ps.scoutTurnDistanceLimit = ps.mainTurnDistanceLimit = ai->settings->getMainHeroTurnDistanceLimit();
 		ps.useHeroChain = false;
 
 		ai->pathfinder->updatePaths(pair.second, ps);
@@ -158,15 +157,13 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 	if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z)
 		hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]);
 
-	std::map<const CGHeroInstance *, HeroRole> townHeroes;
+	std::vector<std::unique_ptr<CGHeroInstance>> temporaryHeroes;
 	std::map<const CGHeroInstance *, const CGTownInstance *> heroTownMap;
-	PathfinderSettings pathfinderSettings;
-
-	pathfinderSettings.mainTurnDistanceLimit = 5;
+	std::map<const CGHeroInstance *, HeroRole> townHeroes;
 
 	auto addTownHero = [&](const CGTownInstance * town)
 	{
-			auto townHero = new CGHeroInstance(town->cb);
+			auto townHero = temporaryHeroes.emplace_back(std::make_unique<CGHeroInstance>(town->cb)).get();
 			CRandomGenerator rng;
 			auto visitablePos = town->visitablePos();
 			
@@ -192,7 +189,10 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 		addTownHero(town);
 	}
 
-	ai->pathfinder->updatePaths(townHeroes, PathfinderSettings());
+	PathfinderSettings ps;
+	ps.mainTurnDistanceLimit = ps.scoutTurnDistanceLimit = ai->settings->getMainHeroTurnDistanceLimit();
+
+	ai->pathfinder->updatePaths(townHeroes, ps);
 
 	pforeachTilePos(mapSize, [&](const int3 & pos)
 		{
@@ -226,7 +226,7 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 				}
 			}
 
-			if(ourDistance == enemyDistance)
+			if(vstd::isAlmostEqual(ourDistance, enemyDistance))
 			{
 				hitMap[pos.x][pos.y][pos.z].closestTown = nullptr;
 			}
@@ -285,8 +285,6 @@ const HitMapNode & DangerHitMapAnalyzer::getTileThreat(const int3 & tile) const
 	return hitMap[tile.x][tile.y][tile.z];
 }
 
-const std::set<const CGObjectInstance *> empty = {};
-
 std::set<const CGObjectInstance *> DangerHitMapAnalyzer::getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const
 {
 	std::set<const CGObjectInstance *> result;

+ 1 - 1
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h

@@ -18,7 +18,7 @@ struct AIPath;
 
 struct HitMapInfo
 {
-	static HitMapInfo NoThreat;
+	static const HitMapInfo NoThreat;
 
 	uint64_t danger;
 	uint8_t turn;

+ 4 - 3
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -17,7 +17,7 @@
 namespace NKAI
 {
 
-SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluator(
+const SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluator(
 	{
 		std::make_shared<SecondarySkillScoreMap>(
 			std::map<SecondarySkill, float>
@@ -46,7 +46,7 @@ SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluato
 		std::make_shared<AtLeastOneMagicRule>()
 	});
 
-SecondarySkillEvaluator HeroManager::scountSkillsScores = SecondarySkillEvaluator(
+const SecondarySkillEvaluator HeroManager::scountSkillsScores = SecondarySkillEvaluator(
 	{
 		std::make_shared<SecondarySkillScoreMap>(
 			std::map<SecondarySkill, float>
@@ -187,6 +187,7 @@ bool HeroManager::heroCapReached() const
 	int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
 
 	return heroCount >= ALLOWED_ROAMING_HEROES
+		|| heroCount >= ai->settings->getMaxRoamingHeroes()
 		|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)
 		|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
 }
@@ -332,7 +333,7 @@ void WisdomRule::evaluateScore(const CGHeroInstance * hero, SecondarySkill skill
 		score += 1.5;
 }
 
-std::vector<SecondarySkill> AtLeastOneMagicRule::magicSchools = {
+const std::vector<SecondarySkill> AtLeastOneMagicRule::magicSchools = {
 	SecondarySkill::AIR_MAGIC,
 	SecondarySkill::EARTH_MAGIC,
 	SecondarySkill::FIRE_MAGIC,

+ 3 - 3
AI/Nullkiller/Analyzers/HeroManager.h

@@ -58,8 +58,8 @@ public:
 class DLL_EXPORT HeroManager : public IHeroManager
 {
 private:
-	static SecondarySkillEvaluator wariorSkillsScores;
-	static SecondarySkillEvaluator scountSkillsScores;
+	static const SecondarySkillEvaluator wariorSkillsScores;
+	static const SecondarySkillEvaluator scountSkillsScores;
 
 	CCallback * cb; //this is enough, but we downcast from CCallback
 	const Nullkiller * ai;
@@ -114,7 +114,7 @@ public:
 class AtLeastOneMagicRule : public ISecondarySkillRule
 {
 private:
-	static std::vector<SecondarySkill> magicSchools;
+	static const std::vector<SecondarySkill> magicSchools;
 
 public:
 	void evaluateScore(const CGHeroInstance * hero, SecondarySkill skill, float & score) const override;

+ 107 - 99
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -189,15 +189,7 @@ bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const
 		return true; //all of the following is met
 }
 
-void ObjectClusterizer::clusterize()
-{
-	auto start = std::chrono::high_resolution_clock::now();
-
-	nearObjects.reset();
-	farObjects.reset();
-	blockedObjects.clear();
-
-	Obj ignoreObjects[] = {
+Obj ObjectClusterizer::IgnoredObjectTypes[] = {
 		Obj::BOAT,
 		Obj::EYE_OF_MAGI,
 		Obj::MONOLITH_ONE_WAY_ENTRANCE,
@@ -216,7 +208,15 @@ void ObjectClusterizer::clusterize()
 		Obj::REDWOOD_OBSERVATORY,
 		Obj::CARTOGRAPHER,
 		Obj::PILLAR_OF_FIRE
-	};
+};
+
+void ObjectClusterizer::clusterize()
+{
+	auto start = std::chrono::high_resolution_clock::now();
+
+	nearObjects.reset();
+	farObjects.reset();
+	blockedObjects.clear();
 
 	logAi->debug("Begin object clusterization");
 
@@ -224,101 +224,107 @@ void ObjectClusterizer::clusterize()
 		ai->memory->visitableObjs.begin(),
 		ai->memory->visitableObjs.end());
 
-	parallel_for(blocked_range<size_t>(0, objs.size()), [&](const blocked_range<size_t> & r)
-	{
+#if NKAI_TRACE_LEVEL == 0
+	parallel_for(blocked_range<size_t>(0, objs.size()), [&](const blocked_range<size_t> & r) {
+#else
+	blocked_range<size_t> r(0, objs.size());
+#endif
 		auto priorityEvaluator = ai->priorityEvaluators->acquire();
 
 		for(int i = r.begin(); i != r.end(); i++)
 		{
-			auto obj = objs[i];
-
-			if(!shouldVisitObject(obj))
-			{
-#if NKAI_TRACE_LEVEL >= 2
-				logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
+			clusterizeObject(objs[i], priorityEvaluator.get());
+		}
+#if NKAI_TRACE_LEVEL == 0
+	});
 #endif
-				continue;
-			}
 
-#if NKAI_TRACE_LEVEL >= 2
-			logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
-#endif
+	logAi->trace("Near objects count: %i", nearObjects.objects.size());
+	logAi->trace("Far objects count: %i", farObjects.objects.size());
 
-			auto paths = ai->pathfinder->getPathInfo(obj->visitablePos());
+	for(auto pair : blockedObjects)
+	{
+		logAi->trace("Cluster %s %s count: %i", pair.first->getObjectName(), pair.first->visitablePos().toString(), pair.second->objects.size());
 
-			if(paths.empty())
-			{
-#if NKAI_TRACE_LEVEL >= 2
-				logAi->trace("No paths found.");
+#if NKAI_TRACE_LEVEL >= 1
+		for(auto obj : pair.second->getObjects())
+		{
+			logAi->trace("Object %s %s", obj->getObjectName(), obj->visitablePos().toString());
+		}
 #endif
-				continue;
-			}
-
-			std::sort(paths.begin(), paths.end(), [](const AIPath & p1, const AIPath & p2) -> bool
-			{
-				return p1.movementCost() < p2.movementCost();
-			});
+	}
 
-			if(vstd::contains(ignoreObjects, obj->ID))
-			{
-				farObjects.addObject(obj, paths.front(), 0);
+	logAi->trace("Clusterization complete in %ld", timeElapsed(start));
+}
 
+void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator)
+{
+	if(!shouldVisitObject(obj))
+	{
 #if NKAI_TRACE_LEVEL >= 2
-				logAi->trace("Object ignored. Moved to far objects with path %s", paths.front().toString());
+		logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
 #endif
+		return;
+	}
 
-				continue;
-			}
-
-			std::set<const CGHeroInstance *> heroesProcessed;
-
-			for(auto & path : paths)
-			{
 #if NKAI_TRACE_LEVEL >= 2
-				logAi->trace("Checking path %s", path.toString());
+	logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
 #endif
 
-				if(!shouldVisit(ai, path.targetHero, obj))
-				{
+	auto paths = ai->pathfinder->getPathInfo(obj->visitablePos(), true);
+
+	if(paths.empty())
+	{
 #if NKAI_TRACE_LEVEL >= 2
-					logAi->trace("Hero %s does not need to visit %s", path.targetHero->getObjectName(), obj->getObjectName());
+		logAi->trace("No paths found.");
 #endif
-					continue;
-				}
+		return;
+	}
 
-				if(path.nodes.size() > 1)
-				{
-					auto blocker = getBlocker(path);
+	std::sort(paths.begin(), paths.end(), [](const AIPath & p1, const AIPath & p2) -> bool
+		{
+			return p1.movementCost() < p2.movementCost();
+		});
+
+	if(vstd::contains(IgnoredObjectTypes, obj->ID))
+	{
+		farObjects.addObject(obj, paths.front(), 0);
 
-					if(blocker)
-					{
-						if(vstd::contains(heroesProcessed, path.targetHero))
-						{
 #if NKAI_TRACE_LEVEL >= 2
-							logAi->trace("Hero %s is already processed.", path.targetHero->getObjectName());
+		logAi->trace("Object ignored. Moved to far objects with path %s", paths.front().toString());
 #endif
-							continue;
-						}
 
-						heroesProcessed.insert(path.targetHero);
+		return;
+	}
 
-						float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
+	std::set<const CGHeroInstance *> heroesProcessed;
 
-						if(priority < MIN_PRIORITY)
-							continue;
+	for(auto & path : paths)
+	{
+#if NKAI_TRACE_LEVEL >= 2
+		logAi->trace("Checking path %s", path.toString());
+#endif
 
-						ClusterMap::accessor cluster;
-						blockedObjects.insert(
-							cluster,
-							ClusterMap::value_type(blocker, std::make_shared<ObjectCluster>(blocker)));
+		if(!shouldVisit(ai, path.targetHero, obj))
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Hero %s does not need to visit %s", path.targetHero->getObjectName(), obj->getObjectName());
+#endif
+			continue;
+		}
 
-						cluster->second->addObject(obj, path, priority);
+		if(path.nodes.size() > 1)
+		{
+			auto blocker = getBlocker(path);
 
+			if(blocker)
+			{
+				if(vstd::contains(heroesProcessed, path.targetHero))
+				{
 #if NKAI_TRACE_LEVEL >= 2
-						logAi->trace("Path added to cluster %s%s", blocker->getObjectName(), blocker->visitablePos().toString());
+					logAi->trace("Hero %s is already processed.", path.targetHero->getObjectName());
 #endif
-						continue;
-					}
+					continue;
 				}
 
 				heroesProcessed.insert(path.targetHero);
@@ -328,44 +334,46 @@ void ObjectClusterizer::clusterize()
 				if(priority < MIN_PRIORITY)
 					continue;
 
-				bool interestingObject = path.turn() <= 2 || priority > 0.5f;
+				ClusterMap::accessor cluster;
+				blockedObjects.insert(
+					cluster,
+					ClusterMap::value_type(blocker, std::make_shared<ObjectCluster>(blocker)));
 
-				if(interestingObject)
-				{
-					nearObjects.addObject(obj, path, priority);
-				}
-				else
-				{
-					farObjects.addObject(obj, path, priority);
-				}
+				cluster->second->addObject(obj, path, priority);
 
 #if NKAI_TRACE_LEVEL >= 2
-				logAi->trace("Path %s added to %s objects. Turn: %d, priority: %f",
-					path.toString(),
-					interestingObject ? "near" : "far",
-					path.turn(),
-					priority);
+				logAi->trace("Path added to cluster %s%s", blocker->getObjectName(), blocker->visitablePos().toString());
 #endif
+				continue;
 			}
 		}
-	});
 
-	logAi->trace("Near objects count: %i", nearObjects.objects.size());
-	logAi->trace("Far objects count: %i", farObjects.objects.size());
+		heroesProcessed.insert(path.targetHero);
 
-	for(auto pair : blockedObjects)
-	{
-		logAi->trace("Cluster %s %s count: %i", pair.first->getObjectName(), pair.first->visitablePos().toString(), pair.second->objects.size());
+		float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
 
-#if NKAI_TRACE_LEVEL >= 1
-		for(auto obj : pair.second->getObjects())
+		if(priority < MIN_PRIORITY)
+			continue;
+
+		bool interestingObject = path.turn() <= 2 || priority > 0.5f;
+
+		if(interestingObject)
 		{
-			logAi->trace("Object %s %s", obj->getObjectName(), obj->visitablePos().toString());
+			nearObjects.addObject(obj, path, priority);
 		}
+		else
+		{
+			farObjects.addObject(obj, path, priority);
+		}
+
+#if NKAI_TRACE_LEVEL >= 2
+		logAi->trace("Path %s added to %s objects. Turn: %d, priority: %f",
+			path.toString(),
+			interestingObject ? "near" : "far",
+			path.turn(),
+			priority);
 #endif
 	}
-
-	logAi->trace("Clusterization complete in %ld", timeElapsed(start));
 }
 
 }

+ 5 - 0
AI/Nullkiller/Analyzers/ObjectClusterizer.h

@@ -49,9 +49,13 @@ public:
 
 using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
 
+class PriorityEvaluator;
+
 class ObjectClusterizer
 {
 private:
+	static Obj IgnoredObjectTypes[];
+
 	ObjectCluster nearObjects;
 	ObjectCluster farObjects;
 	ClusterMap blockedObjects;
@@ -68,6 +72,7 @@ public:
 
 private:
 	bool shouldVisitObject(const CGObjectInstance * obj) const;
+	void clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator);
 };
 
 }

+ 2 - 2
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

@@ -47,13 +47,13 @@ Goals::TGoalVec BuildingBehavior::decompose() const
 		totalDevelopmentCost.toString());
 
 	auto & developmentInfos = ai->nullkiller->buildAnalyzer->getDevelopmentInfo();
-	auto goldPreasure = ai->nullkiller->buildAnalyzer->getGoldPreasure();
+	auto isGoldPreasureLow = !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh();
 
 	for(auto & developmentInfo : developmentInfos)
 	{
 		for(auto & buildingInfo : developmentInfo.toBuild)
 		{
-			if(goldPreasure < MAX_GOLD_PEASURE || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
+			if(isGoldPreasureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
 			{
 				if(buildingInfo.notEnoughRes)
 				{

+ 3 - 3
AI/Nullkiller/Behaviors/BuildingBehavior.h

@@ -25,9 +25,9 @@ namespace Goals
 		{
 		}
 
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
-		virtual bool operator==(const BuildingBehavior & other) const override
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
+		bool operator==(const BuildingBehavior & other) const override
 		{
 			return true;
 		}

+ 1 - 2
AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp

@@ -46,8 +46,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose() const
 
 		for(const CGHeroInstance * targetHero : heroes)
 		{
-			if(ai->nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE
-				&& !town->hasBuilt(BuildingID::CITY_HALL))
+			if(ai->nullkiller->buildAnalyzer->isGoldPreasureHigh()	&& !town->hasBuilt(BuildingID::CITY_HALL))
 			{
 				continue;
 			}

+ 3 - 3
AI/Nullkiller/Behaviors/BuyArmyBehavior.h

@@ -24,9 +24,9 @@ namespace Goals
 		{
 		}
 
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
-		virtual bool operator==(const BuyArmyBehavior & other) const override
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
+		bool operator==(const BuyArmyBehavior & other) const override
 		{
 			return true;
 		}

+ 17 - 2
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -73,13 +73,25 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 		}
 
 		if(objToVisit && !shouldVisit(ai->nullkiller.get(), path.targetHero, objToVisit))
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Ignore path. Hero %s should not visit obj %s", path.targetHero->getNameTranslated(), objToVisit->getObjectName());
+#endif
 			continue;
+		}
 
 		auto hero = path.targetHero;
 		auto danger = path.getTotalDanger();
 
-		if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && path.exchangeCount > 1)
+		if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
+			&& (path.getTotalDanger() == 0 || path.turn() > 0)
+			&& path.exchangeCount > 1)
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Ignore path. Hero %s is SCOUT, chain used and no danger", path.targetHero->getNameTranslated());
+#endif
 			continue;
+		}
 
 		auto firstBlockedAction = path.getFirstBlockedAction();
 		if(firstBlockedAction)
@@ -178,8 +190,11 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 #endif
 
 			const int3 pos = objToVisit->visitablePos();
+			bool useObjectGraph = ai->nullkiller->settings->isObjectGraphAllowed()
+				&& ai->nullkiller->getScanDepth() != ScanDepth::SMALL;
+
+			auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, useObjectGraph);
 
-			auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
 			std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
 			std::shared_ptr<ExecuteHeroChain> closestWay;
 					

+ 3 - 3
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h

@@ -48,8 +48,8 @@ namespace Goals
 			specificObjects = true;
 		}
 
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
 
 		CaptureObjectsBehavior & ofType(int type)
 		{
@@ -65,7 +65,7 @@ namespace Goals
 			return *this;
 		}
 
-		virtual bool operator==(const CaptureObjectsBehavior & other) const override;
+		bool operator==(const CaptureObjectsBehavior & other) const override;
 
 		static Goals::TGoalVec getVisitGoals(const std::vector<AIPath> & paths, const CGObjectInstance * objToVisit = nullptr);
 

+ 1 - 1
AI/Nullkiller/Behaviors/ClusterBehavior.cpp

@@ -42,7 +42,7 @@ Goals::TGoalVec ClusterBehavior::decompose() const
 Goals::TGoalVec ClusterBehavior::decomposeCluster(std::shared_ptr<ObjectCluster> cluster) const
 {
 	auto center = cluster->calculateCenter();
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(center->visitablePos());
+	auto paths = ai->nullkiller->pathfinder->getPathInfo(center->visitablePos(), ai->nullkiller->settings->isObjectGraphAllowed());
 	auto blockerPos = cluster->blocker->visitablePos();
 	std::vector<AIPath> blockerPaths;
 

+ 3 - 3
AI/Nullkiller/Behaviors/ClusterBehavior.h

@@ -28,10 +28,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const ClusterBehavior & other) const override
+		bool operator==(const ClusterBehavior & other) const override
 		{
 			return true;
 		}

+ 4 - 0
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -443,6 +443,10 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
 						heroToDismiss = town->garrisonHero.get();
 					}
 				}
+
+				// avoid dismissing one weak hero in order to recruit another.
+				if(heroToDismiss && heroToDismiss->getArmyStrength() + 500 > hero->getArmyStrength())
+					continue;
 			}
 			else if(ai->nullkiller->heroManager->heroCapReached())
 			{

+ 3 - 3
AI/Nullkiller/Behaviors/DefenceBehavior.h

@@ -29,10 +29,10 @@ namespace Goals
 		{
 		}
 
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const DefenceBehavior & other) const override
+		bool operator==(const DefenceBehavior & other) const override
 		{
 			return true;
 		}

+ 3 - 3
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -246,7 +246,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 	{
 		auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
 
-		if(heroRole == HeroRole::MAIN && path.turn() < SCOUT_TURN_DISTANCE_LIMIT)
+		if(heroRole == HeroRole::MAIN && path.turn() < ai->nullkiller->settings->getScoutHeroTurnDistanceLimit())
 			hasMainAround = true;
 	}
 
@@ -335,7 +335,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 			if(!upgrade.upgradeValue
 				&& armyToGetOrBuy.upgradeValue > 20000
 				&& ai->nullkiller->heroManager->canRecruitHero(town)
-				&& path.turn() < SCOUT_TURN_DISTANCE_LIMIT)
+				&& path.turn() < ai->nullkiller->settings->getScoutHeroTurnDistanceLimit())
 			{
 				for(auto hero : cb->getAvailableHeroes(town))
 				{
@@ -344,7 +344,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 
 					if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
 						&& ai->nullkiller->getFreeGold() >20000
-						&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)
+						&& !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh())
 					{
 						Composition recruitHero;
 

+ 3 - 3
AI/Nullkiller/Behaviors/GatherArmyBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const GatherArmyBehavior & other) const override
+		bool operator==(const GatherArmyBehavior & other) const override
 		{
 			return true;
 		}

+ 1 - 2
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -85,8 +85,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
 				continue;
 
 			if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1
-				|| (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000
-					&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE))
+				|| (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh()))
 			{
 				tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3)));
 			}

+ 3 - 3
AI/Nullkiller/Behaviors/RecruitHeroBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const RecruitHeroBehavior & other) const override
+		bool operator==(const RecruitHeroBehavior & other) const override
 		{
 			return true;
 		}

+ 3 - 3
AI/Nullkiller/Behaviors/StartupBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const StartupBehavior & other) const override
+		bool operator==(const StartupBehavior & other) const override
 		{
 			return true;
 		}

+ 3 - 3
AI/Nullkiller/Behaviors/StayAtTownBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const StayAtTownBehavior & other) const override
+		bool operator==(const StayAtTownBehavior & other) const override
 		{
 			return true;
 		}

+ 7 - 3
AI/Nullkiller/CMakeLists.txt

@@ -14,9 +14,11 @@ set(Nullkiller_SRCS
 		Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
 		Pathfinding/Rules/AIMovementToDestinationRule.cpp
 		Pathfinding/Rules/AIPreviousNodeRule.cpp
+		Pathfinding/ObjectGraph.cpp
 		AIUtility.cpp
 		Analyzers/ArmyManager.cpp
 		Analyzers/HeroManager.cpp
+		Engine/Settings.cpp
 		Engine/FuzzyEngines.cpp
 		Engine/FuzzyHelper.cpp
 		Engine/AIMemory.cpp
@@ -77,9 +79,11 @@ set(Nullkiller_HEADERS
 		Pathfinding/Rules/AIMovementAfterDestinationRule.h
 		Pathfinding/Rules/AIMovementToDestinationRule.h
 		Pathfinding/Rules/AIPreviousNodeRule.h
+		Pathfinding/ObjectGraph.h
 		AIUtility.h
 		Analyzers/ArmyManager.h
 		Analyzers/HeroManager.h
+		Engine/Settings.h
 		Engine/FuzzyEngines.h
 		Engine/FuzzyHelper.h
 		Engine/AIMemory.h
@@ -125,12 +129,12 @@ set(Nullkiller_HEADERS
 		AIGateway.h
 )
 
-if(NOT ENABLE_STATIC_AI_LIBS)
+if(NOT ENABLE_STATIC_LIBS)
 	list(APPEND Nullkiller_SRCS main.cpp StdInc.cpp)
 endif()
 assign_source_group(${Nullkiller_SRCS} ${Nullkiller_HEADERS})
 
-if(ENABLE_STATIC_AI_LIBS)
+if(ENABLE_STATIC_LIBS)
 	add_library(Nullkiller STATIC ${Nullkiller_SRCS} ${Nullkiller_HEADERS})
 else()
 	add_library(Nullkiller SHARED ${Nullkiller_SRCS} ${Nullkiller_HEADERS})
@@ -138,7 +142,7 @@ else()
 endif()
 
 target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(Nullkiller PUBLIC ${VCMI_LIB_TARGET} fuzzylite::fuzzylite TBB::tbb)
+target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite TBB::tbb)
 
 vcmi_set_output_dir(Nullkiller "AI")
 enable_pch(Nullkiller)

+ 46 - 29
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -27,15 +27,14 @@ namespace NKAI
 
 using namespace Goals;
 
-#if NKAI_TRACE_LEVEL >= 1
-#define MAXPASS 1000000
-#else
-#define MAXPASS 30
-#endif
+// while we play vcmieagles graph can be shared
+std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
 
 Nullkiller::Nullkiller()
+	:activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true)
 {
-	memory.reset(new AIMemory());
+	memory = std::make_unique<AIMemory>();
+	settings = std::make_unique<Settings>();
 }
 
 void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
@@ -43,6 +42,8 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
 	this->cb = cb;
 	this->playerID = playerID;
 
+	baseGraph.reset();
+
 	priorityEvaluator.reset(new PriorityEvaluator(this));
 	priorityEvaluators.reset(
 		new SharedPool<PriorityEvaluator>(
@@ -123,6 +124,12 @@ void Nullkiller::resetAiState()
 	lockedHeroes.clear();
 	dangerHitMap->reset();
 	useHeroChain = true;
+
+	if(!baseGraph && ai->nullkiller->settings->isObjectGraphAllowed())
+	{
+		baseGraph = std::make_unique<ObjectGraph>();
+		baseGraph->updateGraph(this);
+	}
 }
 
 void Nullkiller::updateAiState(int pass, bool fast)
@@ -163,21 +170,27 @@ void Nullkiller::updateAiState(int pass, bool fast)
 
 		PathfinderSettings cfg;
 		cfg.useHeroChain = useHeroChain;
+		cfg.allowBypassObjects = true;
 
-		if(scanDepth == ScanDepth::SMALL)
+		if(scanDepth == ScanDepth::SMALL || settings->isObjectGraphAllowed())
 		{
-			cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT;
+			cfg.mainTurnDistanceLimit = settings->getMainHeroTurnDistanceLimit();
 		}
 
-		if(scanDepth != ScanDepth::ALL_FULL)
+		if(scanDepth != ScanDepth::ALL_FULL || settings->isObjectGraphAllowed())
 		{
-			cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
+			cfg.scoutTurnDistanceLimit =settings->getScoutHeroTurnDistanceLimit();
 		}
 
 		boost::this_thread::interruption_point();
 
 		pathfinder->updatePaths(activeHeroes, cfg);
 
+		if(settings->isObjectGraphAllowed())
+		{
+			pathfinder->updateGraphs(activeHeroes);
+		}
+
 		boost::this_thread::interruption_point();
 
 		objectClusterizer->clusterize();
@@ -235,13 +248,13 @@ void Nullkiller::makeTurn()
 
 	resetAiState();
 
-	for(int i = 1; i <= MAXPASS; i++)
+	for(int i = 1; i <= settings->getMaxPass(); i++)
 	{
 		updateAiState(i);
 
 		Goals::TTask bestTask = taskptr(Goals::Invalid());
 
-		for(;i <= MAXPASS; i++)
+		for(;i <= settings->getMaxPass(); i++)
 		{
 			Goals::TTaskVec fastTasks = {
 				choseBestTask(sptr(BuyArmyBehavior()), 1),
@@ -290,7 +303,8 @@ void Nullkiller::makeTurn()
 
 		// TODO: better to check turn distance here instead of priority
 		if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
-			&& scanDepth == ScanDepth::MAIN_FULL)
+			&& scanDepth == ScanDepth::MAIN_FULL
+			&& !settings->isObjectGraphAllowed())
 		{
 			useHeroChain = false;
 			scanDepth = ScanDepth::SMALL;
@@ -303,22 +317,25 @@ void Nullkiller::makeTurn()
 
 		if(bestTask->priority < MIN_PRIORITY)
 		{
-			auto heroes = cb->getHeroesInfo();
-			auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
-				{
-					return h->movementPointsRemaining() > 100;
-				});
-
-			if(hasMp && scanDepth != ScanDepth::ALL_FULL)
+			if(!settings->isObjectGraphAllowed())
 			{
-				logAi->trace(
-					"Goal %s has too low priority %f so increasing scan depth to full.",
-					taskDescription,
-					bestTask->priority);
+				auto heroes = cb->getHeroesInfo();
+				auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
+					{
+						return h->movementPointsRemaining() > 100;
+					});
 
-				scanDepth = ScanDepth::ALL_FULL;
-				useHeroChain = false;
-				continue;
+				if(hasMp && scanDepth != ScanDepth::ALL_FULL)
+				{
+					logAi->trace(
+						"Goal %s has too low priority %f so increasing scan depth to full.",
+						taskDescription,
+						bestTask->priority);
+
+					scanDepth = ScanDepth::ALL_FULL;
+					useHeroChain = false;
+					continue;
+				}
 			}
 
 			logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", taskDescription);
@@ -328,9 +345,9 @@ void Nullkiller::makeTurn()
 
 		executeTask(bestTask);
 
-		if(i == MAXPASS)
+		if(i == settings->getMaxPass())
 		{
-			logAi->error("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription);
+			logAi->warn("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription);
 		}
 	}
 }

+ 4 - 1
AI/Nullkiller/Engine/Nullkiller.h

@@ -11,6 +11,7 @@
 
 #include "PriorityEvaluator.h"
 #include "FuzzyHelper.h"
+#include "Settings.h"
 #include "AIMemory.h"
 #include "DeepDecomposer.h"
 #include "../Analyzers/DangerHitMapAnalyzer.h"
@@ -23,7 +24,6 @@
 namespace NKAI
 {
 
-const float MAX_GOLD_PEASURE = 0.3f;
 const float MIN_PRIORITY = 0.01f;
 const float SMALL_SCAN_MIN_PRIORITY = 0.4f;
 
@@ -59,6 +59,8 @@ private:
 	bool useHeroChain;
 
 public:
+	static std::unique_ptr<ObjectGraph> baseGraph;
+
 	std::unique_ptr<DangerHitMapAnalyzer> dangerHitMap;
 	std::unique_ptr<BuildAnalyzer> buildAnalyzer;
 	std::unique_ptr<ObjectClusterizer> objectClusterizer;
@@ -71,6 +73,7 @@ public:
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
 	std::unique_ptr<DeepDecomposer> decomposer;
 	std::unique_ptr<ArmyFormation> armyFormation;
+	std::unique_ptr<Settings> settings;
 	PlayerColor playerID;
 	std::shared_ptr<CCallback> cb;
 	std::mutex aiStateMutex;

+ 10 - 10
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -69,7 +69,7 @@ PriorityEvaluator::~PriorityEvaluator()
 
 void PriorityEvaluator::initVisitTile()
 {
-	auto file = CResourceHandler::get()->load(ResourcePath("config/ai/object-priorities.txt"))->readAll();
+	auto file = CResourceHandler::get()->load(ResourcePath("config/ai/nkai/object-priorities.txt"))->readAll();
 	std::string str = std::string((char *)file.first.get(), file.second);
 	engine = fl::FllImporter().fromString(str);
 	armyLossPersentageVariable = engine->getInputVariable("armyLoss");
@@ -702,7 +702,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 class HeroExchangeEvaluator : public IEvaluationContextBuilder
 {
 public:
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::HERO_EXCHANGE)
 			return;
@@ -719,7 +719,7 @@ public:
 class ArmyUpgradeEvaluator : public IEvaluationContextBuilder
 {
 public:
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::ARMY_UPGRADE)
 			return;
@@ -736,7 +736,7 @@ public:
 class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
 {
 public:
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::STAY_AT_TOWN)
 			return;
@@ -771,7 +771,7 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
 class DefendTownEvaluator : public IEvaluationContextBuilder
 {
 public:
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::DEFEND_TOWN)
 			return;
@@ -821,7 +821,7 @@ private:
 public:
 	ExecuteHeroChainEvaluationContextBuilder(const Nullkiller * ai) : ai(ai) {}
 
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::EXECUTE_HERO_CHAIN)
 			return;
@@ -879,7 +879,7 @@ class ClusterEvaluationContextBuilder : public IEvaluationContextBuilder
 public:
 	ClusterEvaluationContextBuilder(const Nullkiller * ai) {}
 
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::UNLOCK_CLUSTER)
 			return;
@@ -926,7 +926,7 @@ public:
 class ExchangeSwapTownHeroesContextBuilder : public IEvaluationContextBuilder
 {
 public:
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::EXCHANGE_SWAP_TOWN_HEROES)
 			return;
@@ -954,7 +954,7 @@ private:
 public:
 	DismissHeroContextBuilder(const Nullkiller * ai) : ai(ai) {}
 
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::DISMISS_HERO)
 			return;
@@ -974,7 +974,7 @@ public:
 class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
 {
 public:
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::BUILD_STRUCTURE)
 			return;

+ 84 - 0
AI/Nullkiller/Engine/Settings.cpp

@@ -0,0 +1,84 @@
+/*
+* Settings.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 <limits>
+
+#include "Settings.h"
+#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
+#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
+#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
+#include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/modding/CModHandler.h"
+#include "../../../lib/VCMI_Lib.h"
+#include "../../../lib/filesystem/Filesystem.h"
+#include "../../../lib/json/JsonNode.h"
+
+namespace NKAI
+{
+	Settings::Settings()
+		: maxRoamingHeroes(8),
+		mainHeroTurnDistanceLimit(10),
+		scoutHeroTurnDistanceLimit(5),
+		maxGoldPreasure(0.3f), 
+		maxpass(30),
+		allowObjectGraph(false)
+	{
+		ResourcePath resource("config/ai/nkai/nkai-settings", EResType::JSON);
+
+		loadFromMod("core", resource);
+
+		for(const auto & modName : VLC->modh->getActiveMods())
+		{
+			if(CResourceHandler::get(modName)->existsResource(resource))
+				loadFromMod(modName, resource);
+		}
+	}
+
+	void Settings::loadFromMod(const std::string & modName, const ResourcePath & resource)
+	{
+		if(!CResourceHandler::get(modName)->existsResource(resource))
+		{
+			logGlobal->error("Failed to load font %s from mod %s", resource.getName(), modName);
+			return;
+		}
+
+	    JsonNode node(JsonPath::fromResource(resource), modName);
+		
+		if(node.Struct()["maxRoamingHeroes"].isNumber())
+		{
+			maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer();
+		}
+
+		if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber())
+		{
+			mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer();
+		}
+
+		if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber())
+		{
+			scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer();
+		}
+
+		if(node.Struct()["maxpass"].isNumber())
+		{
+			maxpass = node.Struct()["maxpass"].Integer();
+		}
+
+		if(node.Struct()["maxGoldPreasure"].isNumber())
+		{
+			maxGoldPreasure = node.Struct()["maxGoldPreasure"].Float();
+		}
+
+		if(!node.Struct()["allowObjectGraph"].isNull())
+		{
+			allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
+		}
+	}
+}

+ 44 - 0
AI/Nullkiller/Engine/Settings.h

@@ -0,0 +1,44 @@
+/*
+* Settings.h, 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
+*
+*/
+#pragma once
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class JsonNode;
+class ResourcePath;
+
+VCMI_LIB_NAMESPACE_END
+
+namespace NKAI
+{
+	class Settings
+	{
+	private:
+		int maxRoamingHeroes;
+		int mainHeroTurnDistanceLimit;
+		int scoutHeroTurnDistanceLimit;
+		int maxpass;
+		float maxGoldPreasure;
+		bool allowObjectGraph;
+
+	public:
+		Settings();
+
+		int getMaxPass() const { return maxpass; }
+		float getMaxGoldPreasure() const { return maxGoldPreasure; }
+		int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
+		int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
+		int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
+		bool isObjectGraphAllowed() const { return allowObjectGraph; }
+
+	private:
+		void loadFromMod(const std::string & modName, const ResourcePath & resource);
+	};
+}

+ 2 - 10
AI/Nullkiller/Goals/AbstractGoal.h

@@ -180,11 +180,7 @@ public:
 	{
 	}
 
-	virtual ~cannotFulfillGoalException() throw ()
-	{
-	};
-
-	const char * what() const throw () override
+	const char * what() const noexcept override
 	{
 		return msg.c_str();
 	}
@@ -203,11 +199,7 @@ public:
 		msg = goal->toString();
 	}
 
-	virtual ~goalFulfilledException() throw ()
-	{
-	};
-
-	const char * what() const throw () override
+	const char * what() const noexcept override
 	{
 		return msg.c_str();
 	}

+ 8 - 2
AI/Nullkiller/Goals/AdventureSpellCast.cpp

@@ -39,12 +39,18 @@ void AdventureSpellCast::accept(AIGateway * ai)
 	if(hero->mana < hero->getSpellCost(spell))
 		throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->getNameTranslated());
 
-	if(spellID == SpellID::TOWN_PORTAL && town && town->visitingHero)
-		throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
 
 	if(town && spellID == SpellID::TOWN_PORTAL)
 	{
 		ai->selectedObject = town->id;
+
+		if(town->visitingHero && town->tempOwner == ai->playerID && !town->getUpperArmy()->stacksCount())
+		{
+			ai->myCb->swapGarrisonHero(town);
+		}
+
+		if(town->visitingHero)
+			throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
 	}
 
 	auto wait = cb->waitTillRealize;

+ 1 - 1
AI/Nullkiller/Goals/AdventureSpellCast.h

@@ -35,7 +35,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const AdventureSpellCast & other) const override;
+		bool operator==(const AdventureSpellCast & other) const override;
 	};
 }
 

+ 1 - 1
AI/Nullkiller/Goals/Build.h

@@ -32,7 +32,7 @@ namespace Goals
 		TSubgoal whatToDoToAchieve() override;
 		bool fulfillsMe(TSubgoal goal) override;
 
-		virtual bool operator==(const Build & other) const override
+		bool operator==(const Build & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/Nullkiller/Goals/BuildBoat.h

@@ -29,7 +29,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const BuildBoat & other) const override;
+		bool operator==(const BuildBoat & other) const override;
 	};
 }
 

+ 2 - 2
AI/Nullkiller/Goals/BuildThis.h

@@ -39,8 +39,8 @@ namespace Goals
 		}
 		BuildThis(BuildingID Bid, const CGTownInstance * tid);
 
-		virtual bool operator==(const BuildThis & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const BuildThis & other) const override;
+		std::string toString() const override;
 		void accept(AIGateway * ai) override;
 	};
 }

+ 3 - 3
AI/Nullkiller/Goals/BuyArmy.h

@@ -36,11 +36,11 @@ namespace Goals
 			priority = 3;//TODO: evaluate?
 		}
 
-		virtual bool operator==(const BuyArmy & other) const override;
+		bool operator==(const BuyArmy & other) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 
-		virtual void accept(AIGateway * ai) override;
+		void accept(AIGateway * ai) override;
 	};
 }
 

+ 5 - 5
AI/Nullkiller/Goals/CGoal.h

@@ -44,7 +44,7 @@ namespace Goals
 			//h & value & resID & objid & aid & tile & hero & town & bid;
 		}
 
-		virtual bool operator==(const AbstractGoal & g) const override
+		bool operator==(const AbstractGoal & g) const override
 		{
 			if(goalType != g.goalType)
 				return false;
@@ -54,7 +54,7 @@ namespace Goals
 
 		virtual bool operator==(const T & other) const = 0;
 
-		virtual TGoalVec decompose() const override
+		TGoalVec decompose() const override
 		{
 			TSubgoal single = decomposeSingle();
 
@@ -90,11 +90,11 @@ namespace Goals
 			return *((T *)this);
 		}
 
-		virtual bool isElementar() const override { return true; }
+		bool isElementar() const override { return true; }
 
-		virtual HeroPtr getHero() const override { return AbstractGoal::hero; }
+		HeroPtr getHero() const override { return AbstractGoal::hero; }
 
-		virtual int getHeroExchangeCount() const override { return 0; }
+		int getHeroExchangeCount() const override { return 0; }
 	};
 }
 

+ 5 - 5
AI/Nullkiller/Goals/CaptureObject.h

@@ -34,11 +34,11 @@ namespace Goals
 			name = obj->getObjectName();
 		}
 
-		virtual bool operator==(const CaptureObject & other) const override;
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
-		virtual bool hasHash() const override { return true; }
-		virtual uint64_t getHash() const override;
+		bool operator==(const CaptureObject & other) const override;
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
+		bool hasHash() const override { return true; }
+		uint64_t getHash() const override;
 	};
 }
 

+ 5 - 5
AI/Nullkiller/Goals/CompleteQuest.h

@@ -29,12 +29,12 @@ namespace Goals
 		{
 		}
 
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
-		virtual bool hasHash() const override { return true; }
-		virtual uint64_t getHash() const override;
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
+		bool hasHash() const override { return true; }
+		uint64_t getHash() const override;
 
-		virtual bool operator==(const CompleteQuest & other) const override;
+		bool operator==(const CompleteQuest & other) const override;
 
 	private:
 		TGoalVec tryCompleteQuest() const;

+ 5 - 5
AI/Nullkiller/Goals/Composition.h

@@ -26,15 +26,15 @@ namespace Goals
 		{
 		}
 
-		virtual bool operator==(const Composition & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const Composition & other) const override;
+		std::string toString() const override;
 		void accept(AIGateway * ai) override;
 		Composition & addNext(const AbstractGoal & goal);
 		Composition & addNext(TSubgoal goal);
 		Composition & addNextSequence(const TGoalVec & taskSequence);
-		virtual TGoalVec decompose() const override;
-		virtual bool isElementar() const override;
-		virtual int getHeroExchangeCount() const override;
+		TGoalVec decompose() const override;
+		bool isElementar() const override;
+		int getHeroExchangeCount() const override;
 	};
 }
 

+ 1 - 1
AI/Nullkiller/Goals/DigAtTile.h

@@ -33,7 +33,7 @@ namespace Goals
 		{
 			tile = Tile;
 		}
-		virtual bool operator==(const DigAtTile & other) const override;
+		bool operator==(const DigAtTile & other) const override;
 
 	private:
 		//TSubgoal decomposeSingle() const override;

+ 1 - 1
AI/Nullkiller/Goals/DismissHero.h

@@ -26,7 +26,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const DismissHero & other) const override;
+		bool operator==(const DismissHero & other) const override;
 	};
 }
 

+ 1 - 1
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h

@@ -31,7 +31,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const ExchangeSwapTownHeroes & other) const override;
+		bool operator==(const ExchangeSwapTownHeroes & other) const override;
 
 		const CGHeroInstance * getGarrisonHero() const { return garrisonHero; }
 		HeroLockedReason getLockingReason() const { return lockingReason; }

+ 2 - 2
AI/Nullkiller/Goals/ExecuteHeroChain.h

@@ -30,10 +30,10 @@ namespace Goals
 		
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const ExecuteHeroChain & other) const override;
+		bool operator==(const ExecuteHeroChain & other) const override;
 		const AIPath & getPath() const { return chainPath; }
 
-		virtual int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
+		int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
 
 	private:
 		bool moveHeroToTile(const CGHeroInstance * hero, const int3 & tile);

+ 1 - 1
AI/Nullkiller/Goals/GatherArmy.h

@@ -36,7 +36,7 @@ namespace Goals
 		TGoalVec getAllPossibleSubgoals() override;
 		TSubgoal whatToDoToAchieve() override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const GatherArmy & other) const override;
+		bool operator==(const GatherArmy & other) const override;
 	};
 }
 

+ 3 - 3
AI/Nullkiller/Goals/Invalid.h

@@ -32,17 +32,17 @@ namespace Goals
 			return TGoalVec();
 		}
 
-		virtual bool operator==(const Invalid & other) const override
+		bool operator==(const Invalid & other) const override
 		{
 			return true;
 		}
 
-		virtual std::string toString() const override
+		std::string toString() const override
 		{
 			return "Invalid";
 		}
 
-		virtual void accept(AIGateway * ai) override
+		void accept(AIGateway * ai) override
 		{
 			throw cannotFulfillGoalException("Can not fulfill Invalid goal!");
 		}

+ 2 - 2
AI/Nullkiller/Goals/RecruitHero.h

@@ -38,12 +38,12 @@ namespace Goals
 		{
 		}
 
-		virtual bool operator==(const RecruitHero & other) const override
+		bool operator==(const RecruitHero & other) const override
 		{
 			return true;
 		}
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 		void accept(AIGateway * ai) override;
 	};
 }

+ 1 - 1
AI/Nullkiller/Goals/SaveResources.h

@@ -28,7 +28,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const SaveResources & other) const override;
+		bool operator==(const SaveResources & other) const override;
 	};
 }
 

+ 2 - 2
AI/Nullkiller/Goals/StayAtTown.h

@@ -26,8 +26,8 @@ namespace Goals
 	public:
 		StayAtTown(const CGTownInstance * town, AIPath & path);
 
-		virtual bool operator==(const StayAtTown & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const StayAtTown & other) const override;
+		std::string toString() const override;
 		void accept(AIGateway * ai) override;
 		float getMovementWasted() const { return movementWasted; }
 	};

+ 1 - 1
AI/Nullkiller/Goals/Trade.h

@@ -34,7 +34,7 @@ namespace Goals
 			value = val;
 			objid = Objid;
 		}
-		virtual bool operator==(const Trade & other) const override;
+		bool operator==(const Trade & other) const override;
 	};
 }
 

+ 2 - 2
AI/Nullkiller/Markers/ArmyUpgrade.h

@@ -29,8 +29,8 @@ namespace Goals
 		ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
 		ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
 
-		virtual bool operator==(const ArmyUpgrade & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const ArmyUpgrade & other) const override;
+		std::string toString() const override;
 
 		uint64_t getUpgradeValue() const { return upgradeValue; }
 		uint64_t getInitialArmyValue() const { return initialValue; }

+ 2 - 2
AI/Nullkiller/Markers/DefendTown.h

@@ -30,8 +30,8 @@ namespace Goals
 		DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false);
 		DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender);
 
-		virtual bool operator==(const DefendTown & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const DefendTown & other) const override;
+		std::string toString() const override;
 
 		const HitMapInfo & getTreat() const { return treat; }
 

+ 2 - 2
AI/Nullkiller/Markers/HeroExchange.h

@@ -28,8 +28,8 @@ namespace Goals
 			sethero(targetHero);
 		}
 
-		virtual bool operator==(const HeroExchange & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const HeroExchange & other) const override;
+		std::string toString() const override;
 
 		uint64_t getReinforcementArmyStrength() const;
 	};

+ 2 - 2
AI/Nullkiller/Markers/UnlockCluster.h

@@ -36,8 +36,8 @@ namespace Goals
 			sethero(pathToCenter.targetHero);
 		}
 
-		virtual bool operator==(const UnlockCluster & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const UnlockCluster & other) const override;
+		std::string toString() const override;
 		std::shared_ptr<ObjectCluster> getCluster() const { return cluster; }
 		const AIPath & getPathToCenter() { return pathToCenter; }
 	};

+ 8 - 7
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -332,7 +332,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
 	return neighbours;
 }
 
-EPathfindingLayer phisycalLayers[2] = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
+constexpr std::array phisycalLayers = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
 
 bool AINodeStorage::increaseHeroChainTurnLimit()
 {
@@ -843,6 +843,7 @@ ExchangeCandidate HeroChainCalculationTask::calculateExchange(
 	candidate.turns = carrierParentNode->turns;
 	candidate.setCost(carrierParentNode->getCost() + otherParentNode->getCost() / 1000.0);
 	candidate.moveRemains = carrierParentNode->moveRemains;
+	candidate.danger = carrierParentNode->danger;
 
 	if(carrierParentNode->turns < otherParentNode->turns)
 	{
@@ -1122,14 +1123,14 @@ void AINodeStorage::calculateTownPortal(
 	{
 		for(const CGTownInstance * targetTown : towns)
 		{
-			// TODO: allow to hide visiting hero in garrison
-			if(targetTown->visitingHero && maskMap.find(targetTown->visitingHero.get()) != maskMap.end())
+			if(targetTown->visitingHero
+				&& targetTown->getUpperArmy()->stacksCount()
+				&& maskMap.find(targetTown->visitingHero.get()) != maskMap.end())
 			{
 				auto basicMask = maskMap.at(targetTown->visitingHero.get());
-				bool heroIsInChain = (actor->chainMask & basicMask) != 0;
 				bool sameActorInTown = actor->chainMask == basicMask;
 
-				if(sameActorInTown || !heroIsInChain)
+				if(!sameActorInTown)
 					continue;
 			}
 
@@ -1264,8 +1265,8 @@ bool AINodeStorage::hasBetterChain(
 				&& nodeActor->heroFightingStrength >= candidateActor->heroFightingStrength
 				&& node.getCost() <= candidateNode->getCost())
 			{
-				if(nodeActor->heroFightingStrength == candidateActor->heroFightingStrength
-					&& node.getCost() == candidateNode->getCost()
+				if(vstd::isAlmostEqual(nodeActor->heroFightingStrength, candidateActor->heroFightingStrength)
+					&& vstd::isAlmostEqual(node.getCost(), candidateNode->getCost())
 					&& &node < candidateNode)
 				{
 					continue;

+ 4 - 12
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -11,6 +11,7 @@
 #pragma once
 
 #define NKAI_PATHFINDER_TRACE_LEVEL 0
+constexpr int NKAI_GRAPH_TRACE_LEVEL = 0;
 #define NKAI_TRACE_LEVEL 0
 
 #include "../../../lib/pathfinder/CGPathNode.h"
@@ -24,20 +25,11 @@
 
 namespace NKAI
 {
-	const int SCOUT_TURN_DISTANCE_LIMIT = 5;
-	const int MAIN_TURN_DISTANCE_LIMIT = 10;
-
 namespace AIPathfinding
 {
-#ifdef ENVIRONMENT64
-	const int BUCKET_COUNT = 7;
-#else
-	const int BUCKET_COUNT = 5;
-#endif // ENVIRONMENT64
-
+	const int BUCKET_COUNT = 3;
 	const int BUCKET_SIZE = 5;
 	const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
-	const int THREAD_COUNT = 8;
 	const int CHAIN_MAX_DEPTH = 4;
 }
 
@@ -188,7 +180,7 @@ public:
 	bool selectFirstActor();
 	bool selectNextActor();
 
-	virtual std::vector<CGPathNode *> getInitialNodes() override;
+	std::vector<CGPathNode *> getInitialNodes() override;
 
 	virtual std::vector<CGPathNode *> calculateNeighbours(
 		const PathNodeInfo & source,
@@ -200,7 +192,7 @@ public:
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) override;
 
-	virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override;
+	void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override;
 
 	void commit(
 		AIPathNode * destination,

+ 47 - 4
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -33,7 +33,7 @@ bool AIPathfinder::isTileAccessible(const HeroPtr & hero, const int3 & tile) con
 		|| storage->isTileAccessible(hero, tile, EPathfindingLayer::SAIL);
 }
 
-std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile) const
+std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile, bool includeGraph) const
 {
 	const TerrainTile * tileInfo = cb->getTile(tile, false);
 
@@ -42,10 +42,23 @@ std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile) const
 		return std::vector<AIPath>();
 	}
 
-	return storage->getChainInfo(tile, !tileInfo->isWater());
+	auto info = storage->getChainInfo(tile, !tileInfo->isWater());
+
+	if(includeGraph)
+	{
+		for(auto hero : cb->getHeroesInfo())
+		{
+			auto graph = heroGraphs.find(hero->id);
+
+			if(graph != heroGraphs.end())
+				graph->second.addChainInfo(info, tile, hero, ai);
+		}
+	}
+
+	return info;
 }
 
-void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes, PathfinderSettings pathfinderSettings)
+void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings)
 {
 	if(!storage)
 	{
@@ -71,7 +84,7 @@ void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes
 		storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs);
 	}
 
-	auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage);
+	auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage, pathfinderSettings.allowBypassObjects);
 
 	logAi->trace("Recalculate paths pass %d", pass++);
 	cb->calculatePaths(config);
@@ -112,4 +125,34 @@ void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes
 	logAi->trace("Recalculated paths in %ld", timeElapsed(start));
 }
 
+void AIPathfinder::updateGraphs(const std::map<const CGHeroInstance *, HeroRole> & heroes)
+{
+	auto start = std::chrono::high_resolution_clock::now();
+	std::vector<const CGHeroInstance *> heroesVector;
+
+	heroGraphs.clear();
+
+	for(auto hero : heroes)
+	{
+		if(heroGraphs.try_emplace(hero.first->id, GraphPaths()).second)
+			heroesVector.push_back(hero.first);
+	}
+
+	parallel_for(blocked_range<size_t>(0, heroesVector.size()), [this, &heroesVector](const blocked_range<size_t> & r)
+		{
+			for(auto i = r.begin(); i != r.end(); i++)
+				heroGraphs.at(heroesVector[i]->id).calculatePaths(heroesVector[i], ai);
+		});
+
+	if(NKAI_GRAPH_TRACE_LEVEL >= 1)
+	{
+		for(auto hero : heroes)
+		{
+			heroGraphs[hero.first->id].dumpToLog();
+		}
+	}
+
+	logAi->trace("Graph paths updated in %lld", timeElapsed(start));
+}
+
 }

+ 13 - 3
AI/Nullkiller/Pathfinding/AIPathfinder.h

@@ -11,6 +11,7 @@
 #pragma once
 
 #include "AINodeStorage.h"
+#include "ObjectGraph.h"
 #include "../AIUtility.h"
 
 namespace NKAI
@@ -23,11 +24,13 @@ struct PathfinderSettings
 	bool useHeroChain;
 	uint8_t scoutTurnDistanceLimit;
 	uint8_t mainTurnDistanceLimit;
+	bool allowBypassObjects;
 
 	PathfinderSettings()
 		:useHeroChain(false),
 		scoutTurnDistanceLimit(255),
-		mainTurnDistanceLimit(255)
+		mainTurnDistanceLimit(255),
+		allowBypassObjects(true)
 	{ }
 };
 
@@ -37,13 +40,20 @@ private:
 	std::shared_ptr<AINodeStorage> storage;
 	CPlayerSpecificInfoCallback * cb;
 	Nullkiller * ai;
+	std::map<ObjectInstanceID, GraphPaths>  heroGraphs;
 
 public:
 	AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * ai);
-	std::vector<AIPath> getPathInfo(const int3 & tile) const;
+	std::vector<AIPath> getPathInfo(const int3 & tile, bool includeGraph = false) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const;
-	void updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes, PathfinderSettings pathfinderSettings);
+	void updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings);
+	void updateGraphs(const std::map<const CGHeroInstance *, HeroRole> & heroes);
 	void init();
+
+	std::shared_ptr<AINodeStorage>getStorage()
+	{
+		return storage;
+	}
 };
 
 }

+ 13 - 11
AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp

@@ -24,16 +24,17 @@ namespace AIPathfinding
 	std::vector<std::shared_ptr<IPathfindingRule>> makeRuleset(
 		CPlayerSpecificInfoCallback * cb,
 		Nullkiller * ai,
-		std::shared_ptr<AINodeStorage> nodeStorage)
+		std::shared_ptr<AINodeStorage> nodeStorage,
+		bool allowBypassObjects)
 	{
-		std::vector<std::shared_ptr<IPathfindingRule>> rules = {
-			std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage),
-			std::make_shared<DestinationActionRule>(),
-			std::make_shared<AIMovementToDestinationRule>(nodeStorage),
-			std::make_shared<MovementCostRule>(),
-			std::make_shared<AIPreviousNodeRule>(nodeStorage),
-			std::make_shared<AIMovementAfterDestinationRule>(cb, nodeStorage)
-		};
+			std::vector<std::shared_ptr<IPathfindingRule>> rules = {
+				std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage),
+				std::make_shared<DestinationActionRule>(),
+				std::make_shared<AIMovementToDestinationRule>(nodeStorage, allowBypassObjects),
+				std::make_shared<MovementCostRule>(),
+				std::make_shared<AIPreviousNodeRule>(nodeStorage),
+				std::make_shared<AIMovementAfterDestinationRule>(cb, nodeStorage, allowBypassObjects)
+			};
 
 		return rules;
 	}
@@ -41,8 +42,9 @@ namespace AIPathfinding
 	AIPathfinderConfig::AIPathfinderConfig(
 		CPlayerSpecificInfoCallback * cb,
 		Nullkiller * ai,
-		std::shared_ptr<AINodeStorage> nodeStorage)
-		:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), aiNodeStorage(nodeStorage)
+		std::shared_ptr<AINodeStorage> nodeStorage,
+		bool allowBypassObjects)
+		:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage, allowBypassObjects)), aiNodeStorage(nodeStorage)
 	{
 		options.canUseCast = true;
 	}

+ 3 - 2
AI/Nullkiller/Pathfinding/AIPathfinderConfig.h

@@ -30,11 +30,12 @@ namespace AIPathfinding
 		AIPathfinderConfig(
 			CPlayerSpecificInfoCallback * cb,
 			Nullkiller * ai,
-			std::shared_ptr<AINodeStorage> nodeStorage);
+			std::shared_ptr<AINodeStorage> nodeStorage,
+			bool allowBypassObjects);
 
 		~AIPathfinderConfig();
 
-		virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
+		CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
 	};
 }
 

+ 3 - 3
AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h

@@ -29,7 +29,7 @@ namespace AIPathfinding
 	public:
 		AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero, DayFlags flagsToAdd = DayFlags::NONE);
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
 		virtual void applyOnDestination(
 			const CGHeroInstance * hero,
@@ -38,9 +38,9 @@ namespace AIPathfinding
 			AIPathNode * dstMode,
 			const AIPathNode * srcNode) const override;
 
-		virtual bool canAct(const AIPathNode * source) const override;
+		bool canAct(const AIPathNode * source) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 	};
 
 	class WaterWalkingAction : public AdventureCastAction

+ 2 - 2
AI/Nullkiller/Pathfinding/Actions/BattleAction.h

@@ -28,9 +28,9 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 	};
 }
 

+ 10 - 10
AI/Nullkiller/Pathfinding/Actions/BoatActions.h

@@ -25,7 +25,7 @@ namespace AIPathfinding
 	class SummonBoatAction : public VirtualBoatAction
 	{
 	public:
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
 		virtual void applyOnDestination(
 			const CGHeroInstance * hero,
@@ -34,11 +34,11 @@ namespace AIPathfinding
 			AIPathNode * dstMode,
 			const AIPathNode * srcNode) const override;
 
-		virtual bool canAct(const AIPathNode * source) const override;
+		bool canAct(const AIPathNode * source) const override;
 
-		virtual const ChainActor * getActor(const ChainActor * sourceActor) const override;
+		const ChainActor * getActor(const ChainActor * sourceActor) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 
 	private:
 		int32_t getManaCost(const CGHeroInstance * hero) const;
@@ -56,17 +56,17 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual bool canAct(const AIPathNode * source) const override;
+		bool canAct(const AIPathNode * source) const override;
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
-		virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
+		Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
 
-		virtual const ChainActor * getActor(const ChainActor * sourceActor) const override;
+		const ChainActor * getActor(const ChainActor * sourceActor) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 
-		virtual const CGObjectInstance * targetObject() const override;
+		const CGObjectInstance * targetObject() const override;
 	};
 }
 

+ 8 - 3
AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp

@@ -19,14 +19,19 @@ namespace NKAI
 namespace AIPathfinding
 {
 	bool QuestAction::canAct(const AIPathNode * node) const
+	{
+		return canAct(node->actor->hero);
+	}
+
+	bool QuestAction::canAct(const CGHeroInstance * hero) const
 	{
 		if(questInfo.obj->ID == Obj::BORDER_GATE || questInfo.obj->ID == Obj::BORDERGUARD)
 		{
-			return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(node->actor->hero);
+			return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(hero);
 		}
 
-		return questInfo.quest->activeForPlayers.count(node->actor->hero->getOwner())
-			|| questInfo.quest->checkQuest(node->actor->hero);
+		return questInfo.quest->activeForPlayers.count(hero->getOwner())
+			|| questInfo.quest->checkQuest(hero);
 	}
 
 	Goals::TSubgoal QuestAction::decompose(const CGHeroInstance * hero) const

+ 6 - 4
AI/Nullkiller/Pathfinding/Actions/QuestAction.h

@@ -28,13 +28,15 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual bool canAct(const AIPathNode * node) const override;
+		bool canAct(const AIPathNode * node) const override;
 
-		virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
+		bool canAct(const CGHeroInstance * hero) const;
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
 
-		virtual std::string toString() const override;
+		void execute(const CGHeroInstance * hero) const override;
+
+		std::string toString() const override;
 	};
 }
 

+ 2 - 2
AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h

@@ -29,9 +29,9 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 	};
 }
 

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

@@ -18,7 +18,7 @@
 
 using namespace NKAI;
 
-CCreatureSet emptyArmy;
+const CCreatureSet emptyArmy;
 
 bool HeroExchangeArmy::needsLastStack() const
 {

+ 18 - 18
AI/Nullkiller/Pathfinding/Actors.h

@@ -28,7 +28,7 @@ class HeroExchangeArmy : public CArmedInstance
 public:
 	TResources armyCost;
 	bool requireBuyArmy;
-	virtual bool needsLastStack() const override;
+	bool needsLastStack() const override;
 	std::shared_ptr<SpecialAction> getActorAction() const;
 
 	HeroExchangeArmy(): CArmedInstance(nullptr, true), requireBuyArmy(false) {}
@@ -51,24 +51,24 @@ protected:
 
 public:
 	uint64_t chainMask;
-	bool isMovable;
-	bool allowUseResources;
-	bool allowBattle;
-	bool allowSpellCast;
+	bool isMovable = false;
+	bool allowUseResources = false;
+	bool allowBattle = false;
+	bool allowSpellCast = false;
 	std::shared_ptr<SpecialAction> actorAction;
 	const CGHeroInstance * hero;
 	HeroRole heroRole;
-	const CCreatureSet * creatureSet;
-	const ChainActor * battleActor;
-	const ChainActor * castActor;
-	const ChainActor * resourceActor;
-	const ChainActor * carrierParent;
-	const ChainActor * otherParent;
-	const ChainActor * baseActor;
+	const CCreatureSet * creatureSet = nullptr;
+	const ChainActor * battleActor = nullptr;
+	const ChainActor * castActor = nullptr;
+	const ChainActor * resourceActor = nullptr;
+	const ChainActor * carrierParent = nullptr;
+	const ChainActor * otherParent = nullptr;
+	const ChainActor * baseActor = nullptr;
 	int3 initialPosition;
 	EPathfindingLayer layer;
-	uint32_t initialMovement;
-	uint32_t initialTurn;
+	uint32_t initialMovement = 0;
+	uint32_t initialTurn = 0;
 	uint64_t armyValue;
 	float heroFightingStrength;
 	uint8_t actorExchangeCount;
@@ -126,7 +126,7 @@ public:
 	HeroActor(const ChainActor * carrier, const ChainActor * other, const HeroExchangeArmy * army, const Nullkiller * ai);
 
 protected:
-	virtual ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override;
+	ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override;
 };
 
 class ObjectActor : public ChainActor
@@ -136,7 +136,7 @@ private:
 
 public:
 	ObjectActor(const CGObjectInstance * obj, const CCreatureSet * army, uint64_t chainMask, int initialTurn);
-	virtual std::string toString() const override;
+	std::string toString() const override;
 	const CGObjectInstance * getActorObject() const override;
 };
 
@@ -154,7 +154,7 @@ private:
 public:
 	DwellingActor(const CGDwelling * dwelling, uint64_t chainMask, bool waitForGrowth, int dayOfWeek);
 	~DwellingActor();
-	virtual std::string toString() const override;
+	std::string toString() const override;
 
 protected:
 	int getInitialTurn(bool waitForGrowth, int dayOfWeek);
@@ -168,7 +168,7 @@ private:
 
 public:
 	TownGarrisonActor(const CGTownInstance * town, uint64_t chainMask);
-	virtual std::string toString() const override;
+	std::string toString() const override;
 };
 
 }

+ 671 - 0
AI/Nullkiller/Pathfinding/ObjectGraph.cpp

@@ -0,0 +1,671 @@
+/*
+* ObjectGraph.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 "ObjectGraph.h"
+#include "AIPathfinderConfig.h"
+#include "../../../lib/CRandomGenerator.h"
+#include "../../../CCallback.h"
+#include "../../../lib/mapping/CMap.h"
+#include "../Engine/Nullkiller.h"
+#include "../../../lib/logging/VisualLogger.h"
+#include "Actions/QuestAction.h"
+
+namespace NKAI
+{
+
+struct ConnectionCostInfo
+{
+	float totalCost = 0;
+	float avg = 0;
+	int connectionsCount = 0;
+};
+
+class ObjectGraphCalculator
+{
+private:
+	ObjectGraph * target;
+	const Nullkiller * ai;
+
+	std::map<const CGHeroInstance *, HeroRole> actors;
+	std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
+
+	std::vector<std::unique_ptr<CGBoat>> temporaryBoats;
+	std::vector<std::unique_ptr<CGHeroInstance>> temporaryActorHeroes;
+
+public:
+	ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
+		:ai(ai), target(target)
+	{
+	}
+
+	void setGraphObjects()
+	{
+		for(auto obj : ai->memory->visitableObjs)
+		{
+			if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT)
+			{
+				addObjectActor(obj);
+			}
+		}
+
+		for(auto town : ai->cb->getTownsInfo())
+		{
+			addObjectActor(town);
+		}
+	}
+
+	void calculateConnections()
+	{
+		updatePaths();
+
+		foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+			{
+				calculateConnections(pos);
+			});
+
+		removeExtraConnections();
+	}
+
+	void addMinimalDistanceJunctions()
+	{
+		foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+			{
+				if(target->hasNodeAt(pos))
+					return;
+
+				if(ai->cb->getGuardingCreaturePosition(pos).valid())
+					return;
+
+				ConnectionCostInfo currentCost = getConnectionsCost(pos);
+
+				if(currentCost.connectionsCount <= 2)
+					return;
+
+				float neighborCost = currentCost.avg + 0.001f;
+
+				if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+				{
+					logAi->trace("Checking junction %s", pos.toString());
+				}
+
+				foreach_neighbour(
+					ai->cb.get(),
+					pos,
+					[this, &neighborCost](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
+					{
+						auto costTotal = this->getConnectionsCost(neighbor);
+
+						if(costTotal.avg < neighborCost)
+						{
+							neighborCost = costTotal.avg;
+
+							if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+							{
+								logAi->trace("Better node found at %s", neighbor.toString());
+							}
+						}
+					});
+
+				if(currentCost.avg < neighborCost)
+				{
+					addJunctionActor(pos);
+				}
+			});
+	}
+
+private:
+	void updatePaths()
+	{
+		PathfinderSettings ps;
+
+		ps.mainTurnDistanceLimit = 5;
+		ps.scoutTurnDistanceLimit = 1;
+		ps.allowBypassObjects = false;
+
+		ai->pathfinder->updatePaths(actors, ps);
+	}
+
+	void calculateConnections(const int3 & pos)
+	{
+		if(target->hasNodeAt(pos))
+		{
+			foreach_neighbour(
+				ai->cb.get(),
+				pos,
+				[this, &pos](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
+				{
+					if(target->hasNodeAt(neighbor))
+					{
+						auto paths = ai->pathfinder->getPathInfo(neighbor);
+
+						for(auto & path : paths)
+						{
+							if(pos == path.targetHero->visitablePos())
+							{
+								target->tryAddConnection(pos, neighbor, path.movementCost(), path.getTotalDanger());
+							}
+						}
+					}
+				});
+
+			return;
+		}
+
+		auto guardPos = ai->cb->getGuardingCreaturePosition(pos);
+		auto paths = ai->pathfinder->getPathInfo(pos);
+
+		for(AIPath & path1 : paths)
+		{
+			for(AIPath & path2 : paths)
+			{
+				if(path1.targetHero == path2.targetHero)
+					continue;
+
+				auto pos1 = path1.targetHero->visitablePos();
+				auto pos2 = path2.targetHero->visitablePos();
+
+				if(guardPos.valid() && guardPos != pos1 && guardPos != pos2)
+					continue;
+
+				auto obj1 = actorObjectMap[path1.targetHero];
+				auto obj2 = actorObjectMap[path2.targetHero];
+
+				auto tile1 = cb->getTile(pos1);
+				auto tile2 = cb->getTile(pos2);
+
+				if(tile2->isWater() && !tile1->isWater())
+				{
+					if(!cb->getTile(pos)->isWater())
+						continue;
+
+					if(obj1 && (obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD))
+						continue;
+				}
+
+				auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true);
+
+				auto updated = target->tryAddConnection(
+					pos1,
+					pos2,
+					path1.movementCost() + path2.movementCost(),
+					danger);
+
+				if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated)
+				{
+					logAi->trace(
+						"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
+						obj1 ? obj1->getObjectName() : "J", pos1.toString(),
+						obj2 ? obj2->getObjectName() : "J", pos2.toString(),
+						pos.toString(),
+						path1.movementCost() + path2.movementCost());
+				}
+			}
+		}
+	}
+
+	bool isExtraConnection(float direct, float side1, float side2) const
+	{
+		float sideRatio = (side1 + side2) / direct;
+
+		return sideRatio < 1.25f && direct > side1 && direct > side2;
+	}
+
+	void removeExtraConnections()
+	{
+		std::vector<std::pair<int3, int3>> connectionsToRemove;
+
+		for(auto & actor : temporaryActorHeroes)
+		{
+			auto pos = actor->visitablePos();
+			auto & currentNode = target->getNode(pos);
+
+			target->iterateConnections(pos, [this, &pos, &connectionsToRemove, &currentNode](int3 n1, ObjectLink o1)
+				{
+					target->iterateConnections(n1, [&pos, &o1, &currentNode, &connectionsToRemove, this](int3 n2, ObjectLink o2)
+						{
+							auto direct = currentNode.connections.find(n2);
+
+							if(direct != currentNode.connections.end() && isExtraConnection(direct->second.cost, o1.cost, o2.cost))
+							{
+								connectionsToRemove.push_back({pos, n2});
+							}
+						});
+				});
+		}
+
+		vstd::removeDuplicates(connectionsToRemove);
+
+		for(auto & c : connectionsToRemove)
+		{
+			target->removeConnection(c.first, c.second);
+
+			if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+			{
+				logAi->trace("Remove ineffective connection %s->%s", c.first.toString(), c.second.toString());
+			}
+		}
+	}
+
+	void addObjectActor(const CGObjectInstance * obj)
+	{
+		auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(obj->cb)).get();
+
+		CRandomGenerator rng;
+		auto visitablePos = obj->visitablePos();
+
+		objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
+		objectActor->initHero(rng, static_cast<HeroTypeID>(0));
+		objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
+		objectActor->initObj(rng);
+
+		if(cb->getTile(visitablePos)->isWater())
+		{
+			objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
+		}
+
+		assert(objectActor->visitablePos() == visitablePos);
+
+		actorObjectMap[objectActor] = obj;
+		actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::SHIPYARD ? HeroRole::MAIN : HeroRole::SCOUT;
+
+		target->addObject(obj);
+	}
+
+	void addJunctionActor(const int3 & visitablePos)
+	{
+		auto internalCb = temporaryActorHeroes.front()->cb;
+		auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
+
+		CRandomGenerator rng;
+
+		objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
+		objectActor->initHero(rng, static_cast<HeroTypeID>(0));
+		objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
+		objectActor->initObj(rng);
+
+		if(cb->getTile(visitablePos)->isWater())
+		{
+			objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
+		}
+
+		assert(objectActor->visitablePos() == visitablePos);
+
+		actorObjectMap[objectActor] = nullptr;
+		actors[objectActor] = HeroRole::SCOUT;
+
+		target->registerJunction(visitablePos);
+	}
+
+	ConnectionCostInfo getConnectionsCost(const int3 & pos) const
+	{
+		auto paths = ai->pathfinder->getPathInfo(pos);
+		std::map<int3, float> costs;
+
+		for(auto & path : paths)
+		{
+			auto fromPos = path.targetHero->visitablePos();
+			auto cost = costs.find(fromPos);
+			
+			if(cost == costs.end())
+			{
+				costs.emplace(fromPos, path.movementCost());
+			}
+			else
+			{
+				if(path.movementCost() < cost->second)
+				{
+					costs[fromPos] = path.movementCost();
+				}
+			}
+		}
+
+		ConnectionCostInfo result;
+
+		for(auto & cost : costs)
+		{
+			result.totalCost += cost.second;
+			result.connectionsCount++;
+		}
+
+		if(result.connectionsCount)
+		{
+			result.avg = result.totalCost / result.connectionsCount;
+		}
+
+		return result;
+	}
+};
+
+bool ObjectGraph::tryAddConnection(
+	const int3 & from,
+	const int3 & to,
+	float cost,
+	uint64_t danger)
+{
+	return nodes[from].connections[to].update(cost, danger);
+}
+
+void ObjectGraph::removeConnection(const int3 & from, const int3 & to)
+{
+	nodes[from].connections.erase(to);
+}
+
+void ObjectGraph::updateGraph(const Nullkiller * ai)
+{
+	auto cb = ai->cb;
+
+	ObjectGraphCalculator calculator(this, ai);
+
+	calculator.setGraphObjects();
+	calculator.calculateConnections();
+	calculator.addMinimalDistanceJunctions();
+	calculator.calculateConnections();
+
+	if(NKAI_GRAPH_TRACE_LEVEL >= 1)
+		dumpToLog("graph");
+}
+
+void ObjectGraph::addObject(const CGObjectInstance * obj)
+{
+	nodes[obj->visitablePos()].init(obj);
+}
+
+void ObjectGraph::registerJunction(const int3 & pos)
+{
+	nodes[pos].initJunction();
+}
+
+void ObjectGraph::removeObject(const CGObjectInstance * obj)
+{
+	nodes[obj->visitablePos()].objectExists = false;
+
+	if(obj->ID == Obj::BOAT)
+	{
+		vstd::erase_if(nodes[obj->visitablePos()].connections, [&](const std::pair<int3, ObjectLink> & link) -> bool
+			{
+				auto tile = cb->getTile(link.first, false);
+
+				return tile && tile->isWater();
+			});
+	}
+}
+
+void ObjectGraph::connectHeroes(const Nullkiller * ai)
+{
+	for(auto obj : ai->memory->visitableObjs)
+	{
+		if(obj && obj->ID == Obj::HERO)
+		{
+			addObject(obj);
+		}
+	}
+
+	for(auto & node : nodes)
+	{
+		auto pos = node.first;
+		auto paths = ai->pathfinder->getPathInfo(pos);
+
+		for(AIPath & path : paths)
+		{
+			if(path.getFirstBlockedAction())
+				continue;
+
+			auto heroPos = path.targetHero->visitablePos();
+
+			nodes[pos].connections[heroPos].update(
+				path.movementCost(),
+				path.getPathDanger());
+
+			nodes[heroPos].connections[pos].update(
+				path.movementCost(),
+				path.getPathDanger());
+		}
+	}
+}
+
+void ObjectGraph::dumpToLog(std::string visualKey) const
+{
+	logVisual->updateWithLock(visualKey, [&](IVisualLogBuilder & logBuilder)
+		{
+			for(auto & tile : nodes)
+			{
+				for(auto & node : tile.second.connections)
+				{
+					if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+					{
+						logAi->trace(
+							"%s -> %s: %f !%d",
+							node.first.toString(),
+							tile.first.toString(),
+							node.second.cost,
+							node.second.danger);
+					}
+
+					logBuilder.addLine(tile.first, node.first);
+				}
+			}
+		});
+}
+
+bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const
+{
+	return pathNodes.at(lhs.coord)[lhs.nodeType].cost > pathNodes.at(rhs.coord)[rhs.nodeType].cost;
+}
+
+void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai)
+{
+	graph = *ai->baseGraph;
+	graph.connectHeroes(ai);
+
+	visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
+	pathNodes.clear();
+
+	GraphNodeComparer cmp(pathNodes);
+	GraphPathNode::TFibHeap pq(cmp);
+
+	pathNodes[targetHero->visitablePos()][GrapthPathNodeType::NORMAL].cost = 0;
+	pq.emplace(GraphPathNodePointer(targetHero->visitablePos(), GrapthPathNodeType::NORMAL));
+
+	while(!pq.empty())
+	{
+		GraphPathNodePointer pos = pq.top();
+		pq.pop();
+
+		auto & node = getOrCreateNode(pos);
+		std::shared_ptr<SpecialAction> transitionAction;
+
+		if(node.obj)
+		{
+			if(node.obj->ID == Obj::QUEST_GUARD
+				|| node.obj->ID == Obj::BORDERGUARD
+				|| node.obj->ID == Obj::BORDER_GATE)
+			{
+				auto questObj = dynamic_cast<const IQuestObject *>(node.obj);
+				auto questInfo = QuestInfo(questObj->quest, node.obj, pos.coord);
+
+				if(node.obj->ID == Obj::QUEST_GUARD
+					&& questObj->quest->mission == Rewardable::Limiter{}
+					&& questObj->quest->killTarget == ObjectInstanceID::NONE)
+				{
+					continue;
+				}
+
+				auto questAction = std::make_shared<AIPathfinding::QuestAction>(questInfo);
+
+				if(!questAction->canAct(targetHero))
+				{
+					transitionAction = questAction;
+				}
+			}
+		}
+
+		node.isInQueue = false;
+
+		graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, ObjectLink o)
+			{
+				auto targetNodeType = o.danger || transitionAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
+				auto targetPointer = GraphPathNodePointer(target, targetNodeType);
+				auto & targetNode = getOrCreateNode(targetPointer);
+
+				if(targetNode.tryUpdate(pos, node, o))
+				{
+					targetNode.specialAction = transitionAction;
+
+					auto targetGraphNode = graph.getNode(target);
+
+					if(targetGraphNode.objID.hasValue())
+					{
+						targetNode.obj = ai->cb->getObj(targetGraphNode.objID, false);
+
+						if(targetNode.obj && targetNode.obj->ID == Obj::HERO)
+							return;
+					}
+
+					if(targetNode.isInQueue)
+					{
+						pq.increase(targetNode.handle);
+					}
+					else
+					{
+						targetNode.handle = pq.emplace(targetPointer);
+						targetNode.isInQueue = true;
+					}
+				}
+			});
+	}
+}
+
+void GraphPaths::dumpToLog() const
+{
+	logVisual->updateWithLock(visualKey, [&](IVisualLogBuilder & logBuilder)
+		{
+			for(auto & tile : pathNodes)
+			{
+				for(auto & node : tile.second)
+				{
+					if(!node.previous.valid())
+						continue;
+
+					if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+					{
+						logAi->trace(
+							"%s -> %s: %f !%d",
+							node.previous.coord.toString(),
+							tile.first.toString(),
+							node.cost,
+							node.danger);
+					}
+
+					logBuilder.addLine(node.previous.coord, tile.first);
+				}
+			}
+		});
+}
+
+bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link)
+{
+	auto newCost = prev.cost + link.cost;
+
+	if(newCost < cost)
+	{
+		previous = pos;
+		danger = prev.danger + link.danger;
+		cost = newCost;
+
+		return true;
+	}
+
+	return false;
+}
+
+void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const
+{
+	auto nodes = pathNodes.find(tile);
+
+	if(nodes == pathNodes.end())
+		return;
+
+	for(auto & node : nodes->second)
+	{
+		if(!node.reachable())
+			continue;
+
+		std::vector<GraphPathNodePointer> tilesToPass;
+
+		uint64_t danger = node.danger;
+		float cost = node.cost;
+		bool allowBattle = false;
+
+		auto current = GraphPathNodePointer(nodes->first, node.nodeType);
+
+		while(true)
+		{
+			auto currentTile = pathNodes.find(current.coord);
+
+			if(currentTile == pathNodes.end())
+				break;
+
+			auto currentNode = currentTile->second[current.nodeType];
+
+			if(!currentNode.previous.valid())
+				break;
+
+			allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE;
+			vstd::amax(danger, currentNode.danger);
+			vstd::amax(cost, currentNode.cost);
+
+			tilesToPass.push_back(current);
+
+			if(currentNode.cost < 2.0f)
+				break;
+
+			current = currentNode.previous;
+		}
+
+		if(tilesToPass.empty())
+			continue;
+
+		auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
+
+		for(auto & path : entryPaths)
+		{
+			if(path.targetHero != hero)
+				continue;
+
+			for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
+			{
+				AIPathNodeInfo n;
+
+				n.coord = graphTile->coord;
+				n.cost = cost;
+				n.turns = static_cast<ui8>(cost) + 1; // just in case lets select worst scenario
+				n.danger = danger;
+				n.targetHero = hero;
+				n.parentIndex = -1;
+				n.specialAction = getNode(*graphTile).specialAction;
+
+				for(auto & node : path.nodes)
+				{
+					node.parentIndex++;
+				}
+
+				path.nodes.insert(path.nodes.begin(), n);
+			}
+
+			path.armyLoss += ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger);
+			path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle);
+			path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
+
+			paths.push_back(path);
+		}
+	}
+}
+
+}

+ 192 - 0
AI/Nullkiller/Pathfinding/ObjectGraph.h

@@ -0,0 +1,192 @@
+/*
+* ObjectGraph.h, 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
+*
+*/
+
+#pragma once
+
+#include "AINodeStorage.h"
+#include "../AIUtility.h"
+
+namespace NKAI
+{
+
+class Nullkiller;
+
+struct ObjectLink
+{
+	float cost = 100000; // some big number
+	uint64_t danger = 0;
+
+	bool update(float newCost, uint64_t newDanger)
+	{
+		if(cost > newCost)
+		{
+			cost = newCost;
+			danger = newDanger;
+
+			return true;
+		}
+
+		return false;
+	}
+};
+
+struct ObjectNode
+{
+	ObjectInstanceID objID;
+	MapObjectID objTypeID;
+	bool objectExists;
+	std::unordered_map<int3, ObjectLink> connections;
+
+	void init(const CGObjectInstance * obj)
+	{
+		objectExists = true;
+		objID = obj->id;
+		objTypeID = obj->ID;
+	}
+
+	void initJunction()
+	{
+		objectExists = false;
+		objID = ObjectInstanceID();
+		objTypeID = Obj();
+	}
+};
+
+class ObjectGraph
+{
+	std::unordered_map<int3, ObjectNode> nodes;
+
+public:
+	void updateGraph(const Nullkiller * ai);
+	void addObject(const CGObjectInstance * obj);
+	void registerJunction(const int3 & pos);
+	void connectHeroes(const Nullkiller * ai);
+	void removeObject(const CGObjectInstance * obj);
+	bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger);
+	void removeConnection(const int3 & from, const int3 & to);
+	void dumpToLog(std::string visualKey) const;
+
+	template<typename Func>
+	void iterateConnections(const int3 & pos, Func fn)
+	{
+		for(auto & connection : nodes.at(pos).connections)
+		{
+			fn(connection.first, connection.second);
+		}
+	}
+
+	const ObjectNode & getNode(int3 tile) const
+	{
+		return nodes.at(tile);
+	}
+
+	bool hasNodeAt(const int3 & tile) const
+	{
+		return vstd::contains(nodes, tile);
+	}
+};
+
+struct GraphPathNode;
+
+enum GrapthPathNodeType
+{
+	NORMAL,
+
+	BATTLE,
+
+	LAST
+};
+
+struct GraphPathNodePointer
+{
+	int3 coord = int3(-1);
+	GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
+
+	GraphPathNodePointer() = default;
+
+	GraphPathNodePointer(int3 coord, GrapthPathNodeType type)
+		:coord(coord), nodeType(type)
+	{ }
+
+	bool valid() const
+	{
+		return coord.valid();
+	}
+};
+
+typedef std::unordered_map<int3, GraphPathNode[GrapthPathNodeType::LAST]> GraphNodeStorage;
+
+class GraphNodeComparer
+{
+	const GraphNodeStorage & pathNodes;
+
+public:
+	GraphNodeComparer(const GraphNodeStorage & pathNodes)
+		:pathNodes(pathNodes)
+	{
+	}
+
+	bool operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const;
+};
+
+struct GraphPathNode
+{
+	const float BAD_COST = 100000;
+
+	GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
+	GraphPathNodePointer previous;
+	float cost = BAD_COST;
+	uint64_t danger = 0;
+	const CGObjectInstance * obj = nullptr;
+	std::shared_ptr<SpecialAction> specialAction;
+
+	using TFibHeap = boost::heap::fibonacci_heap<GraphPathNodePointer, boost::heap::compare<GraphNodeComparer>>;
+
+	TFibHeap::handle_type handle;
+	bool isInQueue = false;
+
+	bool reachable() const
+	{
+		return cost < BAD_COST;
+	}
+
+	bool tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link);
+};
+
+class GraphPaths
+{
+	ObjectGraph graph;
+	GraphNodeStorage pathNodes;
+	std::string visualKey;
+
+public:
+	void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai);
+	void addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
+	void dumpToLog() const;
+
+private:
+	GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos)
+	{
+		auto & node = pathNodes[pos.coord][pos.nodeType];
+
+		node.nodeType = pos.nodeType;
+
+		return node;
+	}
+
+	const GraphPathNode & getNode(const GraphPathNodePointer & pos) const
+	{
+		auto & node = pathNodes.at(pos.coord)[pos.nodeType];
+
+		return node;
+	}
+};
+
+}

+ 1 - 1
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -131,7 +131,7 @@ namespace AIPathfinding
 		{
 			if(obj->ID != Obj::TOWN) //towns were handled in the previous loop
 			{
-				if(const IShipyard * shipyard = IShipyard::castFrom(obj))
+				if(const auto * shipyard = dynamic_cast<const IShipyard *>(obj))
 					shipyards.push_back(shipyard);
 			}
 		}

+ 26 - 2
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -13,6 +13,7 @@
 #include "../Actions/QuestAction.h"
 #include "../../Goals/Invalid.h"
 #include "AIPreviousNodeRule.h"
+#include "../../../../lib/pathfinder/PathfinderOptions.h"
 
 namespace NKAI
 {
@@ -20,8 +21,9 @@ namespace AIPathfinding
 {
 	AIMovementAfterDestinationRule::AIMovementAfterDestinationRule(
 		CPlayerSpecificInfoCallback * cb, 
-		std::shared_ptr<AINodeStorage> nodeStorage)
-		:cb(cb), nodeStorage(nodeStorage)
+		std::shared_ptr<AINodeStorage> nodeStorage,
+		bool allowBypassObjects)
+		:cb(cb), nodeStorage(nodeStorage), allowBypassObjects(allowBypassObjects)
 	{
 	}
 
@@ -40,10 +42,32 @@ namespace AIPathfinding
 		}
 
 		auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
+
 		if(blocker == BlockingReason::NONE)
 		{
 			destination.blocked = nodeStorage->isDistanceLimitReached(source, destination);
 
+			if(destination.nodeObject
+				&& !destination.blocked
+				&& !allowBypassObjects
+				&& !dynamic_cast<const CGTeleport *>(destination.nodeObject)
+				&& destination.nodeObject->ID != Obj::EVENT)
+			{
+				destination.blocked = true;
+				destination.node->locked = true;
+			}
+
+			return;
+		}
+		
+		if(!allowBypassObjects)
+		{
+			if(destination.nodeObject)
+			{
+				destination.blocked = true;
+				destination.node->locked = true;
+			}
+
 			return;
 		}
 

+ 5 - 1
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.h

@@ -25,9 +25,13 @@ namespace AIPathfinding
 	private:
 		CPlayerSpecificInfoCallback * cb;
 		std::shared_ptr<AINodeStorage> nodeStorage;
+		bool allowBypassObjects;
 
 	public:
-		AIMovementAfterDestinationRule(CPlayerSpecificInfoCallback * cb, std::shared_ptr<AINodeStorage> nodeStorage);
+		AIMovementAfterDestinationRule(
+			CPlayerSpecificInfoCallback * cb,
+			std::shared_ptr<AINodeStorage> nodeStorage,
+			bool allowBypassObjects);
 
 		virtual void process(
 			const PathNodeInfo & source,

+ 25 - 8
AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp

@@ -14,8 +14,10 @@ namespace NKAI
 {
 namespace AIPathfinding
 {
-	AIMovementToDestinationRule::AIMovementToDestinationRule(std::shared_ptr<AINodeStorage> nodeStorage)
-		: nodeStorage(nodeStorage)
+	AIMovementToDestinationRule::AIMovementToDestinationRule(
+		std::shared_ptr<AINodeStorage> nodeStorage,
+		bool allowBypassObjects)
+		: nodeStorage(nodeStorage), allowBypassObjects(allowBypassObjects)
 	{
 	}
 
@@ -37,15 +39,30 @@ namespace AIPathfinding
 			return;
 		}
 
-		if(blocker == BlockingReason::SOURCE_GUARDED && nodeStorage->getAINode(source.node)->actor->allowBattle)
+		if(blocker == BlockingReason::SOURCE_GUARDED)
 		{
+			auto actor = nodeStorage->getAINode(source.node)->actor;
+
+			if(!allowBypassObjects)
+			{
+				if (source.node->getCost() < 0.0001f)
+					return;
+
+				// when actor represents moster graph node, we need to let him escape monster
+				if(cb->getGuardingCreaturePosition(source.coord) == actor->initialPosition)
+					return;
+			}
+
+			if(actor->allowBattle)
+			{
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 1
-			logAi->trace(
-				"Bypass src guard while moving from %s to %s",
-				source.coord.toString(),
-				destination.coord.toString());
+				logAi->trace(
+					"Bypass src guard while moving from %s to %s",
+					source.coord.toString(),
+					destination.coord.toString());
 #endif
-			return;
+				return;
+			}
 		}
 
 		destination.blocked = true;

+ 2 - 1
AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.h

@@ -24,9 +24,10 @@ namespace AIPathfinding
 	{
 	private:
 		std::shared_ptr<AINodeStorage> nodeStorage;
+		bool allowBypassObjects;
 
 	public:
-		AIMovementToDestinationRule(std::shared_ptr<AINodeStorage> nodeStorage);
+		AIMovementToDestinationRule(std::shared_ptr<AINodeStorage> nodeStorage, bool allowBypassObjects);
 
 		virtual void process(
 			const PathNodeInfo & source,

+ 1 - 1
AI/Nullkiller/main.cpp

@@ -14,7 +14,7 @@
 #define strcpy_s(a, b, c) strncpy(a, c, b)
 #endif
 
-static const char * g_cszAiName = "Nullkiller";
+static const char * const g_cszAiName = "Nullkiller";
 
 extern "C" DLL_EXPORT int GetGlobalAiVersion()
 {

+ 3 - 3
AI/StupidAI/CMakeLists.txt

@@ -8,19 +8,19 @@ set(stupidAI_HEADERS
 		StupidAI.h
 )
 
-if(NOT ENABLE_STATIC_AI_LIBS)
+if(NOT ENABLE_STATIC_LIBS)
 	list(APPEND stupidAI_SRCS main.cpp StdInc.cpp)
 endif()
 assign_source_group(${stupidAI_SRCS} ${stupidAI_HEADERS})
 
-if(ENABLE_STATIC_AI_LIBS)
+if(ENABLE_STATIC_LIBS)
 	add_library(StupidAI STATIC ${stupidAI_SRCS} ${stupidAI_HEADERS})
 else()
 	add_library(StupidAI SHARED ${stupidAI_SRCS} ${stupidAI_HEADERS})
 	install(TARGETS StupidAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
 endif()
 
-target_link_libraries(StupidAI PRIVATE ${VCMI_LIB_TARGET})
+target_link_libraries(StupidAI PRIVATE vcmi)
 target_include_directories(StupidAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
 
 vcmi_set_output_dir(StupidAI "AI")

+ 1 - 1
AI/StupidAI/main.cpp

@@ -16,7 +16,7 @@
 #define strcpy_s(a, b, c) strncpy(a, c, b)
 #endif
 
-static const char *g_cszAiName = "Stupid AI 0.1";
+static const char * const g_cszAiName = "Stupid AI 0.1";
 
 extern "C" DLL_EXPORT int GetGlobalAiVersion()
 {

+ 1 - 1
AI/VCAI/BuildingManager.cpp

@@ -148,7 +148,7 @@ BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_
 static const std::vector<BuildingID> unitGrowth = { BuildingID::HORDE_1, BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR };
 static const std::vector<BuildingID> _spells = { BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3,
 BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 };
-static const std::vector<BuildingID> extra = { BuildingID::MARKETPLACE, BuildingID::BLACKSMITH, BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, 
+static const std::vector<BuildingID> extra = { BuildingID::MARKETPLACE, BuildingID::BLACKSMITH, BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2,
 BuildingID::SPECIAL_3, BuildingID::SPECIAL_4, BuildingID::SHIPYARD }; // all remaining buildings
 
 bool BuildingManager::getBuildingOptions(const CGTownInstance * t)

+ 3 - 3
AI/VCAI/CMakeLists.txt

@@ -94,12 +94,12 @@ set(VCAI_HEADERS
 		VCAI.h
 )
 
-if(NOT ENABLE_STATIC_AI_LIBS)
+if(NOT ENABLE_STATIC_LIBS)
 	list(APPEND VCAI_SRCS main.cpp StdInc.cpp)
 endif()
 assign_source_group(${VCAI_SRCS} ${VCAI_HEADERS})
 
-if(ENABLE_STATIC_AI_LIBS)
+if(ENABLE_STATIC_LIBS)
 	add_library(VCAI STATIC ${VCAI_SRCS} ${VCAI_HEADERS})
 else()
 	add_library(VCAI SHARED ${VCAI_SRCS} ${VCAI_HEADERS})
@@ -107,7 +107,7 @@ else()
 endif()
 
 target_include_directories(VCAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(VCAI PUBLIC ${VCMI_LIB_TARGET} fuzzylite::fuzzylite)
+target_link_libraries(VCAI PUBLIC vcmi fuzzylite::fuzzylite)
 
 vcmi_set_output_dir(VCAI "AI")
 enable_pch(VCAI)

+ 46 - 45
AI/VCAI/Goals/AbstractGoal.cpp

@@ -109,51 +109,52 @@ bool AbstractGoal::operator==(const AbstractGoal & g) const
 	return false;
 }
 
-bool AbstractGoal::operator<(AbstractGoal & g) //for std::unique
-{
-	//TODO: make sure it gets goals consistent with == operator
-	if (goalType < g.goalType)
-		return true;
-	if (goalType > g.goalType)
-		return false;
-	if (hero < g.hero)
-		return true;
-	if (hero > g.hero)
-		return false;
-	if (tile < g.tile)
-		return true;
-	if (g.tile < tile)
-		return false;
-	if (objid < g.objid)
-		return true;
-	if (objid > g.objid)
-		return false;
-	if (town < g.town)
-		return true;
-	if (town > g.town)
-		return false;
-	if (value < g.value)
-		return true;
-	if (value > g.value)
-		return false;
-	if (priority < g.priority)
-		return true;
-	if (priority > g.priority)
-		return false;
-	if (resID < g.resID)
-		return true;
-	if (resID > g.resID)
-		return false;
-	if (bid < g.bid)
-		return true;
-	if (bid > g.bid)
-		return false;
-	if (aid < g.aid)
-		return true;
-	if (aid > g.aid)
-		return false;
-	return false;
-}
+// FIXME: unused code?
+//bool AbstractGoal::operator<(AbstractGoal & g) //for std::unique
+//{
+//	//TODO: make sure it gets goals consistent with == operator
+//	if (goalType < g.goalType)
+//		return true;
+//	if (goalType > g.goalType)
+//		return false;
+//	if (hero < g.hero)
+//		return true;
+//	if (hero > g.hero)
+//		return false;
+//	if (tile < g.tile)
+//		return true;
+//	if (g.tile < tile)
+//		return false;
+//	if (objid < g.objid)
+//		return true;
+//	if (objid > g.objid)
+//		return false;
+//	if (town < g.town)
+//		return true;
+//	if (town > g.town)
+//		return false;
+//	if (value < g.value)
+//		return true;
+//	if (value > g.value)
+//		return false;
+//	if (priority < g.priority)
+//		return true;
+//	if (priority > g.priority)
+//		return false;
+//	if (resID < g.resID)
+//		return true;
+//	if (resID > g.resID)
+//		return false;
+//	if (bid < g.bid)
+//		return true;
+//	if (bid > g.bid)
+//		return false;
+//	if (aid < g.aid)
+//		return true;
+//	if (aid > g.aid)
+//		return false;
+//	return false;
+//}
 
 //TODO: find out why the following are not generated automatically on MVS?
 bool TSubgoal::operator==(const TSubgoal & rhs) const

+ 5 - 5
AI/VCAI/Goals/AbstractGoal.h

@@ -165,16 +165,16 @@ namespace Goals
 		virtual float accept(FuzzyHelper * f);
 
 		virtual bool operator==(const AbstractGoal & g) const;
-		bool operator<(AbstractGoal & g); //final
+//		bool operator<(AbstractGoal & g); //final
 		virtual bool fulfillsMe(Goals::TSubgoal goal) //TODO: multimethod instead of type check
 		{
 			return false; //use this method to check if goal is fulfilled by another (not equal) goal, operator == is handled spearately
 		}
 
-		bool operator!=(const AbstractGoal & g) const
-		{
-			return !(*this == g);
-		}
+//		bool operator!=(const AbstractGoal & g) const
+//		{
+//			return !(*this == g);
+//		}
 
 		template<typename Handler> void serialize(Handler & h)
 		{

+ 1 - 1
AI/VCAI/Goals/AdventureSpellCast.h

@@ -39,6 +39,6 @@ namespace Goals
 		void accept(VCAI * ai) override;
 		std::string name() const override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const AdventureSpellCast & other) const override;
+		bool operator==(const AdventureSpellCast & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/Build.h

@@ -29,7 +29,7 @@ namespace Goals
 		TSubgoal whatToDoToAchieve() override;
 		bool fulfillsMe(TSubgoal goal) override;
 
-		virtual bool operator==(const Build & other) const override
+		bool operator==(const Build & other) const override
 		{
 			return true;
 		}

Some files were not shown because too many files changed in this diff