1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-23 22:37:55 +02:00

fix: time-of-check-to-time-of-use (TOCTOU) race condition in AIMemory::removeInvisibleObjects; a bit of refactoring in RecruitHeroBehavior and some todo comments for later on

This commit is contained in:
Mircea TheHonestCTO
2025-09-25 17:09:42 +02:00
parent 46668f7b23
commit a836cd3aed
18 changed files with 153 additions and 114 deletions

View File

@@ -222,7 +222,7 @@ void AIGateway::tileHidden(const FowTilesType & pos)
{
LOG_TRACE(logAi);
nullkiller->memory->removeInvisibleObjects(cc.get());
nullkiller->memory->removeInvisibleOrDeletedObjects(*cc);
}
void AIGateway::tileRevealed(const FowTilesType & pos)
@@ -1008,9 +1008,10 @@ void AIGateway::waitTillFree()
std::vector<const CGObjectInstance *> AIGateway::getFlaggedObjects() const
{
std::vector<const CGObjectInstance *> ret;
for(const CGObjectInstance * obj : nullkiller->memory->visitableObjs)
for(const ObjectInstanceID objId : nullkiller->memory->visitableObjs)
{
if(obj->tempOwner == playerID)
const CGObjectInstance * obj = cc->getObj(objId, false);
if(obj && obj->tempOwner == playerID)
ret.push_back(obj);
}
return ret;
@@ -1628,12 +1629,11 @@ void AIGateway::memorizeRevisitableObjs(const std::unique_ptr<AIMemory> & memory
{
if(cc->getDate(Date::DAY_OF_WEEK) == 1)
{
for(const CGObjectInstance * obj : memory->visitableObjs)
for(const ObjectInstanceID objId : memory->visitableObjs)
{
if(isWeeklyRevisitable(playerID, obj))
{
const CGObjectInstance * obj = cc->getObj(objId, false);
if(obj && isWeeklyRevisitable(playerID, obj))
memory->markObjectUnvisited(obj);
}
}
}
}

View File

@@ -88,8 +88,12 @@ void DangerHitMapAnalyzer::updateHitMap()
std::map<PlayerColor, std::map<const CGHeroInstance *, HeroRole>> heroes;
for(const CGObjectInstance * obj : aiNk->memory->visitableObjs)
for(const ObjectInstanceID objId : aiNk->memory->visitableObjs)
{
const CGObjectInstance * obj = cc->getObj(objId, false);
if(!obj)
continue;
if(obj->ID == Obj::HERO)
{
auto hero = dynamic_cast<const CGHeroInstance *>(obj);
@@ -236,12 +240,11 @@ void DangerHitMapAnalyzer::calculateTileOwners()
townHeroes[townHero] = HeroRole::MAIN;
};
for(const auto *obj : aiNk->memory->visitableObjs)
for(const ObjectInstanceID objId : aiNk->memory->visitableObjs)
{
const CGObjectInstance * obj = cc->getObj(objId, false);
if(obj && obj->ID == Obj::TOWN)
{
addTownHero(dynamic_cast<const CGTownInstance *>(obj));
}
}
for(const auto *town : cc->getTownsInfo())
@@ -336,7 +339,6 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const
{
auto tile = obj->visitablePos();
return getTileThreat(tile);
}

View File

@@ -242,7 +242,7 @@ bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const
const int3 pos = obj->visitablePos();
if((obj->ID != Obj::CREATURE_GENERATOR1 && vstd::contains(aiNk->memory->alreadyVisited, obj))
if((obj->ID != Obj::CREATURE_GENERATOR1 && vstd::contains(aiNk->memory->alreadyVisited, obj->id))
|| obj->wasVisited(aiNk->playerID))
{
return false;
@@ -323,9 +323,7 @@ void ObjectClusterizer::clusterize()
blockedObjects.clear();
invalidated.clear();
objs = std::vector<const CGObjectInstance *>(
aiNk->memory->visitableObjs.begin(),
aiNk->memory->visitableObjs.end());
objs = aiNk->memory->visitableIdsToObjsVector(*aiNk->cc);
}
tbb::parallel_for(

View File

@@ -55,6 +55,8 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * aiNk) const
&*townArmyAvailableToBuy,
TerrainId::NONE);
// TODO: Mircea: Shouldn't matter if hero is MAIN when buying reinforcements if there's a threat around
// Evaluate the entire code with the outside towns loop too.
if(reinforcement)
vstd::amin(reinforcement, aiNk->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town));

View File

@@ -219,12 +219,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose(const Nullkiller * aiNk) const
}
else if(objectTypes.size())
{
decomposeObjects(
tasks,
std::vector<const CGObjectInstance *>(
aiNk->memory->visitableObjs.begin(),
aiNk->memory->visitableObjs.end()),
aiNk);
decomposeObjects(tasks, aiNk->memory->visitableIdsToObjsVector(*aiNk->cc), aiNk);
}
else
{

View File

@@ -38,7 +38,7 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * aiNk) const
{
Goals::TGoalVec tasks;
for(auto town : aiNk->cc->getTownsInfo())
for(const auto town : aiNk->cc->getTownsInfo())
{
evaluateDefence(tasks, town, aiNk);
}
@@ -95,9 +95,7 @@ void handleCounterAttack(
continue;
Composition composition;
composition.addNext(DefendTown(town, threat, path, true)).addNext(goal);
tasks.push_back(Goals::sptr(composition));
}
}
@@ -108,7 +106,6 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
if(aiNk->isHeroLocked(town->getGarrisonHero()))
{
logAi->trace("Hero %s in garrison of town %s is supposed to defend the town", town->getGarrisonHero()->getNameTranslated(), town->getNameTranslated());
return true;
}
@@ -117,12 +114,11 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
if(aiNk->cc->getHeroCount(aiNk->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
{
logAi->trace("Extracting hero %s from garrison of town %s", town->getGarrisonHero()->getNameTranslated(), town->getNameTranslated());
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return false;
}
else if(aiNk->heroManager->getHeroRoleOrDefaultInefficient(town->getGarrisonHero()) == HeroRole::MAIN)
if(aiNk->heroManager->getHeroRoleOrDefaultInefficient(town->getGarrisonHero()) == HeroRole::MAIN)
{
auto armyDismissLimit = 1000;
auto heroToDismiss = aiNk->heroManager->findWeakHeroToDismiss(armyDismissLimit);
@@ -145,13 +141,13 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
auto threatNode = aiNk->dangerHitMap->getObjectThreat(town);
std::vector<HitMapInfo> threats = aiNk->dangerHitMap->getTownThreats(town);
// TODO: Mircea: Why don't we check if there's any danger in threadNode? Maybe map is still unexplored and no danger
// or simply no one is around
threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
if(town->getGarrisonHero() && handleGarrisonHeroFromPreviousTurn(town, tasks, aiNk))
{
return;
}
if(!threatNode.fastestDanger.heroPtr.isVerified())
{
#if NK2AI_TRACE_LEVEL >= 1
@@ -160,13 +156,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
return;
}
uint64_t reinforcement = aiNk->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town);
const uint64_t reinforcement = aiNk->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town);
if(reinforcement)
{
#if NK2AI_TRACE_LEVEL >= 1
logAi->trace("Town %s can buy defence army %lld", town->getNameTranslated(), reinforcement);
#endif
// TODO: Mircea: This won't have any money left because BuyArmyBehavior runs first and could have used all resources by now
tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(0.5f)));
}
@@ -205,7 +202,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
for(int i = 0; i < paths.size(); i++)
{
auto & path = paths[i];
if(!closestWay || path.movementCost() < closestWay->movementCost())
closestWay = &path;
@@ -317,7 +313,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
for(int i : pathsToDefend)
{
AIPath & path = paths[i];
for(int j : defferedPaths[path.targetHero])
{
AIPath & defferedPath = paths[j];
@@ -419,7 +414,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
bool heroAlreadyHiredInOtherTown = false;
for(const auto & task : tasks)
{
if(const auto recruitGoal = dynamic_cast<Goals::RecruitHero *>(task.get()))
if(auto * const recruitGoal = dynamic_cast<Goals::RecruitHero *>(task.get()))
{
if(recruitGoal->getHero() == hero)
{

View File

@@ -33,8 +33,12 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * aiNk) const
{
Goals::TGoalVec tasks;
for (auto obj : aiNk->memory->visitableObjs)
for (const ObjectInstanceID objId : aiNk->memory->visitableObjs)
{
const CGObjectInstance * obj = aiNk->cc->getObj(objId, false);
if(!obj)
continue;
switch (obj->ID.num)
{
case Obj::REDWOOD_OBSERVATORY:

View File

@@ -40,6 +40,9 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * aiNk) const
for(const auto * town : ourTowns)
{
// TODO: Mircea: Should consider removing it if necessary
// What if under threat and there is a stronger hero available?
if(town->getVisitingHero() && town->getGarrisonHero())
continue;
@@ -50,36 +53,24 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * aiNk) const
}
float visitabilityRatio = 0;
for(const auto hero : ourHeroes)
for(const auto * const hero : ourHeroes)
{
if(aiNk->dangerHitMap->getClosestTown(hero->visitablePos()) == town)
visitabilityRatio += 1.0f / ourHeroes.size();
}
// TODO: Mircea: Should check even if hero cap is reached because it should replace heroes if a stronger one appears
if(aiNk->heroManager->canRecruitHero(town))
{
calculateTreasureSources(aiNk->objectClusterizer->getNearbyObjects(), aiNk->playerID, *aiNk->dangerHitMap, treasureSourcesCount, town);
calculateBestHero(aiNk->cc->getAvailableHeroes(town), *aiNk->heroManager, bestChoice, town, closestThreatTurn, visitabilityRatio);
calculateTreasureSources(aiNk->objectClusterizer->getNearbyObjects(), aiNk->playerID, *aiNk->dangerHitMap, treasureSourcesCount, *town);
calculateBestHero(aiNk->cc->getAvailableHeroes(town), *aiNk->heroManager, bestChoice, *town, closestThreatTurn, visitabilityRatio);
}
if(town->hasCapitol())
haveCapitol = true;
}
if(!vstd::isAlmostZero(bestChoice.score))
{
if(ourHeroes.empty()
|| treasureSourcesCount > ourHeroes.size() * 5
// TODO: Mircea: The next condition should always consider a hero if under attack especially if it has towers
|| (bestChoice.hero->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0
&& (bestChoice.closestThreat < 1 || !aiNk->buildAnalyzer->isGoldPressureOverMax()))
|| (aiNk->getFreeResources()[EGameResID::GOLD] > 10000 && !aiNk->buildAnalyzer->isGoldPressureOverMax() && haveCapitol)
|| (aiNk->getFreeResources()[EGameResID::GOLD] > 30000 && !aiNk->buildAnalyzer->isGoldPressureOverMax()))
{
tasks.push_back(Goals::sptr(Goals::RecruitHero(bestChoice.town, bestChoice.hero).setpriority((float)3 / (ourHeroes.size() + 1))));
}
}
calculateFinalDecision(*aiNk, tasks, ourHeroes, bestChoice, haveCapitol, treasureSourcesCount);
return tasks;
}
@@ -88,7 +79,7 @@ void RecruitHeroBehavior::calculateTreasureSources(
const PlayerColor & playerID,
const DangerHitMapAnalyzer & dangerHitMap,
int & treasureSourcesCount,
const CGTownInstance * town
const CGTownInstance & town
)
{
for(const auto * obj : nearbyObjects)
@@ -96,7 +87,7 @@ void RecruitHeroBehavior::calculateTreasureSources(
if(obj->ID == Obj::RESOURCE || obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::CAMPFIRE || isWeeklyRevisitable(playerID, obj)
|| obj->ID == Obj::ARTIFACT)
{
if(town == dangerHitMap.getClosestTown(obj->visitablePos()))
if(&town == dangerHitMap.getClosestTown(obj->visitablePos()))
treasureSourcesCount++; // TODO: Mircea: Shouldn't it be used to determine the best town?
}
}
@@ -106,7 +97,7 @@ void RecruitHeroBehavior::calculateBestHero(
const std::vector<const CGHeroInstance *> & availableHeroes,
const HeroManager & heroManager,
const RecruitHeroChoice & bestChoice,
const CGTownInstance * town,
const CGTownInstance & town,
const uint8_t closestThreatTurn,
const float visitabilityRatio
)
@@ -114,7 +105,7 @@ void RecruitHeroBehavior::calculateBestHero(
for(const auto * const hero : availableHeroes)
{
if((town->getVisitingHero() || town->getGarrisonHero()) && closestThreatTurn < 1 && hero->getArmyCost() < GameConstants::HERO_GOLD_COST / 3.0)
if((town.getVisitingHero() || town.getGarrisonHero()) && closestThreatTurn < 1 && hero->getArmyCost() < GameConstants::HERO_GOLD_COST / 3.0)
continue;
const float heroScore = heroManager.evaluateHero(hero);
@@ -123,20 +114,44 @@ void RecruitHeroBehavior::calculateBestHero(
// TODO: Mircea: Score higher if ballista/tent/ammo cart by the cost in gold? Or should that be covered in evaluateHero?
// getArtifactScoreForHero(hero, ...) ArtifactID::BALLISTA
if(hero->getFactionID() == town->getFactionID())
if(hero->getFactionID() == town.getFactionID())
totalScore += heroScore * 1.5;
// prioritize a more developed town especially if no heroes can visit it (smaller ratio, bigger score)
totalScore += heroScore * town->getTownLevel() * (1 - visitabilityRatio);
totalScore += heroScore * town.getTownLevel() * (1 - visitabilityRatio);
if(totalScore > bestChoice.score)
{
bestChoice.score = totalScore;
bestChoice.hero = hero;
bestChoice.town = town;
bestChoice.town = &town;
bestChoice.closestThreat = closestThreatTurn;
}
}
}
void RecruitHeroBehavior::calculateFinalDecision(
const Nullkiller & aiNk,
Goals::TGoalVec tasks,
const std::vector<const CGHeroInstance *> & ourHeroes,
const RecruitHeroChoice & bestChoice,
const bool haveCapitol,
const int treasureSourcesCount
)
{
if(!vstd::isAlmostZero(bestChoice.score))
{
if(ourHeroes.empty()
|| treasureSourcesCount > ourHeroes.size() * 5
// TODO: Mircea: The next condition should always consider a hero if under attack especially if it has towers
|| (bestChoice.hero->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0
&& (bestChoice.closestThreat < 1 || !aiNk.buildAnalyzer->isGoldPressureOverMax()))
|| (aiNk.getFreeResources()[EGameResID::GOLD] > 10000 && !aiNk.buildAnalyzer->isGoldPressureOverMax() && haveCapitol)
|| (aiNk.getFreeResources()[EGameResID::GOLD] > 30000 && !aiNk.buildAnalyzer->isGoldPressureOverMax()))
{
tasks.push_back(Goals::sptr(Goals::RecruitHero(bestChoice.town, bestChoice.hero).setpriority((float)3 / (ourHeroes.size() + 1))));
}
}
}
}

View File

@@ -46,16 +46,24 @@ namespace Goals
const PlayerColor & playerID,
const DangerHitMapAnalyzer & dangerHitMap,
int & treasureSourcesCount,
const CGTownInstance * town
const CGTownInstance & town
);
static void calculateBestHero(
const std::vector<const CGHeroInstance *> & availableHeroes,
const HeroManager & heroManager,
const RecruitHeroChoice & bestChoice,
const CGTownInstance * town,
const CGTownInstance & town,
uint8_t closestThreatTurn,
float visitabilityRatio
);
static void calculateFinalDecision(
const Nullkiller & aiNk,
Goals::TGoalVec tasks,
const std::vector<const CGHeroInstance *> & ourHeroes,
const RecruitHeroChoice & bestChoice,
bool haveCapitol,
int treasureSourcesCount
);
};
}

View File

@@ -8,6 +8,7 @@
*
*/
#include "../StdInc.h"
#include "AIMemory.h"
namespace NK2AI
@@ -15,25 +16,25 @@ namespace NK2AI
void AIMemory::removeFromMemory(const CGObjectInstance * obj)
{
vstd::erase_if_present(visitableObjs, obj);
vstd::erase_if_present(alreadyVisited, obj);
vstd::erase_if_present(visitableObjs, obj->id);
vstd::erase_if_present(alreadyVisited, obj->id);
//TODO: Find better way to handle hero boat removal
if(auto hero = dynamic_cast<const CGHeroInstance *>(obj))
if(const auto * hero = dynamic_cast<const CGHeroInstance *>(obj))
{
if(hero->inBoat())
{
vstd::erase_if_present(visitableObjs, hero->getBoat());
vstd::erase_if_present(alreadyVisited, hero->getBoat());
vstd::erase_if_present(visitableObjs, hero->getBoat()->id);
vstd::erase_if_present(alreadyVisited, hero->getBoat()->id);
}
}
}
void AIMemory::removeFromMemory(ObjectIdRef obj)
void AIMemory::removeFromMemory(const ObjectIdRef obj)
{
auto matchesId = [&](const CGObjectInstance * hlpObj) -> bool
auto matchesId = [&](const ObjectInstanceID & objId) -> bool
{
return hlpObj->id == obj.id;
return objId == obj.id;
};
vstd::erase_if(visitableObjs, matchesId);
@@ -45,19 +46,15 @@ void AIMemory::addSubterraneanGate(const CGObjectInstance * entrance, const CGOb
knownSubterraneanGates[entrance] = exit;
knownSubterraneanGates[exit] = entrance;
logAi->trace(
"Found a pair of subterranean gates between %s and %s!",
entrance->visitablePos().toString(),
exit->visitablePos().toString());
logAi->trace("Found a pair of subterranean gates between %s and %s!", entrance->visitablePos().toString(), exit->visitablePos().toString());
}
void AIMemory::addVisitableObject(const CGObjectInstance * obj)
{
visitableObjs.insert(obj);
visitableObjs.insert(obj->id);
// All teleport objects seen automatically assigned to appropriate channels
auto teleportObj = dynamic_cast<const CGTeleport *>(obj);
if(teleportObj)
if(const auto teleportObj = dynamic_cast<const CGTeleport *>(obj))
{
CGTeleport::addToChannel(knownTeleportChannels, teleportObj);
}
@@ -67,45 +64,64 @@ void AIMemory::markObjectVisited(const CGObjectInstance * obj)
{
if(!obj)
return;
// TODO: maybe this logic belongs to CaptureObjects::shouldVisit
if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
{
if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_HERO) //we may want to visit it with another hero
if(rewardable->configuration.getVisitMode() == Rewardable::VISIT_HERO) //we may want to visit it with another hero
return;
if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_BONUS) //or another time
if(rewardable->configuration.getVisitMode() == Rewardable::VISIT_BONUS) //or another time
return;
}
if(obj->ID == Obj::MONSTER)
return;
alreadyVisited.insert(obj);
alreadyVisited.insert(obj->id);
}
void AIMemory::markObjectUnvisited(const CGObjectInstance * obj)
{
vstd::erase_if_present(alreadyVisited, obj);
vstd::erase_if_present(alreadyVisited, obj->id);
}
bool AIMemory::wasVisited(const CGObjectInstance * obj) const
{
return vstd::contains(alreadyVisited, obj);
return vstd::contains(alreadyVisited, obj->id);
}
void AIMemory::removeInvisibleObjects(CCallback * cb)
void AIMemory::removeInvisibleOrDeletedObjects(const CCallback & cb)
{
auto shouldBeErased = [&](const CGObjectInstance * obj) -> bool
auto shouldBeErased = [&](const ObjectInstanceID objId) -> bool
{
if(obj)
return !cb->getObj(obj->id, false); // no verbose output needed as we check object visibility
else
return true;
return !cb.getObj(objId, false);
};
vstd::erase_if(visitableObjs, shouldBeErased);
vstd::erase_if(alreadyVisited, shouldBeErased);
}
std::vector<const CGObjectInstance *> AIMemory::visitableIdsToObjsVector(const CCallback & cb) const
{
auto objs = std::vector<const CGObjectInstance *>();
for(const ObjectInstanceID objId : visitableObjs)
{
if(const auto * obj = cb.getObj(objId, false))
objs.push_back(obj);
}
return objs;
}
std::set<const CGObjectInstance *> AIMemory::visitableIdsToObjsSet(const CCallback & cb) const
{
auto objs = std::set<const CGObjectInstance *>();
for(const ObjectInstanceID objId : visitableObjs)
{
if(const auto * obj = cb.getObj(objId, false))
objs.insert(obj);
}
return objs;
}
}

View File

@@ -18,8 +18,8 @@ namespace NK2AI
class AIMemory
{
public:
std::set<const CGObjectInstance *> visitableObjs;
std::set<const CGObjectInstance *> alreadyVisited;
std::set<ObjectInstanceID> visitableObjs;
std::set<ObjectInstanceID> alreadyVisited;
std::map<TeleportChannelID, std::shared_ptr<TeleportChannel>> knownTeleportChannels;
std::map<const CGObjectInstance *, const CGObjectInstance *> knownSubterraneanGates;
@@ -30,7 +30,10 @@ public:
void markObjectVisited(const CGObjectInstance * obj);
void markObjectUnvisited(const CGObjectInstance * obj);
bool wasVisited(const CGObjectInstance * obj) const;
void removeInvisibleObjects(CCallback * cb);
void removeInvisibleOrDeletedObjects(const CCallback & cb);
// Utility method to reuse code, use visitableIds directly where possible to a
std::vector<const CGObjectInstance *> visitableIdsToObjsVector(const CCallback & cb) const;
std::set<const CGObjectInstance *> visitableIdsToObjsSet(const CCallback & cb) const;
};
}

