From c0390a24fd9b0ab622d7ea246b57b16c3cc0f5d8 Mon Sep 17 00:00:00 2001 From: bashermens <31279994+hermensbas@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:54:46 +0200 Subject: [PATCH] feat(Performance): BotActiveAlone activity interval fixes and default settings for avg player (#2250) ## Pull Request Description - Bugfix the jittering on/off of botAlone activity - BotActiveAlone activity duration configurable - Updated the default config values for general user for a smoother experience - Added offset jittering for the check allowedActivity and check next AI delay to prevent cpu spikes (disabled WhenIsFriend can cause race conditions) ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - 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**) In a positive way, bots in your zone and 150 radius will always be active, meanwhile other bots will be active 40% of the time with intervals for 60 seconds per bot. With much lower latencies. All configurable without say. 40% and 60 seconds for more balance for those who seek bots create world feel more natural and live vs bots leveling without killing the server performance. Why not 50 due activity of bots itself 40% will result more into 45-50% like behavior and 50% prolly more 55%-60%. This it not something we want incorporate when calculating the value since it depends on various config and situation. But 40% is good base with default config. - 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**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: Revision Co-authored-by: kadeshar --- conf/playerbots.conf.dist | 28 +++++++---- src/Bot/Engine/PlayerbotAIBase.cpp | 10 ++-- src/Bot/Engine/PlayerbotAIBase.h | 3 +- src/Bot/PlayerbotAI.cpp | 76 +++++++++++++++--------------- src/Bot/PlayerbotAI.h | 3 +- src/PlayerbotAIConfig.cpp | 5 +- src/PlayerbotAIConfig.h | 1 + 7 files changed, 72 insertions(+), 54 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 5df2b0a97..ff1b393b5 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -866,22 +866,32 @@ AiPlayerbot.ExcludedHunterPetFamilies = "" # #################################################################################################### - #################################################################################################### -# ACTIVITIES +# ACTIVITY # +# BotActiveAlone (%) +# - Determines the percentage of bots that remain active when no real players are nearby. +# - Default is 40% (which is practise kinda translates into 45-50%). +# - If `botActiveAloneSmartScale` is enabled, automatically temporarily down scale activity based on latency. +# - 40% will be activated in a random rotation for the amount of seconds specified in . +# - There are multiple conditions when bots will forced to be active e.g. when in BG/instance/attacked, some are configurable below. +#- When 100$ all bots will be active without any rotation or logic applied but comes with performance hit. # -# Specify percent of active bots -# The default is 100% but will be automatically adjusted if botActiveAloneSmartScale -# is enabled. Regardless, this value is only applied to inactive areas where no real players -# are detected. When real players are nearby, the value is always enforced to 100% -AiPlayerbot.BotActiveAlone = 100 +AiPlayerbot.BotActiveAlone = 10 +AiPlayerbot.BotActiveAloneDurationSeconds = 30 -# Force botActiveAlone when bot is within the specified distance of a real player +# Some additional rules that enforces the bot to be active +# +# - bot is within this distance from a real player. +# - bot is in the same zone as a real player. +# - bot is in the same continent as a real player. +# - bot is a real player's friend. +# - bot is in a real player's guild. +# AiPlayerbot.BotActiveAloneForceWhenInRadius = 150 AiPlayerbot.BotActiveAloneForceWhenInZone = 1 AiPlayerbot.BotActiveAloneForceWhenInMap = 0 -AiPlayerbot.BotActiveAloneForceWhenIsFriend = 1 +AiPlayerbot.BotActiveAloneForceWhenIsFriend = 0 AiPlayerbot.BotActiveAloneForceWhenInGuild = 1 # SmartScale (automatic scaling of percentage of active bots based on latency) diff --git a/src/Bot/Engine/PlayerbotAIBase.cpp b/src/Bot/Engine/PlayerbotAIBase.cpp index cf4ad172c..46e8c0bf5 100644 --- a/src/Bot/Engine/PlayerbotAIBase.cpp +++ b/src/Bot/Engine/PlayerbotAIBase.cpp @@ -25,7 +25,7 @@ void PlayerbotAIBase::UpdateAI(uint32 elapsed, bool minimal) return; UpdateAIInternal(elapsed, minimal); - YieldThread(); + YieldThread(nullptr); } void PlayerbotAIBase::SetNextCheckDelay(uint32 const delay) @@ -49,10 +49,14 @@ void PlayerbotAIBase::IncreaseNextCheckDelay(uint32 delay) bool PlayerbotAIBase::CanUpdateAI() { return nextAICheckDelay == 0; } -void PlayerbotAIBase::YieldThread(uint32 delay) +void PlayerbotAIBase::YieldThread(Player* bot, uint32 delay) { if (nextAICheckDelay < delay) - nextAICheckDelay = delay; + { + // Adding a deterministic per-bot slight offset (0–200 ms) to stagger updates and prevent cpu spikes. + uint32 offset = bot ? (bot->GetGUID().GetCounter() % 201) : 0; + nextAICheckDelay = delay + offset; + } } bool PlayerbotAIBase::IsActive() { return nextAICheckDelay < sPlayerbotAIConfig.maxWaitForMove; } diff --git a/src/Bot/Engine/PlayerbotAIBase.h b/src/Bot/Engine/PlayerbotAIBase.h index d0e0b775b..2d6ab31ce 100644 --- a/src/Bot/Engine/PlayerbotAIBase.h +++ b/src/Bot/Engine/PlayerbotAIBase.h @@ -8,6 +8,7 @@ #include "Define.h" #include "PlayerbotAIConfig.h" +#include "Player.h" class PlayerbotAIBase { @@ -17,7 +18,7 @@ public: bool CanUpdateAI(); void SetNextCheckDelay(uint32 const delay); void IncreaseNextCheckDelay(uint32 delay); - void YieldThread(uint32 delay = sPlayerbotAIConfig.reactDelay); + void YieldThread(Player* bot, uint32 delay = sPlayerbotAIConfig.reactDelay); virtual void UpdateAI(uint32 elapsed, bool minimal = false); virtual void UpdateAIInternal(uint32 elapsed, bool minimal = false) = 0; bool IsActive(); diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 446ca8c40..5ea8b3323 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -279,7 +279,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) if (spellTarget && !spellTarget->IsAlive() && !spellInfo->IsAllowingDeadTarget()) { InterruptSpell(); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } @@ -288,7 +288,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) if (goSpellTarget && !goSpellTarget->isSpawned()) { InterruptSpell(); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } @@ -320,7 +320,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) if (isHeal && isSingleTarget && spellTarget && spellTarget->IsFullHealth()) { InterruptSpell(); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } @@ -332,7 +332,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) } // Wait for spell cast - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } } @@ -368,7 +368,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) // Update internal AI UpdateAIInternal(elapsed, minimal); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); } // Helper function for UpdateAI to check group membership and handle removal if necessary @@ -4377,21 +4377,27 @@ Player* PlayerbotAI::GetGroupLeader() return master; } -uint32 PlayerbotAI::GetFixedBotNumer(uint32 maxNum, float cyclePerMin) +uint32 PlayerbotAI::GetFixedBotNumber(uint32 maxNum) { - uint32 randseed = rand32(); // Seed random number - uint32 randnum = bot->GetGUID().GetCounter() + randseed; // Semi-random but fixed number for each bot. + if (maxNum == 0) + return 0; - if (cyclePerMin > 0) - { - uint32 cycle = floor(getMSTime() / (1000)); // Semi-random number adds 1 each second. - cycle = cycle * cyclePerMin / 60; // Cycles cyclePerMin per minute. - randnum += cycle; // Make the random number cylce. - } + // Deterministic pseudo-random hash based on the bot GUID evenly distributed across active slots + uint32 id = bot->GetGUID().GetCounter(); + uint32 h = id; + h ^= h >> 16; + h *= 0x7feb352d; + h ^= h >> 15; + h *= 0x846ca68b; + h ^= h >> 16; - randnum = - (randnum % (maxNum + 1)); // Loops the randomnumber at maxNum. Bassically removes all the numbers above 99. - return randnum; // Now we have a number unique for each bot between 0 and maxNum that increases by cyclePerMin. + // Current time slot + uint32 timeSlot = (getMSTime() / 1000) / sPlayerbotAIConfig.BotActiveAloneDurationSeconds; + + // Mix timeSlot into the hash to reshuffle every rotation window + uint32 mixed = h ^ (timeSlot * 0x9e3779b9); // with multiplicative constant + + return mixed % maxNum; } /* @@ -4408,7 +4414,7 @@ enum GrouperType GrouperType PlayerbotAI::GetGrouperType() { - uint32 grouperNumber = GetFixedBotNumer(100, 0); + uint32 grouperNumber = GetFixedBotNumber(100); if (grouperNumber < 20 && !HasRealPlayerMaster()) return GrouperType::SOLO; @@ -4430,7 +4436,7 @@ GrouperType PlayerbotAI::GetGrouperType() GuilderType PlayerbotAI::GetGuilderType() { - uint32 grouperNumber = GetFixedBotNumer(100, 0); + uint32 grouperNumber = GetFixedBotNumber(100); if (grouperNumber < 20 && !HasRealPlayerMaster()) return GuilderType::SOLO; @@ -4754,44 +4760,40 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) // situations are usable for scaling when enabled. // ####################################################################################### - // Below is code to have a specified % of bots active at all times. - // The default is 100%. With 1% of all bots going active or inactive each minute. + // Base percentage of bots to be active uint32 mod = sPlayerbotAIConfig.botActiveAlone > 100 ? 100 : sPlayerbotAIConfig.botActiveAlone; + + // Apply SmartScale if enabled if (sPlayerbotAIConfig.botActiveAloneSmartScale && bot->GetLevel() >= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMinLevel && bot->GetLevel() <= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMaxLevel) { - mod = AutoScaleActivity(mod); + mod = AutoScaleActivity(mod); // mod reflects on latency throttling } - uint32 ActivityNumber = - GetFixedBotNumer(100, sPlayerbotAIConfig.botActiveAlone * static_cast(mod) / 100 * 0.01f); + // Get deterministic bucket + timeSlot + uint32 ActivityNumber = GetFixedBotNumber(100); - return ActivityNumber <= - (sPlayerbotAIConfig.botActiveAlone * mod) / - 100; // The given percentage of bots should be active and rotate 1% of those active bots each minute. + // Check if this bot is in the active set + return ActivityNumber < mod; // mod is directly the number of bots active (0–100) } bool PlayerbotAI::AllowActivity(ActivityType activityType, bool checkNow) { const int activityIndex = static_cast(activityType); - // Unknown/out-of-range avoid blocking, added logging for further analysing should not happen in the first place. - if (activityIndex <= 0 || activityIndex >= MAX_ACTIVITY_TYPE) - { - LOG_ERROR("playerbots", "AllowActivity received invalid activity type value: {}", activityIndex); - return true; - } - if (!allowActiveCheckTimer[activityIndex]) - allowActiveCheckTimer[activityIndex] = time(nullptr); + allowActiveCheckTimer[activityIndex] = getMSTime(); - if (!checkNow && time(nullptr) < (allowActiveCheckTimer[activityIndex] + 5)) + // 4500ms base + 0–499ms per-bot offset = 4500–4999ms, capping at just under 5 seconds + uint32 offset = bot->GetGUID().GetCounter() % 500; + + if (!checkNow && getMSTime() < (allowActiveCheckTimer[activityIndex] + 4500 + offset)) return allowActive[activityIndex]; const bool allowed = AllowActive(activityType); allowActive[activityIndex] = allowed; - allowActiveCheckTimer[activityIndex] = time(nullptr); + allowActiveCheckTimer[activityIndex] = getMSTime(); return allowed; } diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 9c417561f..1829e9175 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -541,8 +541,7 @@ public: // Checks if the bot is summoned as alt of a player bool IsAlt(); Player* GetGroupLeader(); - // Returns a semi-random (cycling) number that is fixed for each bot. - uint32 GetFixedBotNumer(uint32 maxNum = 100, float cyclePerMin = 1); + uint32 GetFixedBotNumber(uint32 maxNum = 100); GrouperType GetGrouperType(); GuilderType GetGuilderType(); bool HasPlayerNearby(WorldPosition* pos, float range = sPlayerbotAIConfig.reactDistance); diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 6a8d60129..f150f7af1 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -595,11 +595,12 @@ bool PlayerbotAIConfig::Initialize() randomBotHordeRatio = sConfigMgr->GetOption("AiPlayerbot.RandomBotHordeRatio", 50); disableDeathKnightLogin = sConfigMgr->GetOption("AiPlayerbot.DisableDeathKnightLogin", 0); limitTalentsExpansion = sConfigMgr->GetOption("AiPlayerbot.LimitTalentsExpansion", 0); - botActiveAlone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAlone", 100); + botActiveAlone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAlone", 10); + BotActiveAloneDurationSeconds = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneDurationSeconds", 30); BotActiveAloneForceWhenInRadius = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInRadius", 150); BotActiveAloneForceWhenInZone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInZone", 1); BotActiveAloneForceWhenInMap = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInMap", 0); - BotActiveAloneForceWhenIsFriend = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenIsFriend", 1); + BotActiveAloneForceWhenIsFriend = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenIsFriend", 0); BotActiveAloneForceWhenInGuild = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInGuild", 1); botActiveAloneSmartScale = sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScale", 1); botActiveAloneSmartScaleDiffLimitfloor = sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScaleDiffLimitfloor", 50); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 7b9b2fe81..7b6c1eb6f 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -334,6 +334,7 @@ public: bool disableDeathKnightLogin; bool limitTalentsExpansion; uint32 botActiveAlone; + uint32 BotActiveAloneDurationSeconds; uint32 BotActiveAloneForceWhenInRadius; bool BotActiveAloneForceWhenInZone; bool BotActiveAloneForceWhenInMap;