mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
feat(Performance): BotActiveAlone activity interval fixes and default settings for avg player (#2250)
<!--
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
- 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
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->
<!-- Please answer the following: -->
- 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
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->
## 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**)
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
<!--
Bot messages have to be translatable, but you don't need to do the
translations here. You only need to make sure
the message is in a translatable format, and list in the table the
message_key and the default English message.
Search for GetBotTextOrDefault in the codebase for examples.
-->
- Does this change add bot messages to translate?
- - [x] No
- - [ ] Yes (**list messages in the table**)
| Message key | Default message |
| --------------- | ------------------ |
| | |
| | |
## 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.
-->
## 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
<!-- Anything else that's helpful to review or test your pull request.
-->
---------
Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
Co-authored-by: Revision <tkn963@gmail.com>
Co-authored-by: kadeshar <kadeshar@gmail.com>
This commit is contained in:
parent
f76c286353
commit
c0390a24fd
@ -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 <BotActiveAloneDurationSeconds>.
|
||||
# - 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)
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<float>(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<int>(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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -595,11 +595,12 @@ bool PlayerbotAIConfig::Initialize()
|
||||
randomBotHordeRatio = sConfigMgr->GetOption<int32>("AiPlayerbot.RandomBotHordeRatio", 50);
|
||||
disableDeathKnightLogin = sConfigMgr->GetOption<bool>("AiPlayerbot.DisableDeathKnightLogin", 0);
|
||||
limitTalentsExpansion = sConfigMgr->GetOption<bool>("AiPlayerbot.LimitTalentsExpansion", 0);
|
||||
botActiveAlone = sConfigMgr->GetOption<int32>("AiPlayerbot.BotActiveAlone", 100);
|
||||
botActiveAlone = sConfigMgr->GetOption<int32>("AiPlayerbot.BotActiveAlone", 10);
|
||||
BotActiveAloneDurationSeconds = sConfigMgr->GetOption<int32>("AiPlayerbot.BotActiveAloneDurationSeconds", 30);
|
||||
BotActiveAloneForceWhenInRadius = sConfigMgr->GetOption<uint32>("AiPlayerbot.BotActiveAloneForceWhenInRadius", 150);
|
||||
BotActiveAloneForceWhenInZone = sConfigMgr->GetOption<bool>("AiPlayerbot.BotActiveAloneForceWhenInZone", 1);
|
||||
BotActiveAloneForceWhenInMap = sConfigMgr->GetOption<bool>("AiPlayerbot.BotActiveAloneForceWhenInMap", 0);
|
||||
BotActiveAloneForceWhenIsFriend = sConfigMgr->GetOption<bool>("AiPlayerbot.BotActiveAloneForceWhenIsFriend", 1);
|
||||
BotActiveAloneForceWhenIsFriend = sConfigMgr->GetOption<bool>("AiPlayerbot.BotActiveAloneForceWhenIsFriend", 0);
|
||||
BotActiveAloneForceWhenInGuild = sConfigMgr->GetOption<bool>("AiPlayerbot.BotActiveAloneForceWhenInGuild", 1);
|
||||
botActiveAloneSmartScale = sConfigMgr->GetOption<bool>("AiPlayerbot.botActiveAloneSmartScale", 1);
|
||||
botActiveAloneSmartScaleDiffLimitfloor = sConfigMgr->GetOption<uint32>("AiPlayerbot.botActiveAloneSmartScaleDiffLimitfloor", 50);
|
||||
|
||||
@ -334,6 +334,7 @@ public:
|
||||
bool disableDeathKnightLogin;
|
||||
bool limitTalentsExpansion;
|
||||
uint32 botActiveAlone;
|
||||
uint32 BotActiveAloneDurationSeconds;
|
||||
uint32 BotActiveAloneForceWhenInRadius;
|
||||
bool BotActiveAloneForceWhenInZone;
|
||||
bool BotActiveAloneForceWhenInMap;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user