38 Commits

Author SHA1 Message Date
Dustin Hendrickson
3f64862503 Merge pull request #76 from DustinHendrickson/Dustin/BugFixes
Adding in world check
2025-12-11 10:06:45 -06:00
Dustin Hendrickson
6a33cb3257 Adding in world check 2025-12-11 10:06:18 -06:00
Dustin Hendrickson
a926792857 Merge pull request #75 from DustinHendrickson/Dustin/BugFixes
Dustin/bug fixes
2025-12-11 09:57:24 -06:00
Dustin Hendrickson
1faa24cf15 Adding in world check 2025-12-11 09:34:18 -06:00
Dustin Hendrickson
11dc0941fd Adding group check 2025-12-11 09:29:02 -06:00
Dustin Hendrickson
6152f9e77e Merge pull request #73 from DustinHendrickson/Dustin/BugFixes
Fixing a DK edgecase
2025-12-04 16:07:23 -06:00
Dustin Hendrickson
a33f0ca354 Fixing a DK edgecase 2025-12-04 16:05:06 -06:00
Dustin Hendrickson
10c47670cf Merge pull request #71 from DustinHendrickson/Dustin/BugFixes
Dustin/bug fixes
2025-11-16 10:37:03 -06:00
Dustin Hendrickson
d3ab2c899f Adding Arena Team exclusion, on by default 2025-11-16 10:32:58 -06:00
Dustin Hendrickson
d45213c360 Fixing persistance esge case issue 2025-11-16 10:22:20 -06:00
Dustin Hendrickson
93989a902f Merge pull request #66 from DustinHendrickson/Dustin/FixesAndReload
Dustin/fixes and reload
2025-10-18 14:48:12 -05:00
Dustin Hendrickson
0d78babe57 Cleanup 2025-10-18 14:45:13 -05:00
Dustin Hendrickson
82d83f64a4 Urand fix 2025-10-18 14:42:22 -05:00
Dustin Hendrickson
ba34ee7908 Urand fix 2025-10-18 14:36:06 -05:00
Dustin Hendrickson
4205db2b0f Urand fix 2025-10-18 14:29:40 -05:00
Dustin Hendrickson
db5499ee72 Urand fix 2025-10-18 14:26:51 -05:00
Dustin Hendrickson
d2e3ad166c Urand fix 2025-10-18 14:22:16 -05:00
Dustin Hendrickson
e8bf099b03 Urand fix 2025-10-18 14:14:25 -05:00
Dustin Hendrickson
74fb4876f5 Adding reload command 2025-10-18 14:09:37 -05:00
Dustin Hendrickson
8669a4dbcb Adding some fixes 2025-10-18 14:08:01 -05:00
Dustin Hendrickson
a5eb4c6855 Merge pull request #63 from DustinHendrickson/revert-62-feat/ignore_arena_team
Revert "feat: ignore arena team for downgrade"

Players reporting this broke processing. Noticed some weird stuff when I re-reviewed it. Reverting.
2025-10-11 22:22:20 -05:00
Dustin Hendrickson
af1ee91c17 Revert "feat: ignore arena team for downgrade" 2025-10-11 22:21:15 -05:00
Dustin Hendrickson
827ae6ac12 Merge pull request #62 from jl178/feat/ignore_arena_team
feat: ignore arena team for downgrade
2025-10-03 09:13:51 -05:00
Jered Little
402556fc36 feat: ignore arena team for downgrade 2025-10-02 09:23:31 -06:00
Dustin Hendrickson
4d6e972f01 Merge pull request #59 from DustinHendrickson/Dustin/GuildFixes
Dustin/guild fixes
2025-07-31 20:28:15 -07:00
Dustin Hendrickson
d39fd3dc6f Fixes for the guild stuff 2025-07-31 20:22:00 -07:00
Dustin Hendrickson
a5227128ea Fixes for the guild stuff 2025-07-31 20:00:43 -07:00
Dustin Hendrickson
e4e27846f5 Fixes for the guild stuff 2025-07-31 19:57:11 -07:00
Dustin Hendrickson
49b6c1b586 Merge pull request #58 from DustinHendrickson/Dustin/GuildWork
Fixing some issues with tables
2025-07-31 17:37:39 -07:00
Dustin Hendrickson
81ada34e4a Fixing some issues with tables 2025-07-31 17:37:22 -07:00
Dustin Hendrickson
c34da488f5 Merge pull request #57 from DustinHendrickson/Dustin/GuildWork
Fixing some issues with tables
2025-07-31 17:35:23 -07:00
Dustin Hendrickson
6e7c2ff273 Fixing some issues with tables 2025-07-31 17:34:56 -07:00
Dustin Hendrickson
2127d281a6 Merge pull request #56 from DustinHendrickson/Dustin/GuildWork
Fixing some issues with tables
2025-07-31 17:25:38 -07:00
Dustin Hendrickson
5c79ed4cb1 Fixing some issues with tables 2025-07-31 17:24:14 -07:00
Dustin Hendrickson
d47770584b Merge pull request #55 from DustinHendrickson/Dustin/GuildWork
Updating readme and conf
2025-07-31 16:31:24 -07:00
Dustin Hendrickson
9d8e55e0e2 Updating readme and conf 2025-07-31 16:30:56 -07:00
Dustin Hendrickson
ea93ce96fa Merge pull request #54 from DustinHendrickson/Dustin/GuildWork
Addiing support for offline real palyer guild exclusion
2025-07-31 16:24:16 -07:00
Dustin Hendrickson
89fd5268c9 Addiing support for offline real palyer guild exclusion 2025-07-31 16:21:20 -07:00
4 changed files with 415 additions and 49 deletions

