1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-24 22:14:36 +02:00

Merge pull request #3770 from vcmi/nkai-multitask

NKAI: allow multiple tasks to be executed from one calculation
This commit is contained in:
Andrii Danylchenko 2024-04-20 15:21:11 +03:00 committed by GitHub
commit 70fad45316
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 610 additions and 224 deletions

View File

@ -251,6 +251,7 @@ void AIGateway::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance
if(start && visitedObj) //we can end visit with null object, anyway
{
nullkiller->memory->markObjectVisited(visitedObj);
nullkiller->objectClusterizer->invalidate(visitedObj->id);
}
status.heroVisit(visitedObj, start);
@ -373,6 +374,7 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor &
return;
nullkiller->memory->removeFromMemory(obj);
nullkiller->objectClusterizer->onObjectRemoved(obj->id);
if(nullkiller->baseGraph && nullkiller->settings->isObjectGraphAllowed())
{
@ -1109,7 +1111,24 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
if(!recruiter->getSlotFor(creID).validSlot())
{
continue;
for(auto stack : recruiter->Slots())
{
if(!stack.second->type)
continue;
auto duplicatingSlot = recruiter->getSlotFor(stack.second->type);
if(duplicatingSlot != stack.first)
{
cb->mergeStacks(recruiter, recruiter, stack.first, duplicatingSlot);
break;
}
}
if(!recruiter->getSlotFor(creID).validSlot())
{
continue;
}
}
vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost());
@ -1152,11 +1171,6 @@ void AIGateway::retrieveVisitableObjs()
{
for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
{
if(!obj->appearance)
{
logAi->error("Bad!");
}
addVisitableObj(obj);
}
});
@ -1401,7 +1415,7 @@ void AIGateway::tryRealize(Goals::DigAtTile & g)
assert(g.hero->visitablePos() == g.tile); //surely we want to crash here?
if(g.hero->diggingStatus() == EDiggingStatus::CAN_DIG)
{
cb->dig(g.hero.get());
cb->dig(g.hero);
}
else
{

View File

@ -91,33 +91,10 @@ bool HeroPtr::operator<(const HeroPtr & rhs) const
const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const
{
//TODO? check if these all assertions every time we get info about hero affect efficiency
//
//behave terribly when attempting unauthorized access to hero that is not ours (or was lost)
assert(doWeExpectNull || h);
if(h)
{
auto obj = cb->getObj(hid);
//const bool owned = obj && obj->tempOwner == ai->playerID;
if(doWeExpectNull && !obj)
{
return nullptr;
}
else
{
if (!obj)
logAi->error("Accessing no longer accessible hero %s!", h->getNameTranslated());
//assert(obj);
//assert(owned);
}
}
return h;
return get(cb, doWeExpectNull);
}
const CGHeroInstance * HeroPtr::get(CCallback * cb, bool doWeExpectNull) const
const CGHeroInstance * HeroPtr::get(const CPlayerSpecificInfoCallback * cb, bool doWeExpectNull) const
{
//TODO? check if these all assertions every time we get info about hero affect efficiency
//
@ -323,6 +300,19 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
}
int getDuplicatingSlots(const CArmedInstance * army)
{
int duplicatingSlots = 0;
for(auto stack : army->Slots())
{
if(stack.second->type && army->getSlotFor(stack.second->type) != stack.first)
duplicatingSlots++;
}
return duplicatingSlots;
}
// todo: move to obj manager
bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObjectInstance * obj)
{
@ -370,13 +360,14 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
return false;
const CGDwelling * d = dynamic_cast<const CGDwelling *>(obj);
auto duplicatingSlotsCount = getDuplicatingSlots(h);
for(auto level : d->creatures)
{
for(auto c : level.second)
{
if(level.first
&& h->getSlotFor(CreatureID(c)) != SlotID()
&& (h->getSlotFor(CreatureID(c)) != SlotID() || duplicatingSlotsCount > 0)
&& ai->cb->getResourceAmount().canAfford(c.toCreature()->getFullRecruitCost()))
{
return true;

View File

@ -109,7 +109,7 @@ public:
}
const CGHeroInstance * get(bool doWeExpectNull = false) const;
const CGHeroInstance * get(CCallback * cb, bool doWeExpectNull = false) const;
const CGHeroInstance * get(const CPlayerSpecificInfoCallback * cb, bool doWeExpectNull = false) const;
bool validAndSet() const;
@ -242,6 +242,7 @@ 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);
int getDuplicatingSlots(const CArmedInstance * army);
template <class T>
class SharedPool

View File

@ -20,7 +20,7 @@ void ObjectCluster::addObject(const CGObjectInstance * obj, const AIPath & path,
{
ClusterObjects::accessor info;
objects.insert(info, ClusterObjects::value_type(obj, ClusterObjectInfo()));
objects.insert(info, ClusterObjects::value_type(obj->id, ClusterObjectInfo()));
if(info->second.priority < priority)
{
@ -31,15 +31,14 @@ void ObjectCluster::addObject(const CGObjectInstance * obj, const AIPath & path,
}
}
const CGObjectInstance * ObjectCluster::calculateCenter() const
const CGObjectInstance * ObjectCluster::calculateCenter(const CPlayerSpecificInfoCallback * cb) const
{
auto v = getObjects();
auto tile = int3(0);
float priority = 0;
for(auto pair : objects)
for(auto & pair : objects)
{
auto newPoint = pair.first->visitablePos();
auto newPoint = cb->getObj(pair.first)->visitablePos();
float newPriority = std::pow(pair.second.priority, 4); // lets make high priority targets more weghtful
int3 direction = newPoint - tile;
float priorityRatio = newPriority / (priority + newPriority);
@ -48,21 +47,21 @@ const CGObjectInstance * ObjectCluster::calculateCenter() const
priority += newPriority;
}
auto closestPair = *vstd::minElementByFun(objects, [&](const std::pair<const CGObjectInstance *, ClusterObjectInfo> & pair) -> int
auto closestPair = *vstd::minElementByFun(objects, [&](const std::pair<ObjectInstanceID, ClusterObjectInfo> & pair) -> int
{
return pair.first->visitablePos().dist2dSQ(tile);
return cb->getObj(pair.first)->visitablePos().dist2dSQ(tile);
});
return closestPair.first;
return cb->getObj(closestPair.first);
}
std::vector<const CGObjectInstance *> ObjectCluster::getObjects() const
std::vector<const CGObjectInstance *> ObjectCluster::getObjects(const CPlayerSpecificInfoCallback * cb) const
{
std::vector<const CGObjectInstance *> result;
for(auto pair : objects)
for(auto & pair : objects)
{
result.push_back(pair.first);
result.push_back(cb->getObj(pair.first));
}
return result;
@ -70,19 +69,19 @@ std::vector<const CGObjectInstance *> ObjectCluster::getObjects() const
std::vector<const CGObjectInstance *> ObjectClusterizer::getNearbyObjects() const
{
return nearObjects.getObjects();
return nearObjects.getObjects(ai->cb.get());
}
std::vector<const CGObjectInstance *> ObjectClusterizer::getFarObjects() const
{
return farObjects.getObjects();
return farObjects.getObjects(ai->cb.get());
}
std::vector<std::shared_ptr<ObjectCluster>> ObjectClusterizer::getLockedClusters() const
{
std::vector<std::shared_ptr<ObjectCluster>> result;
for(auto pair : blockedObjects)
for(auto & pair : blockedObjects)
{
result.push_back(pair.second);
}
@ -163,6 +162,69 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons
return nullptr;
}
void ObjectClusterizer::invalidate(ObjectInstanceID id)
{
nearObjects.objects.erase(id);
farObjects.objects.erase(id);
invalidated.push_back(id);
for(auto & c : blockedObjects)
{
c.second->objects.erase(id);
}
}
void ObjectClusterizer::validateObjects()
{
std::vector<ObjectInstanceID> toRemove;
auto scanRemovedObjects = [this, &toRemove](const ObjectCluster & cluster)
{
for(auto & pair : cluster.objects)
{
if(!ai->cb->getObj(pair.first, false))
toRemove.push_back(pair.first);
}
};
scanRemovedObjects(nearObjects);
scanRemovedObjects(farObjects);
for(auto & pair : blockedObjects)
{
if(!ai->cb->getObj(pair.first, false) || pair.second->objects.empty())
toRemove.push_back(pair.first);
else
scanRemovedObjects(*pair.second);
}
vstd::removeDuplicates(toRemove);
for(auto id : toRemove)
{
onObjectRemoved(id);
}
}
void ObjectClusterizer::onObjectRemoved(ObjectInstanceID id)
{
invalidate(id);
vstd::erase_if_present(invalidated, id);
NKAI::ClusterMap::accessor cluster;
if(blockedObjects.find(cluster, id))
{
for(auto & unlocked : cluster->second->objects)
{
invalidated.push_back(unlocked.first);
}
blockedObjects.erase(cluster);
}
}
bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const
{
if(isObjectRemovable(obj))
@ -222,17 +284,45 @@ Obj ObjectClusterizer::IgnoredObjectTypes[] = {
void ObjectClusterizer::clusterize()
{
auto start = std::chrono::high_resolution_clock::now();
if(isUpToDate)
{
validateObjects();
}
nearObjects.reset();
farObjects.reset();
blockedObjects.clear();
if(isUpToDate && invalidated.empty())
return;
auto start = std::chrono::high_resolution_clock::now();
logAi->debug("Begin object clusterization");
std::vector<const CGObjectInstance *> objs(
ai->memory->visitableObjs.begin(),
ai->memory->visitableObjs.end());
std::vector<const CGObjectInstance *> objs;
if(isUpToDate)
{
for(auto id : invalidated)
{
auto obj = cb->getObj(id, false);
if(obj)
{
objs.push_back(obj);
}
}
invalidated.clear();
}
else
{
nearObjects.reset();
farObjects.reset();
blockedObjects.clear();
invalidated.clear();
objs = std::vector<const CGObjectInstance *>(
ai->memory->visitableObjs.begin(),
ai->memory->visitableObjs.end());
}
#if NKAI_TRACE_LEVEL == 0
tbb::parallel_for(tbb::blocked_range<size_t>(0, objs.size()), [&](const tbb::blocked_range<size_t> & r) {
@ -256,16 +346,20 @@ void ObjectClusterizer::clusterize()
for(auto pair : blockedObjects)
{
logAi->trace("Cluster %s %s count: %i", pair.first->getObjectName(), pair.first->visitablePos().toString(), pair.second->objects.size());
auto blocker = cb->getObj(pair.first);
logAi->trace("Cluster %s %s count: %i", blocker->getObjectName(), blocker->visitablePos().toString(), pair.second->objects.size());
#if NKAI_TRACE_LEVEL >= 1
for(auto obj : pair.second->getObjects())
for(auto obj : pair.second->getObjects(ai->cb.get()))
{
logAi->trace("Object %s %s", obj->getObjectName(), obj->visitablePos().toString());
}
#endif
}
isUpToDate = true;
logAi->trace("Clusterization complete in %ld", timeElapsed(start));
}
@ -381,7 +475,7 @@ void ObjectClusterizer::clusterizeObject(
ClusterMap::accessor cluster;
blockedObjects.insert(
cluster,
ClusterMap::value_type(blocker, std::make_shared<ObjectCluster>(blocker)));
ClusterMap::value_type(blocker->id, std::make_shared<ObjectCluster>(blocker)));
cluster->second->addObject(obj, path, priority);

View File

@ -23,7 +23,15 @@ struct ClusterObjectInfo
uint8_t turn;
};
using ClusterObjects = tbb::concurrent_hash_map<const CGObjectInstance *, ClusterObjectInfo>;
struct ObjectInstanceIDHash
{
ObjectInstanceID::hash hash;
bool equal(ObjectInstanceID o1, ObjectInstanceID o2) const
{
return o1 == o2;
}
};
using ClusterObjects = tbb::concurrent_hash_map<ObjectInstanceID, ClusterObjectInfo, ObjectInstanceIDHash>;
struct ObjectCluster
{
@ -44,11 +52,11 @@ public:
{
}
std::vector<const CGObjectInstance *> getObjects() const;
const CGObjectInstance * calculateCenter() const;
std::vector<const CGObjectInstance *> getObjects(const CPlayerSpecificInfoCallback * cb) const;
const CGObjectInstance * calculateCenter(const CPlayerSpecificInfoCallback * cb) const;
};
using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
using ClusterMap = tbb::concurrent_hash_map<ObjectInstanceID, std::shared_ptr<ObjectCluster>, ObjectInstanceIDHash>;
class ObjectClusterizer
{
@ -60,6 +68,8 @@ private:
ClusterMap blockedObjects;
const Nullkiller * ai;
RewardEvaluator valueEvaluator;
bool isUpToDate;
std::vector<ObjectInstanceID> invalidated;
public:
void clusterize();
@ -69,7 +79,16 @@ public:
const CGObjectInstance * getBlocker(const AIPath & path) const;
std::optional<const CGObjectInstance *> getBlocker(const AIPathNodeInfo & node) const;
ObjectClusterizer(const Nullkiller * ai): ai(ai), valueEvaluator(ai) {}
ObjectClusterizer(const Nullkiller * ai): ai(ai), valueEvaluator(ai), isUpToDate(false){}
void validateObjects();
void onObjectRemoved(ObjectInstanceID id);
void invalidate(ObjectInstanceID id);
void reset() {
isUpToDate = false;
invalidated.clear();
}
private:
bool shouldVisitObject(const CGObjectInstance * obj) const;

View File

@ -41,7 +41,7 @@ Goals::TGoalVec ClusterBehavior::decompose(const Nullkiller * ai) const
Goals::TGoalVec ClusterBehavior::decomposeCluster(const Nullkiller * ai, std::shared_ptr<ObjectCluster> cluster) const
{
auto center = cluster->calculateCenter();
auto center = cluster->calculateCenter(ai->cb.get());
auto paths = ai->pathfinder->getPathInfo(center->visitablePos(), ai->settings->isObjectGraphAllowed());
auto blockerPos = cluster->blocker->visitablePos();

View File

@ -71,7 +71,15 @@ bool needToRecruitHero(const Nullkiller * ai, const CGTownInstance * startupTown
for(auto obj : ai->objectClusterizer->getNearbyObjects())
{
if((obj->ID == Obj::RESOURCE && dynamic_cast<const CGResource *>(obj)->resourceID() == EGameResID::GOLD)
auto armed = dynamic_cast<const CArmedInstance *>(obj);
if(armed && armed->getArmyStrength() > 0)
continue;
bool isGoldPile = dynamic_cast<const CGResource *>(obj)
&& dynamic_cast<const CGResource *>(obj)->resourceID() == EGameResID::GOLD;
if(isGoldPile
|| obj->ID == Obj::TREASURE_CHEST
|| obj->ID == Obj::CAMPFIRE
|| obj->ID == Obj::WATER_WHEEL)

View File

@ -37,10 +37,8 @@ void DeepDecomposer::reset()
goals.clear();
}
Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit)
void DeepDecomposer::decompose(TGoalVec & result, TSubgoal behavior, int depthLimit)
{
TGoalVec tasks;
goals.clear();
goals.resize(depthLimit);
decompositionCache.resize(depthLimit);
@ -79,7 +77,7 @@ Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit)
#endif
if(!isCompositionLoop(subgoal))
{
tasks.push_back(task);
result.push_back(task);
if(!fromCache)
{
@ -121,8 +119,6 @@ Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit)
}
}
}
return tasks;
}
Goals::TSubgoal DeepDecomposer::aggregateGoals(int startDepth, TSubgoal last)

View File

@ -35,7 +35,7 @@ private:
public:
DeepDecomposer(const Nullkiller * ai);
void reset();
Goals::TGoalVec decompose(Goals::TSubgoal behavior, int depthLimit);
void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int depthLimit);
private:
Goals::TSubgoal aggregateGoals(int startDepth, Goals::TSubgoal last);

View File

@ -64,16 +64,104 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
armyFormation.reset(new ArmyFormation(cb, this));
}
Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
TaskPlanItem::TaskPlanItem(TSubgoal task)
:task(task), affectedObjects(task->asTask()->getAffectedObjects())
{
Goals::TTask bestTask = *vstd::maxElementByFun(tasks, [](Goals::TTask task) -> float{
return task->priority;
});
return bestTask;
}
Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const
Goals::TTaskVec TaskPlan::getTasks() const
{
Goals::TTaskVec result;
for(auto & item : tasks)
{
result.push_back(taskptr(*item.task));
}
vstd::removeDuplicates(result);
return result;
}
void TaskPlan::merge(TSubgoal task)
{
TGoalVec blockers;
for(auto & item : tasks)
{
for(auto objid : item.affectedObjects)
{
if(task == item.task || task->asTask()->isObjectAffected(objid))
{
if(item.task->asTask()->priority >= task->asTask()->priority)
return;
blockers.push_back(item.task);
break;
}
}
}
vstd::erase_if(tasks, [&](const TaskPlanItem & task)
{
return vstd::contains(blockers, task.task);
});
tasks.emplace_back(task);
}
Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
{
if(tasks.empty())
{
return taskptr(Invalid());
}
for(TSubgoal & task : tasks)
{
if(task->asTask()->priority <= 0)
task->asTask()->priority = priorityEvaluator->evaluate(task);
}
auto bestTask = *vstd::maxElementByFun(tasks, [](Goals::TSubgoal task) -> float
{
return task->asTask()->priority;
});
return taskptr(*bestTask);
}
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const
{
TaskPlan taskPlan;
tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks](const tbb::blocked_range<size_t> & r)
{
auto evaluator = this->priorityEvaluators->acquire();
for(size_t i = r.begin(); i != r.end(); i++)
{
auto task = tasks[i];
if(task->asTask()->priority <= 0)
task->asTask()->priority = evaluator->evaluate(task);
}
});
std::sort(tasks.begin(), tasks.end(), [](TSubgoal g1, TSubgoal g2) -> bool
{
return g2->asTask()->priority < g1->asTask()->priority;
});
for(TSubgoal & task : tasks)
{
taskPlan.merge(task);
}
return taskPlan.getTasks();
}
void Nullkiller::decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const
{
boost::this_thread::interruption_point();
@ -81,38 +169,14 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi
auto start = std::chrono::high_resolution_clock::now();
Goals::TGoalVec elementarGoals = decomposer->decompose(behavior, decompositionMaxDepth);
Goals::TTaskVec tasks;
decomposer->decompose(result, behavior, decompositionMaxDepth);
boost::this_thread::interruption_point();
for(auto goal : elementarGoals)
{
Goals::TTask task = Goals::taskptr(*goal);
if(task->priority <= 0)
task->priority = priorityEvaluator->evaluate(goal);
tasks.push_back(task);
}
if(tasks.empty())
{
logAi->debug("Behavior %s found no tasks. Time taken %ld", behavior->toString(), timeElapsed(start));
return Goals::taskptr(Goals::Invalid());
}
auto task = choseBestTask(tasks);
logAi->debug(
"Behavior %s returns %s, priority %f. Time taken %ld",
"Behavior %s. Time taken %ld",
behavior->toString(),
task->toString(),
task->priority,
timeElapsed(start));
return task;
}
void Nullkiller::resetAiState()
@ -124,6 +188,7 @@ void Nullkiller::resetAiState()
lockedHeroes.clear();
dangerHitMap->reset();
useHeroChain = true;
objectClusterizer->reset();
if(!baseGraph && settings->isObjectGraphAllowed())
{
@ -251,7 +316,9 @@ void Nullkiller::makeTurn()
resetAiState();
for(int i = 1; i <= settings->getMaxPass(); i++)
Goals::TGoalVec bestTasks;
for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
{
auto start = std::chrono::high_resolution_clock::now();
updateAiState(i);
@ -260,16 +327,18 @@ void Nullkiller::makeTurn()
for(;i <= settings->getMaxPass(); i++)
{
Goals::TTaskVec fastTasks = {
choseBestTask(sptr(BuyArmyBehavior()), 1),
choseBestTask(sptr(BuildingBehavior()), 1)
};
bestTasks.clear();
bestTask = choseBestTask(fastTasks);
decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
decompose(bestTasks, sptr(BuildingBehavior()), 1);
bestTask = choseBestTask(bestTasks);
if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
{
executeTask(bestTask);
if(!executeTask(bestTask))
return;
updateAiState(i, true);
}
else
@ -278,83 +347,108 @@ void Nullkiller::makeTurn()
}
}
Goals::TTaskVec bestTasks = {
bestTask,
choseBestTask(sptr(RecruitHeroBehavior()), 1),
choseBestTask(sptr(CaptureObjectsBehavior()), 1),
choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH),
choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH),
choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH),
choseBestTask(sptr(StayAtTownBehavior()), MAX_DEPTH)
};
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
decompose(bestTasks, sptr(GatherArmyBehavior()), MAX_DEPTH);
decompose(bestTasks, sptr(StayAtTownBehavior()), MAX_DEPTH);
if(cb->getDate(Date::DAY) == 1)
{
bestTasks.push_back(choseBestTask(sptr(StartupBehavior()), 1));
decompose(bestTasks, sptr(StartupBehavior()), 1);
}
bestTask = choseBestTask(bestTasks);
std::string taskDescription = bestTask->toString();
HeroPtr hero = bestTask->getHero();
HeroRole heroRole = HeroRole::MAIN;
if(hero.validAndSet())
heroRole = heroManager->getHeroRole(hero);
if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
useHeroChain = false;
// TODO: better to check turn distance here instead of priority
if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
&& scanDepth == ScanDepth::MAIN_FULL)
{
useHeroChain = false;
scanDepth = ScanDepth::SMALL;
logAi->trace(
"Goal %s has low priority %f so decreasing scan depth to gain performance.",
taskDescription,
bestTask->priority);
}
if(bestTask->priority < MIN_PRIORITY)
{
auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
{
return h->movementPointsRemaining() > 100;
});
if(hasMp && scanDepth != ScanDepth::ALL_FULL)
{
logAi->trace(
"Goal %s has too low priority %f so increasing scan depth to full.",
taskDescription,
bestTask->priority);
scanDepth = ScanDepth::ALL_FULL;
useHeroChain = false;
continue;
}
logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", taskDescription);
return;
}
auto selectedTasks = buildPlan(bestTasks);
logAi->debug("Decission madel in %ld", timeElapsed(start));
executeTask(bestTask);
if(selectedTasks.empty())
{
return;
}
bool hasAnySuccess = false;
for(auto bestTask : selectedTasks)
{
if(cb->getPlayerStatus(playerID) != EPlayerStatus::INGAME)
return;
std::string taskDescription = bestTask->toString();
HeroPtr hero = bestTask->getHero();
HeroRole heroRole = HeroRole::MAIN;
if(hero.validAndSet())
heroRole = heroManager->getHeroRole(hero);
if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
useHeroChain = false;
// TODO: better to check turn distance here instead of priority
if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
&& scanDepth == ScanDepth::MAIN_FULL)
{
useHeroChain = false;
scanDepth = ScanDepth::SMALL;
logAi->trace(
"Goal %s has low priority %f so decreasing scan depth to gain performance.",
taskDescription,
bestTask->priority);
}
if(bestTask->priority < MIN_PRIORITY)
{
auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
{
return h->movementPointsRemaining() > 100;
});
if(hasMp && scanDepth != ScanDepth::ALL_FULL)
{
logAi->trace(
"Goal %s has too low priority %f so increasing scan depth to full.",
taskDescription,
bestTask->priority);
scanDepth = ScanDepth::ALL_FULL;
useHeroChain = false;
hasAnySuccess = true;
break;;
}
logAi->trace("Goal %s has too low priority. It is not worth doing it.", taskDescription);
continue;
}
if(!executeTask(bestTask))
{
if(hasAnySuccess)
break;
else
return;
}
hasAnySuccess = true;
}
if(!hasAnySuccess)
{
logAi->trace("Nothing was done this turn. Ending turn.");
return;
}
if(i == settings->getMaxPass())
{
logAi->warn("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription);
logAi->warn("Maxpass exceeded. Terminating AI turn.");
}
}
}
void Nullkiller::executeTask(Goals::TTask task)
bool Nullkiller::executeTask(Goals::TTask task)
{
auto start = std::chrono::high_resolution_clock::now();
std::string taskDescr = task->toString();
@ -376,10 +470,10 @@ void Nullkiller::executeTask(Goals::TTask task)
logAi->error("Failed to realize subgoal of type %s, I will stop.", taskDescr);
logAi->error("The error message was: %s", e.what());
#if NKAI_TRACE_LEVEL == 0
throw; // will be recatched and AI turn ended
#endif
return false;
}
return true;
}
TResources Nullkiller::getFreeResources() const

View File

@ -47,6 +47,24 @@ enum class ScanDepth
ALL_FULL = 2
};
struct TaskPlanItem
{
std::vector<ObjectInstanceID> affectedObjects;
Goals::TSubgoal task;
TaskPlanItem(Goals::TSubgoal goal);
};
class TaskPlan
{
private:
std::vector<TaskPlanItem> tasks;
public:
Goals::TTaskVec getTasks() const;
void merge(Goals::TSubgoal task);
};
class Nullkiller
{
private:
@ -102,9 +120,10 @@ public:
private:
void resetAiState();
void updateAiState(int pass, bool fast = false);
Goals::TTask choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const;
Goals::TTask choseBestTask(Goals::TTaskVec & tasks) const;
void executeTask(Goals::TTask task);
void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const;
bool executeTask(Goals::TTask task);
};
}

View File

@ -158,6 +158,8 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
const auto& slots = hero->Slots();
ui64 weakestStackPower = 0;
int duplicatingSlots = getDuplicatingSlots(hero);
if (slots.size() >= GameConstants::ARMY_SIZE)
{
//No free slot, we might discard our weakest stack
@ -172,7 +174,7 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
{
//Only if hero has slot for this creature in the army
auto ccre = dynamic_cast<const CCreature*>(c.data.type);
if (hero->getSlotFor(ccre).validSlot())
if (hero->getSlotFor(ccre).validSlot() || duplicatingSlots > 0)
{
result += (c.data.type->getAIValue() * c.data.count) * c.chance;
}
@ -655,7 +657,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
case Obj::RESOURCE:
{
auto * res = dynamic_cast<const CGResource*>(target);
return res->resourceID() == GameResID::GOLD ? 600 : 100;
return res && res->resourceID() == GameResID::GOLD ? 600 : 100;
}
case Obj::TREASURE_CHEST:
return 1500;
@ -711,8 +713,8 @@ public:
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength());
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get());
evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
}
};
@ -743,7 +745,7 @@ public:
Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero().get());
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
evaluationContext.movementCost += stayAtTown.getMovementWasted();
}
@ -792,7 +794,7 @@ public:
if(defendTown.getTurn() > 0 && defendTown.isCounterAttack())
{
auto ourSpeed = defendTown.hero->movementPointsLimit(true);
auto enemySpeed = treat.hero->movementPointsLimit(true);
auto enemySpeed = treat.hero.get(evaluationContext.evaluator.ai->cb.get())->movementPointsLimit(true);
if(enemySpeed > ourSpeed) multiplier *= 0.7f;
}
@ -847,13 +849,12 @@ public:
evaluationContext.movementCostByRole[role] += pair.second;
}
auto heroPtr = task->hero;
auto hero = heroPtr.get(ai->cb.get());
auto hero = task->hero;
bool checkGold = evaluationContext.danger == 0;
auto army = path.heroArmy;
const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false);
auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(hero);
if(heroRole == HeroRole::MAIN)
evaluationContext.heroRole = heroRole;
@ -887,21 +888,21 @@ public:
Goals::UnlockCluster & clusterGoal = dynamic_cast<Goals::UnlockCluster &>(*task);
std::shared_ptr<ObjectCluster> cluster = clusterGoal.getCluster();
auto hero = clusterGoal.hero.get();
auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(clusterGoal.hero);
auto hero = clusterGoal.hero;
auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(hero);
std::vector<std::pair<const CGObjectInstance *, ClusterObjectInfo>> objects(cluster->objects.begin(), cluster->objects.end());
std::vector<std::pair<ObjectInstanceID, ClusterObjectInfo>> objects(cluster->objects.begin(), cluster->objects.end());
std::sort(objects.begin(), objects.end(), [](std::pair<const CGObjectInstance *, ClusterObjectInfo> o1, std::pair<const CGObjectInstance *, ClusterObjectInfo> o2) -> bool
std::sort(objects.begin(), objects.end(), [](std::pair<ObjectInstanceID, ClusterObjectInfo> o1, std::pair<ObjectInstanceID, ClusterObjectInfo> o2) -> bool
{
return o1.second.priority > o2.second.priority;
});
int boost = 1;
for(auto objInfo : objects)
for(auto & objInfo : objects)
{
auto target = objInfo.first;
auto target = evaluationContext.evaluator.ai->cb->getObj(objInfo.first);
bool checkGold = objInfo.second.danger == 0;
auto army = hero;
@ -960,7 +961,7 @@ public:
return;
Goals::DismissHero & dismissCommand = dynamic_cast<Goals::DismissHero &>(*task);
const CGHeroInstance * dismissedHero = dismissCommand.getHero().get();
const CGHeroInstance * dismissedHero = dismissCommand.getHero();
auto role = ai->heroManager->getHeroRole(dismissedHero);
auto mpLeft = dismissedHero->movementPointsRemaining();
@ -1126,7 +1127,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
}
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
task->toString(),
evaluationContext.armyLossPersentage,
(int)evaluationContext.turn,

View File

@ -31,12 +31,12 @@ TTask Goals::taskptr(const AbstractGoal & tmp)
if(!tmp.isElementar())
throw cannotFulfillGoalException(tmp.toString() + " is not elementar");
ptr.reset(dynamic_cast<ITask *>(tmp.clone()));
ptr.reset(tmp.clone()->asTask());
return ptr;
}
std::string AbstractGoal::toString() const //TODO: virtualize
std::string AbstractGoal::toString() const
{
std::string desc;
switch(goalType)
@ -63,8 +63,10 @@ std::string AbstractGoal::toString() const //TODO: virtualize
default:
return std::to_string(goalType);
}
if(hero.get(true)) //FIXME: used to crash when we lost hero and failed goal
if(hero)
desc += " (" + hero->getNameTranslated() + ")";
return desc;
}