View File

@@ -144,7 +144,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
case Obj::ARTIFACT:
case Obj::RESOURCE:
{
if(!vstd::contains(aiNk->memory->alreadyVisited, obj))
if(!vstd::contains(aiNk->memory->alreadyVisited, obj->id))
return 0;
[[fallthrough]];
}

View File

@@ -107,7 +107,7 @@ void Nullkiller::init(const std::shared_ptr<CCallback> & cbInput, AIGateway * ai
buildAnalyzer.reset(new BuildAnalyzer(this));
objectClusterizer.reset(new ObjectClusterizer(this));
dangerEvaluator.reset(new FuzzyHelper(this));
pathfinder.reset(new AIPathfinder(cc.get(), this));
pathfinder.reset(new AIPathfinder(this));
armyManager.reset(new ArmyManager(cc.get(), this));
heroManager.reset(new HeroManager(cc.get(), this));
decomposer.reset(new DeepDecomposer(this));
@@ -268,7 +268,7 @@ void Nullkiller::updateState()
logAi->trace("Skipping full state regeneration - up to date");
else
{
memory->removeInvisibleObjects(cc.get());
memory->removeInvisibleOrDeletedObjects(*cc);
dangerHitMap->updateHitMap();
dangerHitMap->calculateTileOwners();
@@ -504,7 +504,9 @@ bool Nullkiller::updateStateAndExecutePriorityPass(Goals::TGoalVec & tempResults
{
tempResults.clear();
// TODO: Mircea: Should merge recruiting from DefenseBehavior
decompose(tempResults, sptr(RecruitHeroBehavior()), 1);
// TODO: Mircea: Should merge recruiting from DefenseBehavior
decompose(tempResults, sptr(BuyArmyBehavior()), 1);
decompose(tempResults, sptr(BuildingBehavior()), 1);

View File

@@ -20,10 +20,7 @@ namespace NK2AI
std::map<ObjectInstanceID, std::unique_ptr<GraphPaths>> AIPathfinder::heroGraphs;
AIPathfinder::AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * aiNk)
:cb(cb), aiNk(aiNk)
{
}
AIPathfinder::AIPathfinder(Nullkiller * aiNk) : aiNk(aiNk) {}
bool AIPathfinder::isTileAccessible(const HeroPtr & hero, const int3 & tile) const
{
@@ -46,7 +43,7 @@ void AIPathfinder::calculateQuickPathsWithBlocker(std::vector<AIPath> & result,
void AIPathfinder::calculatePathInfo(std::vector<AIPath> & paths, const int3 & tile, bool includeGraph) const
{
const TerrainTile * tileInfo = cb->getTile(tile, false);
const TerrainTile * tileInfo = aiNk->cc->getTile(tile, false);
paths.clear();
if(!tileInfo)
return;
@@ -55,7 +52,7 @@ void AIPathfinder::calculatePathInfo(std::vector<AIPath> & paths, const int3 & t
if(includeGraph)
{
for(const auto * hero : cb->getHeroesInfo())
for(const auto * hero : aiNk->cc->getHeroesInfo())
{
auto graph = heroGraphs.find(hero->id);
@@ -69,7 +66,7 @@ void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole>
{
if(!storage)
{
storage.reset(new AINodeStorage(aiNk, cb->getMapSize()));
storage.reset(new AINodeStorage(aiNk, aiNk->cc->getMapSize()));
}
auto start = std::chrono::high_resolution_clock::now();
@@ -88,12 +85,12 @@ void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole>
if(pathfinderSettings.useHeroChain)
{
storage->setTownsAndDwellings(cb->getTownsInfo(), aiNk->memory->visitableObjs);
storage->setTownsAndDwellings(aiNk->cc->getTownsInfo(), aiNk->memory->visitableIdsToObjsSet(*aiNk->cc));
}
const auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(aiNk, storage, pathfinderSettings.allowBypassObjects);
logAi->trace("Recalculate paths pass %d", pass++);
cb->calculatePaths(config);
aiNk->cc->calculatePaths(config);
if(!pathfinderSettings.useHeroChain)
{
@@ -115,7 +112,7 @@ void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole>
aiNk->makingTurnInterruption.interruptionPoint();
logAi->trace("Recalculate paths pass %d", pass++);
cb->calculatePaths(config);
aiNk->cc->calculatePaths(config);
}
logAi->trace("Select next actor");
@@ -128,7 +125,7 @@ void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole>
aiNk->makingTurnInterruption.interruptionPoint();
logAi->trace("Recalculate paths pass final");
cb->calculatePaths(config);
aiNk->cc->calculatePaths(config);
}
} while(storage->increaseHeroChainTurnLimit());

View File

@@ -41,12 +41,11 @@ class AIPathfinder
{
private:
std::shared_ptr<AINodeStorage> storage;
CPlayerSpecificInfoCallback * cb;
Nullkiller * aiNk;
static std::map<ObjectInstanceID, std::unique_ptr<GraphPaths>> heroGraphs;
public:
AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * aiNk);
explicit AIPathfinder(Nullkiller * aiNk);
void calculatePathInfo(std::vector<AIPath> & paths, const int3 & tile, bool includeGraph = false) const;
bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const;
void updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings);

View File

@@ -94,8 +94,9 @@ void ObjectGraph::removeObject(const CGObjectInstance * obj, CCallback & cc)
void ObjectGraph::connectHeroes(const Nullkiller * aiNk)
{
for(auto obj : aiNk->memory->visitableObjs)
for(const ObjectInstanceID objId : aiNk->memory->visitableObjs)
{
const CGObjectInstance * obj = aiNk->cc->getObj(objId, false);
if(obj && obj->ID == Obj::HERO)
{
addObject(obj);

View File

@@ -27,8 +27,9 @@ ObjectGraphCalculator::ObjectGraphCalculator(ObjectGraph * target, const Nullkil
void ObjectGraphCalculator::setGraphObjects()
{
for(auto obj : aiNk->memory->visitableObjs)
for(const ObjectInstanceID objId : aiNk->memory->visitableObjs)
{
const CGObjectInstance * obj = aiNk->cc->getObj(objId, false);
if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT)
{
addObjectActor(obj);

View File

@@ -145,9 +145,10 @@ namespace AIPathfinding
shipyards.push_back(t);
}
for(const CGObjectInstance * obj : aiNk->memory->visitableObjs)
for(const ObjectInstanceID objId : aiNk->memory->visitableObjs)
{
if(obj->ID != Obj::TOWN) //towns were handled in the previous loop
const CGObjectInstance * obj = aiNk->cc->getObj(objId, false);
if(obj && obj->ID != Obj::TOWN) //towns were handled in the previous loop
{
if(const auto * shipyard = dynamic_cast<const IShipyard *>(obj))
shipyards.push_back(shipyard);