View File

@@ -21,7 +21,7 @@ Features
- **Death Knight Level Safeguard:** - **Death Knight Level Safeguard:**
Death Knight bots are enforced a minimum level of 55. Death Knight bots are enforced a minimum level of 55.
- **Guild Bot Exclusion:** - **Guild Bot Exclusion:**
When enabled, bots that are in a guild with at least one real (non-bot) player online are excluded from bot bracket calculations and will not be adjusted. When enabled, bots that are in a guild with at least one real (non-bot) player are excluded from bot bracket calculations and will not be adjusted. This feature now uses persistent database tracking to work for both online and offline real players.
- **Friend List Exclusion:** - **Friend List Exclusion:**
When enabled, bots that are on real players' friend lists are excluded from level bracket adjustments. When enabled, bots that are on real players' friend lists are excluded from level bracket adjustments.
- **Dynamic Distribution:** - **Dynamic Distribution:**
@@ -79,11 +79,12 @@ BotLevelBrackets.LiteDebugMode | Enables lite debug logging for th
BotLevelBrackets.CheckFrequency | Frequency (in seconds) at which the bot level distribution check is performed. | 300 | Positive Integer BotLevelBrackets.CheckFrequency | Frequency (in seconds) at which the bot level distribution check is performed. | 300 | Positive Integer
BotLevelBrackets.CheckFlaggedFrequency | Frequency (in seconds) at which the bot level reset is performed for flagged bots that initially failed safety checks. | 15 | Positive Integer BotLevelBrackets.CheckFlaggedFrequency | Frequency (in seconds) at which the bot level reset is performed for flagged bots that initially failed safety checks. | 15 | Positive Integer
BotLevelBrackets.FlaggedProcessLimit | Maximum number of flagged bots to process per pending level change step. | 5 | Positive Integer BotLevelBrackets.FlaggedProcessLimit | Maximum number of flagged bots to process per pending level change step. | 5 | Positive Integer
BotLevelBrackets.Dynamic.UseDynamicDistribution | Enables dynamic bot distribution: when on, brackets with more real players get a higher share of bots, based on the weight below. | 0 | 0 (off) / 1 (on) BotLevelBrackets.Dynamic.UseDynamicDistribution | Enables dynamic bot distribution: when on, brackets with more real players get a higher share of bots in their level bracket, based on the weight below. | 0 | 0 (off) / 1 (on)
BotLevelBrackets.Dynamic.RealPlayerWeight | Controls how much bots "follow" real player activity when dynamic distribution is enabled. 0.0 = bots always spread evenly; 1.0 = mild effect; higher values = more bots go where players are, but the effect is scaled. | 1.0 | ≥ 0.0 (float) BotLevelBrackets.Dynamic.RealPlayerWeight | Controls how much bots "follow" real player activity when dynamic distribution is enabled. 0.0 = bots always spread evenly; 1.0 = minimal effect; 10.0 = heavy effect; higher values = more bots go where players are, but the effect is scaled. | 1.0 | ≥ 0.0 (float)
BotLevelBrackets.Dynamic.SyncFactions | Enables synchronized brackets and weighting between Alliance and Horde factions when Dynamic Distribution is also enabled. | 0 | 0 (off) / 1 (on) BotLevelBrackets.Dynamic.SyncFactions | Enables synchronized brackets and weighting between Alliance and Horde factions when Dynamic Distribution is also enabled. | 0 | 0 (off) / 1 (on)
BotLevelBrackets.IgnoreFriendListed | Ignores bots that are on real players' friend lists from any bracket calculations. | 1 | 0 (off) / 1 (on) BotLevelBrackets.IgnoreFriendListed | Ignores bots that are on real players' friend lists from any bracket calculations. | 1 | 0 (off) / 1 (on)
BotLevelBrackets.IgnoreGuildBotsWithRealPlayers | Excludes bots in a guild with at least one real (non-bot) player online from adjustments. | 1 | 0 (disabled) / 1 (enabled) BotLevelBrackets.IgnoreGuildBotsWithRealPlayers | Excludes bots in a guild with at least one real (non-bot) player from adjustments. Uses persistent database tracking for both online and offline real players. | 1 | 0 (disabled) / 1 (enabled)
BotLevelBrackets.GuildTrackerUpdateFrequency | Frequency (in seconds) at which the persistent guild tracker database is updated to track guilds with real players. | 600 | Positive Integer
BotLevelBrackets.NumRanges | Number of level brackets used for bot distribution. Both factions must have the same number defined. | 9 | Positive Integer BotLevelBrackets.NumRanges | Number of level brackets used for bot distribution. Both factions must have the same number defined. | 9 | Positive Integer
BotLevelBrackets.ExcludeNames | Comma-separated list of case insensitive bot names to exclude from all bracket checks. | | String BotLevelBrackets.ExcludeNames | Comma-separated list of case insensitive bot names to exclude from all bracket checks. | | String

