diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp
index 0e8d6672f..4e7d07bf5 100644
--- a/AI/Nullkiller/AIGateway.cpp
+++ b/AI/Nullkiller/AIGateway.cpp
@@ -1296,7 +1296,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 	else
 	{
 		CGPath path;
-		cb->getPathsInfo(h.get())->getPath(path, dst);
+		nullkiller->getPathsInfo(h.get())->getPath(path, dst);
 		if(path.nodes.empty())
 		{
 			logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString());
@@ -1808,4 +1808,9 @@ bool AIStatus::channelProbing()
 	return ongoingChannelProbing;
 }
 
+void AIGateway::invalidatePaths()
+{
+	nullkiller->invalidatePaths();
+}
+
 }
diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h
index a4a8a845a..969467d27 100644
--- a/AI/Nullkiller/AIGateway.h
+++ b/AI/Nullkiller/AIGateway.h
@@ -159,6 +159,8 @@ public:
 	void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) override;
 	void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
 
+	void invalidatePaths() override;
+
 	void makeTurn();
 
 	void buildArmyIn(const CGTownInstance * t);
diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp
index 69c32974c..5d7686f9b 100644
--- a/AI/Nullkiller/Engine/Nullkiller.cpp
+++ b/AI/Nullkiller/Engine/Nullkiller.cpp
@@ -24,6 +24,8 @@
 #include "../Goals/Composition.h"
 #include "../../../lib/CPlayerState.h"
 #include "../../lib/StartInfo.h"
+#include "../../lib/pathfinder/PathfinderCache.h"
+#include "../../lib/pathfinder/PathfinderOptions.h"
 
 namespace NKAI
 {
@@ -43,6 +45,8 @@ Nullkiller::Nullkiller()
 
 }
 
+Nullkiller::~Nullkiller() = default;
+
 bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
 {
 	if(!cb->getStartInfo()->extraOptionsInfo.cheatsAllowed)
@@ -73,6 +77,14 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
 
 	settings = std::make_unique<Settings>(cb->getStartInfo()->difficulty);
 
+	PathfinderOptions pathfinderOptions(cb.get());
+
+	pathfinderOptions.useTeleportTwoWay = true;
+	pathfinderOptions.useTeleportOneWay = settings->isOneWayMonolithUsageAllowed();
+	pathfinderOptions.useTeleportOneWayRandom = settings->isOneWayMonolithUsageAllowed();
+
+	pathfinderCache = std::make_unique<PathfinderCache>(cb.get(), pathfinderOptions);
+
 	if(canUseOpenMap(cb, playerID))
 	{
 		useObjectGraph = settings->isObjectGraphAllowed();
@@ -721,4 +733,14 @@ bool Nullkiller::handleTrading()
 	return haveTraded;
 }
 
+std::shared_ptr<const CPathsInfo> Nullkiller::getPathsInfo(const CGHeroInstance * h) const
+{
+	return pathfinderCache->getPathsInfo(h);
+}
+
+void Nullkiller::invalidatePaths()
+{
+	pathfinderCache->invalidatePaths();
+}
+
 }
diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h
index 369fe7116..c6240e116 100644
--- a/AI/Nullkiller/Engine/Nullkiller.h
+++ b/AI/Nullkiller/Engine/Nullkiller.h
@@ -21,6 +21,12 @@
 #include "../Analyzers/ObjectClusterizer.h"
 #include "../Helpers/ArmyFormation.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
+class PathfinderCache;
+
+VCMI_LIB_NAMESPACE_END
+
 namespace NKAI
 {
 
@@ -72,6 +78,7 @@ private:
 	int3 targetTile;
 	ObjectInstanceID targetObject;
 	std::map<const CGHeroInstance *, HeroLockedReason> lockedHeroes;
+	std::unique_ptr<PathfinderCache> pathfinderCache;
 	ScanDepth scanDepth;
 	TResources lockedResources;
 	bool useHeroChain;
@@ -101,6 +108,7 @@ public:
 	std::mutex aiStateMutex;
 
 	Nullkiller();
+	~Nullkiller();
 	void init(std::shared_ptr<CCallback> cb, AIGateway * gateway);
 	void makeTurn();
 	bool isActive(const CGHeroInstance * hero) const { return activeHero == hero; }
@@ -124,6 +132,9 @@ public:
 	bool handleTrading();
 	void invalidatePathfinderData();
 
+	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h) const;
+	void invalidatePaths();
+
 private:
 	void resetAiState();
 	void updateAiState(int pass, bool fast = false);
diff --git a/AI/Nullkiller/Engine/Settings.cpp b/AI/Nullkiller/Engine/Settings.cpp
index 11357f9c1..b41f5b1ca 100644
--- a/AI/Nullkiller/Engine/Settings.cpp
+++ b/AI/Nullkiller/Engine/Settings.cpp
@@ -38,6 +38,7 @@ namespace NKAI
 		pathfinderBucketsCount(1),
 		pathfinderBucketSize(32),
 		allowObjectGraph(true),
+		useOneWayMonoliths(false),
 		useTroopsFromGarrisons(false),
 		updateHitmapOnTileReveal(false),
 		openMap(true),
@@ -64,5 +65,6 @@ namespace NKAI
 		openMap = node["openMap"].Bool();
 		useFuzzy = node["useFuzzy"].Bool();
 		useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool();
+		useOneWayMonoliths = node["useOneWayMonoliths"].Bool();
 	}
 }
diff --git a/AI/Nullkiller/Engine/Settings.h b/AI/Nullkiller/Engine/Settings.h
index ff2d1b859..f7947762e 100644
--- a/AI/Nullkiller/Engine/Settings.h
+++ b/AI/Nullkiller/Engine/Settings.h
@@ -36,6 +36,7 @@ namespace NKAI
 		float maxArmyLossTarget;
 		bool allowObjectGraph;
 		bool useTroopsFromGarrisons;
