Merge pull request #12 from DustinHendrickson/Dustin/SafetyChanges

Adding Safety checks, refactoring to flag when not enough safe bots h…
This commit is contained in:
Dustin Hendrickson
2025-02-18 14:53:14 -08:00
committed by GitHub
3 changed files with 608 additions and 197 deletions

106
README.md
View File

@@ -1,44 +1,45 @@
# AzerothCore Module: Bot Level Brackets # AzerothCore Module: Bot Level Brackets
========================================== --------
Overview Overview
-------- --------
The Bot Level Brackets module for AzerothCore ensures an even spread of player bots across configurable level ranges (brackets). It periodically monitors bot levels and automatically adjusts them by transferring bots from overpopulated brackets to those with a deficit. During adjustments, bot levels are reset, equipped items are destroyed, pets are removed, and auto-maintenance actions are executed. The Bot Level Brackets module for AzerothCore ensures an even spread of player bots across configurable level ranges (brackets). It periodically monitors bot levels and automatically adjusts them by transferring bots from overpopulated brackets to those with a deficit. During adjustments, bot levels are reset, equipped items are destroyed, trade skills are removed, quests are cleared, active auras are removed, pets are dismissed, and auto-maintenance actions are executed. Bots that are not immediately safe for level reset (for example, those in combat or engaged in other activities) are flagged for pending adjustment and processed later when they become safe. Additionally, Death Knight bots are safeguarded to never be assigned a level below 55.
Features Features
-------- --------
**Configurable Level Brackets:** - **Configurable Level Brackets:**
Define nine distinct level brackets with customizable lower and upper bounds: Define nine distinct level brackets with configurable lower and upper bounds:
- 1-9 - 1-9
- 10-19 - 10-19
- 20-29 - 20-29
- 30-39 - 30-39
- 40-49 - 40-49
- 50-59 - 50-59
- 60-69 - 60-69
- 70-79 - 70-79
- 80 - 80
**Desired Percentage Distribution:** - **Faction-Specific Configuration:**
Set target percentages for the number of bots within each level bracket. Separate configurations for Alliance and Horde bots allow individual control over desired bot percentages within each bracket.
**Dynamic Bot Adjustment:** - **Desired Percentage Distribution:**
Automatically reassign bots from brackets with a surplus to those with a deficit. Target percentages can be set for the number of bots within each level bracket. The sum of percentages for each faction must equal 100.
**Death Knight Level Safeguard:** - **Dynamic Bot Adjustment:**
Ensures that bots of the Death Knight class are never assigned a level below 55, guaranteeing they only appear in higher brackets. Bots in overpopulated brackets are automatically adjusted to a random level within a bracket with a deficit. Adjustments include resetting XP, removing equipped items, trade skills, learned spells, quests, and active auras, and dismissing pets.
**Auto Maintenance Execution:** - **Death Knight Level Safeguard:**
Executes the AutoMaintenanceOnLevelupAction after adjusting a bots level to ensure proper reinitialization. Bots of the Death Knight class are enforced a minimum level of 55, ensuring they are only assigned to higher brackets.
**Equipment and Pet Reset:** - **Automated Maintenance Execution:**
Destroys all equipped items and removes any pet during a level adjustment. After a level change, the module executes the AutoMaintenanceOnLevelupAction to properly reinitialize the bots state.
**Support for Random Bots:** - **Support for Random Bots:**
Applies exclusively to bots managed by RandomPlayerbotMgr. The module applies exclusively to bots managed by RandomPlayerbotMgr.
**Debug Mode:** - **Debug Mode:**
Provides detailed logging to aid in monitoring and troubleshooting module operations. An optional debug mode provides detailed logging for monitoring bot adjustments and troubleshooting module operations.
Installation Installation
------------ ------------
@@ -68,23 +69,44 @@ Installation
Configuration Options Configuration Options
--------------------- ---------------------
Customize the modules behavior by editing the `mod_player_bot_level_brackets.conf` file: Customize the modules behavior by editing the `mod_player_bot_level_brackets.conf` file. The configuration options are separated for Alliance and Horde bots:
Setting | Description | Default | Valid Values ### Global Settings
-------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------- | --------------------
BotLevelBrackets.DebugMode | Enables detailed debug logging for module operations. | 0 | 0 (off) / 1 (on)
BotLevelBrackets.CheckFrequency | Frequency (in seconds) for performing the bot bracket distribution check. | 300 | Positive Integer
BotLevelBrackets.Range1Pct | Desired percentage of bots in level bracket 1-9. | 11 | 0-100
BotLevelBrackets.Range2Pct | Desired percentage of bots in level bracket 10-19. | 11 | 0-100
BotLevelBrackets.Range3Pct | Desired percentage of bots in level bracket 20-29. | 11 | 0-100
BotLevelBrackets.Range4Pct | Desired percentage of bots in level bracket 30-39. | 11 | 0-100
BotLevelBrackets.Range5Pct | Desired percentage of bots in level bracket 40-49. | 11 | 0-100
BotLevelBrackets.Range6Pct | Desired percentage of bots in level bracket 50-59. | 11 | 0-100
BotLevelBrackets.Range7Pct | Desired percentage of bots in level bracket 60-69. | 11 | 0-100
BotLevelBrackets.Range8Pct | Desired percentage of bots in level bracket 70-79. | 11 | 0-100
BotLevelBrackets.Range9Pct | Desired percentage of bots in level bracket 80. | 12 | 0-100
*Note: The sum of all bracket percentages must equal 100.* Setting | Description | Default | Valid Values
--------------------------------|----------------------------------------------------------------------------------------------|---------|--------------------
BotLevelBrackets.DebugMode | Enables detailed debug logging for module operations. | 0 | 0 (off) / 1 (on)
BotLevelBrackets.CheckFrequency | Frequency (in seconds) for performing the bot bracket distribution check. | 300 | Positive Integer
### Alliance Level Brackets Configuration
*The percentages below must sum to 100.*
Setting | Description | Default | Valid Values
-------------------------------------------|--------------------------------------------------------------|---------|--------------------
BotLevelBrackets.Alliance.Range1Pct | Desired percentage of Alliance bots within level range 1-9. | 11 | 0-100
BotLevelBrackets.Alliance.Range2Pct | Desired percentage of Alliance bots within level range 10-19. | 11 | 0-100
BotLevelBrackets.Alliance.Range3Pct | Desired percentage of Alliance bots within level range 20-29. | 11 | 0-100
BotLevelBrackets.Alliance.Range4Pct | Desired percentage of Alliance bots within level range 30-39. | 11 | 0-100
BotLevelBrackets.Alliance.Range5Pct | Desired percentage of Alliance bots within level range 40-49. | 11 | 0-100
BotLevelBrackets.Alliance.Range6Pct | Desired percentage of Alliance bots within level range 50-59. | 11 | 0-100
BotLevelBrackets.Alliance.Range7Pct | Desired percentage of Alliance bots within level range 60-69. | 11 | 0-100
BotLevelBrackets.Alliance.Range8Pct | Desired percentage of Alliance bots within level range 70-79. | 11 | 0-100
BotLevelBrackets.Alliance.Range9Pct | Desired percentage of Alliance bots at level 80. | 12 | 0-100
### Horde Level Brackets Configuration
*The percentages below must sum to 100.*
Setting | Description | Default | Valid Values
---------------------------------------|-----------------------------------------------------------|---------|--------------------
BotLevelBrackets.Horde.Range1Pct | Desired percentage of Horde bots within level range 1-9. | 11 | 0-100
BotLevelBrackets.Horde.Range2Pct | Desired percentage of Horde bots within level range 10-19.| 11 | 0-100
BotLevelBrackets.Horde.Range3Pct | Desired percentage of Horde bots within level range 20-29.| 11 | 0-100
BotLevelBrackets.Horde.Range4Pct | Desired percentage of Horde bots within level range 30-39.| 11 | 0-100
BotLevelBrackets.Horde.Range5Pct | Desired percentage of Horde bots within level range 40-49.| 11 | 0-100
BotLevelBrackets.Horde.Range6Pct | Desired percentage of Horde bots within level range 50-59.| 11 | 0-100
BotLevelBrackets.Horde.Range7Pct | Desired percentage of Horde bots within level range 60-69.| 11 | 0-100
BotLevelBrackets.Horde.Range8Pct | Desired percentage of Horde bots within level range 70-79.| 11 | 0-100
BotLevelBrackets.Horde.Range9Pct | Desired percentage of Horde bots at level 80. | 12 | 0-100
Debugging Debugging
--------- ---------

