Преглед изворни кода

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

Dydzio пре 1 година
родитељ
комит
70b86e5a87
100 измењених фајлова са 1534 додато и 711 уклоњено
  1. 9 0
      AI/EmptyAI/StdInc.cpp
  2. 9 0
      AI/EmptyAI/StdInc.h
  3. 0 21
      AI/Nullkiller/AIUtility.h
  4. 5 4
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  5. 8 0
      AI/Nullkiller/Analyzers/HeroManager.cpp
  6. 14 29
      AI/Nullkiller/Analyzers/HeroManager.h
  7. 96 52
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  8. 9 4
      AI/Nullkiller/Analyzers/ObjectClusterizer.h
  9. 3 1
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  10. 1 0
      AI/Nullkiller/Behaviors/ClusterBehavior.cpp
  11. 2 2
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  12. 6 1
      AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp
  13. 1 0
      AI/Nullkiller/CMakeLists.txt
  14. 2 3
      AI/Nullkiller/Goals/Build.cpp
  15. 3 3
      AI/Nullkiller/Goals/Build.h
  16. 45 21
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  17. 2 3
      AI/Nullkiller/Goals/GatherArmy.cpp
  18. 3 3
      AI/Nullkiller/Goals/GatherArmy.h
  19. 202 183
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  20. 96 29
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  21. 26 10
      AI/Nullkiller/Pathfinding/AIPathfinder.cpp
  22. 12 2
      AI/Nullkiller/Pathfinding/AIPathfinder.h
  23. 19 4
      AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp
  24. 15 0
      AI/Nullkiller/Pathfinding/Actions/BoatActions.h
  25. 5 0
      AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp
  26. 1 1
      AI/Nullkiller/Pathfinding/Actions/QuestAction.h
  27. 13 0
      AI/Nullkiller/Pathfinding/Actions/SpecialAction.h
  28. 277 51
      AI/Nullkiller/Pathfinding/ObjectGraph.cpp
  29. 21 0
      AI/Nullkiller/Pathfinding/ObjectGraph.h
  30. 4 1
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  31. 4 1
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h
  32. 10 0
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
  33. 9 0
      AI/Nullkiller/StdInc.cpp
  34. 9 0
      AI/Nullkiller/StdInc.h
  35. 50 0
      AI/Nullkiller/pforeach.h
  36. 9 0
      AI/StupidAI/StdInc.cpp
  37. 9 0
      AI/StupidAI/StdInc.h
  38. 1 1
      AI/VCAI/AIhelper.cpp
  39. 9 0
      AI/VCAI/MapObjectsEvaluator.cpp
  40. 11 7
      AI/VCAI/Pathfinding/AINodeStorage.cpp
  41. 3 1
      AI/VCAI/Pathfinding/AINodeStorage.h
  42. 9 0
      AI/VCAI/StdInc.cpp
  43. 9 0
      AI/VCAI/StdInc.h
  44. 1 1
      CI/linux-qt6/before_install.sh
  45. 1 1
      CI/linux/before_install.sh
  46. 40 23
      CMakeLists.txt
  47. 1 0
      Mods/vcmi/config/vcmi/english.json
  48. 1 0
      Mods/vcmi/config/vcmi/ukrainian.json
  49. 1 1
      client/CFocusableHelper.cpp
  50. 1 0
      client/CFocusableHelper.h
  51. 13 2
      client/CMT.cpp
  52. 1 1
      client/Client.cpp
  53. 4 0
      client/NetPacksClient.cpp
  54. 9 0
      client/StdInc.cpp
  55. 9 0
      client/StdInc.h
  56. 69 26
      client/globalLobby/GlobalLobbyClient.cpp
  57. 12 0
      client/globalLobby/GlobalLobbyClient.h
  58. 17 5
      client/globalLobby/GlobalLobbyLoginWindow.cpp
  59. 1 0
      client/globalLobby/GlobalLobbyLoginWindow.h
  60. 1 1
      client/globalLobby/GlobalLobbyWidget.cpp
  61. 5 2
      client/globalLobby/GlobalLobbyWindow.cpp
  62. 1 1
      client/gui/FramerateManager.cpp
  63. 1 0
      client/ios/GameChatKeyboardHandler.h
  64. 1 1
      client/ios/startSDL.mm
  65. 1 1
      client/render/Colors.h
  66. 3 3
      client/widgets/CExchangeController.h
  67. 4 40
      config/schemas/settings.json
  68. 1 7
      debian/control
  69. 1 3
      debian/rules
  70. 9 0
      launcher/StdInc.cpp
  71. 9 0
      launcher/StdInc.h
  72. 0 8
      launcher/modManager/cmodlist.h
  73. 1 4
      launcher/modManager/imageviewer_moc.h
  74. 98 80
      lib/CMakeLists.txt
  75. 9 0
      lib/StdInc.cpp
  76. 9 0
      lib/StdInc.h
  77. 3 2
      lib/VCMIDirs.cpp
  78. 2 1
      lib/battle/PossiblePlayerBattleAction.h
  79. 0 1
      lib/bonuses/IBonusBearer.h
  80. 2 0
      lib/json/JsonValidator.cpp
  81. 1 1
      lib/modding/CModVersion.cpp
  82. 37 34
      lib/pathfinder/CPathfinder.cpp
  83. 1 1
      lib/pathfinder/CPathfinder.h
  84. 3 1
      lib/pathfinder/INodeStorage.h
  85. 13 13
      lib/pathfinder/NodeStorage.cpp
  86. 3 1
      lib/pathfinder/NodeStorage.h
  87. 1 0
      lib/spells/ObstacleCasterProxy.h
  88. 3 3
      lib/spells/ViewSpellInt.h
  89. 9 0
      lib/vstd/DateUtils.cpp
  90. 9 0
      lib/vstd/StringUtils.cpp
  91. 1 1
      lobby/EntryPoint.cpp
  92. 4 1
      lobby/LobbyServer.cpp
  93. 9 0
      lobby/StdInc.cpp
  94. 9 0
      mapeditor/StdInc.cpp
  95. 9 0
      mapeditor/StdInc.h
  96. 1 1
      mapeditor/main.cpp
  97. 1 1
      mapeditor/mapsettings/victoryconditions.cpp
  98. 9 0
      scripting/erm/StdInc.cpp
  99. 9 0
      scripting/erm/StdInc.h
  100. 9 0
      scripting/lua/StdInc.cpp

+ 9 - 0
AI/EmptyAI/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.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
+ *
+ */
 // Creates the precompiled header
 #include "StdInc.h"

+ 9 - 0
AI/EmptyAI/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.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 "../../Global.h"

+ 0 - 21
AI/Nullkiller/AIUtility.h

@@ -50,7 +50,6 @@
 
 #include <chrono>
 
-using namespace tbb;
 
 using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
@@ -246,26 +245,6 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
 // todo: move to obj manager
 bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObjectInstance * obj);
 