+		bool useOneWayMonoliths;
 		bool updateHitmapOnTileReveal;
 		bool openMap;
 		bool useFuzzy;
@@ -58,6 +59,7 @@ namespace NKAI
 		int getPathfinderBucketSize() const { return pathfinderBucketSize; }
 		bool isObjectGraphAllowed() const { return allowObjectGraph; }
 		bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
+		bool isOneWayMonolithUsageAllowed() const { return useOneWayMonoliths; }
 		bool isUpdateHitmapOnTileReveal() const { return updateHitmapOnTileReveal; }
 		bool isOpenMap() const { return openMap; }
 		bool isUseFuzzy() const { return useFuzzy; }
diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp
index 0391a4585..f2d14560b 100644
--- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp
+++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp
@@ -166,7 +166,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						if(nextNode.specialAction || nextNode.chainMask != chainMask)
 							break;
 
-						auto targetNode = cb->getPathsInfo(hero)->getPathInfo(nextNode.coord);
+						auto targetNode = ai->nullkiller->getPathsInfo(hero)->getPathInfo(nextNode.coord);
 
 						if(!targetNode->reachable()
 							|| targetNode->getCost() > nextNode.cost)
@@ -182,7 +182,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 
 				if(node->turns == 0 && node->coord != hero->visitablePos())
 				{
-					auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node->coord);
+					auto targetNode = ai->nullkiller->getPathsInfo(hero)->getPathInfo(node->coord);
 
 					if(targetNode->accessible == EPathAccessibility::NOT_SET
 						|| targetNode->accessible == EPathAccessibility::BLOCKED
@@ -239,7 +239,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						if(hero->movementPointsRemaining() > 0)
 						{
 							CGPath path;
-							bool isOk = cb->getPathsInfo(hero)->getPath(path, node->coord);
+							bool isOk = ai->nullkiller->getPathsInfo(hero)->getPath(path, node->coord);
 
 							if(isOk && path.nodes.back().turns > 0)
 							{
diff --git a/AI/Nullkiller/Goals/ExploreNeighbourTile.cpp b/AI/Nullkiller/Goals/ExploreNeighbourTile.cpp
index 6efa0e0b4..99dd54da8 100644
--- a/AI/Nullkiller/Goals/ExploreNeighbourTile.cpp
+++ b/AI/Nullkiller/Goals/ExploreNeighbourTile.cpp
@@ -35,7 +35,7 @@ void ExploreNeighbourTile::accept(AIGateway * ai)
 		int3 target = int3(-1);
 		foreach_neighbour(pos, [&](int3 tile)
 			{
-				auto pathInfo = ai->myCb->getPathsInfo(hero)->getPathInfo(tile);
+				auto pathInfo = ai->nullkiller->getPathsInfo(hero)->getPathInfo(tile);
 
 				if(pathInfo->turns > 0)
 					return;
diff --git a/AI/Nullkiller/Helpers/ExplorationHelper.cpp b/AI/Nullkiller/Helpers/ExplorationHelper.cpp
index 0c17e0cc2..b0d093921 100644
--- a/AI/Nullkiller/Helpers/ExplorationHelper.cpp
+++ b/AI/Nullkiller/Helpers/ExplorationHelper.cpp
@@ -223,7 +223,7 @@ bool ExplorationHelper::hasReachableNeighbor(const int3 & pos) const
 		if(cbp->isInTheMap(tile))
 		{
 			auto isAccessible = useCPathfinderAccessibility
-				? ai->cb->getPathsInfo(hero)->getPathInfo(tile)->reachable()
+				? ai->getPathsInfo(hero)->getPathInfo(tile)->reachable()
 				: ai->pathfinder->isTileAccessible(hero, tile);
 
 			if(isAccessible)
diff --git a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
index 7bb43fabb..25e55dcb6 100644
--- a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
+++ b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
@@ -50,6 +50,8 @@ namespace AIPathfinding
 		options.allowLayerTransitioningAfterBattle = true;
 		options.useTeleportWhirlpool = true;
 		options.forceUseTeleportWhirlpool = true;
+		options.useTeleportOneWay = ai->settings->isOneWayMonolithUsageAllowed();;
+		options.useTeleportOneWayRandom = ai->settings->isOneWayMonolithUsageAllowed();;
 	}
 
 	AIPathfinderConfig::~AIPathfinderConfig() = default;
diff --git a/AI/VCAI/AIUtility.cpp b/AI/VCAI/AIUtility.cpp
index ff391587f..94cb618a4 100644
--- a/AI/VCAI/AIUtility.cpp
+++ b/AI/VCAI/AIUtility.cpp
@@ -133,8 +133,8 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
 
 bool CDistanceSorter::operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const
 {
-	const CGPathNode * ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos());
-	const CGPathNode * rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos());
+	const CGPathNode * ln = ai->getPathsInfo(hero)->getPathInfo(lhs->visitablePos());
+	const CGPathNode * rn = ai->getPathsInfo(hero)->getPathInfo(rhs->visitablePos());
 
 	return ln->getCost() < rn->getCost();
 }
diff --git a/AI/VCAI/FuzzyEngines.cpp b/AI/VCAI/FuzzyEngines.cpp
index 05db1b15a..ba914a52a 100644
--- a/AI/VCAI/FuzzyEngines.cpp
+++ b/AI/VCAI/FuzzyEngines.cpp
@@ -96,7 +96,7 @@ float HeroMovementGoalEngineBase::calculateTurnDistanceInputValue(const Goals::A
 	}
 	else
 	{
-		auto pathInfo = ai->myCb->getPathsInfo(goal.hero.h)->getPathInfo(goal.tile);
+		auto pathInfo = ai->getPathsInfo(goal.hero.h)->getPathInfo(goal.tile);
 		return pathInfo->getCost();
 	}
 }
diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp
index 3b8b11793..eb086b47f 100644
--- a/AI/VCAI/VCAI.cpp
+++ b/AI/VCAI/VCAI.cpp
@@ -31,6 +31,8 @@
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 #include "../../lib/networkPacks/PacksForServer.h"
 #include "../../lib/serializer/CTypeList.h"
+#include "../../lib/pathfinder/PathfinderCache.h"
+#include "../../lib/pathfinder/PathfinderOptions.h"
 
 #include "AIhelper.h"
 
@@ -621,6 +623,7 @@ void VCAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<C
 	playerID = *myCb->getPlayerID();
 	myCb->waitTillRealize = true;
 	myCb->unlockGsWhenWaiting = true;
+	pathfinderCache = std::make_unique<PathfinderCache>(myCb.get(), PathfinderOptions(myCb.get()));
 
 	if(!fh)
 		fh = new FuzzyHelper();
@@ -628,6 +631,16 @@ void VCAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<C
 	retrieveVisitableObjs();
 }
 
+std::shared_ptr<const CPathsInfo> VCAI::getPathsInfo(const CGHeroInstance * h) const
+{
+	return pathfinderCache->getPathsInfo(h);
+}
+
+void VCAI::invalidatePaths()
+{
+	pathfinderCache->invalidatePaths();
+}
+
 void VCAI::yourTurn(QueryID queryID)
 {
 	LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
@@ -1800,7 +1813,7 @@ bool VCAI::isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies)
 			}
 		}
 	}