View File

@@ -15,47 +15,100 @@ BotLevelBrackets.DebugMode = 0
# Default: 300 # Default: 300
BotLevelBrackets.CheckFrequency = 300 BotLevelBrackets.CheckFrequency = 300
# BotLevelBrackets.Range1Pct #
# Description: Desired percentage of bots within level range 1-9. # Alliance Level Brackets Configuration
# The percentages below must sum to 100.
#
# BotLevelBrackets.Alliance.Range1Pct
# Description: Desired percentage of Alliance bots within level range 1-9.
# Default: 11 # Default: 11
BotLevelBrackets.Range1Pct = 11 BotLevelBrackets.Alliance.Range1Pct = 11
# BotLevelBrackets.Range2Pct # BotLevelBrackets.Alliance.Range2Pct
# Description: Desired percentage of bots within level range 10-19. # Description: Desired percentage of Alliance bots within level range 10-19.
# Default: 11 # Default: 11
BotLevelBrackets.Range2Pct = 11 BotLevelBrackets.Alliance.Range2Pct = 11
# BotLevelBrackets.Range3Pct # BotLevelBrackets.Alliance.Range3Pct
# Description: Desired percentage of bots within level range 20-29. # Description: Desired percentage of Alliance bots within level range 20-29.
# Default: 11 # Default: 11
BotLevelBrackets.Range3Pct = 11 BotLevelBrackets.Alliance.Range3Pct = 11
# BotLevelBrackets.Range4Pct # BotLevelBrackets.Alliance.Range4Pct
# Description: Desired percentage of bots within level range 30-39. # Description: Desired percentage of Alliance bots within level range 30-39.
# Default: 11 # Default: 11
BotLevelBrackets.Range4Pct = 11 BotLevelBrackets.Alliance.Range4Pct = 11
# BotLevelBrackets.Range5Pct # BotLevelBrackets.Alliance.Range5Pct
# Description: Desired percentage of bots within level range 40-49. # Description: Desired percentage of Alliance bots within level range 40-49.
# Default: 11 # Default: 11
BotLevelBrackets.Range5Pct = 11 BotLevelBrackets.Alliance.Range5Pct = 11
# BotLevelBrackets.Range6Pct # BotLevelBrackets.Alliance.Range6Pct
# Description: Desired percentage of bots within level range 50-59. # Description: Desired percentage of Alliance bots within level range 50-59.
# Default: 11 # Default: 11
BotLevelBrackets.Range6Pct = 11 BotLevelBrackets.Alliance.Range6Pct = 11
# BotLevelBrackets.Range7Pct # BotLevelBrackets.Alliance.Range7Pct
# Description: Desired percentage of bots within level range 60-69. # Description: Desired percentage of Alliance bots within level range 60-69.
# Default: 11 # Default: 11
BotLevelBrackets.Range7Pct = 11 BotLevelBrackets.Alliance.Range7Pct = 11
# BotLevelBrackets.Range8Pct # BotLevelBrackets.Alliance.Range8Pct
# Description: Desired percentage of bots within level range 70-79. # Description: Desired percentage of Alliance bots within level range 70-79.
# Default: 11 # Default: 11
BotLevelBrackets.Range8Pct = 11 BotLevelBrackets.Alliance.Range8Pct = 11
# BotLevelBrackets.Range9Pct # BotLevelBrackets.Alliance.Range9Pct
# Description: Desired percentage of bots within level range 80. # Description: Desired percentage of Alliance bots within level range 80.
# Default: 12 # Default: 12
BotLevelBrackets.Range9Pct = 12 BotLevelBrackets.Alliance.Range9Pct = 12
#
# Horde Level Brackets Configuration
# The percentages below must sum to 100.
#
# BotLevelBrackets.Horde.Range1Pct
# Description: Desired percentage of Horde bots within level range 1-9.
# Default: 11
BotLevelBrackets.Horde.Range1Pct = 11
# BotLevelBrackets.Horde.Range2Pct
# Description: Desired percentage of Horde bots within level range 10-19.
# Default: 11
BotLevelBrackets.Horde.Range2Pct = 11
# BotLevelBrackets.Horde.Range3Pct
# Description: Desired percentage of Horde bots within level range 20-29.
# Default: 11
BotLevelBrackets.Horde.Range3Pct = 11
# BotLevelBrackets.Horde.Range4Pct
# Description: Desired percentage of Horde bots within level range 30-39.
# Default: 11
BotLevelBrackets.Horde.Range4Pct = 11
# BotLevelBrackets.Horde.Range5Pct
# Description: Desired percentage of Horde bots within level range 40-49.
# Default: 11
BotLevelBrackets.Horde.Range5Pct = 11
# BotLevelBrackets.Horde.Range6Pct
# Description: Desired percentage of Horde bots within level range 50-59.
# Default: 11
BotLevelBrackets.Horde.Range6Pct = 11
# BotLevelBrackets.Horde.Range7Pct
# Description: Desired percentage of Horde bots within level range 60-69.
# Default: 11
BotLevelBrackets.Horde.Range7Pct = 11
# BotLevelBrackets.Horde.Range8Pct
# Description: Desired percentage of Horde bots within level range 70-79.
# Default: 11
BotLevelBrackets.Horde.Range8Pct = 11
# BotLevelBrackets.Horde.Range9Pct
# Description: Desired percentage of Horde bots within level range 80.
# Default: 12
BotLevelBrackets.Horde.Range9Pct = 12

