Initial commit

This commit is contained in:
Dustin Hendrickson
2025-02-16 20:16:05 -08:00
commit 40de43afb0
20 changed files with 650 additions and 0 deletions

105
.gitattributes vendored Normal file
View File

@@ -0,0 +1,105 @@
## AUTO-DETECT
## Handle line endings automatically for files detected as
## text and leave all files detected as binary untouched.
## This will handle all files NOT defined below.
* text=auto eol=lf
# Text
*.conf text
*.conf.dist text
*.cmake text
## Scripts
*.sh text
*.fish text
*.lua text
## SQL
*.sql text
## C++
*.c text
*.cc text
*.cxx text
*.cpp text
*.c++ text
*.hpp text
*.h text
*.h++ text
*.hh text
## For documentation
# Documents
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
## DOCUMENTATION
*.markdown text
*.md text
*.mdwn text
*.mdown text
*.mkd text
*.mkdn text
*.mdtxt text
*.mdtext text
*.txt text
AUTHORS text
CHANGELOG text
CHANGES text
CONTRIBUTING text
COPYING text
copyright text
*COPYRIGHT* text
INSTALL text
license text
LICENSE text
NEWS text
readme text
*README* text
TODO text
## GRAPHICS
*.ai binary
*.bmp binary
*.eps binary
*.gif binary
*.ico binary
*.jng binary
*.jp2 binary
*.jpg binary
*.jpeg binary
*.jpx binary
*.jxr binary
*.pdf binary
*.png binary
*.psb binary
*.psd binary
*.svg text
*.svgz binary
*.tif binary
*.tiff binary
*.wbmp binary
*.webp binary
## ARCHIVES
*.7z binary
*.gz binary
*.jar binary
*.rar binary
*.tar binary
*.zip binary
## EXECUTABLES
*.exe binary
*.pyc binary

48
.gitignore vendored Normal file
View File

