Performance(Core): Some activity sec to ms init fixes, global activity loop check and some additional minor fixes (#2288)

<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description

1. Corrected the init activity times; Since ive changed (previous) the
calc from seconds to ms to increase more scope for the offset execute
jitter (removed timer(), 0 is correct way todo now with MS) (also broke
self-bot)
2. Global loop checks in activityAllowed instead vs multiple loops, this
function is called very often so we better optimize it.
3 Fixed the broken 'HasManyPlayersNearby' function and then deleted it
:O To fragile due various edge cases and rather expensive call, besides
lets activeAlone deal with this situation which is way more controlled.
4. Some additional small fixes that where unnoticed overtime.
5. Added/Changed inline comments which makes more sense and explains
what it does.
6. Removed dead code freeze bots during init.
7. self-bot fix

## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [ ] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [x] Moderate impact (**explain below**)

When playing with larger amount of real players makes the allowed
activity perform better.

- Does this change modify default bot behavior?
    - - [x] No
    - - [ ] Yes (**explain why**)



- Does this change add new decision branches or increase maintenance
complexity?
    - - [x] No
    - - [ ] Yes (**explain below**)



## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->



<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
This commit is contained in:
bashermens 2026-04-08 19:35:18 +02:00 committed by GitHub
parent 9ebccc23a2
commit 4bcf8fd2c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 92 additions and 206 deletions

View File

@ -119,7 +119,7 @@ PlayerbotAI::PlayerbotAI()
for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++) for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++)
{ {
allowActiveCheckTimer[i] = time(nullptr); allowActiveCheckTimer[i] = 0;
allowActive[i] = false; allowActive[i] = false;
} }
} }
@ -137,19 +137,20 @@ PlayerbotAI::PlayerbotAI(Player* bot)
for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++) for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++)
{ {
allowActiveCheckTimer[i] = time(nullptr); allowActiveCheckTimer[i] = 0;
allowActive[i] = false; allowActive[i] = false;
} }
accountId = bot->GetSession()->GetAccountId(); accountId = bot->GetSession()->GetAccountId();
aiObjectContext = AiFactory::createAiObjectContext(bot, this); aiObjectContext = AiFactory::createAiObjectContext(bot, this);
engines[BOT_STATE_COMBAT] = AiFactory::createCombatEngine(bot, this, aiObjectContext); engines[BOT_STATE_COMBAT] = AiFactory::createCombatEngine(bot, this, aiObjectContext);
engines[BOT_STATE_NON_COMBAT] = AiFactory::createNonCombatEngine(bot, this, aiObjectContext); engines[BOT_STATE_NON_COMBAT] = AiFactory::createNonCombatEngine(bot, this, aiObjectContext);
engines[BOT_STATE_DEAD] = AiFactory::createDeadEngine(bot, this, aiObjectContext); engines[BOT_STATE_DEAD] = AiFactory::createDeadEngine(bot, this, aiObjectContext);
if (sPlayerbotAIConfig.applyInstanceStrategies) if (sPlayerbotAIConfig.applyInstanceStrategies)
ApplyInstanceStrategies(bot->GetMapId()); ApplyInstanceStrategies(bot->GetMapId());
currentEngine = engines[BOT_STATE_NON_COMBAT]; currentEngine = engines[BOT_STATE_NON_COMBAT];
currentState = BOT_STATE_NON_COMBAT; currentState = BOT_STATE_NON_COMBAT;
@ -445,9 +446,11 @@ void PlayerbotAI::UpdateAIInternal([[maybe_unused]] uint32 elapsed, bool minimal
if (!bot->GetMap()) if (!bot->GetMap())
return; // instances are created and destroyed on demand return; // instances are created and destroyed on demand
// kinda expensive call to make on every single updateAI, do we really need this information?
std::string const mapString = WorldPosition(bot).isOverworld() ? std::to_string(bot->GetMapId()) : "I"; std::string const mapString = WorldPosition(bot).isOverworld() ? std::to_string(bot->GetMapId()) : "I";
PerfMonitorOperation* pmo = PerfMonitorOperation* pmo =
sPerfMonitor.start(PERF_MON_TOTAL, "PlayerbotAI::UpdateAIInternal " + mapString); sPerfMonitor.start(PERF_MON_TOTAL, "PlayerbotAI::UpdateAIInternal " + mapString);
ExternalEventHelper helper(aiObjectContext); ExternalEventHelper helper(aiObjectContext);
// chat replies // chat replies
@ -1202,23 +1205,18 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet)
if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID()) if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID())
return; return;
auto itemIds = GetChatHelper()->ExtractAllItemIds(message);
if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) && if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) &&
(GetChatHelper()->ExtractAllItemIds(message).size() > 0 || (itemIds.size() > 0 || GetChatHelper()->ExtractAllQuestIds(message).size() > 0) &&
GetChatHelper()->ExtractAllQuestIds(message).size() > 0) &&
sPlayerbotAIConfig.toxicLinksRepliesChance) sPlayerbotAIConfig.toxicLinksRepliesChance)
{ {
if (urand(0, 50) > 0 || urand(1, 100) > sPlayerbotAIConfig.toxicLinksRepliesChance) if (urand(0, 50) > 0 || urand(1, 100) > sPlayerbotAIConfig.toxicLinksRepliesChance)
{
return; return;
}
} }
else if ((GetChatHelper()->ExtractAllItemIds(message).count(19019) && else if (itemIds.count(19019) && sPlayerbotAIConfig.thunderfuryRepliesChance)
sPlayerbotAIConfig.thunderfuryRepliesChance))
{ {
if (urand(0, 60) > 0 || urand(1, 100) > sPlayerbotAIConfig.thunderfuryRepliesChance) if (urand(0, 60) > 0 || urand(1, 100) > sPlayerbotAIConfig.thunderfuryRepliesChance)
{
return; return;
}
} }
else else
{ {
@ -4459,7 +4457,6 @@ GuilderType PlayerbotAI::GetGuilderType()
bool PlayerbotAI::HasPlayerNearby(WorldPosition* pos, float range) bool PlayerbotAI::HasPlayerNearby(WorldPosition* pos, float range)
{ {
float sqRange = range * range; float sqRange = range * range;
bool nearPlayer = false;
for (auto& player : sRandomPlayerbotMgr.GetPlayers()) for (auto& player : sRandomPlayerbotMgr.GetPlayers())
{ {
if (!player->IsGameMaster() || player->isGMVisible()) if (!player->IsGameMaster() || player->isGMVisible())
@ -4468,19 +4465,18 @@ bool PlayerbotAI::HasPlayerNearby(WorldPosition* pos, float range)
continue; continue;
if (pos->sqDistance(WorldPosition(player)) < sqRange) if (pos->sqDistance(WorldPosition(player)) < sqRange)
nearPlayer = true; return true;
// if player is far check farsight/cinematic camera
WorldObject* viewObj = player->GetViewpoint(); WorldObject* viewObj = player->GetViewpoint();
if (viewObj && viewObj != player) if (viewObj && viewObj != player)
{ {
if (pos->sqDistance(WorldPosition(viewObj)) < sqRange) if (pos->sqDistance(WorldPosition(viewObj)) < sqRange)
nearPlayer = true; return true;
} }
} }
} }
return nearPlayer; return false;
} }
bool PlayerbotAI::HasPlayerNearby(float range) bool PlayerbotAI::HasPlayerNearby(float range)
@ -4489,173 +4485,97 @@ bool PlayerbotAI::HasPlayerNearby(float range)
return HasPlayerNearby(&botPos, range); return HasPlayerNearby(&botPos, range);
}; };
bool PlayerbotAI::HasManyPlayersNearby(uint32 trigerrValue, float range)
{
float sqRange = range * range;
uint32 found = 0;
for (auto& player : sRandomPlayerbotMgr.GetPlayers())
{
if ((!player->IsGameMaster() || player->isGMVisible()) && ServerFacade::instance().GetDistance2d(player, bot) < sqRange)
{
found++;
if (found >= trigerrValue)
return true;
}
}
return false;
}
inline bool HasRealPlayers(Map* map)
{
Map::PlayerList const& players = map->GetPlayers();
if (players.IsEmpty())
{
return false;
}
for (auto const& itr : players)
{
Player* player = itr.GetSource();
if (!player || !player->IsVisible())
{
continue;
}
PlayerbotAI* botAI = GET_PLAYERBOT_AI(player);
if (!botAI || botAI->IsRealPlayer() || botAI->HasRealPlayerMaster())
{
return true;
}
}
return false;
}
inline bool ZoneHasRealPlayers(Player* bot)
{
Map* map = bot->GetMap();
if (!bot || !map)
{
return false;
}
for (Player* player : sRandomPlayerbotMgr.GetPlayers())
{
if (player->GetMapId() != bot->GetMapId())
continue;
if (player->IsGameMaster() && !player->IsVisible())
{
continue;
}
if (player->GetZoneId() == bot->GetZoneId())
{
PlayerbotAI* botAI = GET_PLAYERBOT_AI(player);
if (!botAI || botAI->IsRealPlayer() || botAI->HasRealPlayerMaster())
{
return true;
}
}
}
return false;
}
bool PlayerbotAI::AllowActive(ActivityType activityType) bool PlayerbotAI::AllowActive(ActivityType activityType)
{ {
// Early return if bot is in invalid state // bot is in an invalid state, not safe to process
if (!bot || !bot->GetSession() || !bot->IsInWorld() || bot->IsBeingTeleported() || if (!bot || !bot->GetSession() || !bot->IsInWorld() || bot->IsBeingTeleported() ||
bot->GetSession()->isLogingOut() || bot->IsDuringRemoveFromWorld()) bot->GetSession()->isLogingOut() || bot->IsDuringRemoveFromWorld())
return false; return false;
// when botActiveAlone is 100% and smartScale disabled // always allow packet handling (e.g. group invites, trade, loot, friend requests etc)
if (sPlayerbotAIConfig.botActiveAlone >= 100 && !sPlayerbotAIConfig.botActiveAloneSmartScale) if (activityType == PACKET_ACTIVITY)
{
return true; return true;
}
// Is in combat. Always defend yourself. // all bots forced active, no rotation or scaling needed
if (sPlayerbotAIConfig.botActiveAlone >= 100 && !sPlayerbotAIConfig.botActiveAloneSmartScale)
return true;
// bot is in combat, always defend yourself
if (activityType != OUT_OF_PARTY_ACTIVITY && activityType != PACKET_ACTIVITY) if (activityType != OUT_OF_PARTY_ACTIVITY && activityType != PACKET_ACTIVITY)
{ {
if (bot->IsInCombat()) if (bot->IsInCombat())
{
return true; return true;
}
} }
// only keep updating till initializing time has completed, // bot is inside a BG, dungeon, or raid — always active
// which prevents unneeded expensive GameTime calls.
if (_isBotInitializing)
{
_isBotInitializing = GameTime::GetUptime().count() < sPlayerbotAIConfig.maxRandomBots * 0.11;
// no activity allowed during bot initialization
if (_isBotInitializing)
{
return false;
}
}
// General exceptions
if (activityType == PACKET_ACTIVITY)
{
return true;
}
// bg, raid, dungeon
if (!WorldPosition(bot).isOverworld()) if (!WorldPosition(bot).isOverworld())
{
return true; return true;
}
// bot map has active players. // bot is waiting in a BG queue — stay active to speed up join
if (sPlayerbotAIConfig.BotActiveAloneForceWhenInMap) if (bot->InBattlegroundQueue())
{ return true;
if (HasRealPlayers(bot->GetMap()))
{
return true;
}
}
// bot zone has active players. // bot is in a guild that contains a real player
if (sPlayerbotAIConfig.BotActiveAloneForceWhenInZone)
{
if (ZoneHasRealPlayers(bot))
{
return true;
}
}
// when in real guild
if (sPlayerbotAIConfig.BotActiveAloneForceWhenInGuild) if (sPlayerbotAIConfig.BotActiveAloneForceWhenInGuild)
{ {
if (IsInRealGuild()) if (IsInRealGuild()) // checks cache list
{
return true; return true;
}
// a real player is in the same zone (e.g. Elwynn Forest), same continent or within configured yard radius
// combined into a single loop to multiple iterations since this function is called so often
bool checkMap = sPlayerbotAIConfig.BotActiveAloneForceWhenInMap;
bool checkZone = sPlayerbotAIConfig.BotActiveAloneForceWhenInZone;
bool checkRadius = sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius > 0;
if (checkMap || checkZone || checkRadius)
{
uint32 botMapId = bot->GetMapId();
uint32 botZoneId = checkZone ? bot->GetZoneId() : 0;
float sqRange = 0.0f;
WorldPosition botPos(bot);
if (checkRadius)
{
float range = static_cast<float>(sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius);
sqRange = range * range;
}
for (auto& player : sRandomPlayerbotMgr.GetPlayers())
{
if (!player || player->GetMapId() != botMapId)
continue;
bool isGM = player->IsGameMaster();
// map check
if (checkMap && !(isGM && !player->IsVisible()))
return true;
// zone check
if (checkZone && !(isGM && !player->IsVisible()) && player->GetZoneId() == botZoneId)
return true;
// radius check
if (checkRadius && (!isGM || player->isGMVisible()))
{
if (botPos.sqDistance(WorldPosition(player)) < sqRange)
return true;
WorldObject* viewObj = player->GetViewpoint();
if (viewObj && viewObj != player && botPos.sqDistance(WorldPosition(viewObj)) < sqRange)
return true;
}
} }
} }
// Player is near. Always active. // bot has a real player master (not another bot)
if (HasPlayerNearby(sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius))
{
return true;
}
// Has player master. Always active.
if (GetMaster()) if (GetMaster())
{ {
PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(GetMaster()); PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(GetMaster());
if (!masterBotAI || masterBotAI->IsRealPlayer()) if (!masterBotAI || masterBotAI->IsRealPlayer())
{
return true; return true;
}
} }
// if grouped up // bot is grouped with a real player (or a bot owned by one)
Group* group = bot->GetGroup(); Group* group = bot->GetGroup();
if (group) if (group)
{ {
@ -4666,52 +4586,37 @@ bool PlayerbotAI::AllowActive(ActivityType activityType)
continue; continue;
if (member == bot) if (member == bot)
{
continue; continue;
}
PlayerbotAI* memberBotAI = GET_PLAYERBOT_AI(member); PlayerbotAI* memberBotAI = GET_PLAYERBOT_AI(member);
{
if (!memberBotAI || memberBotAI->HasRealPlayerMaster())
{
return true;
}
}
// group member is a real player or owned by one — stay active
if (!memberBotAI || memberBotAI->HasRealPlayerMaster())
return true;
// if group leader (bot) is inactive, follow suit
if (group->IsLeader(member->GetGUID())) if (group->IsLeader(member->GetGUID()))
{ {
if (!memberBotAI->AllowActivity(PARTY_ACTIVITY)) if (!memberBotAI->AllowActivity(PARTY_ACTIVITY))
{
return false; return false;
}
} }
} }
} }
// In bg queue. Speed up bg queue/join. // bot is in LFG queue — stay active
if (bot->InBattlegroundQueue())
{
return true;
}
bool isLFG = false; bool isLFG = false;
if (group) if (group)
{ {
if (sLFGMgr->GetState(group->GetGUID()) != lfg::LFG_STATE_NONE) if (sLFGMgr->GetState(group->GetGUID()) != lfg::LFG_STATE_NONE)
{
isLFG = true; isLFG = true;
}
} }
if (sLFGMgr->GetState(bot->GetGUID()) != lfg::LFG_STATE_NONE) if (sLFGMgr->GetState(bot->GetGUID()) != lfg::LFG_STATE_NONE)
{
isLFG = true; isLFG = true;
}
if (isLFG)
{
return true;
}
// HasFriend if (isLFG)
return true;
// a real player has this bot on their friends list
if (sPlayerbotAIConfig.BotActiveAloneForceWhenIsFriend) if (sPlayerbotAIConfig.BotActiveAloneForceWhenIsFriend)
{ {
// shouldnt be needed analyse in future // shouldnt be needed analyse in future
@ -4728,54 +4633,37 @@ bool PlayerbotAI::AllowActive(ActivityType activityType)
if (!playerAI || !playerAI->IsRealPlayer()) if (!playerAI || !playerAI->IsRealPlayer())
continue; continue;
// if a real player has the bot as a friend
PlayerSocial* social = player->GetSocial(); PlayerSocial* social = player->GetSocial();
if (social && social->HasFriend(bot->GetGUID())) if (social && social->HasFriend(bot->GetGUID()))
return true; return true;
} }
} }
// Force the bots to spread // pathfinding only runs for bots forced active by the rules above —
if (activityType == OUT_OF_PARTY_ACTIVITY || activityType == GRIND_ACTIVITY) // skip it for bots that would only be active via random rotation
{
if (HasManyPlayersNearby(10, 40))
{
return true;
}
}
// Bots don't need react to PathGenerator activities
if (activityType == DETAILED_MOVE_ACTIVITY) if (activityType == DETAILED_MOVE_ACTIVITY)
{
return false; return false;
}
// #######################################################################################
// Acitivity throttling logic
// #######################################################################################
if (sPlayerbotAIConfig.botActiveAlone <= 0) if (sPlayerbotAIConfig.botActiveAlone <= 0)
{
return false; return false;
}
// ####################################################################################### // base threshold capped at 100
// All mandatory conditations are checked to be active or not, from here the remaining
// situations are usable for scaling when enabled.
// #######################################################################################
// Base percentage of bots to be active
uint32 mod = sPlayerbotAIConfig.botActiveAlone > 100 ? 100 : sPlayerbotAIConfig.botActiveAlone; uint32 mod = sPlayerbotAIConfig.botActiveAlone > 100 ? 100 : sPlayerbotAIConfig.botActiveAlone;
// Apply SmartScale if enabled // reduce threshold based on server tick time when SmartScale is enabled
if (sPlayerbotAIConfig.botActiveAloneSmartScale && if (sPlayerbotAIConfig.botActiveAloneSmartScale &&
bot->GetLevel() >= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMinLevel && bot->GetLevel() >= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMinLevel &&
bot->GetLevel() <= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMaxLevel) bot->GetLevel() <= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMaxLevel)
{ {
mod = AutoScaleActivity(mod); // mod reflects on latency throttling mod = AutoScaleActivity(mod);
} }
// Get deterministic bucket + timeSlot // deterministic rotation — bot is active if its hash falls below the threshold
uint32 ActivityNumber = GetFixedBotNumber(100); uint32 ActivityNumber = GetFixedBotNumber(100);
return ActivityNumber < mod;
// Check if this bot is in the active set
return ActivityNumber < mod; // mod is directly the number of bots active (0100)
} }
bool PlayerbotAI::AllowActivity(ActivityType activityType, bool checkNow) bool PlayerbotAI::AllowActivity(ActivityType activityType, bool checkNow)