-	return cb->getPathsInfo(h.get())->getPathInfo(pos)->reachable();
+	return getPathsInfo(h.get())->getPathInfo(pos)->reachable();
 }
 
 bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
@@ -1837,7 +1850,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 	else
 	{
 		CGPath path;
-		cb->getPathsInfo(h.get())->getPath(path, dst);
+		getPathsInfo(h.get())->getPath(path, dst);
 		if(path.nodes.empty())
 		{
 			logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString());
diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h
index 57f68de19..bf7ebd7b9 100644
--- a/AI/VCAI/VCAI.h
+++ b/AI/VCAI/VCAI.h
@@ -26,6 +26,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 struct QuestInfo;
+class PathfinderCache;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -80,6 +81,7 @@ public:
 	std::vector<ObjectInstanceID> teleportChannelProbingList; //list of teleport channel exits that not visible and need to be (re-)explored
 	//std::vector<const CGObjectInstance *> visitedThisWeek; //only OPWs
 	std::map<HeroPtr, std::set<const CGTownInstance *>> townVisitsThisWeek;
+	std::unique_ptr<PathfinderCache> pathfinderCache;
 
 	//part of mainLoop, but accessible from outside
 	std::vector<Goals::TSubgoal> basicGoals;
@@ -254,6 +256,8 @@ public:
 	std::vector<HeroPtr> getMyHeroes() const;
 	HeroPtr primaryHero() const;
 	void checkHeroArmy(HeroPtr h);
+	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h) const;
+	void invalidatePaths() override;
 
 	void requestSent(const CPackForServer * pack, int requestID) override;
 	void answerQuery(QueryID queryID, int selection);
diff --git a/CCallback.cpp b/CCallback.cpp
index 61c741cc8..a0f8a1aa7 100644
--- a/CCallback.cpp
+++ b/CCallback.cpp
@@ -384,11 +384,6 @@ bool CCallback::canMoveBetween(const int3 &a, const int3 &b)
 	return gs->map->canMoveBetween(a, b);
 }
 
-std::shared_ptr<const CPathsInfo> CCallback::getPathsInfo(const CGHeroInstance * h)
-{
-	return cl->getPathsInfo(h);
-}
-
 std::optional<PlayerColor> CCallback::getPlayerID() const
 {
 	return CBattleCallback::getPlayerID();
diff --git a/CCallback.h b/CCallback.h
index 6e30299c6..db3455890 100644
--- a/CCallback.h
+++ b/CCallback.h
@@ -157,7 +157,6 @@ public:
 	//client-specific functionalities (pathfinding)
 	virtual bool canMoveBetween(const int3 &a, const int3 &b);
 	virtual int3 getGuardingCreaturePosition(int3 tile);
-	virtual std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
 
 	std::optional<PlayerColor> getPlayerID() const override;
 
diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp
index 3c2204559..39fc84306 100644
--- a/client/CPlayerInterface.cpp
+++ b/client/CPlayerInterface.cpp
@@ -97,6 +97,8 @@
 #include "../lib/networkPacks/PacksForServer.h"
 
 #include "../lib/pathfinder/CGPathNode.h"
+#include "../lib/pathfinder/PathfinderCache.h"
+#include "../lib/pathfinder/PathfinderOptions.h"
 
 #include "../lib/serializer/CTypeList.h"
 #include "../lib/serializer/ESerializationVersion.h"
@@ -156,6 +158,7 @@ CPlayerInterface::~CPlayerInterface()
 	if (LOCPLINT == this)
 		LOCPLINT = nullptr;
 }
+
 void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
 {
 	cb = CB;
@@ -164,9 +167,20 @@ void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::
 	CCS->musich->loadTerrainMusicThemes();
 	initializeHeroTownList();
 
+	pathfinderCache = std::make_unique<PathfinderCache>(cb.get(), PathfinderOptions(cb.get()));
 	adventureInt.reset(new AdventureMapInterface());
 }
 
