Explorar o código

NKAI: improve boat handling by object graph, a set of fixes

Andrii Danylchenko hai 1 ano
pai
achega
30d9daf62c

+ 12 - 2
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -329,14 +329,24 @@ void ObjectClusterizer::clusterizeObject(
 		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)
+		else if(path.movementCost() > 4.0f && obj->ID != Obj::TOWN)
 		{
 			auto strategicalValue = valueEvaluator.getStrategicalValue(obj);
 
-			if(strategicalValue < 0.5f)
+			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))

+ 3 - 0
AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp

@@ -49,6 +49,9 @@ Goals::TGoalVec StayAtTownBehavior::decompose() const
 			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())

+ 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 - 2
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -529,7 +529,7 @@ bool AINodeStorage::calculateHeroChain()
 		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);
 
@@ -543,7 +543,7 @@ bool AINodeStorage::calculateHeroChain()
 	else
 	{
 		auto r = tbb::blocked_range<size_t>(0, data.size());
-		HeroChainCalculationTask task(*this, nodes, data, chainMask, heroChainTurn);
+		HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
 
 		task.execute(r);
 		task.flushResult(heroChain);

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

@@ -28,7 +28,7 @@ namespace AIPathfinding
 		bool allowBypassObjects)
 	{
 			std::vector<std::shared_ptr<IPathfindingRule>> rules = {
-				std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage),
+				std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage, allowBypassObjects),
 				std::make_shared<DestinationActionRule>(),
 				std::make_shared<AIMovementToDestinationRule>(nodeStorage, allowBypassObjects),
 				std::make_shared<MovementCostRule>(),

+ 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;

+ 12 - 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,10 @@ public:
 		const AIPathNode * srcNode) const override;
 };
 
+class ISpecialActionFactory
+{
+public:
+	virtual std::shared_ptr<SpecialAction> create(const Nullkiller * ai) = 0;
+};
+
 }

+ 78 - 16
AI/Nullkiller/Pathfinding/ObjectGraph.cpp

@@ -17,6 +17,7 @@
 #include "../../../lib/logging/VisualLogger.h"
 #include "Actions/QuestAction.h"
 #include "../pforeach.h"
+#include "Actions/BoatActions.h"
 
 namespace NKAI
 {
@@ -121,7 +122,7 @@ public:
 
 				ConnectionCostInfo currentCost = getConnectionsCost(paths);
 
-				if(currentCost.connectionsCount <= 2)
+				if(currentCost.connectionsCount <= 1 || currentCost.connectionsCount == 2 && currentCost.totalCost < 3.0f)
 					return;
 
 				float neighborCost = getNeighborConnectionsCost(pos, paths);
@@ -170,7 +171,7 @@ private:
 
 			auto obj = ai->cb->getTopObj(pos);
 
-			if(obj && obj->ID == Obj::BOAT)
+			if((obj && obj->ID == Obj::BOAT) || target->isVirtualBoat(pos))
 			{
 				ai->pathfinder->calculatePathInfo(pathCache, pos);
 
@@ -230,9 +231,9 @@ private:
 					if(!cb->getTile(pos)->isWater())
 						continue;
 
-					auto startingObjIsBoatOrShipyard = obj1 && (obj1->ID == Obj::BOAT || obj1->ID == Obj::SHIPYARD);
+					auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1);
 
-					if(!startingObjIsBoatOrShipyard)
+					if(!startingObjIsBoat)
 						continue;
 				}
 
@@ -320,12 +321,22 @@ private:
 		assert(objectActor->visitablePos() == visitablePos);
 
 		actorObjectMap[objectActor] = obj;
-		actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::SHIPYARD || obj->ID == Obj::BOAT ? 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);
 
@@ -339,7 +350,7 @@ private:
 		objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
 		objectActor->initObj(rng);
 
-		if(ai->cb->getTile(visitablePos)->isWater())
+		if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater())
 		{
 			objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
 		}
@@ -347,7 +358,7 @@ private:
 		assert(objectActor->visitablePos() == visitablePos);
 
 		actorObjectMap[objectActor] = nullptr;
-		actors[objectActor] = HeroRole::SCOUT;
+		actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT;
 
 		target->registerJunction(visitablePos);
 	}
@@ -397,7 +408,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)
@@ -422,19 +441,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
 			{
@@ -512,6 +542,27 @@ GraphPaths::GraphPaths()
 {
 }
 
+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.copyFrom(*ai->baseGraph);
@@ -561,15 +612,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);
 
@@ -704,6 +756,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++;
@@ -806,7 +863,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3
 				}
 			}
 			
-			if(!path.nodes.empty())
+			if(path.nodes.size() > 1)
 				continue;
 
 			for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
@@ -820,6 +877,11 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3
 				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)

+ 14 - 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,20 +63,33 @@ 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>

+ 6 - 2
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -17,8 +17,12 @@ namespace NKAI
 {
 namespace AIPathfinding
 {
-	AILayerTransitionRule::AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage)
-		:cb(cb), ai(ai), nodeStorage(nodeStorage)
+	AILayerTransitionRule::AILayerTransitionRule(
+		CPlayerSpecificInfoCallback * cb,
+		Nullkiller * ai,
+		std::shared_ptr<AINodeStorage> nodeStorage,
+		bool allowEmbark)
+		:cb(cb), ai(ai), nodeStorage(nodeStorage), allowEmbark(allowEmbark)
 	{
 		setup();
 	}

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

@@ -32,9 +32,14 @@ namespace AIPathfinding
 		std::map<const CGHeroInstance *, std::shared_ptr<const SummonBoatAction>> summonableVirtualBoats;
 		std::map<const CGHeroInstance *, std::shared_ptr<const WaterWalkingAction>> waterWalkingActions;
 		std::map<const CGHeroInstance *, std::shared_ptr<const AirWalkingAction>> airWalkingActions;
+		bool allowEmbark;
 
 	public:
-		AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage);
+		AILayerTransitionRule(
+			CPlayerSpecificInfoCallback * cb,
+			Nullkiller * ai,
+			std::shared_ptr<AINodeStorage> nodeStorage,
+			bool allowEmbark);
 
 		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);
 

+ 4 - 0
client/NetPacksClient.cpp

@@ -396,7 +396,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)