17 Commits

Author SHA1 Message Date
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
Dustin Hendrickson
12aac35118 Merge pull request #52 from DustinHendrickson/Dustin/DocUpdated
Clarifying how Dynamic Scaling and player weight works
2025-07-12 11:15:24 -07:00
Dustin Hendrickson
8b91c9549b update 2025-07-12 11:14:41 -07:00
Dustin Hendrickson
813a042b11 Clarifying how Dynamic Scaling and player weight works 2025-07-12 11:10:44 -07:00
4 changed files with 258 additions and 12 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 recalculation of bot distribution percentages based on non-bot player counts per bracket. | 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, based on the weight below. | 0 | 0 (off) / 1 (on)
BotLevelBrackets.Dynamic.RealPlayerWeight | Multiplier applied to each real player's contribution (active only if dynamic distribution is enabled). | 1.0 | Floating point number 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.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,20 @@ 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.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.
@@ -82,12 +90,24 @@ BotLevelBrackets.NumRanges = 9
# Valid values: 0 (off) / 1 (on) # Valid values: 0 (off) / 1 (on)
BotLevelBrackets.Dynamic.UseDynamicDistribution = 0 BotLevelBrackets.Dynamic.UseDynamicDistribution = 0
#
# BotLevelBrackets.Dynamic.RealPlayerWeight # BotLevelBrackets.Dynamic.RealPlayerWeight
# Description: A multiplier applied to each real player's contribution in their level bracket. # Description: This setting controls how much extra weight is given to brackets (level ranges) that contain real (non-bot) players, when dynamic distribution is enabled.
# When combined with inverse scaling by total player count, a higher value significantly boosts the effective weight # 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.
# of a real player when few are online. # 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.
# Default: 1.0 # 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.
# 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.
# What to expect:
# - With 1.0 (default): If 6/10 real players are in one bracket, that bracket gets about 12.77% of bots. All-empty brackets get about 10.69% each.
# - With 3.0: Same scenario: bracket gets about 15.73% of bots. All-empty brackets: 9.93% each.
# - With 5.0: Bracket with 6/10 real players gets 18.31%. All-empty brackets: 9.28% each.
# - If all real players are in one bracket and weight is 5.0, that bracket gets about 21.56% of bots (others: 9.80% each).
# - With 0.0: Every bracket always gets the same number of bots (e.g., 11.11% each for 9 brackets).
# Formula (per bracket):
# bracket_weight = 1.0 + (RealPlayerWeight × (1 / TotalRealPlayers) × log(1 + RealPlayersInBracket))
# All bracket weights are normalized to total 100%.
# Default: 1.0
BotLevelBrackets.Dynamic.RealPlayerWeight = 1.0 BotLevelBrackets.Dynamic.RealPlayerWeight = 1.0
# #

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

@@ -21,6 +21,7 @@
#include "Player.h" #include "Player.h"
// 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();
@@ -54,6 +55,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;
@@ -77,6 +79,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;
@@ -108,6 +113,7 @@ static void LoadBotLevelBracketsConfig()
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);
@@ -341,6 +347,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.
* *
@@ -530,7 +710,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;
} }
@@ -979,7 +1160,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 +1175,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 +1230,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 +1242,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;
@@ -1559,9 +1752,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
}; };