@@ -0,0 +1,48 @@
!.gitignore
#
#Generic
#
.directory
.mailmap
*.orig
*.rej
*.*~
.hg/
*.kdev*
.DS_Store
CMakeLists.txt.user
*.bak
*.patch
*.diff
*.REMOTE.*
*.BACKUP.*
*.BASE.*
*.LOCAL.*
#
# IDE & other softwares
#
/.settings/
/.externalToolBuilders/*
# exclude in all levels
nbproject/
.sync.ffs_db
*.kate-swp
#
# Eclipse
#
*.pydevproject
.metadata
.gradle
tmp/
*.tmp
*.swp
*~.nib
local.properties
.settings/
.loadpath
.project
.cproject

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 AzerothCore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

91
README.md Normal file
View File

@@ -0,0 +1,91 @@
# AzerothCore Module: Bot Level Brackets
==========================================
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.
Features
--------
**Configurable Level Brackets:**
Define eight distinct level brackets with customizable lower and upper bounds.
**Desired Percentage Distribution:**
Set target percentages for the number of bots within each level bracket.
**Dynamic Bot Adjustment:**
Automatically reassign bots from brackets with a surplus to those with a deficit.
**Auto Maintenance Execution:**
Executes the AutoMaintenanceOnLevelupAction after adjusting a bots level to ensure proper reinitialization.
**Equipment and Pet Reset:**
Destroys all equipped items and removes any pet during a level adjustment.
**Support for Random Bots:**
Applies exclusively to bots managed by RandomPlayerbotMgr.
**Debug Mode:**
Provides detailed logging to aid in monitoring and troubleshooting module operations.
Installation
------------
1. **Clone the Module**
Ensure the AzerothCore Playerbots fork is installed and running. Clone the module into your AzerothCore modules directory:
cd /path/to/azerothcore/modules
git clone https://github.com/DustinHendrickson/mod-bot-level-brackets.git
2. **Recompile AzerothCore**
Rebuild the project with the new module:
cd /path/to/azerothcore
mkdir build && cd build
cmake ..
make -j$(nproc)
3. **Configure the Module**
Rename the configuration file:
mv /path/to/azerothcore/modules/mod-bot-level-brackets.conf.dist /path/to/azerothcore/modules/mod-bot-level-brackets.conf
4. **Restart the Server**
Launch the world server:
./worldserver
Configuration Options
---------------------
Customize the modules behavior by editing the `mod-bot-level-brackets.conf` file:
Setting | Description | Default | Valid Values
-------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------- | --------------------
BotDistribution.DebugMode | Enables detailed debug logging for module operations. | 0 | 0 (off) / 1 (on)
BotDistribution.CheckFrequency | Frequency (in seconds) for performing the bot bracket distribution check. | 300 | Positive Integer
BotDistribution.Range1Pct | Desired percentage of bots in level bracket 1-10. | 14 | 0-100
BotDistribution.Range2Pct | Desired percentage of bots in level bracket 11-20. | 12 | 0-100
BotDistribution.Range3Pct | Desired percentage of bots in level bracket 21-30. | 12 | 0-100
BotDistribution.Range4Pct | Desired percentage of bots in level bracket 31-40. | 12 | 0-100
BotDistribution.Range5Pct | Desired percentage of bots in level bracket 41-50. | 12 | 0-100
BotDistribution.Range6Pct | Desired percentage of bots in level bracket 51-60. | 12 | 0-100
BotDistribution.Range7Pct | Desired percentage of bots in level bracket 61-70. | 12 | 0-100
BotDistribution.Range8Pct | Desired percentage of bots in level bracket 71-80. | 14 | 0-100
*Note: The sum of all bracket percentages must equal 100.*
Debugging
---------
To enable detailed debug logging, update the configuration file:
BotDistribution.DebugMode = 1
This setting outputs logs detailing bot level adjustments, item destruction, pet removal, and the execution of auto-maintenance actions.
License
-------
This module is released under the GNU GPL v2 license, consistent with AzerothCore's licensing model.
Contribution
------------
Created by Dustin Hendrickson.
Pull requests and issues are welcome. Please ensure that contributions adhere to AzerothCore's coding standards.

0
apps/.gitkeep Normal file
View File

0
apps/ci/.gitkeep Normal file
View File

40
apps/ci/ci-codestyle.sh Normal file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
set -e
echo "Codestyle check script:"
echo
declare -A singleLineRegexChecks=(
["LOG_.+GetCounter"]="Use ObjectGuid::ToString().c_str() method instead of ObjectGuid::GetCounter() when logging. Check the lines above"
["[[:blank:]]$"]="Remove whitespace at the end of the lines above"
["\t"]="Replace tabs with 4 spaces in the lines above"
)
for check in ${!singleLineRegexChecks[@]}; do
echo " Checking RegEx: '${check}'"
if grep -P -r -I -n ${check} src; then
echo
echo "${singleLineRegexChecks[$check]}"
exit 1
fi
done
declare -A multiLineRegexChecks=(
["LOG_[^;]+GetCounter"]="Use ObjectGuid::ToString().c_str() method instead of ObjectGuid::GetCounter() when logging. Check the lines above"
["\n\n\n"]="Multiple blank lines detected, keep only one. Check the files above"
)
for check in ${!multiLineRegexChecks[@]}; do
echo " Checking RegEx: '${check}'"
if grep -Pzo -r -I ${check} src; then
echo
echo
echo "${multiLineRegexChecks[$check]}"
exit 1
fi
done
echo
echo "Everything looks good"

0
conf/.gitkeep Normal file
View File

View File

@@ -0,0 +1,56 @@
[worldserver]
########################################
# mod-bot-level-distribution configuration
########################################
#
# BotDistribution.DebugMode
# Description: Enables debug logging for the Bot Level Distribution module.
# Default: 0 (disabled)
# Valid values: 0 (off) / 1 (on)
BotDistribution.DebugMode = 0
# BotDistribution.CheckFrequency
# Description: The frequency (in seconds) at which the bot level distribution check is performed.
# Default: 300
BotDistribution.CheckFrequency = 300
# BotDistribution.Range1Pct
# Description: Desired percentage of bots within level range 1-10.
# Default: 14
BotDistribution.Range1Pct = 14
# BotDistribution.Range2Pct
# Description: Desired percentage of bots within level range 11-20.
# Default: 12
BotDistribution.Range2Pct = 12
# BotDistribution.Range3Pct
# Description: Desired percentage of bots within level range 21-30.
# Default: 12
BotDistribution.Range3Pct = 12
# BotDistribution.Range4Pct
# Description: Desired percentage of bots within level range 31-40.
# Default: 12
BotDistribution.Range4Pct = 12
# BotDistribution.Range5Pct
# Description: Desired percentage of bots within level range 41-50.
# Default: 12
BotDistribution.Range5Pct = 12
# BotDistribution.Range6Pct
# Description: Desired percentage of bots within level range 51-60.
# Default: 12
BotDistribution.Range6Pct = 12
# BotDistribution.Range7Pct
# Description: Desired percentage of bots within level range 61-70.
# Default: 12
BotDistribution.Range7Pct = 12
# BotDistribution.Range8Pct
# Description: Desired percentage of bots within level range 71-80.
# Default: 14
BotDistribution.Range8Pct = 14

0
data/.gitkeep Normal file
View File

View File

View File

View File

View File

View File

View File

@@ -0,0 +1,4 @@
SET @ENTRY:=35410;
DELETE FROM `acore_string` WHERE `entry`=@ENTRY;
INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES
(@ENTRY, 'Hello World from Skeleton-Module!', '', '', '', '', '', '¡Hola Mundo desde Skeleton-Module!', '¡Hola Mundo desde Skeleton-Module!', '');

View File

0
include.sh Normal file
View File

View File

@@ -0,0 +1,278 @@
#include "ScriptMgr.h"
#include "Player.h"
#include "ObjectMgr.h"
#include "Chat.h"
#include "Log.h"
#include "PlayerbotAI.h"
#include "PlayerbotMgr.h"
#include "RandomPlayerbotMgr.h"
#include "Configuration/Config.h"
#include "AutoMaintenanceOnLevelupAction.h"
#include "Common.h"
#include <vector>
#include <cmath>
// -----------------------------------------------------------------------------
// LEVEL RANGE CONFIGURATION
// -----------------------------------------------------------------------------
struct LevelRangeConfig
{
uint8 lower; ///< Lower bound (inclusive)
uint8 upper; ///< Upper bound (inclusive)
uint8 desiredPercent;///< Desired percentage of bots in this range
};
static const uint8 NUM_RANGES = 8;
static LevelRangeConfig g_LevelRanges[NUM_RANGES];
static uint32 g_BotDistCheckFrequency = 300; // in seconds
static bool g_BotDistDebugMode = false;
// Loads the configuration from the config file.
// Expected keys (with example default percentages):
// BotDistribution.Range1Pct = 14
// BotDistribution.Range2Pct = 12
// BotDistribution.Range3Pct = 12
// BotDistribution.Range4Pct = 12
// BotDistribution.Range5Pct = 12
// BotDistribution.Range6Pct = 12
// BotDistribution.Range7Pct = 12
// BotDistribution.Range8Pct = 14
// Additionally:
// BotDistribution.CheckFrequency (in seconds)
// BotDistribution.DebugMode (true/false)
static void LoadBotDistributionConfig()
{
g_BotDistDebugMode = sConfigMgr->GetOption<bool>("BotDistribution.DebugMode", false);
g_BotDistCheckFrequency = sConfigMgr->GetOption<uint32>("BotDistribution.CheckFrequency", 60);
g_LevelRanges[0] = { 1, 10, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotDistribution.Range1Pct", 14)) };
g_LevelRanges[1] = { 11, 20, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotDistribution.Range2Pct", 12)) };
g_LevelRanges[2] = { 21, 30, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotDistribution.Range3Pct", 12)) };
g_LevelRanges[3] = { 31, 40, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotDistribution.Range4Pct", 12)) };
g_LevelRanges[4] = { 41, 50, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotDistribution.Range5Pct", 12)) };
g_LevelRanges[5] = { 51, 60, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotDistribution.Range6Pct", 12)) };
g_LevelRanges[6] = { 61, 70, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotDistribution.Range7Pct", 12)) };
g_LevelRanges[7] = { 71, 80, static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotDistribution.Range8Pct", 14)) };
uint32 totalPercent = 0;
for (uint8 i = 0; i < NUM_RANGES; ++i)
totalPercent += g_LevelRanges[i].desiredPercent;
if (totalPercent != 100)
LOG_ERROR("server.loading", "[BotLevelBrackets] Sum of percentages is {} (expected 100).", totalPercent);
}
// Returns the index of the level range that the given level belongs to.
// If the level does not fall within any configured range, returns -1.
static int GetLevelRangeIndex(uint8 level)
{
for (int i = 0; i < NUM_RANGES; ++i)
{
if (level >= g_LevelRanges[i].lower && level <= g_LevelRanges[i].upper)
return i;
}
return -1;
}
// Returns a random level within the provided range.
static uint8 GetRandomLevelInRange(const LevelRangeConfig& range)
{
return urand(range.lower, range.upper);
}
// Adjusts a bot's level by selecting a random level within the target range.
// In addition to setting the new level and resetting XP, this function:
// - 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)
return;
uint8 newLevel = GetRandomLevelInRange(g_LevelRanges[targetRangeIndex]);
bot->SetLevel(newLevel);
bot->SetUInt32Value(PLAYER_XP, 0);
// Inform the bot (or player) about the level 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)
{
PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot);
std::string playerClassName = botAI ? botAI->GetChatHelper()->FormatClass(bot->getClass()) : "Unknown";
LOG_INFO("server.loading", "[BotLevelBrackets] AdjustBotToRange: Bot '{}' - {} adjusted to level {} (target range {}-{}).",
bot->GetName(), playerClassName, newLevel, g_LevelRanges[targetRangeIndex].lower, g_LevelRanges[targetRangeIndex].upper);
}
// Execute the maintenance action.
PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot);
if (botAI)
{
AutoMaintenanceOnLevelupAction maintenanceAction(botAI);
maintenanceAction.Execute(Event());
if (g_BotDistDebugMode)
LOG_INFO("server.loading", "[BotLevelBrackets] AdjustBotToRange: AutoMaintenanceOnLevelupAction executed for bot '{}'.", bot->GetName());
}
else
{
LOG_ERROR("server.loading", "[BotLevelBrackets] AdjustBotToRange: Failed to retrieve PlayerbotAI for bot '{}'.", bot->GetName());
}
}
// -----------------------------------------------------------------------------
// BOT DETECTION HELPERS
// -----------------------------------------------------------------------------
static bool IsPlayerBot(Player* player)
{
if (!player)
return false;
PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(player);
return botAI && botAI->IsBotAI();
}
static bool IsPlayerRandomBot(Player* player)
{
if (!player)
return false;
return sRandomPlayerbotMgr->IsRandomBot(player);
}
// -----------------------------------------------------------------------------
// WORLD SCRIPT: Bot Level Distribution
// -----------------------------------------------------------------------------
class BotLevelBracketsWorldScript : public WorldScript
{
public:
BotLevelBracketsWorldScript() : WorldScript("BotLevelBracketsWorldScript"), m_timer(0) { }
// On server startup, load the configuration and log the settings (if debug mode is enabled).
void OnStartup() override
{
LoadBotDistributionConfig();
if (g_BotDistDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Module loaded. Check frequency: {} seconds.", g_BotDistCheckFrequency);
for (uint8 i = 0; i < NUM_RANGES; ++i)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Range {}: {}-{}, Desired Percentage: {}%",
i + 1, g_LevelRanges[i].lower, g_LevelRanges[i].upper, g_LevelRanges[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
{
m_timer += diff;
if (m_timer < g_BotDistCheckFrequency * 1000)
return;
m_timer = 0;
// Build the current distribution for bots.
uint32 totalBots = 0;
int actualCounts[NUM_RANGES] = {0};
std::vector<Player*> botsByRange[NUM_RANGES];
auto const& allPlayers = ObjectAccessor::GetPlayers();
for (auto const& itr : allPlayers)
{
Player* player = itr.second;
if (!player || !player->IsInWorld())
continue;
if (!IsPlayerBot(player) || !IsPlayerRandomBot(player))
continue;
totalBots++;
int rangeIndex = GetLevelRangeIndex(player->GetLevel());
if (rangeIndex >= 0)
{
actualCounts[rangeIndex]++;
botsByRange[rangeIndex].push_back(player);
}
else if (g_BotDistDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Bot '{}' with level {} does not fall into any defined range.",
player->GetName(), player->GetLevel());
}
}
if (totalBots == 0)
return;
// 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));
if (g_BotDistDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Range {} ({}-{}): Desired = {}, Actual = {}.",
i + 1, g_LevelRanges[i].lower, g_LevelRanges[i].upper,
desiredCounts[i], actualCounts[i]);
}
}
// For each range that has a surplus, reassign bots to ranges that are underpopulated.
for (int i = 0; i < NUM_RANGES; ++i)
{
while (actualCounts[i] > desiredCounts[i] && !botsByRange[i].empty())
{
// Locate a target range with a deficit.
int targetRange = -1;
for (int j = 0; j < NUM_RANGES; ++j)
{
if (actualCounts[j] < desiredCounts[j])
{
targetRange = j;
break;
}
}
if (targetRange == -1)
break; // No underpopulated range found.
// Retrieve one bot from the current (overpopulated) range.
Player* bot = botsByRange[i].back();
botsByRange[i].pop_back();
// Adjust its level to a random level within the target range and perform cleanup.
AdjustBotToRange(bot, targetRange);
actualCounts[i]--;
actualCounts[targetRange]++;
}
}
if (g_BotDistDebugMode)
LOG_INFO("server.loading", "[BotLevelBrackets] Distribution adjustment complete. Total bots: {}.", totalBots);
}
private:
uint32 m_timer;
};
// -----------------------------------------------------------------------------
// ENTRY POINT: Register the Bot Level Distribution Module
// -----------------------------------------------------------------------------
void Addmod_player_bot_level_bracketsScripts()
{
new BotLevelBracketsWorldScript();
}

View File

@@ -0,0 +1,7 @@
#ifndef MOD_BOT_LEVEL_BRACKETS_H
#define MOD_BOT_LEVEL_BRACKETS_H
// Registers the Bot Level Brackets module scripts.
void Addmod_bot_level_bracketsScripts();
#endif // MOD_BOT_LEVEL_BRACKETS_H