+std::shared_ptr<const CPathsInfo> CPlayerInterface::getPathsInfo(const CGHeroInstance * h)
+{
+	return pathfinderCache->getPathsInfo(h);
+}
+
+void CPlayerInterface::invalidatePaths()
+{
+	pathfinderCache->invalidatePaths();
+}
+
 void CPlayerInterface::closeAllDialogs()
 {
 	// remove all active dialogs that do not expect query answer
@@ -467,6 +481,8 @@ void CPlayerInterface::heroSecondarySkillChanged(const CGHeroInstance * hero, in
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	for (auto cuw : GH.windows().findWindows<IMarketHolder>())
 		cuw->updateSecondarySkills();
+
+	localState->verifyPath(hero);
 }
 
 void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero)
@@ -583,6 +599,8 @@ void CPlayerInterface::garrisonsChanged(std::vector<const CArmedInstance *> objs
 
 		if (hero)
 		{
+			localState->verifyPath(hero);
+
 			adventureInt->onHeroChanged(hero);
 			if(hero->inTownGarrison && hero->visitedTown != town)
 				adventureInt->onTownChanged(hero->visitedTown);
diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h
index 3aefb35d9..1845c5b9b 100644
--- a/client/CPlayerInterface.h
+++ b/client/CPlayerInterface.h
@@ -27,6 +27,7 @@ class CGObjectInstance;
 class UpgradeInfo;
 class ConditionalWait;
 struct CPathsInfo;
+class PathfinderCache;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -64,6 +65,7 @@ class CPlayerInterface : public CGameInterface, public IUpdateable
 	std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!)
 
 	std::unique_ptr<HeroMovementController> movementController;
+	std::unique_ptr<PathfinderCache> pathfinderCache;
 public: // TODO: make private
 	std::unique_ptr<ArtifactsUIController> artifactController;
 	std::shared_ptr<Environment> env;
@@ -198,6 +200,8 @@ public: // public interface for use by client via LOCPLINT access
 	void gamePause(bool pause);
 	void endNetwork();
 	void closeAllDialogs();
+	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
+	void invalidatePaths() override;
 
 	///returns true if all events are processed internally
 	bool capturedAllEvents();
diff --git a/client/Client.cpp b/client/Client.cpp
index 0f55cbd52..4392de9c2 100644
--- a/client/Client.cpp
+++ b/client/Client.cpp
@@ -222,8 +222,6 @@ void CClient::initMapHandler()
 		CGI->mh = std::make_shared<CMapHandler>(gs->map);
 		logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff());
 	}
-
-	pathCache.clear();
 }
 
 void CClient::initPlayerEnvironments()
@@ -494,24 +492,7 @@ void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor col
 	}
 }
 
-void CClient::updatePath(const ObjectInstanceID & id)
-{
-	invalidatePaths();
-	auto hero = getHero(id);
-	updatePath(hero);
-}
 
-void CClient::updatePath(const CGHeroInstance * hero)
-{
-	if(LOCPLINT && hero)
-		LOCPLINT->localState->verifyPath(hero);
-}
-
-void CClient::invalidatePaths()
-{
-	boost::unique_lock<boost::mutex> pathLock(pathCacheMutex);
-	pathCache.clear();
-}
 
 vstd::RNG & CClient::getRandomGenerator()
 {
@@ -520,28 +501,6 @@ vstd::RNG & CClient::getRandomGenerator()
 	throw std::runtime_error("Illegal access to random number generator from client code!");
 }
 
-std::shared_ptr<const CPathsInfo> CClient::getPathsInfo(const CGHeroInstance * h)
-{
-	assert(h);
-	boost::unique_lock<boost::mutex> pathLock(pathCacheMutex);
-
-	auto iter = pathCache.find(h);
-
-	if(iter == std::end(pathCache))
-	{
-		auto paths = std::make_shared<CPathsInfo>(getMapSize(), h);
-
-		gs->calculatePaths(h, *paths.get());
-
-		pathCache[h] = paths;
-		return paths;
-	}
-	else
-	{
-		return iter->second;
-	}
-}
-
 #if SCRIPTING_ENABLED
 scripting::Pool * CClient::getGlobalContextPool() const
 {
diff --git a/client/Client.h b/client/Client.h
index ce1276b06..ba4f2b34e 100644
--- a/client/Client.h
+++ b/client/Client.h
@@ -149,11 +149,6 @@ public:
 	void battleFinished(const BattleID & battleID);
 	void startPlayerBattleAction(const BattleID & battleID, PlayerColor color);
 
-	void invalidatePaths(); // clears this->pathCache()
-	void updatePath(const ObjectInstanceID & heroID); // invalidatePaths and update displayed hero path 
-	void updatePath(const CGHeroInstance * hero);
-	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
-
 	friend class CCallback; //handling players actions
 	friend class CBattleCallback; //handling players actions
 
@@ -235,8 +230,5 @@ private:
 #endif
 	std::unique_ptr<events::EventBus> clientEventBus;
 
-	mutable boost::mutex pathCacheMutex;
-	std::map<const CGHeroInstance *, std::shared_ptr<CPathsInfo>> pathCache;
-
 	void reinitScripting();
 };
diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp
index 9b6482b47..dad032b69 100644
--- a/client/NetPacksClient.cpp
+++ b/client/NetPacksClient.cpp
@@ -168,7 +168,6 @@ void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack)
 void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack)
 {
 	const CGHeroInstance *h = cl.getHero(pack.hid);
-	cl.updatePath(h);
 	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h);
 }
 
@@ -194,7 +193,7 @@ void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack)
 				i.second->tileHidden(pack.tiles);
 		}
 	}
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 }
 
 static void dispatchGarrisonChange(CClient & cl, ObjectInstanceID army1, ObjectInstanceID army2)
