mirror of
				https://github.com/vcmi/vcmi.git
				synced 2025-10-31 00:07:39 +02:00 
			
		
		
		
	Nullkiller: small optimization of AIPathfinder for big maps
This commit is contained in:
		
				
					committed by
					
						 Andrii Danylchenko
						Andrii Danylchenko
					
				
			
			
				
	
			
			
			
						parent
						
							07b6b0605c
						
					
				
				
					commit
					5bfe71c8f3
				
			| @@ -351,4 +351,11 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj) | ||||
| 		return (dynamic_cast<const CGKeys *>(obj))->wasMyColorVisited(ai->playerID); //FIXME: they could be revisited sooner than in a week | ||||
| 	} | ||||
| 	return false; | ||||
| } | ||||
|  | ||||
| uint64_t timeElapsed(boost::chrono::time_point<boost::chrono::steady_clock> start) | ||||
| { | ||||
| 	auto end = boost::chrono::high_resolution_clock::now(); | ||||
|  | ||||
| 	return boost::chrono::duration_cast<boost::chrono::milliseconds>(end - start).count(); | ||||
| } | ||||
| @@ -182,6 +182,8 @@ bool compareHeroStrength(HeroPtr h1, HeroPtr h2); | ||||
| bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2); | ||||
| bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2); | ||||
|  | ||||
| uint64_t timeElapsed(boost::chrono::time_point<boost::chrono::steady_clock> start); | ||||
|  | ||||
| class CDistanceSorter | ||||
| { | ||||
| 	const CGHeroInstance * hero; | ||||
|   | ||||
| @@ -16,14 +16,17 @@ void DangerHitMapAnalyzer::updateHitMap() | ||||
| 	if(upToDate) | ||||
| 		return; | ||||
|  | ||||
| 	logAi->trace("Update danger hitmap"); | ||||
|  | ||||
| 	upToDate = true; | ||||
| 	auto start = boost::chrono::high_resolution_clock::now(); | ||||
|  | ||||
| 	auto cb = ai->cb.get(); | ||||
| 	auto mapSize = ai->cb->getMapSize(); | ||||
| 	hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); | ||||
| 	enemyHeroAccessibleObjects.clear(); | ||||
|  | ||||
| 	std::map<PlayerColor, std::vector<const CGHeroInstance *>> heroes; | ||||
| 	std::map<PlayerColor, std::map<const CGHeroInstance *, HeroRole>> heroes; | ||||
|  | ||||
| 	for(const CGObjectInstance * obj : ai->memory->visitableObjs) | ||||
| 	{ | ||||
| @@ -31,7 +34,7 @@ void DangerHitMapAnalyzer::updateHitMap() | ||||
| 		{ | ||||
| 			auto hero = dynamic_cast<const CGHeroInstance *>(obj); | ||||
|  | ||||
| 			heroes[hero->tempOwner].push_back(hero); | ||||
| 			heroes[hero->tempOwner][hero] = HeroRole::MAIN; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -83,6 +86,8 @@ void DangerHitMapAnalyzer::updateHitMap() | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	logAi->trace("Danger hit map updated in %ld", timeElapsed(start)); | ||||
| } | ||||
|  | ||||
| uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const | ||||
|   | ||||
| @@ -172,6 +172,8 @@ bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const | ||||
|  | ||||
| void ObjectClusterizer::clusterize() | ||||
| { | ||||
| 	auto start = boost::chrono::high_resolution_clock::now(); | ||||
|  | ||||
| 	nearObjects.reset(); | ||||
| 	farObjects.reset(); | ||||
| 	blockedObjects.clear(); | ||||
| @@ -276,4 +278,6 @@ void ObjectClusterizer::clusterize() | ||||
| 		} | ||||
| #endif | ||||
| 	} | ||||
|  | ||||
| 	logAi->trace("Clusterization complete in %ld", timeElapsed(start)); | ||||
| } | ||||
| @@ -62,6 +62,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior) const | ||||
| 	Goals::TGoalVec goals[MAX_DEPTH + 1]; | ||||
| 	Goals::TTaskVec tasks; | ||||
| 	std::map<Goals::TSubgoal, Goals::TSubgoal> decompositionMap; | ||||
| 	auto start = boost::chrono::high_resolution_clock::now(); | ||||
|  | ||||
| 	goals[0] = {behavior}; | ||||
|  | ||||
| @@ -127,14 +128,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior) const | ||||
|  | ||||
| 	if(tasks.empty()) | ||||
| 	{ | ||||
| 		logAi->debug("Behavior %s found no tasks", behavior->toString()); | ||||
| 		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", behavior->toString(), task->toString(), task->priority); | ||||
| 	logAi->debug( | ||||
| 		"Behavior %s returns %s, priority %f. Time taken %ld", | ||||
| 		behavior->toString(), | ||||
| 		task->toString(), | ||||
| 		task->priority, | ||||
| 		timeElapsed(start)); | ||||
|  | ||||
| 	return task; | ||||
| } | ||||
| @@ -148,26 +154,34 @@ void Nullkiller::resetAiState() | ||||
|  | ||||
| void Nullkiller::updateAiState() | ||||
| { | ||||
| 	auto start = boost::chrono::high_resolution_clock::now(); | ||||
|  | ||||
| 	activeHero = nullptr; | ||||
|  | ||||
| 	memory->removeInvisibleObjects(cb.get()); | ||||
| 	dangerHitMap->updateHitMap(); | ||||
|  | ||||
| 	auto activeHeroes = cb->getHeroesInfo(); | ||||
| 	heroManager->update(); | ||||
| 	logAi->trace("Updating paths"); | ||||
|  | ||||
| 	vstd::erase_if(activeHeroes, [this](const CGHeroInstance * hero) -> bool | ||||
| 	std::map<const CGHeroInstance *, HeroRole> activeHeroes; | ||||
|  | ||||
| 	for(auto hero : cb->getHeroesInfo()) | ||||
| 	{ | ||||
| 		auto lockedReason = getHeroLockedReason(hero); | ||||
| 		if(getHeroLockedReason(hero) == HeroLockedReason::DEFENCE) | ||||
| 			continue; | ||||
|  | ||||
| 		return lockedReason == HeroLockedReason::DEFENCE; | ||||
| 	}); | ||||
| 		activeHeroes[hero] = heroManager->getHeroRole(hero); | ||||
| 	} | ||||
|  | ||||
| 	pathfinder->updatePaths(activeHeroes, true); | ||||
| 	heroManager->update(); | ||||
|  | ||||
| 	armyManager->update(); | ||||
|  | ||||
| 	objectClusterizer->clusterize(); | ||||
| 	buildAnalyzer->update(); | ||||
|  | ||||
| 	logAi->debug("AI state updated in %ld", timeElapsed(start)); | ||||
| } | ||||
|  | ||||
| bool Nullkiller::isHeroLocked(const CGHeroInstance * hero) const | ||||
|   | ||||
| @@ -640,13 +640,6 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal | ||||
| 	return context; | ||||
| } | ||||
|  | ||||
| /// distance | ||||
| /// nearest hero? | ||||
| /// gold income | ||||
| /// army income | ||||
| /// hero strength - hero skills | ||||
| /// danger | ||||
| /// importance | ||||
| float PriorityEvaluator::evaluate(Goals::TSubgoal task) | ||||
| { | ||||
| 	auto evaluationContext = buildEvaluationContext(task); | ||||
| @@ -677,7 +670,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) | ||||
| 		fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); | ||||
|  | ||||
| 		engine->process(); | ||||
| 		//engine.process(VISIT_TILE); //TODO: Process only Visit_Tile | ||||
|  | ||||
| 		result = value->getValue(); | ||||
| 	} | ||||
| 	catch(fl::Exception & fe) | ||||
|   | ||||
| @@ -614,22 +614,45 @@ const std::set<const CGHeroInstance *> AINodeStorage::getAllHeroes() const | ||||
| 	return heroes; | ||||
| } | ||||
|  | ||||
| void AINodeStorage::setHeroes(std::vector<const CGHeroInstance *> heroes) | ||||
| bool AINodeStorage::isDistanceLimitReached(const PathNodeInfo & source, CDestinationNodeInfo & destination) const | ||||
| { | ||||
| 	if(heroChainPass == EHeroChainPass::CHAIN && destination.node->turns > heroChainTurn) | ||||
| 	{ | ||||
| 		return true; | ||||
| 	} | ||||
| 	 | ||||
| 	auto aiNode = getAINode(destination.node); | ||||
| 	 | ||||
| 	if(heroChainPass == EHeroChainPass::FINAL) | ||||
| 	{ | ||||
| 		if(aiNode->actor->heroRole == HeroRole::SCOUT && destination.node->turns > 3) | ||||
| 			return true; | ||||
| 	} | ||||
| 	else if(heroChainPass == EHeroChainPass::INITIAL) | ||||
| 	{ | ||||
| 		if(aiNode->actor->heroRole == HeroRole::SCOUT && destination.node->turns > 5) | ||||
| 			return true; | ||||
| 	} | ||||
|  | ||||
| 	return false; | ||||
| } | ||||
|  | ||||
| void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes) | ||||
| { | ||||
| 	playerID = ai->playerID; | ||||
|  | ||||
| 	for(auto & hero : heroes) | ||||
| 	{ | ||||
| 		uint64_t mask = 1 << actors.size(); | ||||
| 		auto actor = std::make_shared<HeroActor>(hero, mask, ai); | ||||
| 		auto actor = std::make_shared<HeroActor>(hero.first, hero.second, mask, ai); | ||||
|  | ||||
| 		if(hero->tempOwner != ai->playerID) | ||||
| 		if(actor->hero->tempOwner != ai->playerID) | ||||
| 		{ | ||||
| 			bool onLand = !actor->hero->boat; | ||||
| 			actor->initialMovement = actor->hero->maxMovePoints(onLand); | ||||
| 		} | ||||
|  | ||||
| 		playerID = hero->tempOwner; | ||||
| 		playerID = actor->hero->tempOwner; | ||||
|  | ||||
| 		actors.push_back(actor); | ||||
| 	} | ||||
| @@ -926,7 +949,7 @@ bool AINodeStorage::hasBetterChain( | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if(candidateActor->chainMask != node.actor->chainMask) | ||||
| 		if(candidateActor->chainMask != node.actor->chainMask && heroChainPass == EHeroChainPass::CHAIN) | ||||
| 			continue; | ||||
|  | ||||
| 		auto nodeActor = node.actor; | ||||
| @@ -949,29 +972,32 @@ bool AINodeStorage::hasBetterChain( | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		/*if(nodeArmyValue == candidateArmyValue | ||||
| 			&& nodeActor->heroFightingStrength >= candidateActor->heroFightingStrength | ||||
| 			&& node.cost <= candidateNode->cost) | ||||
| 		if(heroChainPass == EHeroChainPass::FINAL) | ||||
| 		{ | ||||
| 			if(nodeActor->heroFightingStrength == candidateActor->heroFightingStrength | ||||
| 				&& node.cost == candidateNode->cost | ||||
| 				&& &node < candidateNode) | ||||
| 			if(nodeArmyValue == candidateArmyValue | ||||
| 				&& nodeActor->heroFightingStrength >= candidateActor->heroFightingStrength | ||||
| 				&& node.cost <= candidateNode->cost) | ||||
| 			{ | ||||
| 				continue; | ||||
| 			} | ||||
| 				if(nodeActor->heroFightingStrength == candidateActor->heroFightingStrength | ||||
| 					&& node.cost == candidateNode->cost | ||||
| 					&& &node < candidateNode) | ||||
| 				{ | ||||
| 					continue; | ||||
| 				} | ||||
|  | ||||
| #if AI_TRACE_LEVEL >= 2 | ||||
| 			logAi->trace( | ||||
| 				"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i", | ||||
| 				source->coord.toString(), | ||||
| 				candidateNode->coord.toString(), | ||||
| 				candidateNode->actor->hero->name, | ||||
| 				candidateNode->actor->chainMask, | ||||
| 				candidateNode->actor->armyValue, | ||||
| 				node.moveRemains - candidateNode->moveRemains); | ||||
| 				logAi->trace( | ||||
| 					"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i", | ||||
| 					source->coord.toString(), | ||||
| 					candidateNode->coord.toString(), | ||||
| 					candidateNode->actor->hero->name, | ||||
| 					candidateNode->actor->chainMask, | ||||
| 					candidateNode->actor->armyValue, | ||||
| 					node.moveRemains - candidateNode->moveRemains); | ||||
| #endif | ||||
| 			return true; | ||||
| 		}*/ | ||||
| 				return true; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return false; | ||||
| @@ -1170,7 +1196,7 @@ std::string AIPath::toString() const | ||||
| { | ||||
| 	std::stringstream str; | ||||
|  | ||||
| 	str << targetHero->name << "[" << std::hex << chainMask << std::dec << "]" << ": "; | ||||
| 	str << targetHero->name << "[" << std::hex << chainMask << std::dec << "]" << ", turn " << (int)(turn()) << ": "; | ||||
|  | ||||
| 	for(auto node : nodes) | ||||
| 		str << node.targetHero->name << "[" << std::hex << node.chainMask << std::dec << "]" << "->" << node.coord.toString() << "; "; | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #define PATHFINDER_TRACE_LEVEL 0 | ||||
| #define AI_TRACE_LEVEL 1 | ||||
| #define AI_TRACE_LEVEL 0 | ||||
|  | ||||
| #include "../../../lib/CPathfinder.h" | ||||
| #include "../../../lib/mapObjects/CGHeroInstance.h" | ||||
| @@ -162,10 +162,7 @@ public: | ||||
| 		return hasBetterChain(source, destination); | ||||
| 	} | ||||
|  | ||||
| 	bool isDistanceLimitReached(const PathNodeInfo & source, CDestinationNodeInfo & destination) const | ||||
| 	{ | ||||
| 		return heroChainPass == EHeroChainPass::CHAIN && destination.node->turns > heroChainTurn; | ||||
| 	} | ||||
| 	bool isDistanceLimitReached(const PathNodeInfo & source, CDestinationNodeInfo & destination) const; | ||||
|  | ||||
| 	template<class NodeRange> | ||||
| 	bool hasBetterChain( | ||||
| @@ -176,7 +173,7 @@ public: | ||||
| 	boost::optional<AIPathNode *> getOrCreateNode(const int3 & coord, const EPathfindingLayer layer, const ChainActor * actor); | ||||
| 	std::vector<AIPath> getChainInfo(const int3 & pos, bool isOnLand) const; | ||||
| 	bool isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const; | ||||
| 	void setHeroes(std::vector<const CGHeroInstance *> heroes); | ||||
| 	void setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes); | ||||
| 	void setTownsAndDwellings( | ||||
| 		const std::vector<const CGTownInstance *> & towns, | ||||
| 		const std::set<const CGObjectInstance *> & visitableObjs); | ||||
|   | ||||
| @@ -42,13 +42,14 @@ std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile) const | ||||
| 	return storage->getChainInfo(tile, !tileInfo->isWater()); | ||||
| } | ||||
|  | ||||
| void AIPathfinder::updatePaths(std::vector<const CGHeroInstance *> heroes, bool useHeroChain) | ||||
| void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes, bool useHeroChain) | ||||
| { | ||||
| 	if(!storage) | ||||
| 	{ | ||||
| 		storage.reset(new AINodeStorage(ai, cb->getMapSize())); | ||||
| 	} | ||||
|  | ||||
| 	auto start = boost::chrono::high_resolution_clock::now(); | ||||
| 	logAi->debug("Recalculate all paths"); | ||||
| 	int pass = 0; | ||||
|  | ||||
| @@ -89,4 +90,6 @@ void AIPathfinder::updatePaths(std::vector<const CGHeroInstance *> heroes, bool | ||||
| 			cb->calculatePaths(config); | ||||
| 		} | ||||
| 	} while(storage->increaseHeroChainTurnLimit()); | ||||
|  | ||||
| 	logAi->trace("Recalculated paths in %ld", timeElapsed(start)); | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,6 @@ public: | ||||
| 	AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * ai); | ||||
| 	std::vector<AIPath> getPathInfo(const int3 & tile) const; | ||||
| 	bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const; | ||||
| 	void updatePaths(std::vector<const CGHeroInstance *> heroes, bool useHeroChain = false); | ||||
| 	void updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes, bool useHeroChain = false); | ||||
| 	void init(); | ||||
| }; | ||||
|   | ||||
| @@ -22,8 +22,8 @@ bool HeroExchangeArmy::needsLastStack() const | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
| ChainActor::ChainActor(const CGHeroInstance * hero, uint64_t chainMask) | ||||
| 	:hero(hero), isMovable(true), chainMask(chainMask), creatureSet(hero), | ||||
| ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t chainMask) | ||||
| 	:hero(hero), heroRole(heroRole), isMovable(true), chainMask(chainMask), creatureSet(hero), | ||||
| 	baseActor(this), carrierParent(nullptr), otherParent(nullptr), actorExchangeCount(1), armyCost() | ||||
| { | ||||
| 	initialPosition = hero->visitablePos(); | ||||
| @@ -35,7 +35,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, uint64_t chainMask) | ||||
| } | ||||
|  | ||||
| ChainActor::ChainActor(const ChainActor * carrier, const ChainActor * other, const CCreatureSet * heroArmy) | ||||
| 	:hero(carrier->hero), isMovable(true), creatureSet(heroArmy), chainMask(carrier->chainMask | other->chainMask), | ||||
| 	:hero(carrier->hero), heroRole(carrier->heroRole), isMovable(true), creatureSet(heroArmy), chainMask(carrier->chainMask | other->chainMask), | ||||
| 	baseActor(this), carrierParent(carrier), otherParent(other), heroFightingStrength(carrier->heroFightingStrength), | ||||
| 	actorExchangeCount(carrier->actorExchangeCount + other->actorExchangeCount), armyCost(carrier->armyCost + other->armyCost) | ||||
| { | ||||
| @@ -43,7 +43,7 @@ ChainActor::ChainActor(const ChainActor * carrier, const ChainActor * other, con | ||||
| } | ||||
|  | ||||
| ChainActor::ChainActor(const CGObjectInstance * obj, const CCreatureSet * creatureSet, uint64_t chainMask, int initialTurn) | ||||
| 	:hero(nullptr), isMovable(false), creatureSet(creatureSet), chainMask(chainMask), | ||||
| 	:hero(nullptr), heroRole(HeroRole::MAIN), isMovable(false), creatureSet(creatureSet), chainMask(chainMask), | ||||
| 	baseActor(this), carrierParent(nullptr), otherParent(nullptr), initialTurn(initialTurn), initialMovement(0), | ||||
| 	heroFightingStrength(0), actorExchangeCount(1), armyCost() | ||||
| { | ||||
| @@ -72,8 +72,8 @@ std::string ObjectActor::toString() const | ||||
| 	return object->getObjectName() + " at " + object->visitablePos().toString(); | ||||
| } | ||||
|  | ||||
| HeroActor::HeroActor(const CGHeroInstance * hero, uint64_t chainMask, const Nullkiller * ai) | ||||
| 	:ChainActor(hero, chainMask) | ||||
| HeroActor::HeroActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t chainMask, const Nullkiller * ai) | ||||
| 	:ChainActor(hero, heroRole, chainMask) | ||||
| { | ||||
| 	exchangeMap = new HeroExchangeMap(this, ai); | ||||
| 	setupSpecialActors(); | ||||
| @@ -94,6 +94,7 @@ void ChainActor::setBaseActor(HeroActor * base) | ||||
| { | ||||
| 	baseActor = base; | ||||
| 	hero = base->hero; | ||||
| 	heroRole = base->heroRole; | ||||
| 	layer = base->layer; | ||||
| 	initialMovement = base->initialMovement; | ||||
| 	initialTurn = base->initialTurn; | ||||
|   | ||||
| @@ -27,7 +27,7 @@ public: | ||||
| class ChainActor | ||||
| { | ||||
| protected: | ||||
| 	ChainActor(const CGHeroInstance * hero, uint64_t chainMask); | ||||
| 	ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t chainMask); | ||||
| 	ChainActor(const ChainActor * carrier, const ChainActor * other, const CCreatureSet * heroArmy); | ||||
| 	ChainActor(const CGObjectInstance * obj, const CCreatureSet * army, uint64_t chainMask, int initialTurn); | ||||
|  | ||||
| @@ -38,6 +38,7 @@ public: | ||||
| 	bool allowBattle; | ||||
| 	bool allowSpellCast; | ||||
| 	const CGHeroInstance * hero; | ||||
| 	HeroRole heroRole; | ||||
| 	const CCreatureSet * creatureSet; | ||||
| 	const ChainActor * battleActor; | ||||
| 	const ChainActor * castActor; | ||||
| @@ -105,7 +106,7 @@ public: | ||||
| 	std::shared_ptr<SpecialAction> exchangeAction; | ||||
| 	// chain flags, can be combined meaning hero exchange and so on | ||||
|  | ||||
| 	HeroActor(const CGHeroInstance * hero, uint64_t chainMask, const Nullkiller * ai); | ||||
| 	HeroActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t chainMask, const Nullkiller * ai); | ||||
| 	HeroActor(const ChainActor * carrier, const ChainActor * other, const CCreatureSet * army, const Nullkiller * ai); | ||||
|  | ||||
| 	virtual bool canExchange(const ChainActor * other) const override; | ||||
|   | ||||
| @@ -38,7 +38,11 @@ namespace AIPathfinding | ||||
|  | ||||
| 		auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper); | ||||
| 		if(blocker == BlockingReason::NONE) | ||||
| 		{ | ||||
| 			destination.blocked = nodeStorage->isDistanceLimitReached(source, destination); | ||||
|  | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		auto destGuardians = cb->getGuardingCreatures(destination.coord); | ||||
| 		bool allowBypass = false; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user