-template<typename TFunc>
-void pforeachTilePos(const int3 & mapSize, TFunc fn)
-{
-	for(int z = 0; z < mapSize.z; ++z)
-	{
-		parallel_for(blocked_range<size_t>(0, mapSize.x), [&](const blocked_range<size_t>& r)
-		{
-			int3 pos(0, 0, z);
-
-			for(pos.x = r.begin(); pos.x != r.end(); ++pos.x)
-			{
-				for(pos.y = 0; pos.y < mapSize.y; ++pos.y)
-				{
-					fn(pos);
-				}
-			}
-		});
-	}
-}
-
 class CDistanceSorter
 {
 	const CGHeroInstance * hero;

+ 5 - 4
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -11,6 +11,7 @@
 #include "DangerHitMapAnalyzer.h"
 
 #include "../Engine/Nullkiller.h"
+#include "../pforeach.h"
 #include "../../../lib/CRandomGenerator.h"
 
 namespace NKAI
@@ -82,9 +83,9 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 		boost::this_thread::interruption_point();
 
-		pforeachTilePos(mapSize, [&](const int3 & pos)
+		pforeachTilePaths(mapSize, ai, [&](const int3 & pos, const std::vector<AIPath> & paths)
 		{
-			for(AIPath & path : ai->pathfinder->getPathInfo(pos))
+			for(const AIPath & path : paths)
 			{
 				if(path.getFirstBlockedAction())
 					continue;
@@ -194,14 +195,14 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 
 	ai->pathfinder->updatePaths(townHeroes, ps);
 
-	pforeachTilePos(mapSize, [&](const int3 & pos)
+	pforeachTilePaths(mapSize, ai, [&](const int3 & pos, const std::vector<AIPath> & paths)
 		{
 			float ourDistance = std::numeric_limits<float>::max();
 			float enemyDistance = std::numeric_limits<float>::max();
 			const CGTownInstance * enemyTown = nullptr;
 			const CGTownInstance * ourTown = nullptr;
 
-			for(AIPath & path : ai->pathfinder->getPathInfo(pos))
+			for(const AIPath & path : paths)
 			{
 				if(!path.targetHero || path.getFirstBlockedAction())
 					continue;

+ 8 - 0
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -109,6 +109,7 @@ void HeroManager::update()
 	for(auto & hero : myHeroes)
 	{
 		scores[hero] = evaluateFightingStrength(hero);
+		knownFightingStrength[hero->id] = hero->getFightingStrength();
 	}
 
 	auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
@@ -192,6 +193,13 @@ bool HeroManager::heroCapReached() const
 		|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
 }
 
+float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
+{
+	auto cached = knownFightingStrength.find(hero->id);
+
+	return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength();
+}
+
 float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
 {
 	auto hasFly = hero->spellbookContainsSpell(SpellID::FLY);

+ 14 - 29
AI/Nullkiller/Analyzers/HeroManager.h

@@ -20,23 +20,6 @@
 namespace NKAI
 {
 
-class DLL_EXPORT IHeroManager //: public: IAbstractManager
-{
-public:
-	virtual ~IHeroManager() = default;
-	virtual const std::map<HeroPtr, HeroRole> & getHeroRoles() const = 0;
-	virtual int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const = 0;
-	virtual HeroRole getHeroRole(const HeroPtr & hero) const = 0;
-	virtual void update() = 0;
-	virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
-	virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
-	virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
-	virtual bool heroCapReached() const = 0;
-	virtual const CGHeroInstance * findHeroWithGrail() const = 0;
-	virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0;
-	virtual float getMagicStrength(const CGHeroInstance * hero) const = 0;
-};
-
 class DLL_EXPORT ISecondarySkillRule
 {
 public:
@@ -55,7 +38,7 @@ public:
 	float evaluateSecSkill(const CGHeroInstance * hero, SecondarySkill skill) const;
 };
 
-class DLL_EXPORT HeroManager : public IHeroManager
+class DLL_EXPORT HeroManager
 {
 private:
 	static const SecondarySkillEvaluator wariorSkillsScores;
@@ -64,20 +47,22 @@ private:
 	CCallback * cb; //this is enough, but we downcast from CCallback
 	const Nullkiller * ai;
 	std::map<HeroPtr, HeroRole> heroRoles;
+	std::map<ObjectInstanceID, float> knownFightingStrength;
 
 public:
 	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB), ai(ai) {}
-	const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
-	HeroRole getHeroRole(const HeroPtr & hero) const override;
-	int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;
-	void update() override;
-	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
-	float evaluateHero(const CGHeroInstance * hero) const override;
-	bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
-	bool heroCapReached() const override;
-	const CGHeroInstance * findHeroWithGrail() const override;
-	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override;
-	float getMagicStrength(const CGHeroInstance * hero) const override;
+	const std::map<HeroPtr, HeroRole> & getHeroRoles() const;
+	HeroRole getHeroRole(const HeroPtr & hero) const;
+	int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const;
+	void update();
+	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
+	float evaluateHero(const CGHeroInstance * hero) const;
+	bool canRecruitHero(const CGTownInstance * t = nullptr) const;
+	bool heroCapReached() const;
+	const CGHeroInstance * findHeroWithGrail() const;
+	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const;
+	float getMagicStrength(const CGHeroInstance * hero) const;
+	float getFightingStrengthCached(const CGHeroInstance * hero) const;
 
 private:
 	float evaluateFightingStrength(const CGHeroInstance * hero) const;

+ 96 - 52
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -90,64 +90,74 @@ std::vector<std::shared_ptr<ObjectCluster>> ObjectClusterizer::getLockedClusters
 	return result;
 }
 
-const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) const
+std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPathNodeInfo & node) const
 {
-	for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++)
+	std::vector<const CGObjectInstance *> blockers = {};
+
+	if(node.layer == EPathfindingLayer::LAND || node.layer == EPathfindingLayer::SAIL)
 	{
-		std::vector<const CGObjectInstance *> blockers = {};
+		auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
+
+		blockers = ai->cb->getVisitableObjs(node.coord);
 
-		if(node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL)
+		if(guardPos.valid())
 		{
-			auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord);
-			
-			blockers = ai->cb->getVisitableObjs(node->coord);
+			auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
 
-			if(guardPos.valid())
+			if(guard)
 			{
-				auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord));
-
-				if(guard)
-				{
-					blockers.insert(blockers.begin(), guard);
-				}
+				blockers.insert(blockers.begin(), guard);
 			}
 		}
+	}
 
-		if(node->specialAction && node->actionIsBlocked)
-		{
-			auto blockerObject = node->specialAction->targetObject();
+	if(node.specialAction && node.actionIsBlocked)
+	{
+		auto blockerObject = node.specialAction->targetObject();
 
-			if(blockerObject)
-			{
-				blockers.insert(blockers.begin(), blockerObject);
-			}
+		if(blockerObject)
+		{
+			blockers.insert(blockers.begin(), blockerObject);
 		}
+	}
 
-		if(blockers.empty())
-			continue;
-
-		auto blocker = blockers.front();
+	if(blockers.empty())
+		return std::optional< const CGObjectInstance *>();
 
-		if(isObjectPassable(ai, blocker))
-			continue;
+	auto blocker = blockers.front();
 
-		if(blocker->ID == Obj::GARRISON
-			|| blocker->ID == Obj::GARRISON2)
-		{
-			if(dynamic_cast<const CArmedInstance *>(blocker)->getArmyStrength() == 0)
-				continue;
-			else
-				return blocker;
-		}
+	if(isObjectPassable(ai, blocker))
+		return std::optional< const CGObjectInstance *>();
 
-		if(blocker->ID == Obj::MONSTER
-			|| blocker->ID == Obj::BORDERGUARD
-			|| blocker->ID == Obj::BORDER_GATE
-			|| blocker->ID == Obj::SHIPYARD
-			|| (blocker->ID == Obj::QUEST_GUARD && node->actionIsBlocked))
-		{
+	if(blocker->ID == Obj::GARRISON
+		|| blocker->ID == Obj::GARRISON2)
+	{
+		if(dynamic_cast<const CArmedInstance *>(blocker)->getArmyStrength() == 0)
+			return std::optional< const CGObjectInstance *>();
+		else
 			return blocker;
-		}
+	}
+
+	if(blocker->ID == Obj::MONSTER
+		|| blocker->ID == Obj::BORDERGUARD
+		|| blocker->ID == Obj::BORDER_GATE
+		|| blocker->ID == Obj::SHIPYARD
+		|| (blocker->ID == Obj::QUEST_GUARD && node.actionIsBlocked))
+	{
+		return blocker;
+	}
+
+	return std::optional< const CGObjectInstance *>();
+}
+
+const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) const
+{
+	for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++)
+	{
+		auto blocker = getBlocker(*node);
+
+		if(blocker)
+			return *blocker;
 	}
 
 	return nullptr;
@@ -225,15 +235,17 @@ void ObjectClusterizer::clusterize()
 		ai->memory->visitableObjs.end());
 
 #if NKAI_TRACE_LEVEL == 0
-	parallel_for(blocked_range<size_t>(0, objs.size()), [&](const blocked_range<size_t> & r) {
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, objs.size()), [&](const tbb::blocked_range<size_t> & r) {
 #else
-	blocked_range<size_t> r(0, objs.size());
+	tbb::blocked_range<size_t> r(0, objs.size());
 #endif
 		auto priorityEvaluator = ai->priorityEvaluators->acquire();
+		auto heroes = ai->cb->getHeroesInfo();
+		std::vector<AIPath> pathCache;
 
 		for(int i = r.begin(); i != r.end(); i++)
 		{
-			clusterizeObject(objs[i], priorityEvaluator.get());
+			clusterizeObject(objs[i], priorityEvaluator.get(), pathCache, heroes);
 		}
 #if NKAI_TRACE_LEVEL == 0
 	});
@@ -257,7 +269,11 @@ void ObjectClusterizer::clusterize()
 	logAi->trace("Clusterization complete in %ld", timeElapsed(start));
 }
 
-void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator)
+void ObjectClusterizer::clusterizeObject(
+	const CGObjectInstance * obj,
+	PriorityEvaluator * priorityEvaluator,
+	std::vector<AIPath> & pathCache,
+	std::vector<const CGHeroInstance *> & heroes)
 {
 	if(!shouldVisitObject(obj))
 	{
@@ -271,9 +287,14 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
 	logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
 #endif
 
-	auto paths = ai->pathfinder->getPathInfo(obj->visitablePos(), true);
+	if(ai->settings->isObjectGraphAllowed())
+	{
+		ai->pathfinder->calculateQuickPathsWithBlocker(pathCache, heroes, obj->visitablePos());
+	}
+	else
+		ai->pathfinder->calculatePathInfo(pathCache, obj->visitablePos(), false);
 
-	if(paths.empty())
+	if(pathCache.empty())
 	{
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace("No paths found.");
@@ -281,17 +302,17 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
 		return;
 	}
 
-	std::sort(paths.begin(), paths.end(), [](const AIPath & p1, const AIPath & p2) -> bool
+	std::sort(pathCache.begin(), pathCache.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);
+		farObjects.addObject(obj, pathCache.front(), 0);
 
 #if NKAI_TRACE_LEVEL >= 2
-		logAi->trace("Object ignored. Moved to far objects with path %s", paths.front().toString());
+		logAi->trace("Object ignored. Moved to far objects with path %s", pathCache.front().toString());
 #endif
 
 		return;
@@ -299,12 +320,35 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
 
 	std::set<const CGHeroInstance *> heroesProcessed;
 
-	for(auto & path : paths)
+	for(auto & path : pathCache)
 	{
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace("Checking path %s", path.toString());
 #endif
 
+		if(ai->heroManager->getHeroRole(path.targetHero) == HeroRole::SCOUT)
+		{
+			if(path.movementCost() > 2.0f)
+			{
+#if NKAI_TRACE_LEVEL >= 2
+				logAi->trace("Path is too far %f", path.movementCost());
+#endif
+				continue;
+			}
+		}
+		else if(path.movementCost() > 4.0f && obj->ID != Obj::TOWN)
+		{
+			auto strategicalValue = valueEvaluator.getStrategicalValue(obj);
+
+			if(strategicalValue < 0.3f)
+			{
+#if NKAI_TRACE_LEVEL >= 2
+				logAi->trace("Object value is too low %f", strategicalValue);
+#endif
+				continue;
+			}
+		}
+
 		if(!shouldVisit(ai, path.targetHero, obj))
 		{
 #if NKAI_TRACE_LEVEL >= 2

+ 9 - 4
AI/Nullkiller/Analyzers/ObjectClusterizer.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "../Pathfinding/AINodeStorage.h"
+#include "../Engine/PriorityEvaluator.h"
 
 namespace NKAI
 {
@@ -49,8 +50,6 @@ public:
 
 using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
 
-class PriorityEvaluator;
-
 class ObjectClusterizer
 {
 private:
@@ -60,6 +59,7 @@ private:
 	ObjectCluster farObjects;
 	ClusterMap blockedObjects;
 	const Nullkiller * ai;
+	RewardEvaluator valueEvaluator;
 
 public:
 	void clusterize();
@@ -67,12 +67,17 @@ public:
 	std::vector<const CGObjectInstance *> getFarObjects() const;
 	std::vector<std::shared_ptr<ObjectCluster>> getLockedClusters() const;
 	const CGObjectInstance * getBlocker(const AIPath & path) const;
+	std::optional<const CGObjectInstance *> getBlocker(const AIPathNodeInfo & node) const;
 
-	ObjectClusterizer(const Nullkiller * ai): ai(ai) {}
+	ObjectClusterizer(const Nullkiller * ai): ai(ai), valueEvaluator(ai) {}
 
 private:
 	bool shouldVisitObject(const CGObjectInstance * obj) const;
-	void clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator);
+	void clusterizeObject(
+		const CGObjectInstance * obj,
+		PriorityEvaluator * priorityEvaluator,
+		std::vector<AIPath> & pathCache,
+		std::vector<const CGHeroInstance *> & heroes);
 };
 
 }

+ 3 - 1
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -180,6 +180,8 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 
 		logAi->debug("Scanning objects, count %d", objs.size());
 
+		std::vector<AIPath> paths;
+
 		for(auto objToVisit : objs)
 		{
 			if(!objectMatchesFilter(objToVisit))
@@ -193,7 +195,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 			bool useObjectGraph = ai->nullkiller->settings->isObjectGraphAllowed()
 				&& ai->nullkiller->getScanDepth() != ScanDepth::SMALL;
 
-			auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, useObjectGraph);
+			ai->nullkiller->pathfinder->calculatePathInfo(paths, pos, useObjectGraph);
 
 			std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
 			std::shared_ptr<ExecuteHeroChain> closestWay;

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

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

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

@@ -69,7 +69,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 	logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString());
 #endif
 
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
+	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, ai->nullkiller->settings->isObjectGraphAllowed());
 
 #if NKAI_TRACE_LEVEL >= 1
 	logAi->trace("Gather army found %d paths", paths.size());
@@ -231,7 +231,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 	logAi->trace("Checking ways to upgrade army in town %s, %s", upgrader->getObjectName(), pos.toString());
 #endif
 	
-	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
+	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, ai->nullkiller->settings->isObjectGraphAllowed());
 	auto goals = CaptureObjectsBehavior::getVisitGoals(paths);
 
 	std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;

+ 6 - 1
AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp

@@ -35,18 +35,23 @@ Goals::TGoalVec StayAtTownBehavior::decompose() const
 	if(!towns.size())
 		return tasks;
 
+	std::vector<AIPath> paths;
+
 	for(auto town : towns)
 	{
 		if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
 			continue;
 
-		auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos());
+		ai->nullkiller->pathfinder->calculatePathInfo(paths, town->visitablePos());
 
 		for(auto & path : paths)
 		{
 			if(town->visitingHero && town->visitingHero.get() != path.targetHero)
 				continue;
 
+			if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit())
+				continue;
+
 			if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
 			{
 				if(path.targetHero->mana == path.targetHero->manaLimit())

+ 1 - 0
AI/Nullkiller/CMakeLists.txt

@@ -81,6 +81,7 @@ set(Nullkiller_HEADERS
 		Pathfinding/Rules/AIPreviousNodeRule.h
 		Pathfinding/ObjectGraph.h
 		AIUtility.h
+		pforeach.h
 		Analyzers/ArmyManager.h
 		Analyzers/HeroManager.h
 		Engine/Settings.h

+ 2 - 3
AI/Nullkiller/Goals/Build.cpp

@@ -1,6 +1,3 @@
-namespace Nullkiller
-{
-
 /*
 * Build.cpp, part of VCMI engine
 *
@@ -23,6 +20,8 @@ namespace Nullkiller
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/StringConstants.h"
 
+namespace Nullkiller
+{
 
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<AIGateway> ai;

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

@@ -1,6 +1,3 @@
-namespace Nullkiller
-{
-
 /*
 * Build.h, part of VCMI engine
 *
@@ -14,6 +11,9 @@ namespace Nullkiller
 
 #include "CGoal.h"
 
+namespace Nullkiller
+{
+
 struct HeroPtr;
 class AIGateway;
 class FuzzyHelper;

+ 45 - 21
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -67,40 +67,40 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 
 	for(int i = chainPath.nodes.size() - 1; i >= 0; i--)
 	{
-		auto & node = chainPath.nodes[i];
+		auto  * node = &chainPath.nodes[i];
 
-		const CGHeroInstance * hero = node.targetHero;
+		const CGHeroInstance * hero = node->targetHero;
 		HeroPtr heroPtr = hero;
 
-		if(node.parentIndex >= i)
+		if(node->parentIndex >= i)
 		{
-			logAi->error("Invalid parentIndex while executing node " + node.coord.toString());
+			logAi->error("Invalid parentIndex while executing node " + node->coord.toString());
 		}
 
 		if(vstd::contains(blockedIndexes, i))
 		{
-			blockedIndexes.insert(node.parentIndex);
+			blockedIndexes.insert(node->parentIndex);
 			ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 
 			continue;
 		}
 
-		logAi->debug("Executing chain node %d. Moving hero %s to %s", i, hero->getNameTranslated(), node.coord.toString());
+		logAi->debug("Executing chain node %d. Moving hero %s to %s", i, hero->getNameTranslated(), node->coord.toString());
 
 		try
 		{
 			if(hero->movementPointsRemaining() > 0)
 			{
-				ai->nullkiller->setActive(hero, node.coord);
+				ai->nullkiller->setActive(hero, node->coord);
 
-				if(node.specialAction)
+				if(node->specialAction)
 				{
-					if(node.actionIsBlocked)
+					if(node->actionIsBlocked)
 					{
 						throw cannotFulfillGoalException("Path is nondeterministic.");
 					}
 					
-					node.specialAction->execute(hero);
+					node->specialAction->execute(hero);
 					
 					if(!heroPtr.validAndSet())
 					{
@@ -109,10 +109,34 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						return;
 					}
 				}
+				else if(i > 0 && ai->nullkiller->settings->isObjectGraphAllowed())
+				{
+					auto chainMask = i < chainPath.nodes.size() - 1 ? chainPath.nodes[i + 1].chainMask : node->chainMask;
+
+					for(auto j = i - 1; j >= 0; j--)
+					{
+						auto & nextNode = chainPath.nodes[j];
+
+						if(nextNode.specialAction || nextNode.chainMask != chainMask)
+							break;
+
+						auto targetNode = cb->getPathsInfo(hero)->getPathInfo(nextNode.coord);
+
+						if(!targetNode->reachable()
+							|| targetNode->getCost() > nextNode.cost)
+							break;
+
+						i = j;
+						node = &nextNode;
+
+						if(targetNode->action == EPathNodeAction::BATTLE || targetNode->action == EPathNodeAction::TELEPORT_BATTLE)
+							break;
+					}
+				}
 
-				if(node.turns == 0 && node.coord != hero->visitablePos())
+				if(node->turns == 0 && node->coord != hero->visitablePos())
 				{
-					auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node.coord);
+					auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node->coord);
 
 					if(targetNode->accessible == EPathAccessibility::NOT_SET
 						|| targetNode->accessible == EPathAccessibility::BLOCKED
@@ -122,7 +146,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						logAi->error(
 							"Unable to complete chain. Expected hero %s to arrive to %s in 0 turns but he cannot do this",
 							hero->getNameTranslated(),
-							node.coord.toString());
+							node->coord.toString());
 
 						return;
 					}
@@ -132,7 +156,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 				{
 					try
 					{
-						if(moveHeroToTile(hero, node.coord))
+						if(moveHeroToTile(hero, node->coord))
 						{
 							continue;
 						}
@@ -149,11 +173,11 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						if(hero->movementPointsRemaining() > 0)
 						{
 							CGPath path;
-							bool isOk = cb->getPathsInfo(hero)->getPath(path, node.coord);
+							bool isOk = cb->getPathsInfo(hero)->getPath(path, node->coord);
 
 							if(isOk && path.nodes.back().turns > 0)
 							{
-								logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movementPointsRemaining(), node.coord.toString());
+								logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movementPointsRemaining(), node->coord.toString());
 
 								ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 								return;
@@ -165,15 +189,15 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 				}
 			}
 
-			if(node.coord == hero->visitablePos())
+			if(node->coord == hero->visitablePos())
 				continue;
 
-			if(node.turns == 0)
+			if(node->turns == 0)
 			{
 				logAi->error(
 					"Unable to complete chain. Expected hero %s to arive to %s but he is at %s",
 					hero->getNameTranslated(),
-					node.coord.toString(),
+					node->coord.toString(),
 					hero->visitablePos().toString());
 
 				return;
@@ -181,13 +205,13 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 			
 			// no exception means we were not able to reach the tile
 			ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
-			blockedIndexes.insert(node.parentIndex);
+			blockedIndexes.insert(node->parentIndex);
 		}
 		catch(const goalFulfilledException &)
 		{
 			if(!heroPtr.validAndSet())
 			{
-				logAi->debug("Hero %s was killed while attempting to reach %s", heroPtr.name, node.coord.toString());
+				logAi->debug("Hero %s was killed while attempting to reach %s", heroPtr.name, node->coord.toString());
 
 				return;
 			}

+ 2 - 3
AI/Nullkiller/Goals/GatherArmy.cpp

@@ -1,6 +1,3 @@
-namespace Nullkiller
-{
-
 /*
 * GatherArmy.cpp, part of VCMI engine
 *
@@ -22,6 +19,8 @@ namespace Nullkiller
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/StringConstants.h"
 
+namespace Nullkiller
+{
 
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<AIGateway> ai;

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

@@ -1,6 +1,3 @@
-namespace Nullkiller
-{
-
 /*
 * GatherArmy.h, part of VCMI engine
 *
@@ -14,6 +11,9 @@ namespace Nullkiller
 
 #include "CGoal.h"
 
+namespace Nullkiller
+{
+
 struct HeroPtr;
 class AIGateway;
 class FuzzyHelper;

+ 202 - 183
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -24,7 +24,8 @@
 namespace NKAI
 {
 
-std::shared_ptr<boost::multi_array<AIPathNode, 5>> AISharedStorage::shared;
+std::shared_ptr<boost::multi_array<AIPathNode, 4>> AISharedStorage::shared;
+uint64_t AISharedStorage::version = 0;
 boost::mutex AISharedStorage::locker;
 std::set<int3> commitedTiles;
 std::set<int3> commitedTilesInitial;
@@ -40,11 +41,24 @@ const bool DO_NOT_SAVE_TO_COMMITED_TILES = false;
 AISharedStorage::AISharedStorage(int3 sizes)
 {
 	if(!shared){
-		shared.reset(new boost::multi_array<AIPathNode, 5>(
-			boost::extents[EPathfindingLayer::NUM_LAYERS][sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
-	}
+		shared.reset(new boost::multi_array<AIPathNode, 4>(
+			boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
+
+		nodes = shared;
 
-	nodes = shared;
+		foreach_tile_pos([&](const int3 & pos)
+			{
+				for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++)
+				{
+					auto & node = get(pos)[i];
+						
+					node.version = -1;
+					node.coord = pos;
+				}
+			});
+	}
+	else
+		nodes = shared;
 }
 
 AISharedStorage::~AISharedStorage()
@@ -80,6 +94,9 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
 AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
 	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
 {
+	accesibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
+		boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
+
 	dangerEvaluator.reset(new FuzzyHelper(ai));
 }
 
@@ -90,6 +107,8 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
 	if(heroChainPass)
 		return;
 
+	AISharedStorage::version++;
+
 	//TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline
 	const PlayerColor fowPlayer = ai->playerID;
 	const auto & fow = static_cast<const CGameInfoCallback *>(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap;
@@ -97,7 +116,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
 
 	//Each thread gets different x, but an array of y located next to each other in memory
 
-	parallel_for(blocked_range<size_t>(0, sizes.x), [&](const blocked_range<size_t>& r)
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, sizes.x), [&](const tbb::blocked_range<size_t>& r)
 	{
 		int3 pos;
 
@@ -152,9 +171,9 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 {
 	int bucketIndex = ((uintptr_t)actor) % AIPathfinding::BUCKET_COUNT;
 	int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
-	auto chains = nodes.get(pos, layer);
+	auto chains = nodes.get(pos);
 
-	if(chains[0].blocked())
+	if(blocked(pos, layer))
 	{
 		return std::nullopt;
 	}
@@ -163,15 +182,17 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 	{
 		AIPathNode & node = chains[i + bucketOffset];
 
-		if(node.actor == actor)
+		if(node.version != AISharedStorage::version)
 		{
+			node.reset(layer, getAccessibility(pos, layer));
+			node.version = AISharedStorage::version;
+			node.actor = actor;
+
 			return &node;
 		}
 
-		if(!node.actor)
+		if(node.actor == actor && node.layer == layer)
 		{
-			node.actor = actor;
-
 			return &node;
 		}
 	}
@@ -226,21 +247,6 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 	return initialNodes;
 }
 
-void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, EPathAccessibility accessibility)
-{
-	for(AIPathNode & heroNode : nodes.get(coord, layer))
-{
-		heroNode.actor = nullptr;
-		heroNode.danger = 0;
-		heroNode.manaCost = 0;
-		heroNode.specialAction.reset();
-		heroNode.armyLoss = 0;
-		heroNode.chainOther = nullptr;
-		heroNode.dayFlags = DayFlags::NONE;
-		heroNode.update(coord, layer, accessibility);
-	}
-}
-
 void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInfo & source)
 {
 	const AIPathNode * srcNode = getAINode(source.node);
@@ -306,30 +312,31 @@ void AINodeStorage::commit(
 	}
 }
 
-std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
+void AINodeStorage::calculateNeighbours(
+	std::vector<CGPathNode *> & result,
 	const PathNodeInfo & source,
+	EPathfindingLayer layer,
 	const PathfinderConfig * pathfinderConfig,
 	const CPathfinderHelper * pathfinderHelper)
 {
-	std::vector<CGPathNode *> neighbours;
-	neighbours.reserve(16);
+	std::vector<int3> accessibleNeighbourTiles;
+
+	result.clear();
+	accessibleNeighbourTiles.reserve(8);
+
+	pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);
+
 	const AIPathNode * srcNode = getAINode(source.node);
-	auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
 
 	for(auto & neighbour : accessibleNeighbourTiles)
 	{
-		for(EPathfindingLayer i = EPathfindingLayer::LAND; i < EPathfindingLayer::NUM_LAYERS; i.advance(1))
-		{
-			auto nextNode = getOrCreateNode(neighbour, i, srcNode->actor);
+		auto nextNode = getOrCreateNode(neighbour, layer, srcNode->actor);
 
-			if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
-				continue;
+		if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
+			continue;
 
-			neighbours.push_back(nextNode.value());
-		}
+		result.push_back(nextNode.value());
 	}
-	
-	return neighbours;
 }
 
 constexpr std::array phisycalLayers = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
@@ -346,19 +353,16 @@ bool AINodeStorage::increaseHeroChainTurnLimit()
 	{
 		foreach_tile_pos([&](const int3 & pos)
 		{
-			auto chains = nodes.get(pos, layer);
-
-			if(!chains[0].blocked())
-			{
-				for(AIPathNode & node : chains)
+			iterateValidNodesUntil(pos, layer, [&](AIPathNode & node)
 				{
 					if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
 					{
 						commitedTiles.insert(pos);
-						break;
+						return true;
 					}
-				}
-			}
+
+					return false;
+				});
 		});
 	}
 
@@ -374,22 +378,17 @@ bool AINodeStorage::calculateHeroChainFinal()
 	{
 		foreach_tile_pos([&](const int3 & pos)
 		{
-			auto chains = nodes.get(pos, layer);
-
-			if(!chains[0].blocked())
-			{
-				for(AIPathNode & node : chains)
+			iterateValidNodes(pos, layer, [&](AIPathNode & node)
 				{
 					if(node.turns > heroChainTurn
 						&& !node.locked
 						&& node.action != EPathNodeAction::UNKNOWN
 						&& node.actor->actorExchangeCount > 1
-						&& !hasBetterChain(&node, &node, chains))
+						&& !hasBetterChain(&node, node))
 					{
 						heroChain.push_back(&node);
 					}
-				}
-			}
+				});
 		});
 	}
 
@@ -413,7 +412,6 @@ struct DelayedWork
 class HeroChainCalculationTask
 {
 private:
-	AISharedStorage & nodes;
 	AINodeStorage & storage;
 	std::vector<AIPathNode *> existingChains;
 	std::vector<ExchangeCandidate> newChains;
@@ -425,14 +423,14 @@ private:
 
 public:
 	HeroChainCalculationTask(
-		AINodeStorage & storage, AISharedStorage & nodes, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
-		:existingChains(), newChains(), delayedWork(), nodes(nodes), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
+		AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
+		:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
 	{
 		existingChains.reserve(AIPathfinding::NUM_CHAINS);
 		newChains.reserve(AIPathfinding::NUM_CHAINS);
 	}
 
-	void execute(const blocked_range<size_t>& r)
+	void execute(const tbb::blocked_range<size_t>& r)
 	{
 		std::random_device randomDevice;
 		std::mt19937 randomEngine(randomDevice());
@@ -443,21 +441,19 @@ public:
 
 			for(auto layer : phisycalLayers)
 			{
-				auto chains = nodes.get(pos, layer);
+				existingChains.clear();
 
-				// fast cut inactive nodes
-				if(chains[0].blocked())
+				storage.iterateValidNodes(pos, layer, [this](AIPathNode & node)
+					{
+						if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
+							existingChains.push_back(&node);
+					});
+
+				if(existingChains.empty())
 					continue;
 
-				existingChains.clear();
 				newChains.clear();
 
-				for(AIPathNode & node : chains)
-				{
-					if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
-						existingChains.push_back(&node);
-				}
-
 				std::shuffle(existingChains.begin(), existingChains.end(), randomEngine);
 
 				for(AIPathNode * node : existingChains)
@@ -530,10 +526,10 @@ bool AINodeStorage::calculateHeroChain()
 
 		std::shuffle(data.begin(), data.end(), randomEngine);
 
-		parallel_for(blocked_range<size_t>(0, data.size()), [&](const blocked_range<size_t>& r)
+		tbb::parallel_for(tbb::blocked_range<size_t>(0, data.size()), [&](const tbb::blocked_range<size_t>& r)
 		{
 			//auto r = blocked_range<size_t>(0, data.size());
-			HeroChainCalculationTask task(*this, nodes, data, chainMask, heroChainTurn);
+			HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
 
 			task.execute(r);
 
@@ -546,8 +542,8 @@ bool AINodeStorage::calculateHeroChain()
 	}
 	else
 	{
-		auto r = blocked_range<size_t>(0, data.size());
-		HeroChainCalculationTask task(*this, nodes, data, chainMask, heroChainTurn);
+		auto r = tbb::blocked_range<size_t>(0, data.size());
+		HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
 
 		task.execute(r);
 		task.flushResult(heroChain);
@@ -612,14 +608,20 @@ bool AINodeStorage::selectNextActor()
 	return false;
 }
 
+uint64_t AINodeStorage::evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const
+{
+	float fightingStrength = ai->heroManager->getFightingStrengthCached(hero);
+	double ratio = (double)danger / (armyValue * fightingStrength);
+
+	return (uint64_t)(armyValue * ratio * ratio);
+}
+
 void HeroChainCalculationTask::cleanupInefectiveChains(std::vector<ExchangeCandidate> & result) const
 {
 	vstd::erase_if(result, [&](const ExchangeCandidate & chainInfo) -> bool
 	{
-		auto pos = chainInfo.coord;
-		auto chains = nodes.get(pos, EPathfindingLayer::LAND);
-		auto isNotEffective = storage.hasBetterChain(chainInfo.carrierParent, &chainInfo, chains)
-			|| storage.hasBetterChain(chainInfo.carrierParent, &chainInfo, result);
+		auto isNotEffective = storage.hasBetterChain(chainInfo.carrierParent, chainInfo)
+			|| storage.hasBetterChain(chainInfo.carrierParent, chainInfo, result);
 
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 		if(isNotEffective)
@@ -645,7 +647,7 @@ void HeroChainCalculationTask::calculateHeroChain(
 {
 	for(AIPathNode * node : variants)
 	{
-		if(node == srcNode || !node->actor)
+		if(node == srcNode || !node->actor || node->version != AISharedStorage::version)
 			continue;
 
 		if((node->actor->chainMask & chainMask) == 0 && (srcNode->actor->chainMask & chainMask) == 0)
@@ -1174,7 +1176,7 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
 
 	if(actorsVector.size() * initialNodes.size() > 1000)
 	{
-		parallel_for(blocked_range<size_t>(0, actorsVector.size()), [&](const blocked_range<size_t> & r)
+		tbb::parallel_for(tbb::blocked_range<size_t>(0, actorsVector.size()), [&](const tbb::blocked_range<size_t> & r)
 			{
 				for(int i = r.begin(); i != r.end(); i++)
 				{
@@ -1195,95 +1197,116 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
 
 bool AINodeStorage::hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
 {
-	auto pos = destination.coord;
-	auto chains = nodes.get(pos, EPathfindingLayer::LAND);
+	auto candidateNode = getAINode(destination.node);
 
-	return hasBetterChain(source.node, getAINode(destination.node), chains);
+	return hasBetterChain(source.node, *candidateNode);
 }
 
-template<class NodeRange>
 bool AINodeStorage::hasBetterChain(
-	const CGPathNode * source, 
-	const AIPathNode * candidateNode,
-	const NodeRange & chains) const
+	const CGPathNode * source,
+	const AIPathNode & candidateNode) const
 {
-	auto candidateActor = candidateNode->actor;
+	return iterateValidNodesUntil(
+		candidateNode.coord,
+		candidateNode.layer,
+		[this, &source, candidateNode](const AIPathNode & node) -> bool
+		{
+			return isOtherChainBetter(source, candidateNode, node);
+		});
+}
 
-	for(const AIPathNode & node : chains)
+template<class NodeRange>
+bool AINodeStorage::hasBetterChain(
+	const CGPathNode * source,
+	const AIPathNode & candidateNode,
+	const NodeRange & nodes) const
+{
+	for(const AIPathNode & node : nodes)
 	{
-		auto sameNode = node.actor == candidateNode->actor;
-
-		if(sameNode	|| node.action == EPathNodeAction::UNKNOWN || !node.actor || !node.actor->hero)
-		{
-			continue;
-		}
+		if(isOtherChainBetter(source, candidateNode, node))
+			return true;
+	}
 
-		if(node.danger <= candidateNode->danger && candidateNode->actor == node.actor->battleActor)
-		{
-			if(node.getCost() < candidateNode->getCost())
-			{
-#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
-				logAi->trace(
-					"Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i",
-					source->coord.toString(),
-					candidateNode->coord.toString(),
-					candidateNode->actor->hero->getNameTranslated(),
-					candidateNode->actor->chainMask,
-					candidateNode->actor->armyValue,
-					node.moveRemains - candidateNode->moveRemains);
-#endif
-				return true;
-			}
-		}
+	return false;
+}
 
-		if(candidateActor->chainMask != node.actor->chainMask && heroChainPass != EHeroChainPass::FINAL)
-			continue;
+bool AINodeStorage::isOtherChainBetter(
+	const CGPathNode * source,
+	const AIPathNode & candidateNode,
+	const AIPathNode & other) const
+{
+	auto sameNode = other.actor == candidateNode.actor;
 
-		auto nodeActor = node.actor;
-		auto nodeArmyValue = nodeActor->armyValue - node.armyLoss;
-		auto candidateArmyValue = candidateActor->armyValue - candidateNode->armyLoss;
+	if(sameNode || other.action == EPathNodeAction::UNKNOWN || !other.actor || !other.actor->hero)
+	{
+		return false;
+	}
 
-		if(nodeArmyValue > candidateArmyValue
-			&& node.getCost() <= candidateNode->getCost())
+	if(other.danger <= candidateNode.danger && candidateNode.actor == other.actor->battleActor)
+	{
+		if(other.getCost() < candidateNode.getCost())
 		{
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 			logAi->trace(
-				"Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i",
+				"Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i",
 				source->coord.toString(),
-				candidateNode->coord.toString(),
-				candidateNode->actor->hero->getNameTranslated(),
-				candidateNode->actor->chainMask,
-				candidateNode->actor->armyValue,
-				node.moveRemains - candidateNode->moveRemains);
+				candidateNode.coord.toString(),
+				candidateNode.actor->hero->getNameTranslated(),
+				candidateNode.actor->chainMask,
+				candidateNode.actor->armyValue,
+				other.moveRemains - candidateNode.moveRemains);
 #endif
 			return true;
 		}
+	}
+
+	if(candidateNode.actor->chainMask != other.actor->chainMask && heroChainPass != EHeroChainPass::FINAL)
+		return false;
 
-		if(heroChainPass == EHeroChainPass::FINAL)
+	auto nodeActor = other.actor;
+	auto nodeArmyValue = nodeActor->armyValue - other.armyLoss;
+	auto candidateArmyValue = candidateNode.actor->armyValue - candidateNode.armyLoss;
+
+	if(nodeArmyValue > candidateArmyValue
+		&& other.getCost() <= candidateNode.getCost())
+	{
+#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
+		logAi->trace(
+			"Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i",
+			source->coord.toString(),
+			candidateNode.coord.toString(),
+			candidateNode.actor->hero->getNameTranslated(),
+			candidateNode.actor->chainMask,
+			candidateNode.actor->armyValue,
+			other.moveRemains - candidateNode.moveRemains);
+#endif
+		return true;
+	}
+
+	if(heroChainPass == EHeroChainPass::FINAL)
+	{
+		if(nodeArmyValue == candidateArmyValue
+			&& nodeActor->heroFightingStrength >= candidateNode.actor->heroFightingStrength
+			&& other.getCost() <= candidateNode.getCost())
 		{
-			if(nodeArmyValue == candidateArmyValue
-				&& nodeActor->heroFightingStrength >= candidateActor->heroFightingStrength
-				&& node.getCost() <= candidateNode->getCost())
+			if(vstd::isAlmostEqual(nodeActor->heroFightingStrength, candidateNode.actor->heroFightingStrength)
+				&& vstd::isAlmostEqual(other.getCost(), candidateNode.getCost())
+				&& &other < &candidateNode)
 			{
-				if(vstd::isAlmostEqual(nodeActor->heroFightingStrength, candidateActor->heroFightingStrength)
-					&& vstd::isAlmostEqual(node.getCost(), candidateNode->getCost())
-					&& &node < candidateNode)
-				{
-					continue;
-				}
+				return false;
+			}
 
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
-				logAi->trace(
-					"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i",
-					source->coord.toString(),
-					candidateNode->coord.toString(),
-					candidateNode->actor->hero->getNameTranslated(),
-					candidateNode->actor->chainMask,
-					candidateNode->actor->armyValue,
-					node.moveRemains - candidateNode->moveRemains);
+			logAi->trace(
+				"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i",
+				source->coord.toString(),
+				candidateNode.coord.toString(),
+				candidateNode.actor->hero->getNameTranslated(),
+				candidateNode.actor->chainMask,
+				candidateNode.actor->armyValue,
+				other.moveRemains - candidateNode.moveRemains);
 #endif
-				return true;
-			}
+			return true;
 		}
 	}
 
@@ -1292,12 +1315,15 @@ bool AINodeStorage::hasBetterChain(
 
 bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const
 {
-	auto chains = nodes.get(pos, layer);
+	auto chains = nodes.get(pos);
 
 	for(const AIPathNode & node : chains)
 	{
-		if(node.action != EPathNodeAction::UNKNOWN 
-			&& node.actor && node.actor->hero == hero.h)
+		if(node.version == AISharedStorage::version
+			&& node.layer == layer
+			&& node.action != EPathNodeAction::UNKNOWN 
+			&& node.actor
+			&& node.actor->hero == hero.h)
 		{
 			return true;
 		}
@@ -1306,22 +1332,23 @@ bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, con
 	return false;
 }
 
-std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand) const
+void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 & pos, bool isOnLand) const
 {
-	std::vector<AIPath> paths;
-
-	paths.reserve(AIPathfinding::NUM_CHAINS / 4);
-
-	auto chains = nodes.get(pos, isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL);
+	auto layer = isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL;
+	auto chains = nodes.get(pos);
 
 	for(const AIPathNode & node : chains)
 	{
-		if(node.action == EPathNodeAction::UNKNOWN || !node.actor || !node.actor->hero)
+		if(node.version != AISharedStorage::version
+			|| node.layer != layer
+			|| node.action == EPathNodeAction::UNKNOWN
+			|| !node.actor
+			|| !node.actor->hero)
 		{
 			continue;
 		}
 
-		AIPath path;
+		AIPath & path = paths.emplace_back();
 
 		path.targetHero = node.actor->hero;
 		path.heroArmy = node.actor->creatureSet;
@@ -1332,11 +1359,7 @@ std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand)
 		path.exchangeCount = node.actor->actorExchangeCount;
 		
 		fillChainInfo(&node, path, -1);
-
-		paths.push_back(path);
 	}
-
-	return paths;
 }
 
 void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const
@@ -1349,33 +1372,29 @@ void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int pa
 		if(node->chainOther)
 			fillChainInfo(node->chainOther, path, parentIndex);
 
-		//if(node->actor->hero->visitablePos() != node->coord)
-		{
-			AIPathNodeInfo pathNode;
-
-			pathNode.cost = node->getCost();
-			pathNode.targetHero = node->actor->hero;
-			pathNode.chainMask = node->actor->chainMask;
-			pathNode.specialAction = node->specialAction;
-			pathNode.turns = node->turns;
-			pathNode.danger = node->danger;
-			pathNode.coord = node->coord;
-			pathNode.parentIndex = parentIndex;
-			pathNode.actionIsBlocked = false;
-			pathNode.layer = node->layer;
-
-			if(pathNode.specialAction)
-			{
-				auto targetNode =node->theNodeBefore ?  getAINode(node->theNodeBefore) : node;
+		AIPathNodeInfo pathNode;
 
-				pathNode.actionIsBlocked = !pathNode.specialAction->canAct(targetNode);
-			}
+		pathNode.cost = node->getCost();
+		pathNode.targetHero = node->actor->hero;
+		pathNode.chainMask = node->actor->chainMask;
+		pathNode.specialAction = node->specialAction;
+		pathNode.turns = node->turns;
+		pathNode.danger = node->danger;
+		pathNode.coord = node->coord;
+		pathNode.parentIndex = parentIndex;
+		pathNode.actionIsBlocked = false;
+		pathNode.layer = node->layer;
 
-			parentIndex = path.nodes.size();
+		if(pathNode.specialAction)
+		{
+			auto targetNode =node->theNodeBefore ?  getAINode(node->theNodeBefore) : node;
 
-			path.nodes.push_back(pathNode);
+			pathNode.actionIsBlocked = !pathNode.specialAction->canAct(targetNode);
 		}
-		
+
+		parentIndex = path.nodes.size();
+
+		path.nodes.push_back(pathNode);
 		node = getAINode(node->theNodeBefore);
 	}
 }

+ 96 - 29
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -28,7 +28,7 @@ namespace NKAI
 namespace AIPathfinding
 {
 	const int BUCKET_COUNT = 3;
-	const int BUCKET_SIZE = 5;
+	const int BUCKET_SIZE = 7;
 	const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
 	const int CHAIN_MAX_DEPTH = 4;
 }
@@ -49,15 +49,24 @@ struct AIPathNode : public CGPathNode
 	const AIPathNode * chainOther;
 	std::shared_ptr<const SpecialAction> specialAction;
 	const ChainActor * actor;
+	uint64_t version;
 
-	STRONG_INLINE
-	bool blocked() const
+	void addSpecialAction(std::shared_ptr<const SpecialAction> action);
+
+	inline void reset(EPathfindingLayer layer, EPathAccessibility accessibility)
 	{
-		return accessible == EPathAccessibility::NOT_SET
-			|| accessible == EPathAccessibility::BLOCKED;
+		CGPathNode::reset();
+
+		actor = nullptr;
+		danger = 0;
+		manaCost = 0;
+		specialAction.reset();
+		armyLoss = 0;
+		chainOther = nullptr;
+		dayFlags = DayFlags::NONE;
+		this->layer = layer;
+		accessible = accessibility;
 	}
-
-	void addSpecialAction(std::shared_ptr<const SpecialAction> action);
 };
 
 struct AIPathNodeInfo
@@ -133,21 +142,21 @@ enum EHeroChainPass
 
 class AISharedStorage
 {
-	// 1 - layer (air, water, land)
-	// 2-4 - position on map[z][x][y]
-	// 5 - chain (normal, battle, spellcast and combinations)
-	static std::shared_ptr<boost::multi_array<AIPathNode, 5>> shared;
-	std::shared_ptr<boost::multi_array<AIPathNode, 5>> nodes;
+	// 1-3 - position on map[z][x][y]
+	// 4 - chain + layer (normal, battle, spellcast and combinations, water, air)
+	static std::shared_ptr<boost::multi_array<AIPathNode, 4>> shared;
+	std::shared_ptr<boost::multi_array<AIPathNode, 4>> nodes;
 public:
 	static boost::mutex locker;
+	static uint64_t version;
 
 	AISharedStorage(int3 mapSize);
 	~AISharedStorage();
 
 	STRONG_INLINE
-	boost::detail::multi_array::sub_array<AIPathNode, 1> get(int3 tile, EPathfindingLayer layer) const
+	boost::detail::multi_array::sub_array<AIPathNode, 1> get(int3 tile) const
 	{
-		return (*nodes)[layer][tile.z][tile.x][tile.y];
+		return (*nodes)[tile.z][tile.x][tile.y];
 	}
 };
 
@@ -156,6 +165,8 @@ class AINodeStorage : public INodeStorage
 private:
 	int3 sizes;
 
+	std::unique_ptr<boost::multi_array<EPathAccessibility, 4>> accesibility;
+
 	const CPlayerSpecificInfoCallback * cb;
 	const Nullkiller * ai;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
@@ -182,8 +193,10 @@ public:
 
 	std::vector<CGPathNode *> getInitialNodes() override;
 
-	virtual std::vector<CGPathNode *> calculateNeighbours(
+	virtual void calculateNeighbours(
+		std::vector<CGPathNode *> & result,
 		const PathNodeInfo & source,
+		EPathfindingLayer layer,
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) override;
 
@@ -222,23 +235,37 @@ public:
 		return aiNode->actor->hero;
 	}
 
-	bool hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
-
-	bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
+	inline bool blocked(const int3 & tile, EPathfindingLayer layer) const
 	{
-		return hasBetterChain(source, destination);
+		EPathAccessibility accessible = getAccessibility(tile, layer);
+
+		return accessible == EPathAccessibility::NOT_SET
+			|| accessible == EPathAccessibility::BLOCKED;
 	}
 
-	bool isDistanceLimitReached(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
+	bool hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
+	bool hasBetterChain(const CGPathNode * source, const AIPathNode & candidateNode) const;
 
 	template<class NodeRange>
 	bool hasBetterChain(
 		const CGPathNode * source, 
-		const AIPathNode * destinationNode,
+		const AIPathNode & destinationNode,
 		const NodeRange & chains) const;
 
+	bool isOtherChainBetter(
+		const CGPathNode * source,
+		const AIPathNode & candidateNode,
+		const AIPathNode & other) const;
+
+	bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
+	{
+		return hasBetterChain(source, destination);
+	}
+
+	bool isDistanceLimitReached(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
+
 	std::optional<AIPathNode *> getOrCreateNode(const int3 & coord, const EPathfindingLayer layer, const ChainActor * actor);
-	std::vector<AIPath> getChainInfo(const int3 & pos, bool isOnLand) const;
+	void calculateChainInfo(std::vector<AIPath> & result, const int3 & pos, bool isOnLand) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const;
 	void setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes);
 	void setScoutTurnDistanceLimit(uint8_t distanceLimit) { turnDistanceLimit[HeroRole::SCOUT] = distanceLimit; }
@@ -256,17 +283,19 @@ public:
 		return dangerEvaluator->evaluateDanger(tile, hero, checkGuards);
 	}
 
-	inline uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const
-	{
-		double ratio = (double)danger / (armyValue * hero->getFightingStrength());
+	uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const;
 
-		return (uint64_t)(armyValue * ratio * ratio);
+	inline EPathAccessibility getAccessibility(const int3 & tile, EPathfindingLayer layer) const
+	{
+		return (*this->accesibility)[tile.z][tile.x][tile.y][layer];
 	}
 
-	STRONG_INLINE
-	void resetTile(const int3 & tile, EPathfindingLayer layer, EPathAccessibility accessibility);
+	inline void resetTile(const int3 & tile, EPathfindingLayer layer, EPathAccessibility tileAccessibility)
+	{
+		(*this->accesibility)[tile.z][tile.x][tile.y][layer] = tileAccessibility;
+	}
 
-	STRONG_INLINE int getBucket(const ChainActor * actor) const
+	inline int getBucket(const ChainActor * actor) const
 	{
 		return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT;
 	}
@@ -274,6 +303,44 @@ public:
 	void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);
 	void fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const;
 
+	template<typename Fn>
+	void iterateValidNodes(const int3 & pos, EPathfindingLayer layer, Fn fn)
+	{
+		if(blocked(pos, layer))
+			return;
+
+		auto chains = nodes.get(pos);
+
+		for(AIPathNode & node : chains)
+		{
+			if(node.version != AISharedStorage::version || node.layer != layer)
+				continue;
+
+			fn(node);
+		}
+	}
+
+	template<typename Fn>
+	bool iterateValidNodesUntil(const int3 & pos, EPathfindingLayer layer, Fn predicate) const
+	{
+		if(blocked(pos, layer))
+			return false;
+
+		auto chains = nodes.get(pos);
+
+		for(AIPathNode & node : chains)
+		{
+			if(node.version != AISharedStorage::version || node.layer != layer)
+				continue;
+
+			if(predicate(node))
+				return true;
+		}
+
+		return false;
+	}
+
+
 private:
 	template<class TVector>
 	void calculateTownPortal(

+ 26 - 10
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -33,16 +33,31 @@ bool AIPathfinder::isTileAccessible(const HeroPtr & hero, const int3 & tile) con
 		|| storage->isTileAccessible(hero, tile, EPathfindingLayer::SAIL);
 }
 
-std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile, bool includeGraph) const
+void AIPathfinder::calculateQuickPathsWithBlocker(std::vector<AIPath> & result, const std::vector<const CGHeroInstance *> & heroes, const int3 & tile)
+{
+	result.clear();
+
+	for(auto hero : heroes)
+	{
+		auto graph = heroGraphs.find(hero->id);
+
+		if(graph != heroGraphs.end())
+			graph->second->quickAddChainInfoWithBlocker(result, tile, hero, ai);
+	}
+}
+
+void AIPathfinder::calculatePathInfo(std::vector<AIPath> & result, const int3 & tile, bool includeGraph) const
 {
 	const TerrainTile * tileInfo = cb->getTile(tile, false);
 
+	result.clear();
+
 	if(!tileInfo)
 	{
-		return std::vector<AIPath>();
+		return;
 	}
 
-	auto info = storage->getChainInfo(tile, !tileInfo->isWater());
+	storage->calculateChainInfo(result, tile, !tileInfo->isWater());
 
 	if(includeGraph)
 	{
@@ -51,11 +66,9 @@ std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile, bool includeGra
 			auto graph = heroGraphs.find(hero->id);
 
 			if(graph != heroGraphs.end())
-				graph->second.addChainInfo(info, tile, hero, ai);
+				graph->second->addChainInfo(result, tile, hero, ai);
 		}
 	}
-
-	return info;
 }
 
 void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings)
@@ -134,21 +147,24 @@ void AIPathfinder::updateGraphs(const std::map<const CGHeroInstance *, HeroRole>
 
 	for(auto hero : heroes)
 	{
-		if(heroGraphs.try_emplace(hero.first->id, GraphPaths()).second)
+		if(heroGraphs.try_emplace(hero.first->id).second)
+		{
+			heroGraphs[hero.first->id] = std::make_unique<GraphPaths>();
 			heroesVector.push_back(hero.first);
+		}
 	}
 
-	parallel_for(blocked_range<size_t>(0, heroesVector.size()), [this, &heroesVector](const blocked_range<size_t> & r)
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, heroesVector.size()), [this, &heroesVector](const tbb::blocked_range<size_t> & r)
 		{
 			for(auto i = r.begin(); i != r.end(); i++)
-				heroGraphs.at(heroesVector[i]->id).calculatePaths(heroesVector[i], ai);
+				heroGraphs.at(heroesVector[i]->id)->calculatePaths(heroesVector[i], ai);
 		});
 
 	if(NKAI_GRAPH_TRACE_LEVEL >= 1)
 	{
 		for(auto hero : heroes)
 		{
-			heroGraphs[hero.first->id].dumpToLog();
+			heroGraphs[hero.first->id]->dumpToLog();
 		}
 	}
 

+ 12 - 2
AI/Nullkiller/Pathfinding/AIPathfinder.h

@@ -40,20 +40,30 @@ private:
 	std::shared_ptr<AINodeStorage> storage;
 	CPlayerSpecificInfoCallback * cb;
 	Nullkiller * ai;
-	std::map<ObjectInstanceID, GraphPaths>  heroGraphs;
+	std::map<ObjectInstanceID, std::unique_ptr<GraphPaths>>  heroGraphs;
 
 public:
 	AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * ai);
-	std::vector<AIPath> getPathInfo(const int3 & tile, bool includeGraph = false) const;
+	void calculatePathInfo(std::vector<AIPath> & paths, const int3 & tile, bool includeGraph = false) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const;
 	void updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings);
 	void updateGraphs(const std::map<const CGHeroInstance *, HeroRole> & heroes);
+	void calculateQuickPathsWithBlocker(std::vector<AIPath> & result, const std::vector<const CGHeroInstance *> & heroes, const int3 & tile);
 	void init();
 
 	std::shared_ptr<AINodeStorage>getStorage()
 	{
 		return storage;
 	}
+
+	std::vector<AIPath> getPathInfo(const int3 & tile, bool includeGraph = false)
+	{
+		std::vector<AIPath> result;
+
+		calculatePathInfo(result, tile, includeGraph);
+
+		return result;
+	}
 };
 
 }

+ 19 - 4
AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp

@@ -37,10 +37,8 @@ namespace AIPathfinding
 		return Goals::sptr(Goals::Invalid());
 	}
 
-	bool BuildBoatAction::canAct(const AIPathNode * source) const
+	bool BuildBoatAction::canAct(const CGHeroInstance * hero, const TResources & reservedResources) const
 	{
-		auto hero = source->actor->hero;
-
 		if(cb->getPlayerRelations(hero->tempOwner, shipyard->getObject()->getOwner()) == PlayerRelations::ENEMIES)
 		{
 #if NKAI_TRACE_LEVEL > 1
@@ -53,7 +51,7 @@ namespace AIPathfinding
 
 		shipyard->getBoatCost(boatCost);
 
-		if(!cb->getResourceAmount().canAfford(source->actor->armyCost + boatCost))
+		if(!cb->getResourceAmount().canAfford(reservedResources + boatCost))
 		{
 #if NKAI_TRACE_LEVEL > 1
 			logAi->trace("Can not build a boat. Not enough resources.");
@@ -65,6 +63,18 @@ namespace AIPathfinding
 		return true;
 	}
 
+	bool BuildBoatAction::canAct(const AIPathNode * source) const
+	{
+		return canAct(source->actor->hero, source->actor->armyCost);
+	}
+
+	bool BuildBoatAction::canAct(const AIPathNodeInfo & source) const
+	{
+		TResources res;
+
+		return canAct(source.targetHero, res);
+	}
+
 	const CGObjectInstance * BuildBoatAction::targetObject() const
 	{
 		return dynamic_cast<const CGObjectInstance*>(shipyard);
@@ -75,6 +85,11 @@ namespace AIPathfinding
 		return sourceActor->resourceActor;
 	}
 
+	std::shared_ptr<SpecialAction> BuildBoatActionFactory::create(const Nullkiller * ai)
+	{
+		return std::make_shared<BuildBoatAction>(ai->cb.get(), dynamic_cast<const IShipyard * >(ai->cb->getObj(shipyard)));
+	}
+
 	void SummonBoatAction::execute(const CGHeroInstance * hero) const
 	{
 		Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai);

+ 15 - 0
AI/Nullkiller/Pathfinding/Actions/BoatActions.h

@@ -57,6 +57,8 @@ namespace AIPathfinding
 		}
 
 		bool canAct(const AIPathNode * source) const override;
+		bool canAct(const AIPathNodeInfo & source) const override;
+		bool canAct(const CGHeroInstance * hero, const TResources & reservedResources) const;
 
 		void execute(const CGHeroInstance * hero) const override;
 
@@ -68,6 +70,19 @@ namespace AIPathfinding
 
 		const CGObjectInstance * targetObject() const override;
 	};
+
+	class BuildBoatActionFactory : public ISpecialActionFactory
+	{
+		ObjectInstanceID shipyard;
+
+	public:
+		BuildBoatActionFactory(ObjectInstanceID shipyard)
+			:shipyard(shipyard)
+		{
+		}
+
+		std::shared_ptr<SpecialAction> create(const Nullkiller * ai) override;
+	};
 }
 
 }

+ 5 - 0
AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp

@@ -23,6 +23,11 @@ namespace AIPathfinding
 		return canAct(node->actor->hero);
 	}
 
+	bool QuestAction::canAct(const AIPathNodeInfo & node) const
+	{
+		return canAct(node.targetHero);
+	}
+
 	bool QuestAction::canAct(const CGHeroInstance * hero) const
 	{
 		if(questInfo.obj->ID == Obj::BORDER_GATE || questInfo.obj->ID == Obj::BORDERGUARD)

+ 1 - 1
AI/Nullkiller/Pathfinding/Actions/QuestAction.h

@@ -29,7 +29,7 @@ namespace AIPathfinding
 		}
 
 		bool canAct(const AIPathNode * node) const override;
-
+		bool canAct(const AIPathNodeInfo & node) const override;
 		bool canAct(const CGHeroInstance * hero) const;
 
 		Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;

+ 13 - 0
AI/Nullkiller/Pathfinding/Actions/SpecialAction.h

@@ -22,6 +22,7 @@ namespace NKAI
 {
 
 struct AIPathNode;
+struct AIPathNodeInfo;
 class ChainActor;
 
 class SpecialAction
@@ -34,6 +35,11 @@ public:
 		return true;
 	}
 
+	virtual bool canAct(const AIPathNodeInfo & source) const
+	{
+		return true;
+	}
+
 	virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const;
 
 	virtual void execute(const CGHeroInstance * hero) const;
@@ -89,4 +95,11 @@ public:
 		const AIPathNode * srcNode) const override;
 };
 
+class ISpecialActionFactory
+{
+public:
+	virtual std::shared_ptr<SpecialAction> create(const Nullkiller * ai) = 0;
+	virtual ~ISpecialActionFactory() = default;
+};
+
 }

+ 277 - 51
AI/Nullkiller/Pathfinding/ObjectGraph.cpp

@@ -16,6 +16,8 @@
 #include "../Engine/Nullkiller.h"
 #include "../../../lib/logging/VisualLogger.h"
 #include "Actions/QuestAction.h"
+#include "../pforeach.h"
+#include "Actions/BoatActions.h"
 
 namespace NKAI
 {
@@ -32,6 +34,7 @@ class ObjectGraphCalculator
 private:
 	ObjectGraph * target;
 	const Nullkiller * ai;
+	std::mutex syncLock;
 
 	std::map<const CGHeroInstance *, HeroRole> actors;
 	std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
@@ -41,7 +44,7 @@ private:
 
 public:
 	ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
-		:ai(ai), target(target)
+		:ai(ai), target(target), syncLock()
 	{
 	}
 
@@ -65,17 +68,51 @@ public:
 	{
 		updatePaths();
 
-		foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+		std::vector<AIPath> pathCache;
+
+		foreach_tile_pos(ai->cb.get(), [this, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
 			{
-				calculateConnections(pos);
+				calculateConnections(pos, pathCache);
 			});
 
 		removeExtraConnections();
 	}
 
+	float getNeighborConnectionsCost(const int3 & pos, std::vector<AIPath> & pathCache)
+	{
+		float neighborCost = std::numeric_limits<float>::max();
+
+		if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+		{
+			logAi->trace("Checking junction %s", pos.toString());
+		}
+
+		foreach_neighbour(
+			ai->cb.get(),
+			pos,
+			[this, &neighborCost, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
+			{
+				ai->pathfinder->calculatePathInfo(pathCache, neighbor);
+
+				auto costTotal = this->getConnectionsCost(pathCache);
+
+				if(costTotal.connectionsCount > 2 && costTotal.avg < neighborCost)
+				{
+					neighborCost = costTotal.avg;
+
+					if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+					{
+						logAi->trace("Better node found at %s", neighbor.toString());
+					}
+				}
+			});
+
+		return neighborCost;
+	}
+
 	void addMinimalDistanceJunctions()
 	{
-		foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+		pforeachTilePaths(ai->cb->getMapSize(), ai, [this](const int3 & pos, std::vector<AIPath> & paths)
 			{
 				if(target->hasNodeAt(pos))
 					return;
@@ -83,35 +120,12 @@ public:
 				if(ai->cb->getGuardingCreaturePosition(pos).valid())
 					return;
 
-				ConnectionCostInfo currentCost = getConnectionsCost(pos);
+				ConnectionCostInfo currentCost = getConnectionsCost(paths);
 
 				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());
-							}
-						}
-					});
+				float neighborCost = getNeighborConnectionsCost(pos, paths);
 
 				if(currentCost.avg < neighborCost)
 				{
@@ -132,20 +146,20 @@ private:
 		ai->pathfinder->updatePaths(actors, ps);
 	}
 
-	void calculateConnections(const int3 & pos)
+	void calculateConnections(const int3 & pos, std::vector<AIPath> & pathCache)
 	{
 		if(target->hasNodeAt(pos))
 		{
 			foreach_neighbour(
 				ai->cb.get(),
 				pos,
-				[this, &pos](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
+				[this, &pos, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
 				{
 					if(target->hasNodeAt(neighbor))
 					{
-						auto paths = ai->pathfinder->getPathInfo(neighbor);
+						ai->pathfinder->calculatePathInfo(pathCache, neighbor);
 
-						for(auto & path : paths)
+						for(auto & path : pathCache)
 						{
 							if(pos == path.targetHero->visitablePos())
 							{
@@ -155,15 +169,46 @@ private:
 					}
 				});
 
+			auto obj = ai->cb->getTopObj(pos);
+
+			if((obj && obj->ID == Obj::BOAT) || target->isVirtualBoat(pos))
+			{
+				ai->pathfinder->calculatePathInfo(pathCache, pos);
+
+				for(AIPath & path : pathCache)
+				{
+					auto from = path.targetHero->visitablePos();
+					auto fromObj = actorObjectMap[path.targetHero];
+
+					auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos, path.targetHero, true);
+					auto updated = target->tryAddConnection(
+						from,
+						pos,
+						path.movementCost(),
+						danger);
+
+					if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated)
+					{
+						logAi->trace(
+							"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
+							fromObj ? fromObj->getObjectName() : "J", from.toString(),
+							"Boat", pos.toString(),
+							pos.toString(),
+							path.movementCost());
+					}
+				}
+			}
+
 			return;
 		}
 
 		auto guardPos = ai->cb->getGuardingCreaturePosition(pos);
-		auto paths = ai->pathfinder->getPathInfo(pos);
+		
+		ai->pathfinder->calculatePathInfo(pathCache, pos);
 
-		for(AIPath & path1 : paths)
+		for(AIPath & path1 : pathCache)
 		{
-			for(AIPath & path2 : paths)
+			for(AIPath & path2 : pathCache)
 			{
 				if(path1.targetHero == path2.targetHero)
 					continue;
@@ -185,7 +230,9 @@ private:
 					if(!cb->getTile(pos)->isWater())
 						continue;
 
-					if(obj1 && (obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD))
+					auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1);
+
+					if(!startingObjIsBoat)
 						continue;
 				}
 
@@ -273,13 +320,25 @@ private:
 		assert(objectActor->visitablePos() == visitablePos);
 
 		actorObjectMap[objectActor] = obj;
-		actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::SHIPYARD ? HeroRole::MAIN : HeroRole::SCOUT;
+		actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::BOAT ? HeroRole::MAIN : HeroRole::SCOUT;
 
 		target->addObject(obj);
+
+		auto shipyard = dynamic_cast<const IShipyard *>(obj);
+		
+		if(shipyard && shipyard->bestLocation().valid())
+		{
+			int3 virtualBoat = shipyard->bestLocation();
+			
+			addJunctionActor(virtualBoat, true);
+			target->addVirtualBoat(virtualBoat, obj);
+		}
 	}
 
-	void addJunctionActor(const int3 & visitablePos)
+	void addJunctionActor(const int3 & visitablePos, bool isVirtualBoat = false)
 	{
+		std::lock_guard<std::mutex> lock(syncLock);
+
 		auto internalCb = temporaryActorHeroes.front()->cb;
 		auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
 
@@ -290,7 +349,7 @@ private:
 		objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
 		objectActor->initObj(rng);
 
-		if(cb->getTile(visitablePos)->isWater())
+		if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater())
 		{
 			objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
 		}
@@ -298,14 +357,13 @@ private:
 		assert(objectActor->visitablePos() == visitablePos);
 
 		actorObjectMap[objectActor] = nullptr;
-		actors[objectActor] = HeroRole::SCOUT;
+		actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT;
 
 		target->registerJunction(visitablePos);
 	}
 
-	ConnectionCostInfo getConnectionsCost(const int3 & pos) const
+	ConnectionCostInfo getConnectionsCost(std::vector<AIPath> & paths) const
 	{
-		auto paths = ai->pathfinder->getPathInfo(pos);
 		std::map<int3, float> costs;
 
 		for(auto & path : paths)
@@ -349,7 +407,15 @@ bool ObjectGraph::tryAddConnection(
 	float cost,
 	uint64_t danger)
 {
-	return nodes[from].connections[to].update(cost, danger);
+	auto result =  nodes[from].connections[to].update(cost, danger);
+	auto & connection = nodes[from].connections[to];
+
+	if(result && isVirtualBoat(to) && !connection.specialAction)
+	{
+		connection.specialAction = std::make_shared<AIPathfinding::BuildBoatActionFactory>(virtualBoats[to]);
+	}
+
+	return result;
 }
 
 void ObjectGraph::removeConnection(const int3 & from, const int3 & to)
@@ -374,19 +440,30 @@ void ObjectGraph::updateGraph(const Nullkiller * ai)
 
 void ObjectGraph::addObject(const CGObjectInstance * obj)
 {
-	nodes[obj->visitablePos()].init(obj);
+	if(!hasNodeAt(obj->visitablePos()))
+		nodes[obj->visitablePos()].init(obj);
+}
+
+void ObjectGraph::addVirtualBoat(const int3 & pos, const CGObjectInstance * shipyard)
+{
+	if(!isVirtualBoat(pos))
+	{
+		virtualBoats[pos] = shipyard->id;
+	}
 }
 
 void ObjectGraph::registerJunction(const int3 & pos)
 {
-	nodes[pos].initJunction();
+	if(!hasNodeAt(pos))
+		nodes[pos].initJunction();
+
 }
 
 void ObjectGraph::removeObject(const CGObjectInstance * obj)
 {
 	nodes[obj->visitablePos()].objectExists = false;
 
-	if(obj->ID == Obj::BOAT)
+	if(obj->ID == Obj::BOAT && !isVirtualBoat(obj->visitablePos()))
 	{
 		vstd::erase_if(nodes[obj->visitablePos()].connections, [&](const std::pair<int3, ObjectLink> & link) -> bool
 			{
@@ -459,9 +536,35 @@ bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const Graph
 	return pathNodes.at(lhs.coord)[lhs.nodeType].cost > pathNodes.at(rhs.coord)[rhs.nodeType].cost;
 }
 
+GraphPaths::GraphPaths()
+	: visualKey(""), graph(), pathNodes()
+{
+}
+
+std::shared_ptr<SpecialAction> getCompositeAction(
+	const Nullkiller * ai,
+	std::shared_ptr<ISpecialActionFactory> linkActionFactory,
+	std::shared_ptr<SpecialAction> transitionAction)
+{
+	if(!linkActionFactory)
+		return transitionAction;
+
+	auto linkAction = linkActionFactory->create(ai);
+
+	if(!transitionAction)
+		return linkAction;
+
+	std::vector<std::shared_ptr<const SpecialAction>> actionsArray = {
+		transitionAction,
+		linkAction
+	};
+
+	return std::make_shared<CompositeAction>(actionsArray);
+}
+
 void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai)
 {
-	graph = *ai->baseGraph;
+	graph.copyFrom(*ai->baseGraph);
 	graph.connectHeroes(ai);
 
 	visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
@@ -508,15 +611,16 @@ void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkil
 
 		node.isInQueue = false;
 
-		graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, ObjectLink o)
+		graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, const ObjectLink & o)
 			{
-				auto targetNodeType = o.danger || transitionAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
+				auto compositeAction = getCompositeAction(ai, o.specialAction, transitionAction);
+				auto targetNodeType = o.danger || compositeAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
 				auto targetPointer = GraphPathNodePointer(target, targetNodeType);
 				auto & targetNode = getOrCreateNode(targetPointer);
 
 				if(targetNode.tryUpdate(pos, node, o))
 				{
-					targetNode.specialAction = transitionAction;
+					targetNode.specialAction = compositeAction;
 
 					auto targetGraphNode = graph.getNode(target);
 
@@ -651,6 +755,11 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 				n.parentIndex = -1;
 				n.specialAction = getNode(*graphTile).specialAction;
 
+				if(n.specialAction)
+				{
+					n.actionIsBlocked = !n.specialAction->canAct(n);
+				}
+
 				for(auto & node : path.nodes)
 				{
 					node.parentIndex++;
@@ -668,4 +777,121 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 	}
 }
 
+void GraphPaths::quickAddChainInfoWithBlocker(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 & targetNode : nodes->second)
+	{
+		if(!targetNode.reachable())
+			continue;
+
+		std::vector<GraphPathNodePointer> tilesToPass;
+
+		uint64_t danger = targetNode.danger;
+		float cost = targetNode.cost;
+		bool allowBattle = false;
+
+		auto current = GraphPathNodePointer(nodes->first, targetNode.nodeType);
+
+		while(true)
+		{
+			auto currentTile = pathNodes.find(current.coord);
+
+			if(currentTile == pathNodes.end())
+				break;
+
+			auto currentNode = currentTile->second[current.nodeType];
+
+			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 & entryPath : entryPaths)
+		{
+			if(entryPath.targetHero != hero)
+				continue;
+
+			auto & path = paths.emplace_back();
+
+			path.targetHero = entryPath.targetHero;
+			path.heroArmy = entryPath.heroArmy;
+			path.exchangeCount = entryPath.exchangeCount;
+			path.armyLoss = entryPath.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);
+
+			AIPathNodeInfo n;
+
+			n.targetHero = hero;
+			n.parentIndex = -1;
+
+			// final node
+			n.coord = tile;
+			n.cost = targetNode.cost;
+			n.danger = targetNode.danger;
+			n.parentIndex = path.nodes.size();
+			path.nodes.push_back(n);
+
+			for(auto entryNode = entryPath.nodes.rbegin(); entryNode != entryPath.nodes.rend(); entryNode++)
+			{
+				auto blocker = ai->objectClusterizer->getBlocker(*entryNode);
+
+				if(blocker)
+				{
+					// blocker node
+					path.nodes.push_back(*entryNode);
+					path.nodes.back().parentIndex = path.nodes.size() - 1;
+					break;
+				}
+			}
+			
+			if(path.nodes.size() > 1)
+				continue;
+
+			for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
+			{
+				auto & node = getNode(*graphTile);
+
+				n.coord = graphTile->coord;
+				n.cost = node.cost;
+				n.turns = static_cast<ui8>(node.cost);
+				n.danger = node.danger;
+				n.specialAction = node.specialAction;
+				n.parentIndex = path.nodes.size();
+
+				if(n.specialAction)
+				{
+					n.actionIsBlocked = !n.specialAction->canAct(n);
+				}
+
+				auto blocker = ai->objectClusterizer->getBlocker(n);
+
+				if(!blocker)
+					continue;
+
+				// blocker node
+				path.nodes.push_back(n);
+				break;
+			}
+		}
+	}
+}
+
 }

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

@@ -22,6 +22,7 @@ struct ObjectLink
 {
 	float cost = 100000; // some big number
 	uint64_t danger = 0;
+	std::shared_ptr<ISpecialActionFactory> specialAction;
 
 	bool update(float newCost, uint64_t newDanger)
 	{
@@ -62,17 +63,35 @@ struct ObjectNode
 class ObjectGraph
 {
 	std::unordered_map<int3, ObjectNode> nodes;
+	std::unordered_map<int3, ObjectInstanceID> virtualBoats;
 
 public:
+	ObjectGraph()
+		:nodes(), virtualBoats()
+	{
+	}
+
 	void updateGraph(const Nullkiller * ai);
 	void addObject(const CGObjectInstance * obj);
 	void registerJunction(const int3 & pos);
+	void addVirtualBoat(const int3 & pos, const CGObjectInstance * shipyard);
 	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;
 
+	bool isVirtualBoat(const int3 & tile) const
+	{
+		return vstd::contains(virtualBoats, tile);
+	}
+
+	void copyFrom(const ObjectGraph & other)
+	{
+		nodes = other.nodes;
+		virtualBoats = other.virtualBoats;
+	}
+
 	template<typename Func>
 	void iterateConnections(const int3 & pos, Func fn)
 	{
@@ -167,8 +186,10 @@ class GraphPaths
 	std::string visualKey;
 
 public:
+	GraphPaths();
 	void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai);
 	void addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
+	void quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
 	void dumpToLog() const;
 
 private:

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

@@ -17,7 +17,10 @@ namespace NKAI
 {
 namespace AIPathfinding
 {
-	AILayerTransitionRule::AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage)
+	AILayerTransitionRule::AILayerTransitionRule(
+		CPlayerSpecificInfoCallback * cb,
+		Nullkiller * ai,
+		std::shared_ptr<AINodeStorage> nodeStorage)
 		:cb(cb), ai(ai), nodeStorage(nodeStorage)
 	{
 		setup();

+ 4 - 1
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h

@@ -34,7 +34,10 @@ namespace AIPathfinding
 		std::map<const CGHeroInstance *, std::shared_ptr<const AirWalkingAction>> airWalkingActions;
 
 	public:
-		AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage);
+		AILayerTransitionRule(
+			CPlayerSpecificInfoCallback * cb,
+			Nullkiller * ai,
+			std::shared_ptr<AINodeStorage> nodeStorage);
 
 		virtual void process(
 			const PathNodeInfo & source,

+ 10 - 0
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -40,6 +40,16 @@ namespace AIPathfinding
 
 			return;
 		}
+		
+		if(!allowBypassObjects
+			&& destination.action == EPathNodeAction::EMBARK
+			&& source.node->layer == EPathfindingLayer::LAND
+			&& destination.node->layer == EPathfindingLayer::SAIL)
+		{
+			destination.blocked = true;
+
+			return;
+		}
 
 		auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
 

+ 9 - 0
AI/Nullkiller/StdInc.cpp

@@ -1 +1,10 @@
+/*
+ * StdInc.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"

+ 9 - 0
AI/Nullkiller/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.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 "../../Global.h"
 VCMI_LIB_USING_NAMESPACE

+ 50 - 0
AI/Nullkiller/pforeach.h

@@ -0,0 +1,50 @@
+#pragma once
+
+#include "Engine/Nullkiller.h"
+
+namespace NKAI
+{
+
+template<typename TFunc>
+void pforeachTilePos(const int3 & mapSize, TFunc fn)
+{
+	for(int z = 0; z < mapSize.z; ++z)
+	{
+		tbb::parallel_for(tbb::blocked_range<size_t>(0, mapSize.x), [&](const tbb::blocked_range<size_t> & r)
+			{
+				int3 pos(0, 0, z);
+
+				for(pos.x = r.begin(); pos.x != r.end(); ++pos.x)
+				{
+					for(pos.y = 0; pos.y < mapSize.y; ++pos.y)
+					{
+						fn(pos);
+					}
+				}
+			});
+	}
+}
+
+template<typename TFunc>
+void pforeachTilePaths(const int3 & mapSize, const Nullkiller * ai, TFunc fn)
+{
+	for(int z = 0; z < mapSize.z; ++z)
+	{
+		tbb::parallel_for(tbb::blocked_range<size_t>(0, mapSize.x), [&](const tbb::blocked_range<size_t> & r)
+			{
+				int3 pos(0, 0, z);
+				std::vector<AIPath> paths;
+
+				for(pos.x = r.begin(); pos.x != r.end(); ++pos.x)
+				{
+					for(pos.y = 0; pos.y < mapSize.y; ++pos.y)
+					{
+						ai->pathfinder->calculatePathInfo(paths, pos);
+						fn(pos, paths);
+					}
+				}
+			});
+	}
+}
+
+}

+ 9 - 0
AI/StupidAI/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.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
+ *
+ */
 // Creates the precompiled header
 #include "StdInc.h"

+ 9 - 0
AI/StupidAI/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.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 "../../Global.h"

+ 1 - 1
AI/VCAI/AIhelper.cpp

@@ -1,5 +1,5 @@
 /*
-* AIhelper.h, part of VCMI engine
+* AIhelper.cpp, part of VCMI engine
 *
 * Authors: listed in file AUTHORS in main folder
 *

+ 9 - 0
AI/VCAI/MapObjectsEvaluator.cpp

@@ -1,3 +1,12 @@
+/*
+ * MapObjectsEvaluator.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 "MapObjectsEvaluator.h"
 #include "../../lib/GameConstants.h"

+ 11 - 7
AI/VCAI/Pathfinding/AINodeStorage.cpp

@@ -155,15 +155,21 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf
 	});
 }
 
-std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
+void AINodeStorage::calculateNeighbours(
+	std::vector<CGPathNode *> & result,
 	const PathNodeInfo & source,
+	EPathfindingLayer layer,
 	const PathfinderConfig * pathfinderConfig,
 	const CPathfinderHelper * pathfinderHelper)
 {
-	std::vector<CGPathNode *> neighbours;
-	neighbours.reserve(16);
+	std::vector<int3> accessibleNeighbourTiles;
+
+	result.clear();
+	accessibleNeighbourTiles.reserve(8);
+
+	pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);
+
 	const AIPathNode * srcNode = getAINode(source.node);
-	auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
 
 	for(auto & neighbour : accessibleNeighbourTiles)
 	{
@@ -174,11 +180,9 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
 			if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
 				continue;
 
-			neighbours.push_back(nextNode.value());
+			result.push_back(nextNode.value());
 		}
 	}
-
-	return neighbours;
 }
 
 void AINodeStorage::setHero(HeroPtr heroPtr, const VCAI * _ai)

+ 3 - 1
AI/VCAI/Pathfinding/AINodeStorage.h

@@ -89,8 +89,10 @@ public:
 
 	std::vector<CGPathNode *> getInitialNodes() override;
 
-	virtual std::vector<CGPathNode *> calculateNeighbours(
+	virtual void calculateNeighbours(
+		std::vector<CGPathNode *> & result,
 		const PathNodeInfo & source,
+		EPathfindingLayer layer,
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) override;
 

+ 9 - 0
AI/VCAI/StdInc.cpp

@@ -1 +1,10 @@
+/*
+ * StdInc.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"

+ 9 - 0
AI/VCAI/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.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 "../../Global.h"
 

+ 1 - 1
CI/linux-qt6/before_install.sh

@@ -3,7 +3,7 @@
 sudo apt-get update
 
 # Dependencies
-sudo apt-get install libboost-all-dev \
+sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \

+ 1 - 1
CI/linux/before_install.sh

@@ -3,7 +3,7 @@
 sudo apt-get update
 
 # Dependencies
-sudo apt-get install libboost-all-dev \
+sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 qtbase5-dev \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \

+ 40 - 23
CMakeLists.txt

@@ -38,11 +38,13 @@ endif()
 
 # Platform-independent options
 
+option(ENABLE_CLIENT "Enable compilation of game client" ON)
 option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF)
 option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF)
 option(ENABLE_TRANSLATIONS "Enable generation of translations for launcher and editor" ON)
 option(ENABLE_NULLKILLER_AI "Enable compilation of Nullkiller AI library" ON)
 option(ENABLE_GITVERSION "Enable Version.cpp with Git commit hash" ON)
+option(ENABLE_MINIMAL_LIB "Build only core parts of vcmi library that are required for game lobby" OFF)
 
 # Compilation options
 
@@ -247,6 +249,10 @@ if(ENABLE_SINGLE_APP_BUILD)
 	add_definitions(-DENABLE_SINGLE_APP_BUILD)
 endif()
 
+if(ENABLE_MINIMAL_LIB)
+	add_definitions(-DENABLE_MINIMAL_LIB)
+endif()
+
 if(APPLE_IOS)
 	set(CMAKE_MACOSX_RPATH 1)
 	set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0)
@@ -450,12 +456,6 @@ if(TARGET zlib::zlib)
 	add_library(ZLIB::ZLIB ALIAS zlib::zlib)
 endif()
 
-set(FFMPEG_COMPONENTS avutil swscale avformat avcodec)
-if(APPLE_IOS AND NOT USING_CONAN)
-	list(APPEND FFMPEG_COMPONENTS swresample)
-endif()
-find_package(ffmpeg COMPONENTS ${FFMPEG_COMPONENTS})
-
 option(FORCE_BUNDLED_MINIZIP "Force bundled Minizip library" OFF)
 if(NOT FORCE_BUNDLED_MINIZIP)
 	find_package(minizip)
@@ -464,18 +464,26 @@ if(NOT FORCE_BUNDLED_MINIZIP)
 	endif()
 endif()
 
-find_package(SDL2 REQUIRED)
-find_package(SDL2_image REQUIRED)
-if(TARGET SDL2_image::SDL2_image)
-	add_library(SDL2::Image ALIAS SDL2_image::SDL2_image)
-endif()
-find_package(SDL2_mixer REQUIRED)
-if(TARGET SDL2_mixer::SDL2_mixer)
-	add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer)
-endif()
-find_package(SDL2_ttf REQUIRED)
-if(TARGET SDL2_ttf::SDL2_ttf)
-	add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
+if (ENABLE_CLIENT)
+	set(FFMPEG_COMPONENTS avutil swscale avformat avcodec)
+	if(APPLE_IOS AND NOT USING_CONAN)
+		list(APPEND FFMPEG_COMPONENTS swresample)
+	endif()
+	find_package(ffmpeg COMPONENTS ${FFMPEG_COMPONENTS})
+
+	find_package(SDL2 REQUIRED)
+	find_package(SDL2_image REQUIRED)
+	if(TARGET SDL2_image::SDL2_image)
+		add_library(SDL2::Image ALIAS SDL2_image::SDL2_image)
+	endif()
+	find_package(SDL2_mixer REQUIRED)
+	if(TARGET SDL2_mixer::SDL2_mixer)
+		add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer)
+	endif()
+	find_package(SDL2_ttf REQUIRED)
+	if(TARGET SDL2_ttf::SDL2_ttf)
+		add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
+	endif()
 endif()
 
 if(ENABLE_LOBBY)
@@ -493,7 +501,7 @@ if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
 	endif()
 endif()
 
-if(ENABLE_NULLKILLER_AI)
+if(ENABLE_NULLKILLER_AI AND ENABLE_CLIENT)
 	find_package(TBB REQUIRED)
 endif()
 
@@ -603,10 +611,15 @@ if(APPLE_IOS)
 	add_subdirectory(ios)
 endif()
 
-add_subdirectory_with_folder("AI" AI)
+if (ENABLE_CLIENT)
+	add_subdirectory_with_folder("AI" AI)
+endif()
 
 add_subdirectory(lib)
-add_subdirectory(server)
+
+if (ENABLE_CLIENT OR ENABLE_SERVER)
+	add_subdirectory(server)
+endif()
 
 if(ENABLE_ERM)
 	add_subdirectory(scripting/erm)
@@ -633,7 +646,9 @@ if(ENABLE_LOBBY)
 	add_subdirectory(lobby)
 endif()
 
-add_subdirectory(client)
+if (ENABLE_CLIENT)
+	add_subdirectory(client)
+endif()
 
 if(ENABLE_SERVER)
 	add_subdirectory(serverapp)
@@ -677,7 +692,9 @@ if(ANDROID)
 	")
 else()
 	install(DIRECTORY config DESTINATION ${DATA_DIR})
-	install(DIRECTORY Mods DESTINATION ${DATA_DIR})
+	if (ENABLE_CLIENT OR ENABLE_SERVER)
+		install(DIRECTORY Mods DESTINATION ${DATA_DIR})
+	endif()
 endif()
 if(ENABLE_LUA)
 	install(DIRECTORY scripts DESTINATION ${DATA_DIR})

+ 1 - 0
Mods/vcmi/config/vcmi/english.json

@@ -79,6 +79,7 @@
 	"vcmi.lobby.login.error" : "Connection error: %s",
 	"vcmi.lobby.login.create" : "New Account",
 	"vcmi.lobby.login.login" : "Login",
+	"vcmi.lobby.login.as" : "Login as %s",
 	"vcmi.lobby.header.rooms" : "Game Rooms - %d",
 	"vcmi.lobby.header.channels" : "Chat Channels",
 	"vcmi.lobby.header.chat.global" : "Global Game Chat - %s", // %s -> language name

+ 1 - 0
Mods/vcmi/config/vcmi/ukrainian.json

@@ -78,6 +78,7 @@
 	"vcmi.lobby.login.error" : "Помилка з'єднання: %s",
 	"vcmi.lobby.login.create" : "Створити акаунт",
 	"vcmi.lobby.login.login" : "Увійти",
+	"vcmi.lobby.login.as" : "Увійти як %s",
 	"vcmi.lobby.header.rooms" : "Активні кімнати - %d",
 	"vcmi.lobby.header.channels" : "Канали чату",
 	"vcmi.lobby.header.chat.global" : "Глобальний ігровий чат - %s", // %s -> language name

+ 1 - 1
client/CFocusableHelper.cpp

@@ -7,9 +7,9 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
+#include "StdInc.h"
 #include "CFocusableHelper.h"
 #include "../Global.h"
-#include "StdInc.h"
 #include "widgets/TextControls.h"
 
 void removeFocusFromActiveInput()

+ 1 - 0
client/CFocusableHelper.h

@@ -7,5 +7,6 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
+#pragma once
 
 void removeFocusFromActiveInput();

+ 13 - 2
client/CMT.cpp

@@ -56,6 +56,7 @@ namespace po = boost::program_options;
 namespace po_style = boost::program_options::command_line_style;
 
 static std::atomic<bool> quitRequestedDuringOpeningPlayback = false;
+static std::atomic<bool> headlessQuit = false;
 
 #ifndef VCMI_IOS
 void processCommand(const std::string &message);
@@ -371,8 +372,10 @@ int main(int argc, char * argv[])
 	}
 	else
 	{
-		while(true)
+		while(!headlessQuit)
 			boost::this_thread::sleep_for(boost::chrono::milliseconds(200));
+
+		quitApplication();
 	}
 
 	return 0;
@@ -481,7 +484,15 @@ void handleQuit(bool ask)
 	// proper solution would be to abort init thread (or wait for it to finish)
 	if(!ask)
 	{
-		quitApplication();
+		if(settings["session"]["headless"].Bool())
+		{
+			headlessQuit = true;
+		}
+		else
+		{
+			quitApplication();
+		}
+
 		return;
 	}
 

+ 1 - 1
client/Client.cpp

@@ -7,8 +7,8 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
-#include "Global.h"
 #include "StdInc.h"
+#include "Global.h"
 #include "Client.h"
 
 #include "CGameInfo.h"

+ 4 - 0
client/NetPacksClient.cpp

@@ -410,7 +410,11 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
 
 	// In auto testing pack.mode we always close client if red pack.player won or lose
 	if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0))
+	{
+		logAi->info("Red player %s. Ending game.", pack.victoryLossCheckResult.victory() ? "won" : "lost");
+
 		handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not
+	}
 }
 
 void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface & pack)

+ 9 - 0
client/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.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
+ *
+ */
 // Creates the precompiled header
 #include "StdInc.h"

+ 9 - 0
client/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.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 "../Global.h"

+ 69 - 26
client/globalLobby/GlobalLobbyClient.cpp

@@ -85,14 +85,9 @@ void GlobalLobbyClient::receiveAccountCreated(const JsonNode & json)
 		throw std::runtime_error("lobby connection finished without active login window!");
 
 	{
-		Settings configID = settings.write["lobby"]["accountID"];
-		configID->String() = json["accountID"].String();
-
-		Settings configName = settings.write["lobby"]["displayName"];
-		configName->String() = json["displayName"].String();
-
-		Settings configCookie = settings.write["lobby"]["accountCookie"];
-		configCookie->String() = json["accountCookie"].String();
+		setAccountID(json["accountID"].String());
+		setAccountDisplayName(json["displayName"].String());
+		setAccountCookie(json["accountCookie"].String());
 	}
 
 	sendClientLogin();
@@ -105,18 +100,15 @@ void GlobalLobbyClient::receiveOperationFailed(const JsonNode & json)
 	if(loginWindowPtr)
 		loginWindowPtr->onConnectionFailed(json["reason"].String());
 
+	logGlobal->warn("Operation failed! Reason: %s", json["reason"].String());
 	// TODO: handle errors in lobby menu
 }
 
 void GlobalLobbyClient::receiveClientLoginSuccess(const JsonNode & json)
 {
-	{
-		Settings configCookie = settings.write["lobby"]["accountCookie"];
-		configCookie->String() = json["accountCookie"].String();
-
-		Settings configName = settings.write["lobby"]["displayName"];
-		configName->String() = json["displayName"].String();
-	}
+	accountLoggedIn = true;
+	setAccountDisplayName(json["displayName"].String());
+	setAccountCookie(json["accountCookie"].String());
 
 	auto loginWindowPtr = loginWindow.lock();
 
@@ -288,8 +280,8 @@ void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json)
 		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, {});
 		CSH->loadMode = ELoadMode::MULTI;
 
-		std::string hostname = settings["lobby"]["hostname"].String();
-		int16_t port = settings["lobby"]["port"].Integer();
+		std::string hostname = getServerHost();
+		uint16_t port = getServerPort();
 		CSH->connectToServer(hostname, port);
 	}
 
@@ -324,8 +316,8 @@ void GlobalLobbyClient::sendClientLogin()
 {
 	JsonNode toSend;
 	toSend["type"].String() = "clientLogin";
-	toSend["accountID"] = settings["lobby"]["accountID"];
-	toSend["accountCookie"] = settings["lobby"]["accountCookie"];
+	toSend["accountID"].String() = getAccountID();
+	toSend["accountCookie"].String() = getAccountCookie();
 	toSend["language"].String() = CGI->generaltexth->getPreferredLanguage();
 	toSend["version"].String() = VCMI_VERSION_STRING;
 	sendMessage(toSend);
@@ -350,6 +342,7 @@ void GlobalLobbyClient::onDisconnected(const std::shared_ptr<INetworkConnection>
 
 	assert(connection == networkConnection);
 	networkConnection.reset();
+	accountLoggedIn = false;
 
 	while (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
 	{
@@ -370,7 +363,7 @@ void GlobalLobbyClient::sendOpenRoom(const std::string & mode, int playerLimit)
 {
 	JsonNode toSend;
 	toSend["type"].String() = "activateGameRoom";
-	toSend["hostAccountID"] = settings["lobby"]["accountID"];
+	toSend["hostAccountID"].String() = getAccountID();
 	toSend["roomType"].String() = mode;
 	toSend["playerLimit"].Integer() = playerLimit;
 	sendMessage(toSend);
@@ -378,11 +371,16 @@ void GlobalLobbyClient::sendOpenRoom(const std::string & mode, int playerLimit)
 
 void GlobalLobbyClient::connect()
 {
-	std::string hostname = settings["lobby"]["hostname"].String();
-	int16_t port = settings["lobby"]["port"].Integer();
+	std::string hostname = getServerHost();
+	uint16_t port = getServerPort();
 	CSH->getNetworkHandler().connectToRemote(*this, hostname, port);
 }
 
+bool GlobalLobbyClient::isLoggedIn() const
+{
+	return networkConnection != nullptr && accountLoggedIn;
+}
+
 bool GlobalLobbyClient::isConnected() const
 {
 	return networkConnection != nullptr;
@@ -468,7 +466,7 @@ void GlobalLobbyClient::activateInterface()
 	if (!GH.windows().findWindows<GlobalLobbyLoginWindow>().empty())
 		return;
 
-	if (isConnected())
+	if (isLoggedIn())
 		GH.windows().pushWindow(createLobbyWindow());
 	else
 		GH.windows().pushWindow(createLoginWindow());
@@ -479,12 +477,55 @@ void GlobalLobbyClient::activateRoomInviteInterface()
 	GH.windows().createAndPushWindow<GlobalLobbyInviteWindow>();
 }
 
+void GlobalLobbyClient::setAccountID(const std::string & accountID)
+{
+	Settings configID = persistentStorage.write["lobby"][getServerHost()]["accountID"];
+	configID->String() = accountID;
+}
+
+void GlobalLobbyClient::setAccountCookie(const std::string & accountCookie)
+{
+	Settings configCookie = persistentStorage.write["lobby"][getServerHost()]["accountCookie"];
+	configCookie->String() = accountCookie;
+}
+
+void GlobalLobbyClient::setAccountDisplayName(const std::string & accountDisplayName)
+{
+	Settings configName = persistentStorage.write["lobby"][getServerHost()]["displayName"];
+	configName->String() = accountDisplayName;
+}
+
+const std::string & GlobalLobbyClient::getAccountID() const
+{
+	return persistentStorage["lobby"][getServerHost()]["accountID"].String();
+}
+
+const std::string & GlobalLobbyClient::getAccountCookie() const
+{
+	return persistentStorage["lobby"][getServerHost()]["accountCookie"].String();
+}
+
+const std::string & GlobalLobbyClient::getAccountDisplayName() const
+{
+	return persistentStorage["lobby"][getServerHost()]["displayName"].String();
+}
+
+const std::string & GlobalLobbyClient::getServerHost() const
+{
+	return settings["lobby"]["hostname"].String();
+}
+
+uint16_t GlobalLobbyClient::getServerPort() const
+{
+	return settings["lobby"]["port"].Integer();
+}
+
 void GlobalLobbyClient::sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection)
 {
 	JsonNode toSend;
 	toSend["type"].String() = "clientProxyLogin";
-	toSend["accountID"] = settings["lobby"]["accountID"];
-	toSend["accountCookie"] = settings["lobby"]["accountCookie"];
+	toSend["accountID"].String() = getAccountID();
+	toSend["accountCookie"].String() = getAccountCookie();
 	toSend["gameRoomID"].String() = currentGameRoomUUID;
 
 	assert(JsonUtils::validate(toSend, "vcmi:lobbyProtocol/" + toSend["type"].String(), toSend["type"].String() + " pack"));
@@ -498,7 +539,7 @@ void GlobalLobbyClient::resetMatchState()
 
 void GlobalLobbyClient::sendMatchChatMessage(const std::string & messageText)
 {
-	if (!isConnected())
+	if (!isLoggedIn())
 		return; // we are not playing with lobby
 
 	if (currentGameRoomUUID.empty())
@@ -510,6 +551,8 @@ void GlobalLobbyClient::sendMatchChatMessage(const std::string & messageText)
 	toSend["channelName"].String() = currentGameRoomUUID;
 	toSend["messageText"].String() = messageText;
 
+	assert(TextOperations::isValidUnicodeString(messageText));
+
 	CSH->getGlobalLobby().sendMessage(toSend);
 }
 

+ 12 - 0
client/globalLobby/GlobalLobbyClient.h

@@ -34,6 +34,7 @@ class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyabl
 
 	std::shared_ptr<INetworkConnection> networkConnection;
 	std::string currentGameRoomUUID;
+	bool accountLoggedIn = false;
 
 	std::weak_ptr<GlobalLobbyLoginWindow> loginWindow;
 	std::weak_ptr<GlobalLobbyWindow> lobbyWindow;
@@ -58,6 +59,10 @@ class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyabl
 	std::shared_ptr<GlobalLobbyLoginWindow> createLoginWindow();
 	std::shared_ptr<GlobalLobbyWindow> createLobbyWindow();
 
+	void setAccountID(const std::string & accountID);
+	void setAccountCookie(const std::string & accountCookie);
+	void setAccountDisplayName(const std::string & accountDisplayName);
+
 public:
 	explicit GlobalLobbyClient();
 	~GlobalLobbyClient();
@@ -68,6 +73,12 @@ public:
 	const std::vector<GlobalLobbyRoom> & getMatchesHistory() const;
 	const std::vector<GlobalLobbyChannelMessage> & getChannelHistory(const std::string & channelType, const std::string & channelName) const;
 
+	const std::string & getAccountID() const;
+	const std::string & getAccountCookie() const;
+	const std::string & getAccountDisplayName() const;
+	const std::string & getServerHost() const;
+	uint16_t getServerPort() const;
+
 	/// Activate interface and pushes lobby UI as top window
 	void activateInterface();
 	void activateRoomInviteInterface();
@@ -83,5 +94,6 @@ public:
 
 	void connect();
 	bool isConnected() const;
+	bool isLoggedIn() const;
 	bool isInvitedToRoom(const std::string & gameRoomID);
 };

+ 17 - 5
client/globalLobby/GlobalLobbyLoginWindow.cpp

@@ -36,9 +36,14 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
 	pos.w = 284;
 	pos.h = 220;
 
+	MetaString loginAs;
+	loginAs.appendTextID("vcmi.lobby.login.as");
+	loginAs.replaceTextID(CSH->getGlobalLobby().getAccountDisplayName());
+
 	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
 	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title"));
-	labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username"));
+	labelUsernameTitle = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username"));
+	labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString());
 	backgroundUsername = std::make_shared<TransparentFilledRectangle>(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
 	inputUsername = std::make_shared<CTextInput>(Rect(15, 93, 260, 16), FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true);
 	buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); });
@@ -56,7 +61,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
 	toggleMode->setSelected(settings["lobby"]["roomType"].Integer());
 	toggleMode->addCallback([this](int index){onLoginModeChanged(index);});
 
-	if (settings["lobby"]["accountID"].String().empty())
+	if (CSH->getGlobalLobby().getAccountID().empty())
 	{
 		buttonLogin->block(true);
 		toggleMode->setSelected(0);
@@ -77,12 +82,19 @@ void GlobalLobbyLoginWindow::onLoginModeChanged(int value)
 {
 	if (value == 0)
 	{
-		inputUsername->setText("");
+		inputUsername->enable();
+		backgroundUsername->enable();
+		labelUsernameTitle->enable();
+		labelUsername->disable();
 	}
 	else
 	{
-		inputUsername->setText(settings["lobby"]["displayName"].String());
+		inputUsername->disable();
+		backgroundUsername->disable();
+		labelUsernameTitle->disable();
+		labelUsername->enable();
 	}
+	redraw();
 }
 
 void GlobalLobbyLoginWindow::onClose()
@@ -104,7 +116,7 @@ void GlobalLobbyLoginWindow::onLogin()
 
 void GlobalLobbyLoginWindow::onConnectionSuccess()
 {
-	std::string accountID = settings["lobby"]["accountID"].String();
+	std::string accountID = CSH->getGlobalLobby().getAccountID();
 
 	if(toggleMode->getSelected() == 0)
 		CSH->getGlobalLobby().sendClientRegister(inputUsername->getText());

+ 1 - 0
client/globalLobby/GlobalLobbyLoginWindow.h

@@ -23,6 +23,7 @@ class GlobalLobbyLoginWindow : public CWindowObject
 {
 	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
 	std::shared_ptr<CLabel> labelTitle;
+	std::shared_ptr<CLabel> labelUsernameTitle;
 	std::shared_ptr<CLabel> labelUsername;
 	std::shared_ptr<CTextBox> labelStatus;
 	std::shared_ptr<TransparentFilledRectangle> backgroundUsername;

+ 1 - 1
client/globalLobby/GlobalLobbyWidget.cpp

@@ -272,7 +272,7 @@ GlobalLobbyMatchCard::GlobalLobbyMatchCard(GlobalLobbyWindow * window, const Glo
 
 	if (matchDescription.participants.size() == 2)
 	{
-		std::string ourAccountID = settings["lobby"]["accountID"].String();
+		std::string ourAccountID = CSH->getGlobalLobby().getAccountID();
 
 		opponentDescription.appendTextID("vcmi.lobby.match.duel");
 		// Find display name of our one and only opponent in this game

+ 5 - 2
client/globalLobby/GlobalLobbyWindow.cpp

@@ -24,6 +24,7 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/Languages.h"
 #include "../../lib/MetaString.h"
+#include "../../lib/TextOperations.h"
 
 GlobalLobbyWindow::GlobalLobbyWindow()
 	: CWindowObject(BORDERED)
@@ -33,7 +34,7 @@ GlobalLobbyWindow::GlobalLobbyWindow()
 	pos = widget->pos;
 	center();
 
-	widget->getAccountNameLabel()->setText(settings["lobby"]["displayName"].String());
+	widget->getAccountNameLabel()->setText(CSH->getGlobalLobby().getAccountDisplayName());
 	doOpenChannel("global", "english", Languages::getLanguageOptions("english").nameNative);
 
 	widget->getChannelListHeader()->setText(MetaString::createFromTextID("vcmi.lobby.header.channels").toString());
@@ -54,7 +55,7 @@ void GlobalLobbyWindow::doOpenChannel(const std::string & channelType, const std
 
 	auto history = CSH->getGlobalLobby().getChannelHistory(channelType, channelName);
 
-	for (auto const & entry : history)
+	for(const auto & entry : history)
 		onGameChatMessage(entry.displayName, entry.messageText, entry.timeFormatted, channelType, channelName);
 
 	MetaString text;
@@ -80,6 +81,8 @@ void GlobalLobbyWindow::doSendChatMessage()
 	toSend["channelName"].String() = currentChannelName;
 	toSend["messageText"].String() = messageText;
 
+	assert(TextOperations::isValidUnicodeString(messageText));
+
 	CSH->getGlobalLobby().sendMessage(toSend);
 
 	widget->getMessageInput()->setText("");

+ 1 - 1
client/gui/FramerateManager.cpp

@@ -1,5 +1,5 @@
 /*
- * FramerateManager.h, part of VCMI engine
+ * FramerateManager.cpp, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *

+ 1 - 0
client/ios/GameChatKeyboardHandler.h

@@ -7,6 +7,7 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
+#pragma once
 
 #import <UIKit/UIKit.h>
 

+ 1 - 1
client/ios/startSDL.mm

@@ -7,8 +7,8 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
-#import "startSDL.h"
 #include "StdInc.h"
+#import "startSDL.h"
 #import "GameChatKeyboardHandler.h"
 
 #include "../Global.h"

+ 1 - 1
client/render/Colors.h

@@ -1,5 +1,5 @@
 /*
- * ICursor.h, part of VCMI engine
+ * Colors.h, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *

+ 3 - 3
client/widgets/CExchangeController.h

@@ -7,10 +7,10 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
- #pragma once
+#pragma once
  
- #include "../windows/CWindowObject.h"
- #include "CWindowWithArtifacts.h"
+#include "../windows/CWindowObject.h"
+#include "CWindowWithArtifacts.h"
  
 class CCallback;
 

+ 4 - 40
config/schemas/settings.json

@@ -501,11 +501,7 @@
 				"enableInstalledMods", 
 				"autoCheckRepositories", 
 				"updateOnStartup", 
-				"updateConfigUrl", 
-				"lobbyUrl", 
-				"lobbyPort", 
-				"lobbyUsername", 
-				"connectionTimeout"
+				"updateConfigUrl"
 			],
 			"properties" : {
 				"defaultRepositoryEnabled" : {
@@ -543,26 +539,6 @@
 				"updateConfigUrl" : {
 					"type" : "string",
 					"default" : "https://raw.githubusercontent.com/vcmi/vcmi-updates/master/vcmi-updates.json"
-				},
-				"lobbyUrl" : {
-					"type" : "string",
-					"description" : "ip address or web link to remote proxy server",
-					"default" : "beholder.vcmi.eu"
-				},
-				"lobbyPort" : {
-					"type" : "number",
-					"description" : "connection port for remote proxy server",
-					"default" : 5002
-				},
-				"lobbyUsername" : {
-					"type" : "string",
-					"description" : "username for the client on the remote proxy server",
-					"default" : ""
-				},
-				"connectionTimeout" : {
-					"type" : "number",
-					"description" : "maximum time in ms, should be enough to establish socket connection to remote proxy server.",
-					"default" : 2000
 				}
 			}
 		},
@@ -570,31 +546,19 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"default" : {},
-			"required" : [ "mapPreview", "accountID", "accountCookie", "displayName", "hostname", "port", "roomPlayerLimit", "roomType", "roomMode" ],
+			"required" : [ "mapPreview", "hostname", "port", "roomPlayerLimit", "roomType", "roomMode" ],
 			"properties" : {
 				"mapPreview" : {
 					"type" : "boolean",
 					"default" : true
 				},
-				"accountID" : {
-					"type" : "string",
-					"default" : ""
-				},
-				"accountCookie" : {
-					"type" : "string",
-					"default" : ""
-				},
-				"displayName" : {
-					"type" : "string",
-					"default" : ""
-				},
 				"hostname" : {
 					"type" : "string",
-					"default" : "127.0.0.1"
+					"default" : "beholder.vcmi.eu"
 				},
 				"port" : {
 					"type" : "number",
-					"default" : 30303
+					"default" : 3031
 				},
 				"roomPlayerLimit" : {
 					"type" : "number",

+ 1 - 7
debian/control

@@ -2,7 +2,7 @@ Source: vcmi
 Section: games
 Priority: optional
 Maintainer: Ivan Savenko <[email protected]>
-Build-Depends: debhelper (>= 8), cmake, libsdl2-dev, libsdl2-image-dev, libsdl2-ttf-dev, libsdl2-mixer-dev, zlib1g-dev, libavformat-dev, libswscale-dev, libboost-dev (>=1.48), libboost-program-options-dev (>=1.48), libboost-filesystem-dev (>=1.48), libboost-system-dev (>=1.48), libboost-locale-dev (>=1.48), libboost-thread-dev (>=1.48), qtbase5-dev, libtbb2-dev, libfuzzylite-dev, libminizip-dev, libluajit-5.1-dev, qttools5-dev
+Build-Depends: debhelper (>= 8), cmake, libsdl2-dev, libsdl2-image-dev, libsdl2-ttf-dev, libsdl2-mixer-dev, zlib1g-dev, libavformat-dev, libswscale-dev, libboost-dev (>=1.48), libboost-program-options-dev (>=1.48), libboost-filesystem-dev (>=1.48), libboost-system-dev (>=1.48), libboost-locale-dev (>=1.48), libboost-thread-dev (>=1.48), qtbase5-dev, libtbb-dev, libfuzzylite-dev, libminizip-dev, libluajit-5.1-dev, qttools5-dev
 Standards-Version: 3.9.1
 Homepage: http://vcmi.eu
 Vcs-Git: git://github.com/vcmi/vcmi.git
@@ -19,9 +19,3 @@ Description: Rewrite of the Heroes of Might and Magic 3 engine
  .
  In its current state it already supports maps of any sizes, higher
  resolutions and extended engine limits.
-
-Package: vcmi-dbg
-Architecture: any
-Section: debug
-Depends: vcmi (= ${binary:Version}),    ${misc:Depends} 
-Description: Debug symbols for vcmi package

+ 1 - 3
debian/rules

@@ -8,14 +8,12 @@ override_dh_auto_configure:
 	dh_auto_configure -- \
 		-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON \
 		-DCMAKE_INSTALL_RPATH=/usr/lib/$(DEB_HOST_MULTIARCH)/vcmi \
-		-DCMAKE_BUILD_TYPE=RelWithDebInfo \
+		-DCMAKE_BUILD_TYPE=Release \
 		-DENABLE_GITVERSION=OFF \
 		-DBIN_DIR=games \
 		-DFORCE_BUNDLED_FL=OFF \
 		-DENABLE_TEST=0
 
-override_dh_strip:
-	dh_strip --dbg-package=vcmi-dbg
 override_dh_auto_install:
 	dh_auto_install --destdir=debian/vcmi
 override_dh_installdocs:

+ 9 - 0
launcher/StdInc.cpp

@@ -1 +1,10 @@
+/*
+ * StdInc.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"

+ 9 - 0
launcher/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.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 "../Global.h"

+ 0 - 8
launcher/modManager/cmodlist.h

@@ -9,18 +9,10 @@
  */
 #pragma once
 
-#include "StdInc.h"
-
 #include <QVariantMap>
 #include <QVariant>
 #include <QVector>
 
-VCMI_LIB_NAMESPACE_BEGIN
-
-class JsonNode;
-
-VCMI_LIB_NAMESPACE_END
-
 namespace ModStatus
 {
 enum EModStatus

+ 1 - 4
launcher/modManager/imageviewer_moc.h

@@ -7,8 +7,7 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
-#ifndef IMAGEVIEWER_H
-#define IMAGEVIEWER_H
+#pragma once
 
 #include <QDialog>
 
@@ -38,5 +37,3 @@ protected:
 private:
 	Ui::ImageViewer * ui;
 };
-
-#endif // IMAGEVIEWER_H

+ 98 - 80
lib/CMakeLists.txt

@@ -1,6 +1,47 @@
 set(lib_SRCS
 	StdInc.cpp
 
+	filesystem/AdapterLoaders.cpp
+	filesystem/CArchiveLoader.cpp
+	filesystem/CBinaryReader.cpp
+	filesystem/CCompressedStream.cpp
+	filesystem/CFileInputStream.cpp
+	filesystem/CFilesystemLoader.cpp
+	filesystem/CMemoryBuffer.cpp
+	filesystem/CMemoryStream.cpp
+	filesystem/CZipLoader.cpp
+	filesystem/CZipSaver.cpp
+	filesystem/FileInfo.cpp
+	filesystem/Filesystem.cpp
+	filesystem/MinizipExtensions.cpp
+	filesystem/ResourcePath.cpp
+
+	json/JsonNode.cpp
+	json/JsonParser.cpp
+	json/JsonUtils.cpp
+	json/JsonValidator.cpp
+	json/JsonWriter.cpp
+
+	logging/CBasicLogConfigurator.cpp
+	logging/CLogger.cpp
+	logging/VisualLogger.cpp
+
+	network/NetworkConnection.cpp
+	network/NetworkHandler.cpp
+	network/NetworkServer.cpp
+
+	vstd/DateUtils.cpp
+	vstd/StringUtils.cpp
+
+	CConfigHandler.cpp
+	CConsoleHandler.cpp
+	CThreadHelper.cpp
+	TextOperations.cpp
+	VCMIDirs.cpp
+)
+
+set(lib_MAIN_SRCS
+
 	battle/AccessibilityInfo.cpp
 	battle/BattleAction.cpp
 	battle/BattleAttackInfo.cpp
@@ -46,38 +87,14 @@ set(lib_SRCS
 	events/PlayerGotTurn.cpp
 	events/TurnStarted.cpp
 
-	filesystem/AdapterLoaders.cpp
-	filesystem/CArchiveLoader.cpp
-	filesystem/CBinaryReader.cpp
-	filesystem/CCompressedStream.cpp
-	filesystem/CFileInputStream.cpp
-	filesystem/CFilesystemLoader.cpp
-	filesystem/CMemoryBuffer.cpp
-	filesystem/CMemoryStream.cpp
-	filesystem/CZipLoader.cpp
-	filesystem/CZipSaver.cpp
-	filesystem/FileInfo.cpp
-	filesystem/Filesystem.cpp
-	filesystem/MinizipExtensions.cpp
-	filesystem/ResourcePath.cpp
-
 	json/JsonBonus.cpp
-	json/JsonNode.cpp
-	json/JsonParser.cpp
 	json/JsonRandom.cpp
-	json/JsonUtils.cpp
-	json/JsonValidator.cpp
-	json/JsonWriter.cpp
 
 	gameState/CGameState.cpp
 	gameState/CGameStateCampaign.cpp
 	gameState/InfoAboutArmy.cpp
 	gameState/TavernHeroesPool.cpp
 
-	logging/CBasicLogConfigurator.cpp
-	logging/CLogger.cpp
-	logging/VisualLogger.cpp
-
 	mapObjectConstructors/AObjectTypeHandler.cpp
 	mapObjectConstructors/CBankInstanceConstructor.cpp
 	mapObjectConstructors/CObjectClassesHandler.cpp
@@ -128,10 +145,6 @@ set(lib_SRCS
 	modding/IdentifierStorage.cpp
 	modding/ModUtility.cpp
 
-	network/NetworkConnection.cpp
-	network/NetworkHandler.cpp
-	network/NetworkServer.cpp
-
 	networkPacks/NetPacksLib.cpp
 
 	pathfinder/CGPathNode.cpp
@@ -225,9 +238,6 @@ set(lib_SRCS
 	spells/effects/RemoveObstacle.cpp
 	spells/effects/Sacrifice.cpp
 
-	vstd/DateUtils.cpp
-	vstd/StringUtils.cpp
-
 	ArtifactUtils.cpp
 	BasicTypes.cpp
 	BattleFieldHandler.cpp
@@ -236,8 +246,6 @@ set(lib_SRCS
 	CArtifactInstance.cpp
 	CBonusTypeHandler.cpp
 	CBuildingHandler.cpp
-	CConfigHandler.cpp
-	CConsoleHandler.cpp
 	CCreatureHandler.cpp
 	CCreatureSet.cpp
 	CGameInfoCallback.cpp
@@ -249,7 +257,6 @@ set(lib_SRCS
 	CScriptingModule.cpp
 	CSkillHandler.cpp
 	CStack.cpp
-	CThreadHelper.cpp
 	CTownHandler.cpp
 	GameSettings.cpp
 	IGameCallback.cpp
@@ -264,12 +271,14 @@ set(lib_SRCS
 	RoadHandler.cpp
 	ScriptHandler.cpp
 	TerrainHandler.cpp
-	TextOperations.cpp
 	TurnTimerInfo.cpp
-	VCMIDirs.cpp
 	VCMI_Lib.cpp
 )
 
+if (NOT ENABLE_MINIMAL_LIB)
+	list(APPEND lib_SRCS ${lib_MAIN_SRCS})
+endif()
+
 # Version.cpp is a generated file
 if(ENABLE_GITVERSION)
 	list(APPEND lib_SRCS ${CMAKE_BINARY_DIR}/Version.cpp)
@@ -280,14 +289,59 @@ endif()
 
 set(lib_HEADERS
 	../include/vstd/CLoggerBase.h
+	../include/vstd/DateUtils.h
+	../include/vstd/StringUtils.h
 	../Global.h
 	../AUTHORS.h
 	StdInc.h
 
+	filesystem/AdapterLoaders.h
+	filesystem/CArchiveLoader.h
+	filesystem/CBinaryReader.h
+	filesystem/CCompressedStream.h
+	filesystem/CFileInputStream.h
+	filesystem/CFilesystemLoader.h
+	filesystem/CInputOutputStream.h
+	filesystem/CInputStream.h
+	filesystem/CMemoryBuffer.h
+	filesystem/CMemoryStream.h
+	filesystem/COutputStream.h
+	filesystem/CStream.h
+	filesystem/CZipLoader.h
+	filesystem/CZipSaver.h
+	filesystem/FileInfo.h
+	filesystem/Filesystem.h
+	filesystem/ISimpleResourceLoader.h
+	filesystem/MinizipExtensions.h
+	filesystem/ResourcePath.h
+
+	json/JsonFormatException.h
+	json/JsonNode.h
+	json/JsonParser.h
+	json/JsonUtils.h
+	json/JsonValidator.h
+	json/JsonWriter.h
+
+	logging/CBasicLogConfigurator.h
+	logging/CLogger.h
+	logging/VisualLogger.h
+
+	network/NetworkConnection.h
+	network/NetworkDefines.h
+	network/NetworkHandler.h
+	network/NetworkInterface.h
+	network/NetworkServer.h
+
+	CConfigHandler.h
+	CConsoleHandler.h
+	CThreadHelper.h
+	TextOperations.h
+	VCMIDirs.h
+)
+
+set(lib_MAIN_HEADERS
 	../include/vstd/ContainerUtils.h
 	../include/vstd/RNG.h
-	../include/vstd/DateUtils.h
-	../include/vstd/StringUtils.h
 
 	../include/vcmi/events/AdventureEvents.h
 	../include/vcmi/events/ApplyDamage.h
@@ -385,34 +439,8 @@ set(lib_HEADERS
 	events/PlayerGotTurn.h
 	events/TurnStarted.h
 
-	filesystem/AdapterLoaders.h
-	filesystem/CArchiveLoader.h
-	filesystem/CBinaryReader.h
-	filesystem/CCompressedStream.h
-	filesystem/CFileInputStream.h
-	filesystem/CFilesystemLoader.h
-	filesystem/CInputOutputStream.h
-	filesystem/CInputStream.h
-	filesystem/CMemoryBuffer.h
-	filesystem/CMemoryStream.h
-	filesystem/COutputStream.h
-	filesystem/CStream.h
-	filesystem/CZipLoader.h
-	filesystem/CZipSaver.h
-	filesystem/FileInfo.h
-	filesystem/Filesystem.h
-	filesystem/ISimpleResourceLoader.h
-	filesystem/MinizipExtensions.h
-	filesystem/ResourcePath.h
-
 	json/JsonBonus.h
-	json/JsonFormatException.h
-	json/JsonNode.h
-	json/JsonParser.h
 	json/JsonRandom.h
-	json/JsonUtils.h
-	json/JsonValidator.h
-	json/JsonWriter.h
 
 	gameState/CGameState.h
 	gameState/CGameStateCampaign.h
@@ -423,10 +451,6 @@ set(lib_HEADERS
 	gameState/TavernSlot.h
 	gameState/QuestInfo.h
 
-	logging/CBasicLogConfigurator.h
-	logging/CLogger.h
-	logging/VisualLogger.h
-
 	mapObjectConstructors/AObjectTypeHandler.h
 	mapObjectConstructors/CBankInstanceConstructor.h
 	mapObjectConstructors/CDefaultObjectTypeHandler.h
@@ -487,12 +511,6 @@ set(lib_HEADERS
 	modding/ModUtility.h
 	modding/ModVerificationInfo.h
 
-	network/NetworkConnection.h
-	network/NetworkDefines.h
-	network/NetworkHandler.h
-	network/NetworkInterface.h
-	network/NetworkServer.h
-
 	networkPacks/ArtifactLocation.h
 	networkPacks/BattleChanges.h
 	networkPacks/Component.h
@@ -622,8 +640,6 @@ set(lib_HEADERS
 	CArtifactInstance.h
 	CBonusTypeHandler.h
 	CBuildingHandler.h
-	CConfigHandler.h
-	CConsoleHandler.h
 	CCreatureHandler.h
 	CCreatureSet.h
 	CGameInfoCallback.h
@@ -640,7 +656,6 @@ set(lib_HEADERS
 	CSoundBase.h
 	CStack.h
 	CStopWatch.h
-	CThreadHelper.h
 	CTownHandler.h
 	ExtraOptionsInfo.h
 	FunctionList.h
@@ -667,14 +682,16 @@ set(lib_HEADERS
 	ScopeGuard.h
 	StartInfo.h
 	TerrainHandler.h
-	TextOperations.h
 	TurnTimerInfo.h
 	UnlockGuard.h
-	VCMIDirs.h
 	vcmi_endian.h
 	VCMI_Lib.h
 )
 
+if (NOT ENABLE_MINIMAL_LIB)
+	list(APPEND lib_HEADERS ${lib_MAIN_HEADERS})
+endif()
+
 assign_source_group(${lib_SRCS} ${lib_HEADERS})
 
 if(ENABLE_STATIC_LIBS)
@@ -682,13 +699,14 @@ if(ENABLE_STATIC_LIBS)
 else()
 	add_library(vcmi SHARED ${lib_SRCS} ${lib_HEADERS})
 endif()
+
 set_target_properties(vcmi PROPERTIES COMPILE_DEFINITIONS "VCMI_DLL=1")
 target_link_libraries(vcmi PUBLIC
 	minizip::minizip ZLIB::ZLIB
 	${SYSTEM_LIBS} Boost::boost Boost::thread Boost::filesystem Boost::program_options Boost::locale Boost::date_time
 )
 
-if(ENABLE_STATIC_LIBS)
+if(ENABLE_STATIC_LIBS AND ENABLE_CLIENT)
 	target_compile_definitions(vcmi PRIVATE STATIC_AI)
 	target_link_libraries(vcmi PRIVATE
 		BattleAI

+ 9 - 0
lib/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.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
+ *
+ */
 // Creates the precompiled header
 #include "StdInc.h"

+ 9 - 0
lib/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.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 "../Global.h"

+ 3 - 2
lib/VCMIDirs.cpp

@@ -367,8 +367,9 @@ class IVCMIDirsUNIX : public IVCMIDirs
 bool IVCMIDirsUNIX::developmentMode() const
 {
 	// We want to be able to run VCMI from single directory. E.g to run from build output directory
-	const bool result = bfs::exists("AI") && bfs::exists("config") && bfs::exists("Mods") && bfs::exists("vcmiclient");
-	return result;
+	const bool hasConfigs = bfs::exists("config") && bfs::exists("Mods");
+	const bool hasBinaries = bfs::exists("vcmiclient") || bfs::exists("vcmiserver") || bfs::exists("vcmilobby");
+	return hasConfigs && hasBinaries;
 }
 
 bfs::path IVCMIDirsUNIX::clientPath() const { return binaryPath() / "vcmiclient"; }

+ 2 - 1
lib/battle/PossiblePlayerBattleAction.h

@@ -1,5 +1,5 @@
 /*
- * CBattleInfoCallback.h, part of VCMI engine
+ * PossiblePlayerBattleAction.h, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *
@@ -7,6 +7,7 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
+#pragma once
 
 #include "../GameConstants.h"
 

+ 0 - 1
lib/bonuses/IBonusBearer.h

@@ -1,4 +1,3 @@
-
 /*
  * IBonusBearer.h, part of VCMI engine
  *

+ 2 - 0
lib/json/JsonValidator.cpp

@@ -422,6 +422,7 @@ static std::string additionalPropertiesCheck(JsonValidator & validator, const Js
 
 static bool testFilePresence(const std::string & scope, const ResourcePath & resource)
 {
+#ifndef ENABLE_MINIMAL_LIB
 	std::set<std::string> allowedScopes;
 	if(scope != ModScope::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies
 	{
@@ -441,6 +442,7 @@ static bool testFilePresence(const std::string & scope, const ResourcePath & res
 		if (CResourceHandler::get(entry)->existsResource(resource))
 			return true;
 	}
+#endif
 	return false;
 }
 

+ 1 - 1
lib/modding/CModVersion.cpp

@@ -1,5 +1,5 @@
 /*
- * CModVersion.h, part of VCMI engine
+ * CModVersion.cpp, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *

+ 37 - 34
lib/pathfinder/CPathfinder.cpp

@@ -49,30 +49,27 @@ bool CPathfinderHelper::canMoveFromNode(const PathNodeInfo & source) const
 	return true;
 }
 
-std::vector<int3> CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & source) const
+void CPathfinderHelper::calculateNeighbourTiles(std::vector<int3> & result, const PathNodeInfo & source) const
 {
-	std::vector<int3> neighbourTiles;
+	result.clear();
 
 	if (!canMoveFromNode(source))
-		return neighbourTiles;
+		return;
 
-	neighbourTiles.reserve(8);
 	getNeighbours(
 		*source.tile,
 		source.node->coord,
-		neighbourTiles,
+		result,
 		boost::logic::indeterminate,
 		source.node->layer == EPathfindingLayer::SAIL);
 
 	if(source.isNodeObjectVisitable())
 	{
-		vstd::erase_if(neighbourTiles, [&](const int3 & tile) -> bool
+		vstd::erase_if(result, [&](const int3 & tile) -> bool
 		{
 			return !canMoveBetween(tile, source.nodeObject->visitablePos());
 		});
 	}
-
-	return neighbourTiles;
 }
 
 CPathfinder::CPathfinder(CGameState * _gs, std::shared_ptr<PathfinderConfig> config): 
@@ -129,6 +126,8 @@ void CPathfinder::calculatePaths()
 		pq.push(initialNode);
 	}
 
+	std::vector<CGPathNode *> neighbourNodes;
+
 	while(!pq.empty())
 	{
 		counter++;
@@ -158,43 +157,47 @@ void CPathfinder::calculatePaths()
 		source.updateInfo(hlp, gamestate);
 
 		//add accessible neighbouring nodes to the queue
-		auto neighbourNodes = config->nodeStorage->calculateNeighbours(source, config.get(), hlp);
-		for(CGPathNode * neighbour : neighbourNodes)
+		for(EPathfindingLayer layer = EPathfindingLayer::LAND; layer < EPathfindingLayer::NUM_LAYERS; layer.advance(1))
 		{
-			if(neighbour->locked)
+			if(!hlp->isLayerAvailable(layer))
 				continue;
 
-			if(!hlp->isLayerAvailable(neighbour->layer))
-				continue;
+			config->nodeStorage->calculateNeighbours(neighbourNodes, source, layer, config.get(), hlp);
 
-			destination.setNode(gamestate, neighbour);
-			hlp = config->getOrCreatePathfinderHelper(destination, gamestate);
+			for(CGPathNode * neighbour : neighbourNodes)
+			{
+				if(neighbour->locked)
+					continue;
 
-			if(!hlp->isPatrolMovementAllowed(neighbour->coord))
-				continue;
+				destination.setNode(gamestate, neighbour);
+				hlp = config->getOrCreatePathfinderHelper(destination, gamestate);
 
-			/// Check transition without tile accessability rules
-			if(source.node->layer != neighbour->layer && !isLayerTransitionPossible())
-				continue;
+				if(!hlp->isPatrolMovementAllowed(neighbour->coord))
+					continue;
 
-			destination.turn = turn;
-			destination.movementLeft = movement;
-			destination.cost = cost;
-			destination.updateInfo(hlp, gamestate);
-			destination.isGuardianTile = destination.guarded && isDestinationGuardian();
+				/// Check transition without tile accessability rules
+				if(source.node->layer != neighbour->layer && !isLayerTransitionPossible())
+					continue;
 
-			for(const auto & rule : config->rules)
-			{
-				rule->process(source, destination, config.get(), hlp);
+				destination.turn = turn;
+				destination.movementLeft = movement;
+				destination.cost = cost;
+				destination.updateInfo(hlp, gamestate);
+				destination.isGuardianTile = destination.guarded && isDestinationGuardian();
 
-				if(destination.blocked)
-					break;
-			}
+				for(const auto & rule : config->rules)
+				{
+					rule->process(source, destination, config.get(), hlp);
 
-			if(!destination.blocked)
-				push(destination.node);
+					if(destination.blocked)
+						break;
+				}
 
-		} //neighbours loop
+				if(!destination.blocked)
+					push(destination.node);
+
+			} //neighbours loop
+		}
 
 		//just add all passable teleport exits
 		hlp = config->getOrCreatePathfinderHelper(source, gamestate);

+ 1 - 1
lib/pathfinder/CPathfinder.h

@@ -96,7 +96,7 @@ public:
 	bool addTeleportWhirlpool(const CGWhirlpool * obj) const;
 	bool canMoveBetween(const int3 & a, const int3 & b) const; //checks only for visitable objects that may make moving between tiles impossible, not other conditions (like tiles itself accessibility)
 
-	std::vector<int3> getNeighbourTiles(const PathNodeInfo & source) const;
+	void calculateNeighbourTiles(std::vector<int3> & result, const PathNodeInfo & source) const;
 	std::vector<int3> getTeleportExits(const PathNodeInfo & source) const;
 
 	void getNeighbours(

+ 3 - 1
lib/pathfinder/INodeStorage.h

@@ -31,8 +31,10 @@ public:
 
 	virtual std::vector<CGPathNode *> getInitialNodes() = 0;
 
-	virtual std::vector<CGPathNode *> calculateNeighbours(
+	virtual void calculateNeighbours(
+		std::vector<CGPathNode *> & result,
 		const PathNodeInfo & source,
+		EPathfindingLayer layer,
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) = 0;
 

+ 13 - 13
lib/pathfinder/NodeStorage.cpp

@@ -60,29 +60,29 @@ void NodeStorage::initialize(const PathfinderOptions & options, const CGameState
 	}
 }
 
-std::vector<CGPathNode *> NodeStorage::calculateNeighbours(
+void NodeStorage::calculateNeighbours(
+	std::vector<CGPathNode *> & result,
 	const PathNodeInfo & source,
+	EPathfindingLayer layer,
 	const PathfinderConfig * pathfinderConfig,
 	const CPathfinderHelper * pathfinderHelper)
 {
-	std::vector<CGPathNode *> neighbours;
-	neighbours.reserve(16);
-	auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
+	std::vector<int3> accessibleNeighbourTiles;
+	
+	result.clear();
+	accessibleNeighbourTiles.reserve(8);
+	
+	pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);
 
 	for(auto & neighbour : accessibleNeighbourTiles)
 	{
-		for(EPathfindingLayer i = EPathfindingLayer::LAND; i < EPathfindingLayer::NUM_LAYERS; i.advance(1))
-		{
-			auto * node = getNode(neighbour, i);
+		auto * node = getNode(neighbour, layer);
 
-			if(node->accessible == EPathAccessibility::NOT_SET)
-				continue;
+		if(node->accessible == EPathAccessibility::NOT_SET)
+			continue;
 
-			neighbours.push_back(node);
-		}
+		result.push_back(node);
 	}
-
-	return neighbours;
 }
 
 std::vector<CGPathNode *> NodeStorage::calculateTeleportations(

+ 3 - 1
lib/pathfinder/NodeStorage.h

@@ -36,8 +36,10 @@ public:
 
 	std::vector<CGPathNode *> getInitialNodes() override;
 
-	virtual std::vector<CGPathNode *> calculateNeighbours(
+	virtual void calculateNeighbours(
+		std::vector<CGPathNode *> & result,
 		const PathNodeInfo & source,
+		EPathfindingLayer layer,
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) override;
 

+ 1 - 0
lib/spells/ObstacleCasterProxy.h

@@ -7,6 +7,7 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
+#pragma once
 
 #include "ProxyCaster.h"
 #include "../battle/BattleHex.h"

+ 3 - 3
lib/spells/ViewSpellInt.h

@@ -8,10 +8,10 @@
  *
  */
 
- #pragma once
+#pragma once
 
- #include "../int3.h"
- #include "../GameConstants.h"
+#include "../int3.h"
+#include "../GameConstants.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 9 - 0
lib/vstd/DateUtils.cpp

@@ -1,3 +1,12 @@
+/*
+ * DateUtils.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 <vstd/DateUtils.h>
 

+ 9 - 0
lib/vstd/StringUtils.cpp

@@ -1,3 +1,12 @@
+/*
+ * StringUtils.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 <vstd/StringUtils.h>
 

+ 1 - 1
lobby/EntryPoint.cpp

@@ -16,7 +16,7 @@
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/VCMIDirs.h"
 
-static const int LISTENING_PORT = 30303;
+static const int LISTENING_PORT = 3031;
 
 int main(int argc, const char * argv[])
 {

+ 4 - 1
lobby/LobbyServer.cpp

@@ -293,6 +293,7 @@ void LobbyServer::onDisconnected(const NetworkConnectionPtr & connection, const
 {
 	if(activeAccounts.count(connection))
 	{
+		logGlobal->info("Account %s disconnecting. Accounts online: %d", activeAccounts.at(connection), activeAccounts.size() - 1);
 		database->setAccountOnline(activeAccounts.at(connection), false);
 		activeAccounts.erase(connection);
 	}
@@ -300,6 +301,7 @@ void LobbyServer::onDisconnected(const NetworkConnectionPtr & connection, const
 	if(activeGameRooms.count(connection))
 	{
 		std::string gameRoomID = activeGameRooms.at(connection);
+		logGlobal->info("Game room %s disconnecting. Rooms online: %d", gameRoomID, activeGameRooms.size() - 1);
 
 		if (database->getGameRoomStatus(gameRoomID) == LobbyRoomState::BUSY)
 		{
@@ -489,7 +491,7 @@ void LobbyServer::receiveSendChatMessage(const NetworkConnectionPtr & connection
 	std::string channelName = json["channelName"].String();
 	std::string displayName = database->getAccountDisplayName(senderAccountID);
 
-	if(TextOperations::isValidUnicodeString(messageText))
+	if(!TextOperations::isValidUnicodeString(messageText))
 		return sendOperationFailed(connection, "String contains invalid characters!");
 
 	std::string messageTextClean = sanitizeChatMessage(messageText);
@@ -595,6 +597,7 @@ void LobbyServer::receiveClientLogin(const NetworkConnectionPtr & connection, co
 
 	activeAccounts[connection] = accountID;
 
+	logGlobal->info("%s: Logged in as %s", accountID, displayName);
 	sendClientLoginSuccess(connection, accountCookie, displayName);
 	sendRecentChatHistory(connection, "global", "english");
 	if (language != "english")

+ 9 - 0
lobby/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.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
+ *
+ */
 // Creates the precompiled header
 #include "StdInc.h"

+ 9 - 0
mapeditor/StdInc.cpp

@@ -1 +1,10 @@
+/*
+ * StdInc.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"

+ 9 - 0
mapeditor/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.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 "../Global.h"

+ 1 - 1
mapeditor/main.cpp

@@ -7,8 +7,8 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
-#include <QApplication>
 #include "StdInc.h"
+#include <QApplication>
 #include "mainwindow.h"
 
 int main(int argc, char * argv[])

+ 1 - 1
mapeditor/mapsettings/victoryconditions.cpp

@@ -1,5 +1,5 @@
 /*
- * victoryconditions.h, part of VCMI engine
+ * victoryconditions.cpp, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *

+ 9 - 0
scripting/erm/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.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
+ *
+ */
 // Creates the precompiled header
 #include "StdInc.h"

+ 9 - 0
scripting/erm/StdInc.h

@@ -1,3 +1,12 @@
+/*
+ * StdInc.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 "../../Global.h"

+ 9 - 0
scripting/lua/StdInc.cpp

@@ -1,2 +1,11 @@
+/*
+ * StdInc.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
+ *
+ */
 // Creates the precompiled header
 #include "StdInc.h"

Неке датотеке нису приказане због велике количине промена