View File

@ -10,9 +10,8 @@
#pragma once
#include "../../../lib/VCMI_Lib.h"
#include "../../../lib/CBuildingHandler.h"
#include "../../../lib/CCreatureHandler.h"
#include "../../../lib/CTownHandler.h"
#include "../../../lib/mapObjects/CGTownInstance.h"
#include "../../../lib/mapObjects/CGHeroInstance.h"
#include "../AIUtility.h"
namespace NKAI
@ -106,7 +105,7 @@ namespace Goals
int objid; SETTER(int, objid)
int aid; SETTER(int, aid)
int3 tile; SETTER(int3, tile)
HeroPtr hero; SETTER(HeroPtr, hero)
const CGHeroInstance * hero; SETTER(CGHeroInstance *, hero)
const CGTownInstance *town; SETTER(CGTownInstance *, town)
int bid; SETTER(int, bid)
@ -119,6 +118,7 @@ namespace Goals
objid = -1;
tile = int3(-1, -1, -1);
town = nullptr;
hero = nullptr;
bid = -1;
goldCost = 0;
}
@ -147,6 +147,11 @@ namespace Goals
virtual bool hasHash() const { return false; }
virtual uint64_t getHash() const { return 0; }
virtual ITask * asTask()
{
throw std::runtime_error("Abstract goal is not a task");
}
bool operator!=(const AbstractGoal & g) const
{
@ -165,9 +170,11 @@ namespace Goals
//TODO: make accept work for std::shared_ptr... somehow
virtual void accept(AIGateway * ai) = 0; //unhandled goal will report standard error
virtual std::string toString() const = 0;
virtual HeroPtr getHero() const = 0;
virtual const CGHeroInstance * getHero() const = 0;
virtual ~ITask() {}
virtual int getHeroExchangeCount() const = 0;
virtual bool isObjectAffected(ObjectInstanceID h) const = 0;
virtual std::vector<ObjectInstanceID> getAffectedObjects() const = 0;
};
}