View File

@ -546,7 +546,6 @@ public:
GuilderType GetGuilderType(); GuilderType GetGuilderType();
bool HasPlayerNearby(WorldPosition* pos, float range = sPlayerbotAIConfig.reactDistance); bool HasPlayerNearby(WorldPosition* pos, float range = sPlayerbotAIConfig.reactDistance);
bool HasPlayerNearby(float range = sPlayerbotAIConfig.reactDistance); bool HasPlayerNearby(float range = sPlayerbotAIConfig.reactDistance);
bool HasManyPlayersNearby(uint32 trigerrValue = 20, float range = sPlayerbotAIConfig.sightDistance);
bool AllowActive(ActivityType activityType); bool AllowActive(ActivityType activityType);
bool AllowActivity(ActivityType activityType = ALL_ACTIVITY, bool checkNow = false); bool AllowActivity(ActivityType activityType = ALL_ACTIVITY, bool checkNow = false);
uint32 AutoScaleActivity(uint32 mod); uint32 AutoScaleActivity(uint32 mod);
@ -614,7 +613,6 @@ private:
Item* FindItemInInventory(std::function<bool(ItemTemplate const*)> checkItem) const; Item* FindItemInInventory(std::function<bool(ItemTemplate const*)> checkItem) const;
void HandleCommands(); void HandleCommands();
void HandleCommand(uint32 type, const std::string& text, Player& fromPlayer, const uint32 lang = LANG_UNIVERSAL); void HandleCommand(uint32 type, const std::string& text, Player& fromPlayer, const uint32 lang = LANG_UNIVERSAL);
bool _isBotInitializing = false;
inline bool IsValidUnit(const Unit* unit) const inline bool IsValidUnit(const Unit* unit) const
{ {
return unit && unit->IsInWorld() && !unit->IsDuringRemoveFromWorld(); return unit && unit->IsInWorld() && !unit->IsDuringRemoveFromWorld();