void Bot::RegisterVisibleEnemies() { if(G_ISGHOSTING(self) || GS_MatchState() == MATCH_STATE_COUNTDOWN || GS_ShootingDisabled()) return; CheckIsInThinkFrame(__FUNCTION__); // Compute look dir before loop vec3_t lookDir; AngleVectors(self->s.angles, lookDir, nullptr, nullptr); float fov = 110.0f + 69.0f * Skill(); float dotFactor = cosf((float)DEG2RAD(fov / 2)); struct EntAndDistance { int entNum; float distance; EntAndDistance(int entNum_, float distance_): entNum(entNum_), distance(distance_) {} bool operator<(const EntAndDistance &that) const { return distance < that.distance; } }; // Do not call inPVS() and G_Visible() for potential targets inside a loop for all clients. // In worst case when all bots may see each other we get N^2 traces and PVS tests // First, select all candidate targets along with distance to a bot. // Then choose not more than BotBrain::maxTrackedEnemies nearest enemies for calling OnEnemyViewed() // It may cause data loss (far enemies may have higher logical priority), // but in a common good case (when there are few visible enemies) it preserves data, // and in the worst case mentioned above it does not act weird from player POV and prevents server hang up. // Note: non-client entities also may be candidate targets. StaticVector<EntAndDistance, MAX_EDICTS> candidateTargets; for (int i = 1; i < game.numentities; ++i) { edict_t *ent = game.edicts + i; if (botBrain.MayNotBeFeasibleEnemy(ent)) continue; // Reject targets quickly by fov Vec3 toTarget(ent->s.origin); toTarget -= self->s.origin; float squareDistance = toTarget.SquaredLength(); if (squareDistance < 1) continue; float invDistance = Q_RSqrt(squareDistance); toTarget *= invDistance; if (toTarget.Dot(lookDir) < dotFactor) continue; // It seams to be more instruction cache-friendly to just add an entity to a plain array // and sort it once after the loop instead of pushing an entity in a heap on each iteration candidateTargets.emplace_back(EntAndDistance(ENTNUM(ent), 1.0f / invDistance)); } std::sort(candidateTargets.begin(), candidateTargets.end()); // Select inPVS/visible targets first to aid instruction cache, do not call callbacks in loop StaticVector<edict_t *, MAX_CLIENTS> targetsInPVS; StaticVector<edict_t *, MAX_CLIENTS> visibleTargets; static_assert(AiBaseEnemyPool::MAX_TRACKED_ENEMIES <= MAX_CLIENTS, "targetsInPVS capacity may be exceeded"); for (int i = 0, end = std::min(candidateTargets.size(), botBrain.MaxTrackedEnemies()); i < end; ++i) { edict_t *ent = game.edicts + candidateTargets[i].entNum; if (trap_inPVS(self->s.origin, ent->s.origin)) targetsInPVS.push_back(ent); } for (auto ent: targetsInPVS) if (G_Visible(self, ent)) visibleTargets.push_back(ent); // Call bot brain callbacks on visible targets for (auto ent: visibleTargets) botBrain.OnEnemyViewed(ent); botBrain.AfterAllEnemiesViewed(); CheckAlertSpots(visibleTargets); }
static void FindHubAreas() { if (!hubAreas.empty()) return; AiAasWorld *aasWorld = AiAasWorld::Instance(); if (!aasWorld->IsLoaded()) return; // Select not more than hubAreas.capacity() grounded areas that have highest connectivity to other areas. struct AreaAndReachCount { int area, reachCount; AreaAndReachCount(int area_, int reachCount_): area(area_), reachCount(reachCount_) {} // Ensure that area with lowest reachCount will be evicted in pop_heap(), so use > bool operator<(const AreaAndReachCount &that) const { return reachCount > that.reachCount; } }; StaticVector<AreaAndReachCount, hubAreas.capacity() + 1> bestAreasHeap; for (int i = 1; i < aasWorld->NumAreas(); ++i) { const auto &areaSettings = aasWorld->AreaSettings()[i]; if (!(areaSettings.areaflags & AREA_GROUNDED)) continue; if (areaSettings.areaflags & AREA_DISABLED) continue; if (areaSettings.contents & (AREACONTENTS_DONOTENTER|AREACONTENTS_LAVA|AREACONTENTS_SLIME|AREACONTENTS_WATER)) continue; // Reject degenerate areas, pass only relatively large areas const auto &area = aasWorld->Areas()[i]; if (area.maxs[0] - area.mins[0] < 128.0f) continue; if (area.maxs[1] - area.mins[1] < 128.0f) continue; // Count as useful only several kinds of reachabilities int usefulReachCount = 0; int reachNum = areaSettings.firstreachablearea; int lastReachNum = areaSettings.firstreachablearea + areaSettings.numreachableareas - 1; while (reachNum <= lastReachNum) { const auto &reach = aasWorld->Reachabilities()[reachNum]; if (reach.traveltype == TRAVEL_WALK || reach.traveltype == TRAVEL_WALKOFFLEDGE) usefulReachCount++; ++reachNum; } // Reject early to avoid more expensive call to push_heap() if (!usefulReachCount) continue; bestAreasHeap.push_back(AreaAndReachCount(i, usefulReachCount)); std::push_heap(bestAreasHeap.begin(), bestAreasHeap.end()); // bestAreasHeap size should be always less than its capacity: // 1) to ensure that there is a free room for next area; // 2) to ensure that hubAreas capacity will not be exceeded. if (bestAreasHeap.size() == bestAreasHeap.capacity()) { std::pop_heap(bestAreasHeap.begin(), bestAreasHeap.end()); bestAreasHeap.pop_back(); } } static_assert(bestAreasHeap.capacity() == hubAreas.capacity() + 1, ""); for (const auto &areaAndReachCount: bestAreasHeap) hubAreas.push_back(areaAndReachCount.area); }
SelectedNavEntity BotItemsSelector::SuggestGoalNavEntity( const SelectedNavEntity &currSelectedNavEntity ) { UpdateInternalItemAndGoalWeights(); StaticVector<NavEntityAndWeight, MAX_NAVENTS> rawWeightCandidates; const auto levelTime = level.time; auto *navEntitiesRegistry = NavEntitiesRegistry::Instance(); for( auto it = navEntitiesRegistry->begin(), end = navEntitiesRegistry->end(); it != end; ++it ) { const NavEntity *navEnt = *it; if( navEnt->IsDisabled() ) { continue; } // We cannot just set a zero internal weight for a temporarily disabled nav entity // (it might be overridden by an external weight, and we should not modify external weights // as script users expect them remaining the same unless explicitly changed via script API) if( disabledForSelectionUntil[navEnt->Id()] >= levelTime ) { continue; } // Since movable goals have been introduced (and clients qualify as movable goals), prevent picking itself as a goal. if( navEnt->Id() == ENTNUM( self ) ) { continue; } if( navEnt->Item() && !G_Gametype_CanPickUpItem( navEnt->Item() ) ) { continue; } // Reject an entity quickly if it looks like blocked by an enemy that is close to the entity. // Note than passing this test does not guarantee that entire path to the entity is not blocked by enemies. if( self->ai->botRef->routeCache->AreaDisabled( navEnt->AasAreaNum() ) ) { continue; } // This is a coarse and cheap test, helps to reject recently picked armors and powerups unsigned spawnTime = navEnt->SpawnTime(); // A feasible spawn time (non-zero) always >= level.time. if( !spawnTime || level.time - spawnTime > 15000 ) { continue; } float weight = GetEntityWeight( navEnt->Id() ); if( weight > 0 ) { rawWeightCandidates.push_back( NavEntityAndWeight( navEnt, weight ) ); } } // Sort all pre-selected candidates by their raw weights std::sort( rawWeightCandidates.begin(), rawWeightCandidates.end() ); // Try checking whether the bot is in some floor cluster to give a greater weight for items in the same cluster int currFloorClusterNum = 0; const auto &entityPhysicsState = self->ai->botRef->EntityPhysicsState(); const auto *aasFloorClusterNums = AiAasWorld::Instance()->AreaFloorClusterNums(); if( aasFloorClusterNums[entityPhysicsState->CurrAasAreaNum()] ) { currFloorClusterNum = aasFloorClusterNums[entityPhysicsState->CurrAasAreaNum()]; } else if( aasFloorClusterNums[entityPhysicsState->DroppedToFloorAasAreaNum()] ) { currFloorClusterNum = aasFloorClusterNums[entityPhysicsState->DroppedToFloorAasAreaNum()]; } const NavEntity *currGoalNavEntity = currSelectedNavEntity.navEntity; float currGoalEntWeight = 0.0f; float currGoalEntCost = 0.0f; const NavEntity *bestNavEnt = nullptr; float bestWeight = 0.000001f; float bestNavEntCost = 0.0f; // Test not more than 16 best pre-selected by raw weight candidates. // (We try to avoid too many expensive FindTravelTimeToGoalArea() calls, // thats why we start from the best item to avoid wasting these calls for low-priority items) for( unsigned i = 0, end = std::min( rawWeightCandidates.size(), 16U ); i < end; ++i ) { const NavEntity *navEnt = rawWeightCandidates[i].goal; float weight = rawWeightCandidates[i].weight; unsigned moveDuration = 1; unsigned waitDuration = 1; if( self->ai->botRef->CurrAreaNum() != navEnt->AasAreaNum() ) { // We ignore cost of traveling in goal area, since: // 1) to estimate it we have to retrieve reachability to goal area from last area before the goal area // 2) it is relative low compared to overall travel cost, and movement in areas is cheap anyway moveDuration = self->ai->botRef->botBrain.FindTravelTimeToGoalArea( navEnt->AasAreaNum() ) * 10U; // AAS functions return 0 as a "none" value, 1 as a lowest feasible value if( !moveDuration ) { continue; } if( navEnt->IsDroppedEntity() ) { // Do not pick an entity that is likely to dispose before it may be reached if( navEnt->Timeout() <= level.time + moveDuration ) { continue; } } } unsigned spawnTime = navEnt->SpawnTime(); // The entity is not spawned and respawn time is unknown if( !spawnTime ) { continue; } // Entity origin may be reached at this time unsigned reachTime = level.time + moveDuration; if( reachTime < spawnTime ) { waitDuration = spawnTime - reachTime; } if( waitDuration > navEnt->MaxWaitDuration() ) { continue; } float moveCost = MOVE_TIME_WEIGHT * moveDuration * navEnt->CostInfluence(); float cost = 0.0001f + moveCost + WAIT_TIME_WEIGHT * waitDuration * navEnt->CostInfluence(); weight = ( 1000 * weight ) / cost; // If the bot is inside a floor cluster if( currFloorClusterNum ) { // Greatly increase weight for items in the same floor cluster if( currFloorClusterNum == aasFloorClusterNums[navEnt->AasAreaNum()] ) { weight *= 4.0f; } } // Store current weight of the current goal entity if( currGoalNavEntity == navEnt ) { currGoalEntWeight = weight; // Waiting time is handled by the planner for wait actions separately. currGoalEntCost = moveCost; } if( weight > bestWeight ) { bestNavEnt = navEnt; bestWeight = weight; // Waiting time is handled by the planner for wait actions separately. bestNavEntCost = moveCost; } } if( !bestNavEnt ) { Debug( "Can't find a feasible long-term goal nav. entity\n" ); return SelectedNavEntity( nullptr, std::numeric_limits<float>::max(), 0.0f, level.time + 200 ); } // If it is time to pick a new goal (not just re-evaluate current one), do not be too sticky to the current goal const float currToBestWeightThreshold = currGoalNavEntity != nullptr ? 0.6f : 0.8f; if( currGoalNavEntity && currGoalNavEntity == bestNavEnt ) { constexpr const char *format = "current goal entity %s is kept as still having best weight %.3f\n"; Debug( format, currGoalNavEntity->Name(), bestWeight ); return SelectedNavEntity( bestNavEnt, bestNavEntCost, GetGoalWeight( bestNavEnt->Id() ), level.time + 4000 ); } else if( currGoalEntWeight > 0 && currGoalEntWeight / bestWeight > currToBestWeightThreshold ) { constexpr const char *format = "current goal entity %s is kept as having weight %.3f good enough to not consider picking another one\n"; // If currGoalEntWeight > 0, currLongTermGoalEnt is guaranteed to be non-null Debug( format, currGoalNavEntity->Name(), currGoalEntWeight ); return SelectedNavEntity( currGoalNavEntity, currGoalEntCost, GetGoalWeight( bestNavEnt->Id() ), level.time + 2500 ); } else { if( currGoalNavEntity ) { const char *format = "suggested %s weighted %.3f as a long-term goal instead of %s weighted now as %.3f\n"; Debug( format, bestNavEnt->Name(), bestWeight, currGoalNavEntity->Name(), currGoalEntWeight ); } else { Debug( "suggested %s weighted %.3f as a new long-term goal\n", bestNavEnt->Name(), bestWeight ); } return SelectedNavEntity( bestNavEnt, bestNavEntCost, GetGoalWeight( bestNavEnt->Id() ), level.time + 2500 ); } }