View File

@ -18,12 +18,12 @@ using namespace Goals;
bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const
{
return hero.h == other.hero.h;
return hero == other.hero;
}
void AdventureSpellCast::accept(AIGateway * ai)
{
if(!hero.validAndSet())
if(!hero)
throw cannotFulfillGoalException("Invalid hero!");
auto spell = getSpell();
@ -56,7 +56,7 @@ void AdventureSpellCast::accept(AIGateway * ai)
auto wait = cb->waitTillRealize;
cb->waitTillRealize = true;
cb->castSpell(hero.h, spellID, tile);
cb->castSpell(hero, spellID, tile);
if(town && spellID == SpellID::TOWN_PORTAL)
{

View File

@ -22,7 +22,7 @@ namespace Goals
SpellID spellID;
public:
AdventureSpellCast(HeroPtr hero, SpellID spellID)
AdventureSpellCast(const CGHeroInstance * hero, SpellID spellID)
: ElementarGoal(Goals::ADVENTURE_SPELL_CAST), spellID(spellID)
{
sethero(hero);

View File

@ -14,7 +14,6 @@
namespace NKAI
{
struct HeroPtr;
class AIGateway;
namespace Goals
@ -92,9 +91,37 @@ namespace Goals
bool isElementar() const override { return true; }
HeroPtr getHero() const override { return AbstractGoal::hero; }
const CGHeroInstance * getHero() const override { return AbstractGoal::hero; }
int getHeroExchangeCount() const override { return 0; }
bool isObjectAffected(ObjectInstanceID id) const override
{
return (AbstractGoal::hero && AbstractGoal::hero->id == id)
|| AbstractGoal::objid == id
|| (AbstractGoal::town && AbstractGoal::town->id == id);
}
std::vector<ObjectInstanceID> getAffectedObjects() const override
{
auto result = std::vector<ObjectInstanceID>();
if(AbstractGoal::hero)
result.push_back(AbstractGoal::hero->id);
if(AbstractGoal::objid != -1)
result.push_back(ObjectInstanceID(AbstractGoal::objid));
if(AbstractGoal::town)
result.push_back(AbstractGoal::town->id);
return result;
}
ITask * asTask() override
{
return this;
}
};
}

View File

@ -117,4 +117,36 @@ int Composition::getHeroExchangeCount() const
return result;
}
std::vector<ObjectInstanceID> Composition::getAffectedObjects() const
{
std::vector<ObjectInstanceID> affectedObjects;
for(auto sequence : subtasks)
{
for(auto task : sequence)
{
if(task->isElementar())
vstd::concatenate(affectedObjects, task->asTask()->getAffectedObjects());
}
}
vstd::removeDuplicates(affectedObjects);
return affectedObjects;
}
bool Composition::isObjectAffected(ObjectInstanceID id) const
{
for(auto sequence : subtasks)
{
for(auto task : sequence)
{
if(task->isElementar() && task->asTask()->isObjectAffected(id))
return true;
}
}
return false;
}
}