@@ -235,33 +234,21 @@ void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack)
 void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack)
 {
 	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
-	cl.updatePath(pack.army); //it is possible to remove last non-native unit for current terrain and lose movement penalty
 }
 
 void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack)
 {
 	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
-
-	if(pack.srcArmy != pack.dstArmy)
-		cl.updatePath(pack.dstArmy); // adding/removing units may change terrain type penalty based on creature native terrains
 }
 
 void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack)
 {
 	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
-
-	cl.updatePath(pack.army); // adding/removing units may change terrain type penalty based on creature native terrains
 }
 
 void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack)
 {
 	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
-
-	if(pack.srcArmy != pack.dstArmy)
-	{
-		cl.updatePath(pack.srcArmy); // adding/removing units may change terrain type penalty based on creature native terrains
-		cl.updatePath(pack.dstArmy);
-	}
 }
 
 void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack)
@@ -272,12 +259,6 @@ void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & p
 			? ObjectInstanceID()
 			: pack.moves[0].dstArmy;
 		dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy);
-
-		if(pack.moves[0].srcArmy != destArmy)
-		{
-			cl.updatePath(destArmy); // adding/removing units may change terrain type penalty based on creature native terrains
-			cl.updatePath(pack.moves[0].srcArmy);
-		}
 	}
 }
 
@@ -303,7 +284,6 @@ void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack)
 
 void ApplyClientNetPackVisitor::visitEraseArtifact(BulkEraseArtifacts & pack)
 {
-	cl.updatePath(pack.artHolder);
 	for(const auto & slotErase : pack.posPack)
 		callInterfaceIfPresent(cl, cl.getOwner(pack.artHolder), &IGameEventsReceiver::artifactRemoved, ArtifactLocation(pack.artHolder, slotErase));
 }
@@ -323,9 +303,6 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
 				callInterfaceIfPresent(cl, pack.interfaceOwner, &IGameEventsReceiver::askToAssembleArtifact, dstLoc);
 			if(pack.interfaceOwner != dstOwner)
 				callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::artifactMoved, srcLoc, dstLoc);
-
-			cl.updatePath(pack.srcArtHolder); // hero might have equipped/unequipped Angel Wings
-			cl.updatePath(pack.dstArtHolder);
 		}
 	};
 
@@ -354,15 +331,11 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
 void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack)
 {
 	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactAssembled, pack.al);
-
-	cl.updatePath(pack.al.artHolder); // hero might have equipped/unequipped Angel Wings
 }
 
 void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack)
 {
 	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactDisassembled, pack.al);
-
-	cl.updatePath(pack.al.artHolder); // hero might have equipped/unequipped Angel Wings
 }
 
 void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)
@@ -374,7 +347,7 @@ void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)
 
 void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 
 	if(pack.newWeekNotification)
 	{
@@ -387,7 +360,8 @@ void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack)
 
 void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
+
 	switch(pack.who)
 	{
 	case GiveBonus::ETarget::OBJECT:
@@ -423,7 +397,7 @@ void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack)
 		CGI->mh->onObjectFadeIn(obj, pack.initiator);
 		CGI->mh->waitForOngoingAnimations();
 	}
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 }
 
 void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
@@ -490,7 +464,6 @@ void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface
 
 void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack)
 {
-	cl.invalidatePaths();
 	switch(pack.who)
 	{
 	case GiveBonus::ETarget::OBJECT:
@@ -531,7 +504,8 @@ void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
 
 void ApplyClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
+
 	for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++)
 		i->second->objectRemovedAfter();
 }