View File

@@ -44,12 +44,29 @@ BotLevelBrackets.FlaggedProcessLimit = 5
# #
# BotLevelBrackets.IgnoreGuildBotsWithRealPlayers # BotLevelBrackets.IgnoreGuildBotsWithRealPlayers
# Description: When enabled, bots that are in a guild with at least one real (non-bot) player online are excluded # Description: When enabled, bots that are in a guild with at least one real (non-bot) player are excluded
# from bot bracket calculations and will not be level changed or flagged. # from bot bracket calculations and will not be level changed or flagged.
# This now works for both online and offline real players using persistent database tracking.
# Default: 1 (enabled) # Default: 1 (enabled)
# Valid values: 0 (disabled) / 1 (enabled) # Valid values: 0 (disabled) / 1 (enabled)
BotLevelBrackets.IgnoreGuildBotsWithRealPlayers = 1 BotLevelBrackets.IgnoreGuildBotsWithRealPlayers = 1
#
# BotLevelBrackets.IgnoreArenaTeamBots
# Description: When enabled, bots that are members of arena teams are excluded from bot bracket calculations
# and will not be level changed or flagged. This prevents bots in arena teams from being
# changed, which would break team compositions.
# Default: 1 (enabled)
# Valid values: 0 (disabled) / 1 (enabled)
BotLevelBrackets.IgnoreArenaTeamBots = 1
#
# BotLevelBrackets.GuildTrackerUpdateFrequency
# Description: The frequency (in seconds) at which the persistent guild tracker database is updated.
# This tracks which guilds have real players even when they are offline.
# Default: 600 (10 minutes)
BotLevelBrackets.GuildTrackerUpdateFrequency = 600
# #
# BotLevelBrackets.IgnoreFriendListed # BotLevelBrackets.IgnoreFriendListed
# Description: Ignore bots that are on real players friend's lists from any brackets. # Description: Ignore bots that are on real players friend's lists from any brackets.
@@ -77,7 +94,7 @@ BotLevelBrackets.NumRanges = 9
# #
# BotLevelBrackets.Dynamic.UseDynamicDistribution # BotLevelBrackets.Dynamic.UseDynamicDistribution
# Description: Enables dynamic recalculation of bot distribution percentages based on the number of non-bot players # Description: Enables dynamic recalculation of bot distribution percentages based on the number of non-bot players
# present in each level bracket. # present in each level bracket. This overrides any custom brackets in the conf.
# Default: 0 (disabled) # Default: 0 (disabled)
# Valid values: 0 (off) / 1 (on) # Valid values: 0 (off) / 1 (on)
BotLevelBrackets.Dynamic.UseDynamicDistribution = 0 BotLevelBrackets.Dynamic.UseDynamicDistribution = 0
@@ -87,7 +104,7 @@ BotLevelBrackets.Dynamic.UseDynamicDistribution = 0
# The higher you set this value, the more bots will move to the same level brackets where real players are found, but the effect is *gentle*, not extreme. # The higher you set this value, the more bots will move to the same level brackets where real players are found, but the effect is *gentle*, not extreme.
# A value of 0.0 means bots always distribute evenly across all brackets, regardless of where players are. The default value of 1.0 gives a mild, balanced effect. # A value of 0.0 means bots always distribute evenly across all brackets, regardless of where players are. The default value of 1.0 gives a mild, balanced effect.
# Raising this to 3.0, 5.0, or higher will make bots concentrate more in brackets with real players. # Raising this to 3.0, 5.0, or higher will make bots concentrate more in brackets with real players.
# The value is a multiplier (not a percent): 0.0 = no extra effect, 1.0 = default, 3.0 = stronger, 5.0 = strong but not extreme. # The value is a multiplier (not a percent): 0.0 = no extra effect, 1.0 = minimal, 3.0 = stronger, 5.0 = strong but not extreme.
# Experiment based on your total bot count and real player counts to find a good number for your server. # Experiment based on your total bot count and real player counts to find a good number for your server.
# If you want a large congestion of bots in your level bracket for solo play I recommend 10-15 for RealPlayerWeight. # If you want a large congestion of bots in your level bracket for solo play I recommend 10-15 for RealPlayerWeight.
# What to expect: # What to expect:

View File

@@ -0,0 +1,12 @@
-- Bot Level Brackets Guild Tracker Table
-- This table tracks guilds that have real (non-bot) players to prevent bot level changes
-- when real players are in the guild, even when they are offline.
DROP TABLE IF EXISTS `bot_level_brackets_guild_tracker`;
CREATE TABLE `bot_level_brackets_guild_tracker` (
`guild_id` int(10) unsigned NOT NULL COMMENT 'Guild ID from guild table',
`has_real_players` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Whether this guild has real (non-bot) players',
`last_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Last time this record was updated',
PRIMARY KEY (`guild_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Tracks guilds with real players for bot level bracket restrictions';

View File

@@ -2,6 +2,7 @@
#include "Player.h" #include "Player.h"
#include "ObjectMgr.h" #include "ObjectMgr.h"
#include "Chat.h" #include "Chat.h"
#include "CommandScript.h"
#include "Log.h" #include "Log.h"
#include "PlayerbotAI.h" #include "PlayerbotAI.h"
#include "PlayerbotMgr.h" #include "PlayerbotMgr.h"
@@ -19,8 +20,13 @@
#include "QueryResult.h" #include "QueryResult.h"
#include <string> #include <string>
#include "Player.h" #include "Player.h"
#include "PlayerbotAIConfig.h"
#include "ArenaTeamMgr.h"
using namespace Acore::ChatCommands;
// Forward declarations. // Forward declarations.
class Guild;
static bool IsAlliancePlayerBot(Player* bot); static bool IsAlliancePlayerBot(Player* bot);
static bool IsHordePlayerBot(Player* bot); static bool IsHordePlayerBot(Player* bot);
static void ClampAndBalanceBrackets(); static void ClampAndBalanceBrackets();
@@ -47,6 +53,8 @@ static uint8 g_RandomBotMaxLevel = 80;
static bool g_BotLevelBracketsEnabled = true; static bool g_BotLevelBracketsEnabled = true;
// Ignore bots in guilds with a real player online. Default is true. // Ignore bots in guilds with a real player online. Default is true.
static bool g_IgnoreGuildBotsWithRealPlayers = true; static bool g_IgnoreGuildBotsWithRealPlayers = true;
// Ignore bots in arena teams. Default is true.
static bool g_IgnoreArenaTeamBots = true;
// Use vectors to store the level ranges. // Use vectors to store the level ranges.
static std::vector<LevelRangeConfig> g_AllianceLevelRanges; static std::vector<LevelRangeConfig> g_AllianceLevelRanges;
@@ -54,6 +62,7 @@ static std::vector<LevelRangeConfig> g_HordeLevelRanges;
static uint32 g_BotDistCheckFrequency = 300; // in seconds static uint32 g_BotDistCheckFrequency = 300; // in seconds
static uint32 g_BotDistFlaggedCheckFrequency = 15; // in seconds static uint32 g_BotDistFlaggedCheckFrequency = 15; // in seconds
static uint32 g_GuildTrackerUpdateFrequency = 600; // in seconds (10 minutes)
static bool g_BotDistFullDebugMode = false; static bool g_BotDistFullDebugMode = false;
static bool g_BotDistLiteDebugMode = false; static bool g_BotDistLiteDebugMode = false;
static bool g_UseDynamicDistribution = false; static bool g_UseDynamicDistribution = false;
@@ -69,7 +78,7 @@ static float g_RealPlayerWeight = 1.0f;
static bool g_SyncFactions = false; static bool g_SyncFactions = false;
// Array for character social list friends // Array for character social list friends
std::vector<int> g_SocialFriendsList; std::vector<uint64> g_SocialFriendsList;
// Array for excluded bot names. // Array for excluded bot names.
static std::vector<std::string> g_ExcludeBotNames; static std::vector<std::string> g_ExcludeBotNames;
@@ -77,6 +86,9 @@ static std::vector<std::string> g_ExcludeBotNames;
// Array for real player guild IDs. // Array for real player guild IDs.
std::unordered_set<uint32> g_RealPlayerGuildIds; std::unordered_set<uint32> g_RealPlayerGuildIds;
// Persistent guild tracker - stores guild IDs that have real players (from database)
std::unordered_set<uint32> g_PersistentRealPlayerGuildIds;
struct PendingResetEntry struct PendingResetEntry
{ {
ObjectGuid botGuid; ObjectGuid botGuid;
@@ -103,11 +115,13 @@ static void LoadBotLevelBracketsConfig()
{ {
g_BotLevelBracketsEnabled = sConfigMgr->GetOption<bool>("BotLevelBrackets.Enabled", true); g_BotLevelBracketsEnabled = sConfigMgr->GetOption<bool>("BotLevelBrackets.Enabled", true);
g_IgnoreGuildBotsWithRealPlayers = sConfigMgr->GetOption<bool>("BotLevelBrackets.IgnoreGuildBotsWithRealPlayers", true); g_IgnoreGuildBotsWithRealPlayers = sConfigMgr->GetOption<bool>("BotLevelBrackets.IgnoreGuildBotsWithRealPlayers", true);
g_IgnoreArenaTeamBots = sConfigMgr->GetOption<bool>("BotLevelBrackets.IgnoreArenaTeamBots", true);
g_BotDistFullDebugMode = sConfigMgr->GetOption<bool>("BotLevelBrackets.FullDebugMode", false); g_BotDistFullDebugMode = sConfigMgr->GetOption<bool>("BotLevelBrackets.FullDebugMode", false);
g_BotDistLiteDebugMode = sConfigMgr->GetOption<bool>("BotLevelBrackets.LiteDebugMode", false); g_BotDistLiteDebugMode = sConfigMgr->GetOption<bool>("BotLevelBrackets.LiteDebugMode", false);
g_BotDistCheckFrequency = sConfigMgr->GetOption<uint32>("BotLevelBrackets.CheckFrequency", 300); g_BotDistCheckFrequency = sConfigMgr->GetOption<uint32>("BotLevelBrackets.CheckFrequency", 300);
g_BotDistFlaggedCheckFrequency = sConfigMgr->GetOption<uint32>("BotLevelBrackets.CheckFlaggedFrequency", 15); g_BotDistFlaggedCheckFrequency = sConfigMgr->GetOption<uint32>("BotLevelBrackets.CheckFlaggedFrequency", 15);
g_GuildTrackerUpdateFrequency = sConfigMgr->GetOption<uint32>("BotLevelBrackets.GuildTrackerUpdateFrequency", 600);
g_UseDynamicDistribution = sConfigMgr->GetOption<bool>("BotLevelBrackets.Dynamic.UseDynamicDistribution", false); g_UseDynamicDistribution = sConfigMgr->GetOption<bool>("BotLevelBrackets.Dynamic.UseDynamicDistribution", false);
g_RealPlayerWeight = sConfigMgr->GetOption<float>("BotLevelBrackets.Dynamic.RealPlayerWeight", 1.0f); g_RealPlayerWeight = sConfigMgr->GetOption<float>("BotLevelBrackets.Dynamic.RealPlayerWeight", 1.0f);
g_SyncFactions = sConfigMgr->GetOption<bool>("BotLevelBrackets.Dynamic.SyncFactions", false); g_SyncFactions = sConfigMgr->GetOption<bool>("BotLevelBrackets.Dynamic.SyncFactions", false);
@@ -245,39 +259,6 @@ static bool IsHordePlayerBot(Player* bot)
} }
/**
* @brief Logs the number of player bots at each level if full debug mode is enabled.
*
* This function iterates through all players in the world, counts the number of bots at each level,
* and logs the results. Only bots that are currently in the world are considered. The logging occurs
* only if the global debug mode flag `g_BotDistFullDebugMode` is set to true.
*/
static void LogAllBotLevels()
{
if (g_BotDistFullDebugMode)
{
std::map<uint8, uint32> botLevelCount;
for (auto const& itr : ObjectAccessor::GetPlayers())
{
Player* player = itr.second;
if (!player || !player->IsInWorld())
{
continue;
}
if (!IsPlayerBot(player))
{
continue;
}
botLevelCount[player->GetLevel()]++;
}
for (const auto& entry : botLevelCount)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Level {}: {} bots", entry.first, entry.second);
}
}
}
/** /**
* @brief Removes a bot from the list of pending level resets. * @brief Removes a bot from the list of pending level resets.
* *
@@ -332,7 +313,7 @@ static void LoadSocialFriendList()
do do
{ {
uint32 socialFriendGUID = result->Fetch()->Get<uint32>(); uint32 socialFriendGUID = result->Fetch()->Get<uint32>();
g_SocialFriendsList.push_back(socialFriendGUID); g_SocialFriendsList.push_back(static_cast<uint64>(socialFriendGUID));
if (g_BotDistFullDebugMode) if (g_BotDistFullDebugMode)
{ {
LOG_INFO("server.load", "[BotLevelBrackets] Adding GUID {} to Social Friend List", socialFriendGUID); LOG_INFO("server.load", "[BotLevelBrackets] Adding GUID {} to Social Friend List", socialFriendGUID);
@@ -341,6 +322,180 @@ static void LoadSocialFriendList()
} }
/**
* @brief Loads the persistent guild tracker data from the database.
*
* This function queries the bot_level_brackets_guild_tracker table to load all guild IDs
* that have real players. This provides persistent storage of guild status even when
* real players are offline. The data is loaded into g_PersistentRealPlayerGuildIds.
*/
static void LoadPersistentGuildTracker()
{
g_PersistentRealPlayerGuildIds.clear();
QueryResult result = CharacterDatabase.Query("SELECT guild_id FROM bot_level_brackets_guild_tracker WHERE has_real_players = 1");
if (!result)
{
if (g_BotDistFullDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] No guilds with real players found in persistent storage.");
}
return;
}
if (g_BotDistFullDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Loading persistent guild tracker data from database...");
}
do
{
uint32 guildId = result->Fetch()->Get<uint32>();
g_PersistentRealPlayerGuildIds.insert(guildId);
if (g_BotDistFullDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Loaded guild {} as having real players.", guildId);
}
} while (result->NextRow());
if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Loaded {} guilds with real players from persistent storage.", g_PersistentRealPlayerGuildIds.size());
}
}
/**
* @brief Updates the persistent guild tracker database with current guild status.
*
* This function adds guilds to the tracker when real players are found online in them.
* It never removes guilds from the tracker when players log off - this prevents bot level
* changes from occurring when real players go offline but are still members of the guild.
*/
static void UpdatePersistentGuildTracker()
{
if (g_BotDistFullDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Starting additive-only persistent guild tracker update...");
}
// Find guilds with currently online real players
std::unordered_set<uint32> currentRealPlayerGuilds;
const auto& allPlayers = ObjectAccessor::GetPlayers();
for (const auto& itr : allPlayers)
{
Player* player = itr.second;
if (!player || !player->IsInWorld())
continue;
if (!IsPlayerBot(player))
{
uint32 guildId = player->GetGuildId();
if (guildId != 0)
{
currentRealPlayerGuilds.insert(guildId);
}
}
}
uint32 addedCount = 0;
// Update or insert guilds with real players - ensure has_real_players is set to 1
for (uint32 guildId : currentRealPlayerGuilds)
{
// Use REPLACE INTO to update existing records or insert new ones
CharacterDatabase.Execute(
"REPLACE INTO bot_level_brackets_guild_tracker (guild_id, has_real_players) "
"VALUES ({}, 1)",
guildId
);
// Add to our in-memory cache
g_PersistentRealPlayerGuildIds.insert(guildId);
addedCount++;
}
if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Additive guild tracker update complete. {} guilds processed, {} total tracked guilds.",
addedCount, g_PersistentRealPlayerGuildIds.size());
}
}
/**
* @brief Checks and removes guilds from tracker that no longer have any real players online.
*
* This function scans all guilds currently in the tracker and removes any that don't have
* real players online. This is useful for cleaning up after players leave guilds.
* Should be called manually or as needed, not automatically on logout.
*/
static void CleanupGuildTracker()
{
if (g_BotDistFullDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Starting guild tracker cleanup - removing guilds with no online real players...");
}
// Get current guilds with online real players
std::unordered_set<uint32> currentRealPlayerGuilds;
const auto& allPlayers = ObjectAccessor::GetPlayers();
for (const auto& itr : allPlayers)
{
Player* player = itr.second;
if (!player || !player->IsInWorld())
continue;
if (!IsPlayerBot(player))
{
uint32 guildId = player->GetGuildId();
if (guildId != 0)
{
currentRealPlayerGuilds.insert(guildId);
}
}
}
// Find guilds to remove (those in tracker but not in current real player guilds)
std::vector<uint32> guildsToRemove;
for (uint32 trackedGuildId : g_PersistentRealPlayerGuildIds)
{
if (currentRealPlayerGuilds.find(trackedGuildId) == currentRealPlayerGuilds.end())
{
guildsToRemove.push_back(trackedGuildId);
}
}
// Remove guilds that no longer have real players online
uint32 removedCount = 0;
for (uint32 guildId : guildsToRemove)
{
// Remove from database
CharacterDatabase.Execute(
"UPDATE bot_level_brackets_guild_tracker SET has_real_players = 0 WHERE guild_id = {}",
guildId
);
// Remove from in-memory caches
g_PersistentRealPlayerGuildIds.erase(guildId);
g_RealPlayerGuildIds.erase(guildId);
removedCount++;
if (g_BotDistFullDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Removed guild {} from tracker - no real players online.", guildId);
}
}
if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Guild tracker cleanup complete. {} guilds removed, {} guilds remain.",
removedCount, g_PersistentRealPlayerGuildIds.size());
}
}
/** /**
* @brief Populates the global set of real player guild IDs from the provided player map. * @brief Populates the global set of real player guild IDs from the provided player map.
* *
@@ -484,16 +639,40 @@ static void AdjustBotToRange(Player* bot, int targetRangeIndex, const LevelRange
{ {
lowerBound = 55; lowerBound = 55;
} }
if (lowerBound > upperBound)
{
return;
}
newLevel = urand(lowerBound, upperBound); newLevel = urand(lowerBound, upperBound);
} }
else else
{ {
newLevel = GetRandomLevelInRange(factionRanges[targetRangeIndex]); const LevelRangeConfig& range = factionRanges[targetRangeIndex];
if (range.lower > range.upper)
{
if (g_BotDistFullDebugMode)
{
std::string playerFaction = IsAlliancePlayerBot(bot) ? "Alliance" : "Horde";
LOG_INFO("server.loading",
"[BotLevelBrackets] AdjustBotToRange: Invalid range {}-{} for {} bot '{}'.",
range.lower, range.upper, playerFaction, bot->GetName());
}
return;
}
newLevel = GetRandomLevelInRange(range);
} }
PlayerbotFactory newFactory(bot, newLevel); PlayerbotFactory newFactory(bot, newLevel);
newFactory.Randomize(false); newFactory.Randomize(false);
// Force reset talents if equipment persistence is enabled and bot rolled to max level
// This is to fix an issue with Playerbots and how Randomization works with Equipment Persistence
if (newLevel == g_RandomBotMaxLevel && sPlayerbotAIConfig->equipmentPersistence)
{
PlayerbotFactory tempFactory(bot, newLevel);
tempFactory.InitTalentsTree(false, true, true);
}
if (g_BotDistFullDebugMode) if (g_BotDistFullDebugMode)
{ {
PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot); PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot);
@@ -530,7 +709,8 @@ static bool BotInGuildWithRealPlayer(Player* bot)
{ {
return false; return false;
} }
return g_RealPlayerGuildIds.count(guildId) > 0; // Check both online real players and persistent database storage
return g_RealPlayerGuildIds.count(guildId) > 0 || g_PersistentRealPlayerGuildIds.count(guildId) > 0;
} }
@@ -568,6 +748,30 @@ static bool BotInFriendList(Player* bot)
} }
/**
* @brief Checks if the given bot is a member of any arena team.
*
* This function verifies that the provided Player pointer is valid and
* checks all arena team slots to see if the bot is in any arena team.
*
* @param bot Pointer to the Player object representing the bot.
* @return true if the bot is in an arena team, false otherwise.
*/
static bool BotInArenaTeam(Player* bot)
{
if (!bot)
return false;
for (uint32 slot = 0; slot < MAX_ARENA_SLOT; ++slot)
{
if (ArenaTeam* team = sArenaTeamMgr->GetArenaTeamById(bot->GetArenaTeamId(slot)))
{
return true;
}
}
return false;
}
/** /**
* @brief Clamps and balances the level brackets for Alliance and Horde bot distributions. * @brief Clamps and balances the level brackets for Alliance and Horde bot distributions.
* *
@@ -738,7 +942,7 @@ static bool IsBotSafeForLevelReset(Player* bot)
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{ {
Player* member = ref->GetSource(); Player* member = ref->GetSource();
if (member && !IsPlayerBot(member)) if (member && member->IsInWorld() && !IsPlayerBot(member))
{ {
if (g_BotDistFullDebugMode) if (g_BotDistFullDebugMode)
{ {
@@ -847,6 +1051,32 @@ static void ProcessPendingLevelResets()
continue; continue;
} }
if (g_IgnoreArenaTeamBots && BotInArenaTeam(bot))
{
it = g_PendingLevelResets.erase(it);
continue;
}
// Check if bot is now in a group with real players
if (Group* group = bot->GetGroup())
{
bool hasRealPlayer = false;
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (member && member->IsInWorld() && !IsPlayerBot(member))
{
hasRealPlayer = true;
break;
}
}
if (hasRealPlayer)
{
it = g_PendingLevelResets.erase(it);
continue;
}
}
if (bot && bot->IsInWorld() && IsBotSafeForLevelReset(bot)) if (bot && bot->IsInWorld() && IsBotSafeForLevelReset(bot))
{ {
AdjustBotToRange(bot, targetRange, it->factionRanges); AdjustBotToRange(bot, targetRange, it->factionRanges);
@@ -882,6 +1112,37 @@ static int GetOrFlagPlayerBracket(Player* player)
return -1; return -1;
} }
if (IsPlayerBot(player) && g_IgnoreGuildBotsWithRealPlayers && BotInGuildWithRealPlayer(player))
{
return -1;
}
if (IsPlayerBot(player) && g_IgnoreArenaTeamBots && BotInArenaTeam(player))
{
return -1;
}
// Check if bot is in a group with real players - if so, exclude from bracket processing
if (IsPlayerBot(player))
{
if (Group* group = player->GetGroup())
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (member && member->IsInWorld() && !IsPlayerBot(member))
{
if (g_BotDistFullDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] GetOrFlagPlayerBracket: Bot {} (Level {}) is in group with real player {} - excluding from bracket processing.",
player->GetName(), player->GetLevel(), member->GetName());
}
return -1;
}
}
}
}
int rangeIndex = GetLevelRangeIndex(player->GetLevel(), player->GetTeamId()); int rangeIndex = GetLevelRangeIndex(player->GetLevel(), player->GetTeamId());
if (rangeIndex >= 0) if (rangeIndex >= 0)
{ {
@@ -910,6 +1171,13 @@ static int GetOrFlagPlayerBracket(Player* player)
{ {
continue; continue;
} }
// Skip brackets that Death Knights cannot be assigned to (upper bound < 55)
if (player->getClass() == CLASS_DEATH_KNIGHT && factionRanges[i].upper < 55)
{
continue;
}
int diff = 0; int diff = 0;
if (player->GetLevel() < factionRanges[i].lower) if (player->GetLevel() < factionRanges[i].lower)
{ {
@@ -979,7 +1247,7 @@ static int GetOrFlagPlayerBracket(Player* player)
class BotLevelBracketsWorldScript : public WorldScript class BotLevelBracketsWorldScript : public WorldScript
{ {
public: public:
BotLevelBracketsWorldScript() : WorldScript("BotLevelBracketsWorldScript"), m_timer(0), m_flaggedTimer(0) { } BotLevelBracketsWorldScript() : WorldScript("BotLevelBracketsWorldScript"), m_timer(0), m_flaggedTimer(0), m_guildTrackerTimer(0) { }
/** /**
* @brief Called when the module is started up. * @brief Called when the module is started up.
@@ -994,6 +1262,7 @@ public:
{ {
LoadBotLevelBracketsConfig(); LoadBotLevelBracketsConfig();
LoadSocialFriendList(); LoadSocialFriendList();
LoadPersistentGuildTracker();
if (!g_BotLevelBracketsEnabled) if (!g_BotLevelBracketsEnabled)
{ {
LOG_INFO("server.loading", "[BotLevelBrackets] Module disabled via configuration."); LOG_INFO("server.loading", "[BotLevelBrackets] Module disabled via configuration.");
@@ -1048,6 +1317,7 @@ public:
m_timer += diff; m_timer += diff;
m_flaggedTimer += diff; m_flaggedTimer += diff;
m_guildTrackerTimer += diff;
if (m_flaggedTimer >= g_BotDistFlaggedCheckFrequency * 1000) if (m_flaggedTimer >= g_BotDistFlaggedCheckFrequency * 1000)
{ {
@@ -1059,6 +1329,16 @@ public:
m_flaggedTimer = 0; m_flaggedTimer = 0;
} }
if (m_guildTrackerTimer >= g_GuildTrackerUpdateFrequency * 1000)
{
if (g_BotDistFullDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Guild Tracker Update Triggering.");
}
UpdatePersistentGuildTracker();
m_guildTrackerTimer = 0;
}
if (m_timer < g_BotDistCheckFrequency * 1000) if (m_timer < g_BotDistCheckFrequency * 1000)
{ {
return; return;
@@ -1168,6 +1448,9 @@ public:
applyWeights(g_AllianceLevelRanges, allianceWeights); applyWeights(g_AllianceLevelRanges, allianceWeights);
applyWeights(g_HordeLevelRanges, hordeWeights); applyWeights(g_HordeLevelRanges, hordeWeights);
// Ensure brackets respect global min/max levels and percentages sum to 100
ClampAndBalanceBrackets();
// Debug output for new bracket percentages after normalization // Debug output for new bracket percentages after normalization
if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode) if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode)
{ {
@@ -1239,6 +1522,10 @@ public:
{ {
continue; continue;
} }
if (g_IgnoreArenaTeamBots && BotInArenaTeam(player))
{
continue;
}
if (IsAlliancePlayerBot(player)) if (IsAlliancePlayerBot(player))
{ {
totalAllianceBots++; totalAllianceBots++;
@@ -1559,9 +1846,29 @@ public:
} }
} }
/**
* @brief Manually trigger guild tracker cleanup.
*
* This function can be called to remove guilds from the tracker that no longer have
* real players online. This is useful after players leave guilds to ensure accurate
* tracking and allow bot level changes in guilds that truly have no real players.
*
* Call this periodically or when you know players have left guilds to clean up the tracker.
*/
void ManualGuildTrackerCleanup()
{
if (!g_BotLevelBracketsEnabled || !g_IgnoreGuildBotsWithRealPlayers)
{
return;
}
CleanupGuildTracker();
}
private: private:
uint32 m_timer; // For distribution adjustments uint32 m_timer; // For distribution adjustments
uint32 m_flaggedTimer; // For pending reset checks uint32 m_flaggedTimer; // For pending reset checks
uint32 m_guildTrackerTimer; // For guild tracker updates
}; };
@@ -1586,18 +1893,47 @@ public:
} }
}; };
/**
* @class BotLevelBracketsCommandScript
* @brief Handles chat commands for the Player Bot Level Brackets module.
*
* This script provides administrative commands to manage the bot level brackets configuration.
*/
class BotLevelBracketsCommandScript : public CommandScript
{
public:
BotLevelBracketsCommandScript() : CommandScript("BotLevelBracketsCommandScript") {}
ChatCommandTable GetCommands() const override
{
static ChatCommandTable commandTable =
{
{ "reload", HandleReloadConfig, SEC_ADMINISTRATOR, Console::No }
};
return commandTable;
}
static bool HandleReloadConfig(ChatHandler* handler)
{
LoadBotLevelBracketsConfig();
handler->SendSysMessage("Bot level brackets config reloaded.");
return true;
}
};
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// ENTRY POINT: Register the Bot Level Distribution Module // ENTRY POINT: Register the Bot Level Distribution Module
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
/** /**
* @brief Registers the world and player scripts for the Player Bot Level Brackets module. * @brief Registers the world, player, and command scripts for the Player Bot Level Brackets module.
* *
* This function instantiates and adds the BotLevelBracketsWorldScript and BotLevelBracketsPlayerScript * This function instantiates and adds the BotLevelBracketsWorldScript, BotLevelBracketsPlayerScript,
* to the script system, enabling custom logic for player bot level brackets within the game world. * and BotLevelBracketsCommandScript to the script system, enabling custom logic and commands
* for player bot level brackets within the game world.
*/ */
void Addmod_player_bot_level_bracketsScripts() void Addmod_player_bot_level_bracketsScripts()
{ {
new BotLevelBracketsWorldScript(); new BotLevelBracketsWorldScript();
new BotLevelBracketsPlayerScript(); new BotLevelBracketsPlayerScript();
new BotLevelBracketsCommandScript();
} }