View File

@ -35,6 +35,9 @@ namespace Goals
TGoalVec decompose(const Nullkiller * ai) const override;
bool isElementar() const override;
int getHeroExchangeCount() const override;
std::vector<ObjectInstanceID> getAffectedObjects() const override;
bool isObjectAffected(ObjectInstanceID id) const override;
};
}

View File

@ -20,7 +20,7 @@ using namespace Goals;
bool DigAtTile::operator==(const DigAtTile & other) const
{
return other.hero.h == hero.h && other.tile == tile;
return other.hero == hero && other.tile == tile;
}
//
//TSubgoal DigAtTile::decomposeSingle() const

View File

@ -18,22 +18,22 @@ using namespace Goals;
bool DismissHero::operator==(const DismissHero & other) const
{
return hero.h == other.hero.h;
return hero == other.hero;
}
void DismissHero::accept(AIGateway * ai)
{
if(!hero.validAndSet())
if(!hero)
throw cannotFulfillGoalException("Invalid hero!");
cb->dismissHero(hero.h);
cb->dismissHero(hero);
throw goalFulfilledException(sptr(*this));
}
std::string DismissHero::toString() const
{
return "DismissHero " + hero.name;
return "DismissHero " + heroName;
}
}

View File

@ -17,11 +17,15 @@ namespace Goals
{
class DLL_EXPORT DismissHero : public ElementarGoal<DismissHero>
{
private:
std::string heroName;
public:
DismissHero(HeroPtr hero)
DismissHero(const CGHeroInstance * hero)
: ElementarGoal(Goals::DISMISS_HERO)
{
sethero(hero);
heroName = hero->getNameTranslated();
}
void accept(AIGateway * ai) override;

View File

@ -26,6 +26,26 @@ ExchangeSwapTownHeroes::ExchangeSwapTownHeroes(
{
}
std::vector<ObjectInstanceID> ExchangeSwapTownHeroes::getAffectedObjects() const
{
std::vector<ObjectInstanceID> affectedObjects = { town->id };
if(town->garrisonHero)
affectedObjects.push_back(town->garrisonHero->id);
if(town->visitingHero)
affectedObjects.push_back(town->visitingHero->id);
return affectedObjects;
}
bool ExchangeSwapTownHeroes::isObjectAffected(ObjectInstanceID id) const
{
return town->id == id
|| (town->visitingHero && town->visitingHero->id == id)
|| (town->garrisonHero && town->garrisonHero->id == id);
}
std::string ExchangeSwapTownHeroes::toString() const
{
return "Exchange and swap heroes of " + town->getNameTranslated();

View File

@ -35,6 +35,9 @@ namespace Goals
const CGHeroInstance * getGarrisonHero() const { return garrisonHero; }
HeroLockedReason getLockingReason() const { return lockingReason; }
std::vector<ObjectInstanceID> getAffectedObjects() const override;
bool isObjectAffected(ObjectInstanceID id) const override;
};
}

View File

@ -42,6 +42,38 @@ bool ExecuteHeroChain::operator==(const ExecuteHeroChain & other) const
&& chainPath.chainMask == other.chainPath.chainMask;
}
std::vector<ObjectInstanceID> ExecuteHeroChain::getAffectedObjects() const
{
std::vector<ObjectInstanceID> affectedObjects = { chainPath.targetHero->id };
if(objid != -1)
affectedObjects.push_back(ObjectInstanceID(objid));
for(auto & node : chainPath.nodes)
{
if(node.targetHero)
affectedObjects.push_back(node.targetHero->id);
}
vstd::removeDuplicates(affectedObjects);
return affectedObjects;
}
bool ExecuteHeroChain::isObjectAffected(ObjectInstanceID id) const
{
if(chainPath.targetHero->id == id || objid == id)
return true;
for(auto & node : chainPath.nodes)
{
if(node.targetHero && node.targetHero->id == id)
return true;
}
return false;
}
void ExecuteHeroChain::accept(AIGateway * ai)
{
logAi->debug("Executing hero chain towards %s. Path %s", targetName, chainPath.toString());
@ -72,6 +104,13 @@ void ExecuteHeroChain::accept(AIGateway * ai)
const CGHeroInstance * hero = node->targetHero;
HeroPtr heroPtr = hero;
if(!heroPtr.validAndSet())
{
logAi->error("Hero %s was lost. Exit hero chain.", heroPtr.name);
return;
}
if(node->parentIndex >= i)
{
logAi->error("Invalid parentIndex while executing node " + node->coord.toString());

View File

@ -34,6 +34,9 @@ namespace Goals
int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
std::vector<ObjectInstanceID> getAffectedObjects() const override;
bool isObjectAffected(ObjectInstanceID id) const override;
private:
bool moveHeroToTile(AIGateway * ai, const CGHeroInstance * hero, const int3 & tile);
};

View File

@ -46,7 +46,7 @@ void StayAtTown::accept(AIGateway * ai)
logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
}
ai->nullkiller->lockHero(hero.get(), HeroLockedReason::DEFENCE);
ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
}
}