View File

@@ -11,10 +11,22 @@
#include "Common.h" #include "Common.h"
#include <vector> #include <vector>
#include <cmath> #include <cmath>
#include <utility>
#include "PlayerbotFactory.h"
void RemoveAllEquippedItems(Player* bot);
void RemoveAllTradeSkills(Player* bot);
void RemoveAllLearnedSpells(Player* bot);
void RemoveAllQuests(Player* bot);
void RemoveAllActiveAuras(Player* bot);
static bool IsAlliancePlayerBot(Player* bot);
static bool IsHordePlayerBot(Player* bot);
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// LEVEL RANGE CONFIGURATION // LEVEL RANGE CONFIGURATION
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Same boundaries for both factions; only desired percentages differ.
struct LevelRangeConfig struct LevelRangeConfig
{ {
uint8 lower; ///< Lower bound (inclusive) uint8 lower; ///< Lower bound (inclusive)
@@ -23,54 +35,64 @@ struct LevelRangeConfig
}; };
static const uint8 NUM_RANGES = 9; static const uint8 NUM_RANGES = 9;
static LevelRangeConfig g_LevelRanges[NUM_RANGES];
// Separate arrays for Alliance and Horde.
static LevelRangeConfig g_AllianceLevelRanges[NUM_RANGES];
static LevelRangeConfig g_HordeLevelRanges[NUM_RANGES];
static uint32 g_BotDistCheckFrequency = 300; // in seconds static uint32 g_BotDistCheckFrequency = 300; // in seconds
static bool g_BotDistDebugMode = false; static bool g_BotDistDebugMode = false;
// Loads the configuration from the config file. // Loads the configuration from the config file.
// Expected keys (with example default percentages):
// BotLevelBrackets.Range1Pct = 11
// BotLevelBrackets.Range2Pct = 11
// BotLevelBrackets.Range3Pct = 11
// BotLevelBrackets.Range4Pct = 11
// BotLevelBrackets.Range5Pct = 11
// BotLevelBrackets.Range6Pct = 11
// BotLevelBrackets.Range7Pct = 11
// BotLevelBrackets.Range8Pct = 11
// BotLevelBrackets.Range9Pct = 12
// Additionally:
// BotLevelBrackets.CheckFrequency (in seconds)
// BotLevelBrackets.DebugMode (true/false)
static void LoadBotLevelBracketsConfig() static void LoadBotLevelBracketsConfig()
{ {
g_BotDistDebugMode = sConfigMgr->GetOption<bool>("BotLevelBrackets.DebugMode", false); g_BotDistDebugMode = sConfigMgr->GetOption<bool>("BotLevelBrackets.DebugMode", false);
g_BotDistCheckFrequency = sConfigMgr->GetOption<uint32>("BotLevelBrackets.CheckFrequency", 60); g_BotDistCheckFrequency = sConfigMgr->GetOption<uint32>("BotLevelBrackets.CheckFrequency", 300);
g_LevelRanges[0] = { 1, 9, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Range1Pct", 11)) }; // Alliance configuration.
g_LevelRanges[1] = { 10, 19, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Range2Pct", 11)) }; g_AllianceLevelRanges[0] = { 1, 9, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Alliance.Range1Pct", 11)) };
g_LevelRanges[2] = { 20, 29, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Range3Pct", 11)) }; g_AllianceLevelRanges[1] = { 10, 19, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Alliance.Range2Pct", 11)) };
g_LevelRanges[3] = { 30, 39, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Range4Pct", 11)) }; g_AllianceLevelRanges[2] = { 20, 29, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Alliance.Range3Pct", 11)) };
g_LevelRanges[4] = { 40, 49, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Range5Pct", 11)) }; g_AllianceLevelRanges[3] = { 30, 39, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Alliance.Range4Pct", 11)) };
g_LevelRanges[5] = { 50, 59, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Range6Pct", 11)) }; g_AllianceLevelRanges[4] = { 40, 49, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Alliance.Range5Pct", 11)) };
g_LevelRanges[6] = { 60, 69, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Range7Pct", 11)) }; g_AllianceLevelRanges[5] = { 50, 59, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Alliance.Range6Pct", 11)) };
g_LevelRanges[7] = { 70, 79, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Range8Pct", 11)) }; g_AllianceLevelRanges[6] = { 60, 69, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Alliance.Range7Pct", 11)) };
g_LevelRanges[8] = { 80, 80, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Range9Pct", 12)) }; g_AllianceLevelRanges[7] = { 70, 79, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Alliance.Range8Pct", 11)) };
g_AllianceLevelRanges[8] = { 80, 80, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Alliance.Range9Pct", 12)) };
uint32 totalPercent = 0; // Horde configuration.
g_HordeLevelRanges[0] = { 1, 9, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Horde.Range1Pct", 11)) };
g_HordeLevelRanges[1] = { 10, 19, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Horde.Range2Pct", 11)) };
g_HordeLevelRanges[2] = { 20, 29, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Horde.Range3Pct", 11)) };
g_HordeLevelRanges[3] = { 30, 39, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Horde.Range4Pct", 11)) };
g_HordeLevelRanges[4] = { 40, 49, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Horde.Range5Pct", 11)) };
g_HordeLevelRanges[5] = { 50, 59, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Horde.Range6Pct", 11)) };
g_HordeLevelRanges[6] = { 60, 69, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Horde.Range7Pct", 11)) };
g_HordeLevelRanges[7] = { 70, 79, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Horde.Range8Pct", 11)) };
g_HordeLevelRanges[8] = { 80, 80, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Horde.Range9Pct", 12)) };
uint32 totalAlliancePercent = 0;
uint32 totalHordePercent = 0;
for (uint8 i = 0; i < NUM_RANGES; ++i) for (uint8 i = 0; i < NUM_RANGES; ++i)
totalPercent += g_LevelRanges[i].desiredPercent; {
if (totalPercent != 100) totalAlliancePercent += g_AllianceLevelRanges[i].desiredPercent;
LOG_ERROR("server.loading", "[BotLevelBrackets] Sum of percentages is {} (expected 100).", totalPercent); totalHordePercent += g_HordeLevelRanges[i].desiredPercent;
}
if (totalAlliancePercent != 100)
LOG_ERROR("server.loading", "[BotLevelBrackets] Alliance: Sum of percentages is {} (expected 100).", totalAlliancePercent);
if (totalHordePercent != 100)
LOG_ERROR("server.loading", "[BotLevelBrackets] Horde: Sum of percentages is {} (expected 100).", totalHordePercent);
} }
// Returns the index of the level range that the given level belongs to. // Returns the index of the level range that the given level belongs to (boundaries are the same for both factions).
// If the level does not fall within any configured range, returns -1.
static int GetLevelRangeIndex(uint8 level) static int GetLevelRangeIndex(uint8 level)
{ {
// Here we assume that all ranges share the same lower and upper bounds.
// (If not, you may need to adjust this based on faction.)
for (int i = 0; i < NUM_RANGES; ++i) for (int i = 0; i < NUM_RANGES; ++i)
{ {
if (level >= g_LevelRanges[i].lower && level <= g_LevelRanges[i].upper) // Use Alliance boundaries as reference.
if (level >= g_AllianceLevelRanges[i].lower && level <= g_AllianceLevelRanges[i].upper)
return i; return i;
} }
return -1; return -1;
@@ -83,85 +105,143 @@ static uint8 GetRandomLevelInRange(const LevelRangeConfig& range)
} }
// Adjusts a bot's level by selecting a random level within the target range. // Adjusts a bot's level by selecting a random level within the target range.
// For Death Knight bots, the new level will not be set below 55. // Also resets XP, destroys equipped items, removes the pet, and executes maintenance.
// In addition to setting the new level and resetting XP, this function: static void AdjustBotToRange(Player* bot, int targetRangeIndex, const LevelRangeConfig* factionRanges)
// - Sends a system message indicating the reset.
// - Destroys all equipped items.
// - Removes the pet if present.
// - Executes the auto maintenance action.
static void AdjustBotToRange(Player* bot, int targetRangeIndex)
{ {
if (!bot || targetRangeIndex < 0 || targetRangeIndex >= NUM_RANGES) if (!bot || targetRangeIndex < 0 || targetRangeIndex >= NUM_RANGES)
return; return;
uint8 botOriginalLevel = bot->GetLevel(); PlayerbotFactory factory(bot, bot->GetLevel());
if (bot->IsMounted())
bot->Dismount();
bot->resetTalents(true);
RemoveAllEquippedItems(bot);
RemoveAllTradeSkills(bot);
RemoveAllLearnedSpells(bot);
RemoveAllQuests(bot);
RemoveAllActiveAuras(bot);
if (bot->GetPet())
bot->RemovePet(bot->GetPet(), PET_SAVE_NOT_IN_SLOT, false);
uint8 botOriginalLevel = bot->GetLevel();
uint8 newLevel = 0; uint8 newLevel = 0;
// If the bot is a Death Knight, ensure level is not set below 55. // For Death Knight bots, enforce a minimum level of 55.
if (bot->getClass() == CLASS_DEATH_KNIGHT) if (bot->getClass() == CLASS_DEATH_KNIGHT)
{ {
uint8 lowerBound = g_LevelRanges[targetRangeIndex].lower; uint8 lowerBound = factionRanges[targetRangeIndex].lower;
uint8 upperBound = g_LevelRanges[targetRangeIndex].upper; uint8 upperBound = factionRanges[targetRangeIndex].upper;
if (upperBound < 55) if (upperBound < 55)
{ {
// This target range is invalid for Death Knights.
if (g_BotDistDebugMode) if (g_BotDistDebugMode)
{ {
LOG_INFO("server.loading", "[BotLevelBrackets] AdjustBotToRange: Cannot assign Death Knight '{}' ({}) to range {}-{} (below level 55).", std::string playerFaction = IsAlliancePlayerBot(bot) ? "Alliance" : "Horde";
bot->GetName(), botOriginalLevel, lowerBound, upperBound); LOG_INFO("server.loading",
"[BotLevelBrackets] AdjustBotToRange: Cannot assign {} Death Knight '{}' ({}) to range {}-{} (below level 55).",
playerFaction, bot->GetName(), botOriginalLevel, lowerBound, upperBound);
} }
return; return;
} }
// Adjust lower bound to 55 if necessary.
if (lowerBound < 55) if (lowerBound < 55)
lowerBound = 55; lowerBound = 55;
newLevel = urand(lowerBound, upperBound); newLevel = urand(lowerBound, upperBound);
} }
else else
{ {
newLevel = GetRandomLevelInRange(g_LevelRanges[targetRangeIndex]); newLevel = GetRandomLevelInRange(factionRanges[targetRangeIndex]);
} }
bot->SetLevel(newLevel); bot->SetLevel(newLevel);
bot->SetUInt32Value(PLAYER_XP, 0); bot->SetUInt32Value(PLAYER_XP, 0);
// Inform the bot about the level reset.
ChatHandler(bot->GetSession()).SendSysMessage("[mod-bot-level-brackets] Your level has been reset."); ChatHandler(bot->GetSession()).SendSysMessage("[mod-bot-level-brackets] Your level has been reset.");
// Destroy equipped items.
for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot)
{
if (Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot))
{
std::string itemName = item->GetTemplate()->Name1;
bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true);
}
}
// Remove the pet if present.
if (bot->GetPet())
bot->RemovePet(nullptr, PET_SAVE_NOT_IN_SLOT, false);
if (g_BotDistDebugMode) if (g_BotDistDebugMode)
{ {
PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot); PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot);
std::string playerClassName = botAI ? botAI->GetChatHelper()->FormatClass(bot->getClass()) : "Unknown"; std::string playerClassName = botAI ? botAI->GetChatHelper()->FormatClass(bot->getClass()) : "Unknown";
LOG_INFO("server.loading", "[BotLevelBrackets] AdjustBotToRange: Bot '{}' - {} ({}) adjusted to level {} (target range {}-{}).", std::string playerFaction = IsAlliancePlayerBot(bot) ? "Alliance" : "Horde";
bot->GetName(), playerClassName, botOriginalLevel, newLevel, g_LevelRanges[targetRangeIndex].lower, g_LevelRanges[targetRangeIndex].upper); LOG_INFO("server.loading",
"[BotLevelBrackets] AdjustBotToRange: {} Bot '{}' - {} ({}) adjusted to level {} (target range {}-{}).",
playerFaction, bot->GetName(), playerClassName.c_str(), botOriginalLevel, newLevel,
factionRanges[targetRangeIndex].lower, factionRanges[targetRangeIndex].upper);
} }
// Execute the maintenance action. bot->InitStatsForLevel();
PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot); PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot);
if (botAI) if (botAI)
{ {
AutoMaintenanceOnLevelupAction maintenanceAction(botAI); AutoMaintenanceOnLevelupAction maintenanceAction(botAI);
maintenanceAction.Execute(Event()); maintenanceAction.Execute(Event());
if (g_BotDistDebugMode) if (g_BotDistDebugMode)
LOG_INFO("server.loading", "[BotLevelBrackets] AdjustBotToRange: AutoMaintenanceOnLevelupAction executed for bot '{}'.", bot->GetName()); LOG_INFO("server.loading",
"[BotLevelBrackets] AdjustBotToRange: AutoMaintenanceOnLevelupAction executed for bot '{}'.", bot->GetName());
} }
else else
{ {
LOG_ERROR("server.loading", "[BotLevelBrackets] AdjustBotToRange: Failed to retrieve PlayerbotAI for bot '{}'.", bot->GetName()); LOG_ERROR("server.loading",
"[BotLevelBrackets] AdjustBotToRange: Failed to retrieve PlayerbotAI for bot '{}'.", bot->GetName());
}
}
// -----------------------------------------------------------------------------
// BOT INTERFACE HELPERS
// -----------------------------------------------------------------------------
void RemoveAllLearnedSpells(Player* bot)
{
bot->RemoveAllSpellCooldown();
std::vector<uint32> spellsToRemove;
for (const auto& spellPair : bot->GetSpellMap())
{
const uint32 spellId = spellPair.first;
const PlayerSpellState state = spellPair.second->State;
if (state != PLAYERSPELL_REMOVED)
spellsToRemove.push_back(spellId);
}
for (uint32 spellId : spellsToRemove)
bot->removeSpell(spellId, SPEC_MASK_ALL, false);
}
void RemoveAllQuests(Player* bot)
{
for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (questId)
bot->SetQuestSlot(slot, 0);
}
CharacterDatabase.Execute("DELETE FROM character_queststatus WHERE guid = {}", bot->GetGUID().GetCounter());
}
void RemoveAllActiveAuras(Player* bot)
{
bot->RemoveAllAuras();
}
void RemoveAllEquippedItems(Player* bot)
{
for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot)
{
if (Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot))
bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true);
}
}
void RemoveAllTradeSkills(Player* bot)
{
static const uint32 tradeSkills[] = {
SKILL_ALCHEMY, SKILL_BLACKSMITHING, SKILL_COOKING, SKILL_ENCHANTING,
SKILL_ENGINEERING, SKILL_FIRST_AID, SKILL_FISHING, SKILL_HERBALISM,
SKILL_JEWELCRAFTING, SKILL_LEATHERWORKING, SKILL_MINING, SKILL_SKINNING,
SKILL_TAILORING
};
for (auto skill : tradeSkills)
{
if (bot->HasSkill(skill))
bot->SetSkill(skill, 0, 0, 0);
} }
} }
@@ -172,7 +252,6 @@ static bool IsPlayerBot(Player* player)
{ {
if (!player) if (!player)
return false; return false;
PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(player); PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(player);
return botAI && botAI->IsBotAI(); return botAI && botAI->IsBotAI();
} }
@@ -181,19 +260,85 @@ static bool IsPlayerRandomBot(Player* player)
{ {
if (!player) if (!player)
return false; return false;
return sRandomPlayerbotMgr->IsRandomBot(player); return sRandomPlayerbotMgr->IsRandomBot(player);
} }
// Helper functions to determine faction. Adjust these functions based on your implementation.
static bool IsAlliancePlayerBot(Player* bot)
{
// Assumes GetTeam() returns TEAM_ALLIANCE for Alliance bots.
return bot && (bot->GetTeamId() == TEAM_ALLIANCE);
}
static bool IsHordePlayerBot(Player* bot)
{
// Assumes GetTeam() returns TEAM_HORDE for Horde bots.
return bot && (bot->GetTeamId() == TEAM_HORDE);
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// WORLD SCRIPT: Bot Level Distribution // SAFETY CHECKS FOR LEVEL RESET
// -----------------------------------------------------------------------------
static bool IsBotSafeForLevelReset(Player* bot)
{
if (!bot)
return false;
if (!bot->IsInWorld())
return false;
if (!bot->IsAlive())
return false;
if (bot->IsInCombat())
return false;
if (bot->InBattleground() || bot->InArena() || bot->inRandomLfgDungeon() || bot->InBattlegroundQueue())
return false;
if (bot->IsInFlight())
return false;
if (Group* group = bot->GetGroup())
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (member && !IsPlayerBot(member))
return false;
}
}
return true;
}
// Global container to hold bots flagged for pending level reset.
// Each entry is a pair: the bot and the target level range index along with a pointer to the faction config.
struct PendingResetEntry
{
Player* bot;
int targetRange;
const LevelRangeConfig* factionRanges;
};
static std::vector<PendingResetEntry> g_PendingLevelResets;
static void ProcessPendingLevelResets()
{
for (auto it = g_PendingLevelResets.begin(); it != g_PendingLevelResets.end(); )
{
Player* bot = it->bot;
int targetRange = it->targetRange;
if (bot && bot->IsInWorld() && IsBotSafeForLevelReset(bot))
{
AdjustBotToRange(bot, targetRange, it->factionRanges);
it = g_PendingLevelResets.erase(it);
}
else
++it;
}
}
// -----------------------------------------------------------------------------
// WORLD SCRIPT: Bot Level Distribution with Faction Separation
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
class BotLevelBracketsWorldScript : public WorldScript class BotLevelBracketsWorldScript : public WorldScript
{ {
public: public:
BotLevelBracketsWorldScript() : WorldScript("BotLevelBracketsWorldScript"), m_timer(0) { } BotLevelBracketsWorldScript() : WorldScript("BotLevelBracketsWorldScript"), m_timer(0) { }
// On server startup, load the configuration and log the settings (if debug mode is enabled).
void OnStartup() override void OnStartup() override
{ {
LoadBotLevelBracketsConfig(); LoadBotLevelBracketsConfig();
@@ -202,14 +347,14 @@ public:
LOG_INFO("server.loading", "[BotLevelBrackets] Module loaded. Check frequency: {} seconds.", g_BotDistCheckFrequency); LOG_INFO("server.loading", "[BotLevelBrackets] Module loaded. Check frequency: {} seconds.", g_BotDistCheckFrequency);
for (uint8 i = 0; i < NUM_RANGES; ++i) for (uint8 i = 0; i < NUM_RANGES; ++i)
{ {
LOG_INFO("server.loading", "[BotLevelBrackets] Range {}: {}-{}, Desired Percentage: {}%", LOG_INFO("server.loading", "[BotLevelBrackets] Alliance Range {}: {}-{}, Desired Percentage: {}%",
i + 1, g_LevelRanges[i].lower, g_LevelRanges[i].upper, g_LevelRanges[i].desiredPercent); i + 1, g_AllianceLevelRanges[i].lower, g_AllianceLevelRanges[i].upper, g_AllianceLevelRanges[i].desiredPercent);
LOG_INFO("server.loading", "[BotLevelBrackets] Horde Range {}: {}-{}, Desired Percentage: {}%",
i + 1, g_HordeLevelRanges[i].lower, g_HordeLevelRanges[i].upper, g_HordeLevelRanges[i].desiredPercent);
} }
} }
} }
// Periodically (every g_BotDistCheckFrequency seconds) check the distribution of bot levels
// and adjust bots from overpopulated ranges to underpopulated ranges.
void OnUpdate(uint32 diff) override void OnUpdate(uint32 diff) override
{ {
m_timer += diff; m_timer += diff;
@@ -217,11 +362,19 @@ public:
return; return;
m_timer = 0; m_timer = 0;
// Build the current distribution for bots. ProcessPendingLevelResets();
uint32 totalBots = 0;
int actualCounts[NUM_RANGES] = {0};
std::vector<Player*> botsByRange[NUM_RANGES];
// Containers for Alliance bots.
uint32 totalAllianceBots = 0;
int allianceActualCounts[NUM_RANGES] = {0};
std::vector<Player*> allianceBotsByRange[NUM_RANGES];
// Containers for Horde bots.
uint32 totalHordeBots = 0;
int hordeActualCounts[NUM_RANGES] = {0};
std::vector<Player*> hordeBotsByRange[NUM_RANGES];
// Iterate only over player bots.
auto const& allPlayers = ObjectAccessor::GetPlayers(); auto const& allPlayers = ObjectAccessor::GetPlayers();
for (auto const& itr : allPlayers) for (auto const& itr : allPlayers)
{ {
@@ -231,80 +384,263 @@ public:
if (!IsPlayerBot(player) || !IsPlayerRandomBot(player)) if (!IsPlayerBot(player) || !IsPlayerRandomBot(player))
continue; continue;
totalBots++; if (IsAlliancePlayerBot(player))
int rangeIndex = GetLevelRangeIndex(player->GetLevel());
if (rangeIndex >= 0)
{ {
actualCounts[rangeIndex]++; totalAllianceBots++;
botsByRange[rangeIndex].push_back(player); int rangeIndex = GetLevelRangeIndex(player->GetLevel());
if (rangeIndex >= 0)
{
allianceActualCounts[rangeIndex]++;
allianceBotsByRange[rangeIndex].push_back(player);
}
else if (g_BotDistDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Alliance bot '{}' with level {} does not fall into any defined range.",
player->GetName(), player->GetLevel());
}
} }
else if (g_BotDistDebugMode) else if (IsHordePlayerBot(player))
{ {
LOG_INFO("server.loading", "[BotLevelBrackets] Bot '{}' with level {} does not fall into any defined range.", totalHordeBots++;
player->GetName(), player->GetLevel()); int rangeIndex = GetLevelRangeIndex(player->GetLevel());
if (rangeIndex >= 0)
{
hordeActualCounts[rangeIndex]++;
hordeBotsByRange[rangeIndex].push_back(player);
}
else if (g_BotDistDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Horde bot '{}' with level {} does not fall into any defined range.",
player->GetName(), player->GetLevel());
}
} }
} }
if (totalBots == 0) // Process Alliance bots.
return; if (totalAllianceBots > 0)
// Compute the desired count for each range.
int desiredCounts[NUM_RANGES] = {0};
for (int i = 0; i < NUM_RANGES; ++i)
{ {
desiredCounts[i] = static_cast<int>(round((g_LevelRanges[i].desiredPercent / 100.0) * totalBots)); int allianceDesiredCounts[NUM_RANGES] = {0};
if (g_BotDistDebugMode) for (int i = 0; i < NUM_RANGES; ++i)
{ {
LOG_INFO("server.loading", "[BotLevelBrackets] Range {} ({}-{}): Desired = {}, Actual = {}.", allianceDesiredCounts[i] = static_cast<int>(round((g_AllianceLevelRanges[i].desiredPercent / 100.0) * totalAllianceBots));
i + 1, g_LevelRanges[i].lower, g_LevelRanges[i].upper, if (g_BotDistDebugMode)
desiredCounts[i], actualCounts[i]); {
LOG_INFO("server.loading", "[BotLevelBrackets] Alliance Range {} ({}-{}): Desired = {}, Actual = {}.",
i + 1, g_AllianceLevelRanges[i].lower, g_AllianceLevelRanges[i].upper,
allianceDesiredCounts[i], allianceActualCounts[i]);
}
}
// Adjust overpopulated ranges.
for (int i = 0; i < NUM_RANGES; ++i)
{
std::vector<Player*> safeBots;
std::vector<Player*> flaggedBots;
for (Player* bot : allianceBotsByRange[i])
{
if (IsBotSafeForLevelReset(bot))
safeBots.push_back(bot);
else
flaggedBots.push_back(bot);
}
while (allianceActualCounts[i] > allianceDesiredCounts[i] && !safeBots.empty())
{
Player* bot = safeBots.back();
safeBots.pop_back();
int targetRange = -1;
if (bot->getClass() == CLASS_DEATH_KNIGHT)
{
for (int j = 0; j < NUM_RANGES; ++j)
{
if (allianceActualCounts[j] < allianceDesiredCounts[j] && g_AllianceLevelRanges[j].upper >= 55)
{
targetRange = j;
break;
}
}
}
else
{
for (int j = 0; j < NUM_RANGES; ++j)
{
if (allianceActualCounts[j] < allianceDesiredCounts[j])
{
targetRange = j;
break;
}
}
}
if (targetRange == -1)
break;
AdjustBotToRange(bot, targetRange, g_AllianceLevelRanges);
allianceActualCounts[i]--;
allianceActualCounts[targetRange]++;
}
while (allianceActualCounts[i] > allianceDesiredCounts[i] && !flaggedBots.empty())
{
Player* bot = flaggedBots.back();
flaggedBots.pop_back();
int targetRange = -1;
if (bot->getClass() == CLASS_DEATH_KNIGHT)
{
for (int j = 0; j < NUM_RANGES; ++j)
{
if (allianceActualCounts[j] < allianceDesiredCounts[j] && g_AllianceLevelRanges[j].upper >= 55)
{
targetRange = j;
break;
}
}
}
else
{
for (int j = 0; j < NUM_RANGES; ++j)
{
if (allianceActualCounts[j] < allianceDesiredCounts[j])
{
targetRange = j;
break;
}
}
}
if (targetRange == -1)
break;
// Flag for pending reset.
bool alreadyFlagged = false;
for (auto& entry : g_PendingLevelResets)
{
if (entry.bot == bot)
{
alreadyFlagged = true;
break;
}
}
if (!alreadyFlagged)
{
g_PendingLevelResets.push_back({bot, targetRange, g_AllianceLevelRanges});
if (g_BotDistDebugMode)
LOG_INFO("server.loading", "[BotLevelBrackets] Alliance bot '{}' flagged for pending level reset to range {}-{}.",
bot->GetName(), g_AllianceLevelRanges[targetRange].lower, g_AllianceLevelRanges[targetRange].upper);
}
allianceActualCounts[i]--;
allianceActualCounts[targetRange]++;
}
} }
} }
// For each range that has a surplus, reassign bots to ranges that are underpopulated. // Process Horde bots.
for (int i = 0; i < NUM_RANGES; ++i) if (totalHordeBots > 0)
{ {
while (actualCounts[i] > desiredCounts[i] && !botsByRange[i].empty()) int hordeDesiredCounts[NUM_RANGES] = {0};
for (int i = 0; i < NUM_RANGES; ++i)
{ {
Player* bot = botsByRange[i].back(); hordeDesiredCounts[i] = static_cast<int>(round((g_HordeLevelRanges[i].desiredPercent / 100.0) * totalHordeBots));
botsByRange[i].pop_back(); if (g_BotDistDebugMode)
int targetRange = -1;
// For Death Knights, only consider target ranges where the upper bound is at least 55.
if (bot->getClass() == CLASS_DEATH_KNIGHT)
{ {
for (int j = 0; j < NUM_RANGES; ++j) LOG_INFO("server.loading", "[BotLevelBrackets] Horde Range {} ({}-{}): Desired = {}, Actual = {}.",
i + 1, g_HordeLevelRanges[i].lower, g_HordeLevelRanges[i].upper,
hordeDesiredCounts[i], hordeActualCounts[i]);
}
}
for (int i = 0; i < NUM_RANGES; ++i)
{
std::vector<Player*> safeBots;
std::vector<Player*> flaggedBots;
for (Player* bot : hordeBotsByRange[i])
{
if (IsBotSafeForLevelReset(bot))
safeBots.push_back(bot);
else
flaggedBots.push_back(bot);
}
while (hordeActualCounts[i] > hordeDesiredCounts[i] && !safeBots.empty())
{
Player* bot = safeBots.back();
safeBots.pop_back();
int targetRange = -1;
if (bot->getClass() == CLASS_DEATH_KNIGHT)
{ {
if (actualCounts[j] < desiredCounts[j] && g_LevelRanges[j].upper >= 55) for (int j = 0; j < NUM_RANGES; ++j)
{ {
targetRange = j; if (hordeActualCounts[j] < hordeDesiredCounts[j] && g_HordeLevelRanges[j].upper >= 55)
{
targetRange = j;
break;
}
}
}
else
{
for (int j = 0; j < NUM_RANGES; ++j)
{
if (hordeActualCounts[j] < hordeDesiredCounts[j])
{
targetRange = j;
break;
}
}
}
if (targetRange == -1)
break;
AdjustBotToRange(bot, targetRange, g_HordeLevelRanges);
hordeActualCounts[i]--;
hordeActualCounts[targetRange]++;
}
while (hordeActualCounts[i] > hordeDesiredCounts[i] && !flaggedBots.empty())
{
Player* bot = flaggedBots.back();
flaggedBots.pop_back();
int targetRange = -1;
if (bot->getClass() == CLASS_DEATH_KNIGHT)
{
for (int j = 0; j < NUM_RANGES; ++j)
{
if (hordeActualCounts[j] < hordeDesiredCounts[j] && g_HordeLevelRanges[j].upper >= 55)
{
targetRange = j;
break;
}
}
}
else
{
for (int j = 0; j < NUM_RANGES; ++j)
{
if (hordeActualCounts[j] < hordeDesiredCounts[j])
{
targetRange = j;
break;
}
}
}
if (targetRange == -1)
break;
bool alreadyFlagged = false;
for (auto& entry : g_PendingLevelResets)
{
if (entry.bot == bot)
{
alreadyFlagged = true;
break; break;
} }
} }
} if (!alreadyFlagged)
else
{
for (int j = 0; j < NUM_RANGES; ++j)
{ {
if (actualCounts[j] < desiredCounts[j]) g_PendingLevelResets.push_back({bot, targetRange, g_HordeLevelRanges});
{ if (g_BotDistDebugMode)
targetRange = j; LOG_INFO("server.loading", "[BotLevelBrackets] Horde bot '{}' flagged for pending level reset to range {}-{}.",
break; bot->GetName(), g_HordeLevelRanges[targetRange].lower, g_HordeLevelRanges[targetRange].upper);
}
} }
hordeActualCounts[i]--;
hordeActualCounts[targetRange]++;
} }
if (targetRange == -1)
break; // No appropriate underpopulated range found.
AdjustBotToRange(bot, targetRange);
actualCounts[i]--;
actualCounts[targetRange]++;
} }
} }
if (g_BotDistDebugMode) if (g_BotDistDebugMode)
LOG_INFO("server.loading", "[BotLevelBrackets] Distribution adjustment complete. Total bots: {}.", totalBots); {
LOG_INFO("server.loading", "[BotLevelBrackets] Distribution adjustment complete. Alliance bots: {}, Horde bots: {}.",
totalAllianceBots, totalHordeBots);
}
} }
private: private: