From 30d9daf62cd40b4acca8fbe6ca07054d7baddb9c Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Thu, 28 Mar 2024 13:39:15 +0200 Subject: [PATCH] NKAI: improve boat handling by object graph, a set of fixes --- AI/Nullkiller/Analyzers/ObjectClusterizer.cpp | 14 ++- .../Behaviors/StayAtTownBehavior.cpp | 3 + AI/Nullkiller/Goals/ExecuteHeroChain.cpp | 68 +++++++++----- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 4 +- .../Pathfinding/AIPathfinderConfig.cpp | 2 +- .../Pathfinding/Actions/BoatActions.cpp | 23 ++++- .../Pathfinding/Actions/BoatActions.h | 15 +++ .../Pathfinding/Actions/QuestAction.cpp | 5 + .../Pathfinding/Actions/QuestAction.h | 2 +- .../Pathfinding/Actions/SpecialAction.h | 12 +++ AI/Nullkiller/Pathfinding/ObjectGraph.cpp | 94 +++++++++++++++---- AI/Nullkiller/Pathfinding/ObjectGraph.h | 14 +++ .../Rules/AILayerTransitionRule.cpp | 8 +- .../Pathfinding/Rules/AILayerTransitionRule.h | 7 +- .../Rules/AIMovementAfterDestinationRule.cpp | 10 ++ client/NetPacksClient.cpp | 4 + 16 files changed, 234 insertions(+), 51 deletions(-) diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index f301b4d20..69c2afb52 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/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)) diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp index 4e69c6185..34d7c609c 100644 --- a/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp +++ b/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()) diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index 438c88b07..85ecac1b2 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/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; } } - - if(node.turns == 0 && node.coord != hero->visitablePos()) + else if(i > 0 && ai->nullkiller->settings->isObjectGraphAllowed()) { - auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node.coord); + 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()) + { + 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; } diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index d061fad19..d0226a985 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -529,7 +529,7 @@ bool AINodeStorage::calculateHeroChain() tbb::parallel_for(tbb::blocked_range(0, data.size()), [&](const tbb::blocked_range& r) { //auto r = blocked_range(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(0, data.size()); - HeroChainCalculationTask task(*this, nodes, data, chainMask, heroChainTurn); + HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn); task.execute(r); task.flushResult(heroChain); diff --git a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp index afd046bc8..c06f3cd8d 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp @@ -28,7 +28,7 @@ namespace AIPathfinding bool allowBypassObjects) { std::vector> rules = { - std::make_shared(cb, ai, nodeStorage), + std::make_shared(cb, ai, nodeStorage, allowBypassObjects), std::make_shared(), std::make_shared(nodeStorage, allowBypassObjects), std::make_shared(), diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp b/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp index 7a6ab3f10..a7e86a681 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp +++ b/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(shipyard); @@ -75,6 +85,11 @@ namespace AIPathfinding return sourceActor->resourceActor; } + std::shared_ptr BuildBoatActionFactory::create(const Nullkiller * ai) + { + return std::make_shared(ai->cb.get(), dynamic_cast(ai->cb->getObj(shipyard))); + } + void SummonBoatAction::execute(const CGHeroInstance * hero) const { Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai); diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h index 2a4dd67de..c8aa2c039 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h +++ b/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 create(const Nullkiller * ai) override; + }; } } diff --git a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp index 7327ed0e5..4880e623e 100644 --- a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp +++ b/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) diff --git a/AI/Nullkiller/Pathfinding/Actions/QuestAction.h b/AI/Nullkiller/Pathfinding/Actions/QuestAction.h index da0ef66c9..d86afac02 100644 --- a/AI/Nullkiller/Pathfinding/Actions/QuestAction.h +++ b/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; diff --git a/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h b/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h index 77270ce1a..a1a4c9f0b 100644 --- a/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h +++ b/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 create(const Nullkiller * ai) = 0; +}; + } diff --git a/AI/Nullkiller/Pathfinding/ObjectGraph.cpp b/AI/Nullkiller/Pathfinding/ObjectGraph.cpp index 7f64bd1e4..5dd0d0f2f 100644 --- a/AI/Nullkiller/Pathfinding/ObjectGraph.cpp +++ b/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(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 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(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(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 & link) -> bool { @@ -512,6 +542,27 @@ GraphPaths::GraphPaths() { } +std::shared_ptr getCompositeAction( + const Nullkiller * ai, + std::shared_ptr linkActionFactory, + std::shared_ptr transitionAction) +{ + if(!linkActionFactory) + return transitionAction; + + auto linkAction = linkActionFactory->create(ai); + + if(!transitionAction) + return linkAction; + + std::vector> actionsArray = { + transitionAction, + linkAction + }; + + return std::make_shared(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 & 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 & 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 & 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) diff --git a/AI/Nullkiller/Pathfinding/ObjectGraph.h b/AI/Nullkiller/Pathfinding/ObjectGraph.h index 2b0e6dea9..14e1e8739 100644 --- a/AI/Nullkiller/Pathfinding/ObjectGraph.h +++ b/AI/Nullkiller/Pathfinding/ObjectGraph.h @@ -22,6 +22,7 @@ struct ObjectLink { float cost = 100000; // some big number uint64_t danger = 0; + std::shared_ptr specialAction; bool update(float newCost, uint64_t newDanger) { @@ -62,20 +63,33 @@ struct ObjectNode class ObjectGraph { std::unordered_map nodes; + std::unordered_map 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 diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp index d71aa9cb0..18c2468c7 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -17,8 +17,12 @@ namespace NKAI { namespace AIPathfinding { - AILayerTransitionRule::AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr nodeStorage) - :cb(cb), ai(ai), nodeStorage(nodeStorage) + AILayerTransitionRule::AILayerTransitionRule( + CPlayerSpecificInfoCallback * cb, + Nullkiller * ai, + std::shared_ptr nodeStorage, + bool allowEmbark) + :cb(cb), ai(ai), nodeStorage(nodeStorage), allowEmbark(allowEmbark) { setup(); } diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h index 243cb96a9..128dda493 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h @@ -32,9 +32,14 @@ namespace AIPathfinding std::map> summonableVirtualBoats; std::map> waterWalkingActions; std::map> airWalkingActions; + bool allowEmbark; public: - AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr nodeStorage); + AILayerTransitionRule( + CPlayerSpecificInfoCallback * cb, + Nullkiller * ai, + std::shared_ptr nodeStorage, + bool allowEmbark); virtual void process( const PathNodeInfo & source, diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index 1d2e2644d..3b0e4f798 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/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); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index ff6c037b1..f367b727e 100644 --- a/client/NetPacksClient.cpp +++ b/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)