View File

@ -22,7 +22,7 @@ ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * up
: CGoal(Goals::ARMY_UPGRADE), upgrader(upgrader), upgradeValue(upgrade.upgradeValue),
initialValue(upgradePath.heroArmy->getArmyStrength()), goldCost(upgrade.upgradeCost[EGameResID::GOLD])
{
sethero(upgradePath.targetHero);
hero = upgradePath.targetHero;
}
ArmyUpgrade::ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade)

View File

@ -22,7 +22,7 @@ DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, co
: CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), counterattack(isCounterAttack)
{
settown(town);
sethero(defencePath.targetHero);
hero = defencePath.targetHero;
}
DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender)

View File

@ -26,12 +26,12 @@ bool HeroExchange::operator==(const HeroExchange & other) const
std::string HeroExchange::toString() const
{
return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString();
return "Hero exchange for " +hero->getObjectName() + " by " + exchangePath.toString();
}
uint64_t HeroExchange::getReinforcementArmyStrength(const Nullkiller * ai) const
{
uint64_t armyValue = ai->armyManager->howManyReinforcementsCanGet(hero.get(), exchangePath.heroArmy);
uint64_t armyValue = ai->armyManager->howManyReinforcementsCanGet(hero, exchangePath.heroArmy);
return armyValue;
}

View File

@ -33,7 +33,7 @@ namespace Goals
: CGoal(Goals::UNLOCK_CLUSTER), cluster(cluster), pathToCenter(pathToCenter)
{
tile = cluster->blocker->visitablePos();
sethero(pathToCenter.targetHero);
hero = pathToCenter.targetHero;
}
bool operator==(const UnlockCluster & other) const override;

View File

@ -105,7 +105,11 @@ void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole>
cb->calculatePaths(config);
if(!pathfinderSettings.useHeroChain)
{
logAi->trace("Recalculated paths in %ld", timeElapsed(start));
return;
}
do
{

View File

@ -35,7 +35,10 @@ namespace AIPathfinding
return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(hero);
}
return questInfo.quest->activeForPlayers.count(hero->getOwner())
auto notActivated = !questInfo.obj->wasVisited(ai->playerID)
&& !questInfo.quest->activeForPlayers.count(hero->getOwner());
return notActivated
|| questInfo.quest->checkQuest(hero);
}

View File

@ -375,6 +375,8 @@ int main(int argc, char * argv[])
while(!headlessQuit)
boost::this_thread::sleep_for(boost::chrono::milliseconds(200));
boost::this_thread::sleep_for(boost::chrono::milliseconds(500));
quitApplication();
}