@@ -561,7 +535,7 @@ void ApplyFirstClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
 void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
 {
 	const CGHeroInstance *h = cl.getHero(pack.id);
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 
 	if(CGI->mh)
 	{
@@ -976,7 +950,8 @@ void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & p
 
 void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
+
 	auto caster = cl.getHero(pack.casterID);
 	if(caster)
 		//consider notifying other interfaces that see hero?
@@ -1068,7 +1043,7 @@ void ApplyClientNetPackVisitor::visitCenterView(CenterView & pack)
 
 void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 
 	const CGObjectInstance *obj = pack.newObject;
 	if(CGI->mh)
@@ -1101,5 +1076,5 @@ void ApplyClientNetPackVisitor::visitSetAvailableArtifacts(SetAvailableArtifacts
 
 void ApplyClientNetPackVisitor::visitEntitiesChanged(EntitiesChanged & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 }
diff --git a/client/PlayerLocalState.cpp b/client/PlayerLocalState.cpp
index 29e664498..3574f5620 100644
--- a/client/PlayerLocalState.cpp
+++ b/client/PlayerLocalState.cpp
@@ -54,7 +54,7 @@ bool PlayerLocalState::hasPath(const CGHeroInstance * h) const
 bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destination)
 {
 	CGPath path;
-	if(!owner.cb->getPathsInfo(h)->getPath(path, destination))
+	if(!owner.getPathsInfo(h)->getPath(path, destination))
 	{
 		paths.erase(h); //invalidate previously possible path if selected (before other hero blocked only path / fly spell expired)
 		syncronizeState();
diff --git a/client/PlayerLocalState.h b/client/PlayerLocalState.h
index 3372b6052..009b6c698 100644
--- a/client/PlayerLocalState.h
+++ b/client/PlayerLocalState.h
@@ -17,6 +17,7 @@ class CArmedInstance;
 class JsonNode;
 struct CGPath;
 class int3;
+struct CPathsInfo;
 
 VCMI_LIB_NAMESPACE_END
 
diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp
index 3b918de77..55dd66e1d 100644
--- a/client/adventureMap/AdventureMapInterface.cpp
+++ b/client/adventureMap/AdventureMapInterface.cpp
@@ -107,6 +107,9 @@ void AdventureMapInterface::onHeroMovementStarted(const CGHeroInstance * hero)
 
 void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h)
 {
+	if (h)
+		LOCPLINT->localState->verifyPath(h);
+
 	widget->getHeroList()->updateElement(h);
 
 	if (h && h == LOCPLINT->localState->getCurrentHero() && !widget->getInfoBar()->showingComponents())
@@ -546,7 +549,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
 	{
 		isHero = true;
 
-		const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(targetPosition);
+		const CGPathNode *pn = LOCPLINT->getPathsInfo(currentHero)->getPathInfo(targetPosition);
 		if(currentHero == topBlocking) //clicked selected hero
 		{
 			LOCPLINT->openHeroWindow(currentHero);
@@ -685,7 +688,7 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
 		std::array<Cursor::Map, 4> cursorVisit     = { Cursor::Map::T1_VISIT,      Cursor::Map::T2_VISIT,      Cursor::Map::T3_VISIT,      Cursor::Map::T4_VISIT,      };
 		std::array<Cursor::Map, 4> cursorSailVisit = { Cursor::Map::T1_SAIL_VISIT, Cursor::Map::T2_SAIL_VISIT, Cursor::Map::T3_SAIL_VISIT, Cursor::Map::T4_SAIL_VISIT, };
 
-		const CGPathNode * pathNode = LOCPLINT->cb->getPathsInfo(hero)->getPathInfo(targetPosition);
+		const CGPathNode * pathNode = LOCPLINT->getPathsInfo(hero)->getPathInfo(targetPosition);
 		assert(pathNode);
 
 		if((GH.isKeyboardAltDown() || settings["gameTweaks"]["forceMovementInfo"].Bool()) && pathNode->reachable()) //overwrite status bar text with movement info
diff --git a/config/ai/nkai/nkai-settings.json b/config/ai/nkai/nkai-settings.json
index 25cbcda38..10b49811f 100644
--- a/config/ai/nkai/nkai-settings.json
+++ b/config/ai/nkai/nkai-settings.json
@@ -42,6 +42,7 @@
 		"maxGoldPressure" : 0.3,
 		"updateHitmapOnTileReveal" : true,
 		"useTroopsFromGarrisons" : true,
+		"useOneWayMonoliths" : false,
 		"openMap": true,
 		"allowObjectGraph": false,
 		"pathfinderBucketsCount" : 3,
@@ -63,6 +64,7 @@
 		"maxGoldPressure" : 0.3,
 		"updateHitmapOnTileReveal" : true,
 		"useTroopsFromGarrisons" : true,
+		"useOneWayMonoliths" : false,
 		"openMap": true,
 		"allowObjectGraph": false,
 		"pathfinderBucketsCount" : 3,
@@ -84,6 +86,7 @@
 		"maxGoldPressure" : 0.3,
 		"updateHitmapOnTileReveal" : true,
 		"useTroopsFromGarrisons" : true,
+		"useOneWayMonoliths" : false,
 		"openMap": true,
 		"allowObjectGraph": false,
 		"pathfinderBucketsCount" : 3,
@@ -105,6 +108,7 @@
 		"maxGoldPressure" : 0.3,
 		"updateHitmapOnTileReveal" : true,
 		"useTroopsFromGarrisons" : true,
+		"useOneWayMonoliths" : false,
 		"openMap": true,
 		"allowObjectGraph": false,
 		"pathfinderBucketsCount" : 3,
@@ -126,6 +130,7 @@
 		"maxGoldPressure" : 0.3,
 		"updateHitmapOnTileReveal" : true,
 		"useTroopsFromGarrisons" : true,
+		"useOneWayMonoliths" : false,
 		"openMap": true,
 		"allowObjectGraph": false,
 		"pathfinderBucketsCount" : 3,
diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp
index 1b642c25f..6ae9c1afe 100644
--- a/lib/CGameInfoCallback.cpp
+++ b/lib/CGameInfoCallback.cpp
@@ -473,7 +473,7 @@ std::vector <const CGObjectInstance *> CGameInfoCallback::getVisitableObjs(int3
 
 	for(const CGObjectInstance * obj : t->visitableObjects)
 	{
-		if(getPlayerID() || obj->ID != Obj::EVENT) //hide events from players
+		if(!getPlayerID().has_value() || obj->ID != Obj::EVENT) //hide events from players
 			ret.push_back(obj);
 	}
 
@@ -945,16 +945,11 @@ void CGameInfoCallback::getVisibleTilesInRange(std::unordered_set<int3> &tiles,
 	gs->getTilesInRange(tiles, pos, radious, ETileVisibility::REVEALED, *getPlayerID(),  distanceFormula);
 }
 
-void CGameInfoCallback::calculatePaths(const std::shared_ptr<PathfinderConfig> & config)
+void CGameInfoCallback::calculatePaths(const std::shared_ptr<PathfinderConfig> & config) const
 {
 	gs->calculatePaths(config);
 }
 
-void CGameInfoCallback::calculatePaths( const CGHeroInstance *hero, CPathsInfo &out)
-{
-	gs->calculatePaths(hero, out);
-}
-
 const CArtifactInstance * CGameInfoCallback::getArtInstance( ArtifactInstanceID aid ) const
 {
 	return gs->map->artInstances.at(aid.num);
diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h
index a4df86d3f..fcd923601 100644
--- a/lib/CGameInfoCallback.h
+++ b/lib/CGameInfoCallback.h
@@ -207,8 +207,7 @@ public:
 	virtual std::shared_ptr<const boost::multi_array<TerrainTile*, 3>> getAllVisibleTiles() const;
 	virtual bool isInTheMap(const int3 &pos) const;
 	virtual void getVisibleTilesInRange(std::unordered_set<int3> &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula = int3::DIST_2D) const;
-	virtual void calculatePaths(const std::shared_ptr<PathfinderConfig> & config);
-	virtual void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out);
+	virtual void calculatePaths(const std::shared_ptr<PathfinderConfig> & config) const;
 	virtual EDiggingStatus getTileDigStatus(int3 tile, bool verbose = true) const;
 
 	//town
diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h
index 9719801d7..6f5c3964f 100644
--- a/lib/CGameInterface.h
+++ b/lib/CGameInterface.h
@@ -56,6 +56,7 @@ class CSaveFile;
 class BattleStateInfo;
 struct ArtifactLocation;
 class BattleStateInfoForRetreat;
+struct CPathsInfo;
 
 #if SCRIPTING_ENABLED
 namespace scripting
@@ -108,6 +109,9 @@ public:
 	virtual void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain){};
 
 	virtual std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0;
+
+	/// Invalidates and destroys all paths for all heroes
+	virtual void invalidatePaths(){};
 };
 
 class DLL_LINKAGE CDynLibHandler
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
index 6be9b534f..2f0740163 100644
--- a/lib/CMakeLists.txt
+++ b/lib/CMakeLists.txt
@@ -172,6 +172,7 @@ set(lib_MAIN_SRCS
 	pathfinder/CGPathNode.cpp
 	pathfinder/CPathfinder.cpp
 	pathfinder/NodeStorage.cpp
+	pathfinder/PathfinderCache.cpp
 	pathfinder/PathfinderOptions.cpp
 	pathfinder/PathfindingRules.cpp
 	pathfinder/TurnInfo.cpp
@@ -584,6 +585,7 @@ set(lib_MAIN_HEADERS
 	pathfinder/CGPathNode.h
 	pathfinder/CPathfinder.h
 	pathfinder/NodeStorage.h
+	pathfinder/PathfinderCache.h
 	pathfinder/PathfinderOptions.h
 	pathfinder/PathfinderUtil.h
 	pathfinder/PathfindingRules.h
diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp
index 9c3cc7387..9b57cfff9 100644
--- a/lib/gameState/CGameState.cpp
+++ b/lib/gameState/CGameState.cpp
@@ -1144,15 +1144,9 @@ void CGameState::apply(CPackForClient & pack)
 	pack.applyGs(this);
 }
 
-void CGameState::calculatePaths(const CGHeroInstance *hero, CPathsInfo &out)
+void CGameState::calculatePaths(const std::shared_ptr<PathfinderConfig> & config) const
 {
-	calculatePaths(std::make_shared<SingleHeroPathfinderConfig>(out, this, hero));
-}
-
-void CGameState::calculatePaths(const std::shared_ptr<PathfinderConfig> & config)
-{
-	//FIXME: creating pathfinder is costly, maybe reset / clear is enough?
-	CPathfinder pathfinder(this, config);
+	CPathfinder pathfinder(const_cast<CGameState*>(this), config);
 	pathfinder.calculatePaths();
 }
 
diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h
index 5d4249e08..47c52a916 100644
--- a/lib/gameState/CGameState.h
+++ b/lib/gameState/CGameState.h
@@ -96,8 +96,7 @@ public:
 	void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const override;
 	PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const override;
 	bool checkForVisitableDir(const int3 & src, const int3 & dst) const; //check if src tile is visitable from dst tile
-	void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out) override; //calculates possible paths for hero, by default uses current hero position and movement left; returns pointer to newly allocated CPath or nullptr if path does not exists
-	void calculatePaths(const std::shared_ptr<PathfinderConfig> & config) override;
+	void calculatePaths(const std::shared_ptr<PathfinderConfig> & config) const override;
 	int3 guardingCreaturePosition (int3 pos) const override;
 	std::vector<CGObjectInstance*> guardingCreatures (int3 pos) const;
 
diff --git a/lib/pathfinder/CGPathNode.cpp b/lib/pathfinder/CGPathNode.cpp
index 057161295..9cdc20808 100644
--- a/lib/pathfinder/CGPathNode.cpp
+++ b/lib/pathfinder/CGPathNode.cpp
@@ -56,6 +56,7 @@ CPathsInfo::CPathsInfo(const int3 & Sizes, const CGHeroInstance * hero_)
 	: sizes(Sizes), hero(hero_)
 {
 	nodes.resize(boost::extents[ELayer::NUM_LAYERS][sizes.z][sizes.x][sizes.y]);
+	heroBonusTreeVersion = hero->getTreeVersion();
 }
 
 CPathsInfo::~CPathsInfo() = default;
diff --git a/lib/pathfinder/CGPathNode.h b/lib/pathfinder/CGPathNode.h
index 2598633cc..7821143f0 100644
--- a/lib/pathfinder/CGPathNode.h
+++ b/lib/pathfinder/CGPathNode.h
@@ -188,6 +188,8 @@ struct DLL_LINKAGE CPathsInfo
 	const CGHeroInstance * hero;
 	int3 hpos;
 	int3 sizes;
+	/// Bonus tree version for which this information can be considered to be valid
+	int heroBonusTreeVersion = 0;
 	boost::multi_array<CGPathNode, 4> nodes; //[layer][level][w][h]
 
 	CPathsInfo(const int3 & Sizes, const CGHeroInstance * hero_);
diff --git a/lib/pathfinder/PathfinderCache.cpp b/lib/pathfinder/PathfinderCache.cpp
new file mode 100644
index 000000000..8de2b0bac
--- /dev/null
+++ b/lib/pathfinder/PathfinderCache.cpp
@@ -0,0 +1,66 @@
+/*
+ * PathfinderCache.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 "PathfinderCache.h"
+
+#include "CGPathNode.h"
+#include "PathfinderOptions.h"
+
+#include "../CGameInfoCallback.h"
+#include "../mapObjects/CGHeroInstance.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+std::shared_ptr<PathfinderConfig> PathfinderCache::createConfig(const CGHeroInstance * h, CPathsInfo & out)
+{
+	auto config = std::make_shared<SingleHeroPathfinderConfig>(out, cb, h);
+	config->options = options;
+
+	return config;
+}
+
+std::shared_ptr<CPathsInfo> PathfinderCache::buildPaths(const CGHeroInstance * h)
+{
+	std::shared_ptr<CPathsInfo> result = std::make_shared<CPathsInfo>(cb->getMapSize(), h);
+	auto config = createConfig(h, *result);
+
+	cb->calculatePaths(config);
+	return result;
+}
+
+PathfinderCache::PathfinderCache(const CGameInfoCallback * cb, const PathfinderOptions & options)
+	: cb(cb)
+	, options(options)
+{
+}
+
+void PathfinderCache::invalidatePaths()
+{
+	std::lock_guard lock(pathCacheMutex);
+	pathCache.clear();
+}
+
+std::shared_ptr<const CPathsInfo> PathfinderCache::getPathsInfo(const CGHeroInstance * h)
+{
+	std::lock_guard lock(pathCacheMutex);
+
+	auto iter = pathCache.find(h);
+	if(iter == std::end(pathCache) || iter->second->heroBonusTreeVersion != h->getTreeVersion())
+	{
+		auto result = buildPaths(h);
+		pathCache[h] = result;
+
+		return result;
+	}
+	else
+		return iter->second;
+}
+
+VCMI_LIB_NAMESPACE_END
diff --git a/lib/pathfinder/PathfinderCache.h b/lib/pathfinder/PathfinderCache.h
new file mode 100644
index 000000000..e6fd823d3
--- /dev/null
+++ b/lib/pathfinder/PathfinderCache.h
@@ -0,0 +1,40 @@
+/*
+ * PathfinderCache.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 "PathfinderOptions.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CGameInfoCallback;
+class CGHeroInstance;
+class PathfinderConfig;
+struct CPathsInfo;
+
+class DLL_LINKAGE PathfinderCache
+{
+	const CGameInfoCallback * cb;
+	std::mutex pathCacheMutex;
+	std::map<const CGHeroInstance *, std::shared_ptr<CPathsInfo>> pathCache;
+	PathfinderOptions options;
+
+	std::shared_ptr<PathfinderConfig> createConfig(const CGHeroInstance *h, CPathsInfo &out);
+	std::shared_ptr<CPathsInfo> buildPaths(const CGHeroInstance *h);
+public:
+	PathfinderCache(const CGameInfoCallback * cb, const PathfinderOptions & options);
+
+	/// Invalidates and erases all existing paths from the cache
+	void invalidatePaths();
+
+	/// Returns compute path information for requested hero
+	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
+};
+
+VCMI_LIB_NAMESPACE_END
diff --git a/lib/pathfinder/PathfinderOptions.cpp b/lib/pathfinder/PathfinderOptions.cpp
index 51dbdddb3..017ae7487 100644
--- a/lib/pathfinder/PathfinderOptions.cpp
+++ b/lib/pathfinder/PathfinderOptions.cpp
@@ -59,14 +59,17 @@ std::vector<std::shared_ptr<IPathfindingRule>> SingleHeroPathfinderConfig::build
 
 SingleHeroPathfinderConfig::~SingleHeroPathfinderConfig() = default;
 
-SingleHeroPathfinderConfig::SingleHeroPathfinderConfig(CPathsInfo & out, CGameState * gs, const CGHeroInstance * hero)
+SingleHeroPathfinderConfig::SingleHeroPathfinderConfig(CPathsInfo & out, const CGameInfoCallback * gs, const CGHeroInstance * hero)
 	: PathfinderConfig(std::make_shared<NodeStorage>(out, hero), gs, buildRuleSet())
+	, hero(hero)
 {
-	pathfinderHelper = std::make_unique<CPathfinderHelper>(gs, hero, options);
 }
 
 CPathfinderHelper * SingleHeroPathfinderConfig::getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs)
 {
+	if (!pathfinderHelper)
+		pathfinderHelper = std::make_unique<CPathfinderHelper>(gs, hero, options);
+
 	return pathfinderHelper.get();
 }
 
diff --git a/lib/pathfinder/PathfinderOptions.h b/lib/pathfinder/PathfinderOptions.h
index d7c39d4f5..98cc3026a 100644
--- a/lib/pathfinder/PathfinderOptions.h
+++ b/lib/pathfinder/PathfinderOptions.h
@@ -108,9 +108,10 @@ class DLL_LINKAGE SingleHeroPathfinderConfig : public PathfinderConfig
 {
 private:
 	std::unique_ptr<CPathfinderHelper> pathfinderHelper;
+	const CGHeroInstance * hero;
 
 public:
-	SingleHeroPathfinderConfig(CPathsInfo & out, CGameState * gs, const CGHeroInstance * hero);
+	SingleHeroPathfinderConfig(CPathsInfo & out, const CGameInfoCallback * gs, const CGHeroInstance * hero);
 	virtual ~SingleHeroPathfinderConfig();
 
 	CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
diff --git a/lib/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp
index d8911332a..515f9fa14 100644
--- a/lib/pathfinder/TurnInfo.cpp
+++ b/lib/pathfinder/TurnInfo.cpp
@@ -120,7 +120,7 @@ TurnInfo::TurnInfo(TurnInfoCache * sharedCache, const CGHeroInstance * target, i
 	{
 		static const CSelector selector = Selector::type()(BonusType::ROUGH_TERRAIN_DISCOUNT);
 		const auto & bonuses = sharedCache->roughTerrainDiscount.getBonusList(target, selector);
-		roughTerrainDiscountValue = bonuses->getFirst(daySelector) != nullptr;
+		roughTerrainDiscountValue = bonuses->valOfBonuses(daySelector);
 	}
 
 	{