Merge pull request #37 from DustinHendrickson/Dustin/SyncBrackets

Sync Brackets between factions when dynamic distribution is enabled
This commit is contained in:
Dustin Hendrickson
2025-05-20 10:04:06 -07:00
committed by GitHub
3 changed files with 119 additions and 125 deletions

View File

@@ -26,6 +26,8 @@ Features
When enabled, bots that are on real players' friend lists are excluded from level bracket adjustments.
- **Dynamic Distribution:**
Optionally enable dynamic recalculation of bot distribution percentages based on the number of non-bot players present in each bracket.
- **Sync Factions Bracket:**
Requires Dynamic Distribution to be on. Optionally enable synchronized bracket and weighting logic between Alliance and Horde. When enabled, both bracket range definitions must match exactly for both factions and real player activity from either faction influences bot bracket distribution for both factions.
- **Debug Modes:**
Full and Lite debug modes provide detailed logging for troubleshooting and monitoring bot adjustments.
@@ -76,8 +78,9 @@ BotLevelBrackets.FullDebugMode | Enables full debug logging for th
BotLevelBrackets.LiteDebugMode | Enables lite debug logging for the Bot Level Brackets module. | 0 | 0 (off) / 1 (on)
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.UseDynamicDistribution | Enables dynamic recalculation of bot distribution percentages based on non-bot player counts per bracket. | 0 | 0 (off) / 1 (on)
BotLevelBrackets.RealPlayerWeight | Multiplier applied to each real player's contribution (active only if dynamic distribution is enabled). | 1.0 | Floating point number
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.RealPlayerWeight | Multiplier applied to each real player's contribution (active only if dynamic distribution is enabled). | 1.0 | Floating point number
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.IgnoreGuildBotsWithRealPlayers | Excludes bots in a guild with at least one real (non-bot) player online from adjustments. | 1 | 0 (disabled) / 1 (enabled)
BotLevelBrackets.NumRanges | Number of level brackets used for bot distribution. Both factions must have the same number defined. | 9 | Positive Integer

View File

@@ -44,22 +44,6 @@ BotLevelBrackets.CheckFlaggedFrequency = 15
# Valid values: 0 (disabled) / 1 (enabled)
BotLevelBrackets.IgnoreGuildBotsWithRealPlayers = 1
#
# BotLevelBrackets.UseDynamicDistribution
# Description: Enables dynamic recalculation of bot distribution percentages based on the number of non-bot players
# present in each level bracket.
# Default: 0 (disabled)
# Valid values: 0 (off) / 1 (on)
BotLevelBrackets.UseDynamicDistribution = 0
#
# BotLevelBrackets.RealPlayerWeight
# Description: A multiplier applied to each real player's contribution in their level bracket.
# When combined with inverse scaling by total player count, a higher value significantly boosts the effective weight
# of a real player when few are online.
# Default: 1.0
BotLevelBrackets.RealPlayerWeight = 1.0
#
# BotLevelBrackets.IgnoreFriendListed
# Description: Ignore bots that are on real players friend's lists from any brackets.
@@ -79,6 +63,32 @@ BotLevelBrackets.IgnoreFriendListed = 1
# sum of the Pct values for each faction remains 100.
BotLevelBrackets.NumRanges = 9
#
# BotLevelBrackets.Dynamic.UseDynamicDistribution
# Description: Enables dynamic recalculation of bot distribution percentages based on the number of non-bot players
# present in each level bracket.
# Default: 0 (disabled)
# Valid values: 0 (off) / 1 (on)
BotLevelBrackets.Dynamic.UseDynamicDistribution = 0
#
# BotLevelBrackets.Dynamic.RealPlayerWeight
# Description: A multiplier applied to each real player's contribution in their level bracket.
# When combined with inverse scaling by total player count, a higher value significantly boosts the effective weight
# of a real player when few are online.
# Default: 1.0
BotLevelBrackets.Dynamic.RealPlayerWeight = 1.0
#
# BotLevelBrackets.Dynamic.SyncFactions
# Description: If enabled, both Alliance and Horde must have identical bracket definitions (same number, same level bounds).
# All real players (regardless of faction) influence the dynamic distribution for both factions.
# Default: 0 (disabled)
# Valid values: 0 (off) / 1 (on)
# WARNING: Server will fail to start if brackets do not match when enabled.
#
BotLevelBrackets.Dynamic.SyncFactions = 0
##############################################
# Alliance Level Brackets Configuration
##############################################

View File

@@ -42,9 +42,9 @@ static uint8 g_NumRanges = 9;
static uint8 g_RandomBotMinLevel = 1;
static uint8 g_RandomBotMaxLevel = 80;
// New configuration option to enable/disable the mod. Default is true.
// Enable/disable the mod. Default is true.
static bool g_BotLevelBracketsEnabled = true;
// New configuration option to 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;
// Use vectors to store the level ranges.
@@ -61,6 +61,11 @@ static bool g_IgnoreFriendListed = true;
// Real player weight to boost bracket contributions.
static float g_RealPlayerWeight = 1.0f;
// If true, synchronize bracket logic and real player influence across both factions.
// This option requires both Alliance and Horde bracket definitions to match perfectly.
// When enabled, all real players (regardless of faction) affect the dynamic distribution for both factions.
static bool g_SyncFactions = false;
// Array for character social list friends
std::vector<int> SocialFriendsList;
@@ -76,8 +81,9 @@ static void LoadBotLevelBracketsConfig()
g_BotDistLiteDebugMode = sConfigMgr->GetOption<bool>("BotLevelBrackets.LiteDebugMode", false);
g_BotDistCheckFrequency = sConfigMgr->GetOption<uint32>("BotLevelBrackets.CheckFrequency", 300);
g_BotDistFlaggedCheckFrequency = sConfigMgr->GetOption<uint32>("BotLevelBrackets.CheckFlaggedFrequency", 15);
g_UseDynamicDistribution = sConfigMgr->GetOption<bool>("BotLevelBrackets.UseDynamicDistribution", false);
g_RealPlayerWeight = sConfigMgr->GetOption<float>("BotLevelBrackets.RealPlayerWeight", 1.0f);
g_UseDynamicDistribution = sConfigMgr->GetOption<bool>("BotLevelBrackets.Dynamic.UseDynamicDistribution", false);
g_RealPlayerWeight = sConfigMgr->GetOption<float>("BotLevelBrackets.Dynamic.RealPlayerWeight", 1.0f);
g_SyncFactions = sConfigMgr->GetOption<bool>("BotLevelBrackets.Dynamic.SyncFactions", false);
g_IgnoreFriendListed = sConfigMgr->GetOption<bool>("BotLevelBrackets.IgnoreFriendListed", true);
// Load the bot level restrictions.
@@ -107,6 +113,23 @@ static void LoadBotLevelBracketsConfig()
g_HordeLevelRanges[i].desiredPercent = static_cast<uint8>(sConfigMgr->GetOption<uint32>("BotLevelBrackets.Horde.Range" + idx + ".Pct", 11));
}
// If SyncFactions is enabled, ensure bracket definitions match exactly for both factions.
// Any mismatch results in immediate server shutdown with an error message.
if (g_SyncFactions) {
for (uint8 i = 0; i < g_NumRanges; ++i) {
if (g_AllianceLevelRanges[i].lower != g_HordeLevelRanges[i].lower ||
g_AllianceLevelRanges[i].upper != g_HordeLevelRanges[i].upper) {
LOG_ERROR("server.loading", "[BotLevelBrackets] FATAL: Bracket mismatch detected between factions at index {}. "
"Alliance: {}-{}, Horde: {}-{}. "
"When SyncFactions is enabled, both bracket number and min/max levels must match exactly. "
"Check your configuration.",
i, g_AllianceLevelRanges[i].lower, g_AllianceLevelRanges[i].upper,
g_HordeLevelRanges[i].lower, g_HordeLevelRanges[i].upper);
std::terminate();
}
}
}
ClampAndBalanceBrackets();
}
@@ -687,26 +710,22 @@ public:
if (g_UseDynamicDistribution)
{
// Calculate real player bracket counts
std::vector<int> allianceRealCounts(g_NumRanges, 0);
std::vector<int> hordeRealCounts(g_NumRanges, 0);
uint32 totalAllianceReal = 0;
uint32 totalHordeReal = 0;
for (auto const& itr : ObjectAccessor::GetPlayers())
{
Player* player = itr.second;
if (!player || !player->IsInWorld())
{
continue;
}
if (IsPlayerBot(player))
{
continue; // Skip bots.
}
continue; // Only count real players.
int rangeIndex = GetOrFlagPlayerBracket(player);
if (rangeIndex < 0)
{
continue;
}
if (player->GetTeamId() == TEAM_ALLIANCE)
{
allianceRealCounts[rangeIndex]++;
@@ -718,127 +737,89 @@ public:
totalHordeReal++;
}
}
const float baseline = 1.0f;
std::vector<float> allianceWeights(g_NumRanges, 0.0f);
std::vector<float> hordeWeights(g_NumRanges, 0.0f);
float allianceTotalWeight = 0.0f;
float hordeTotalWeight = 0.0f;
for (int i = 0; i < g_NumRanges; ++i)
// SYNCED MODE: Real player weighting is combined for both factions, applied to both bracket tables.
if (g_SyncFactions)
{
if (g_AllianceLevelRanges[i].lower > g_AllianceLevelRanges[i].upper)
uint32 totalCombinedReal = totalAllianceReal + totalHordeReal;
for (int i = 0; i < g_NumRanges; ++i)
{
allianceWeights[i] = 0.0f;
int combinedReal = allianceRealCounts[i] + hordeRealCounts[i];
float weight = baseline + g_RealPlayerWeight *
(totalCombinedReal > 0 ? (1.0f / float(totalCombinedReal)) : 1.0f) *
log(1 + combinedReal);
allianceWeights[i] = weight;
hordeWeights[i] = weight;
}
else
{
allianceWeights[i] = baseline + g_RealPlayerWeight *
(totalAllianceReal > 0 ? (1.0f / totalAllianceReal) : 1.0f) *
log(1 + allianceRealCounts[i]);
}
if (g_HordeLevelRanges[i].lower > g_HordeLevelRanges[i].upper)
{
hordeWeights[i] = 0.0f;
}
else
{
hordeWeights[i] = baseline + g_RealPlayerWeight *
(totalHordeReal > 0 ? (1.0f / totalHordeReal) : 1.0f) *
log(1 + hordeRealCounts[i]);
}
allianceTotalWeight += allianceWeights[i];
hordeTotalWeight += hordeWeights[i];
}
for (int i = 0; i < g_NumRanges; ++i)
else
{
g_AllianceLevelRanges[i].desiredPercent = static_cast<uint8>(round((allianceWeights[i] / allianceTotalWeight) * 100));
if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode)
// Separate dynamic weighting for each faction
for (int i = 0; i < g_NumRanges; ++i)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Dynamic Distribution - Alliance Range {}: {}-{}, Real Players: {} (weight: {:.2f}), New Desired: {}%",
i + 1, g_AllianceLevelRanges[i].lower, g_AllianceLevelRanges[i].upper, allianceRealCounts[i], allianceWeights[i], g_AllianceLevelRanges[i].desiredPercent);
if (g_AllianceLevelRanges[i].lower > g_AllianceLevelRanges[i].upper)
allianceWeights[i] = 0.0f;
else
allianceWeights[i] = baseline + g_RealPlayerWeight *
(totalAllianceReal > 0 ? (1.0f / totalAllianceReal) : 1.0f) *
log(1 + allianceRealCounts[i]);
if (g_HordeLevelRanges[i].lower > g_HordeLevelRanges[i].upper)
hordeWeights[i] = 0.0f;
else
hordeWeights[i] = baseline + g_RealPlayerWeight *
(totalHordeReal > 0 ? (1.0f / totalHordeReal) : 1.0f) *
log(1 + hordeRealCounts[i]);
}
}
uint8 sumAlliance = 0;
for (int i = 0; i < g_NumRanges; ++i)
// Helper for normalizing weights so percentages sum to 100
auto applyWeights = [](std::vector<LevelRangeConfig>& ranges, const std::vector<float>& weights)
{
sumAlliance += g_AllianceLevelRanges[i].desiredPercent;
}
if (sumAlliance < 100 && allianceTotalWeight > 0)
{
uint8 missing = 100 - sumAlliance;
if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode)
float total = 0.0f;
for (int i = 0; i < g_NumRanges; ++i)
total += weights[i];
int pctSum = 0;
for (int i = 0; i < g_NumRanges; ++i)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Alliance normalization: current sum = {}, missing = {}", sumAlliance, missing);
uint8 pct = (total > 0.0f) ? static_cast<uint8>(round((weights[i] / total) * 100)) : 0;
ranges[i].desiredPercent = pct;
pctSum += pct;
}
while (missing > 0)
// Fix rounding drift so sum = 100
int missing = 100 - pctSum;
for (int i = 0; i < g_NumRanges && missing > 0; ++i)
{
for (int i = 0; i < g_NumRanges && missing > 0; ++i)
if (ranges[i].lower <= ranges[i].upper && ranges[i].desiredPercent > 0)
{
if (g_AllianceLevelRanges[i].lower <= g_AllianceLevelRanges[i].upper && allianceWeights[i] > 0)
{
g_AllianceLevelRanges[i].desiredPercent++;
missing--;
}
ranges[i].desiredPercent++;
missing--;
}
}
if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Alliance normalized percentages:");
for (int i = 0; i < g_NumRanges; ++i)
{
LOG_INFO("server.loading", " Range {}: {}% ({}-{})", i + 1, g_AllianceLevelRanges[i].desiredPercent,
g_AllianceLevelRanges[i].lower, g_AllianceLevelRanges[i].upper);
}
}
}
for (int i = 0; i < g_NumRanges; ++i)
{
g_HordeLevelRanges[i].desiredPercent = static_cast<uint8>(round((hordeWeights[i] / hordeTotalWeight) * 100));
if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Dynamic Distribution - Horde Range {}: {}-{}, Real Players: {} (weight: {:.2f}), New Desired: {}%",
i + 1, g_HordeLevelRanges[i].lower, g_HordeLevelRanges[i].upper, hordeRealCounts[i], hordeWeights[i], g_HordeLevelRanges[i].desiredPercent);
}
}
};
uint8 sumHorde = 0;
for (int i = 0; i < g_NumRanges; ++i)
applyWeights(g_AllianceLevelRanges, allianceWeights);
applyWeights(g_HordeLevelRanges, hordeWeights);
// Debug output for new bracket percentages after normalization
if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode)
{
sumHorde += g_HordeLevelRanges[i].desiredPercent;
}
if (sumHorde < 100 && hordeTotalWeight > 0)
{
uint8 missing = 100 - sumHorde;
if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode)
for (int i = 0; i < g_NumRanges; ++i)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Horde normalization: current sum = {}, missing = {}", sumHorde, missing);
}
while (missing > 0)
{
for (int i = 0; i < g_NumRanges && missing > 0; ++i)
{
if (g_HordeLevelRanges[i].lower <= g_HordeLevelRanges[i].upper && hordeWeights[i] > 0)
{
g_HordeLevelRanges[i].desiredPercent++;
missing--;
}
}
}
if (g_BotDistFullDebugMode || g_BotDistLiteDebugMode)
{
LOG_INFO("server.loading", "[BotLevelBrackets] Horde normalized percentages:");
for (int i = 0; i < g_NumRanges; ++i)
{
LOG_INFO("server.loading", " Range {}: {}% ({}-{})", i + 1, g_HordeLevelRanges[i].desiredPercent,
g_HordeLevelRanges[i].lower, g_HordeLevelRanges[i].upper);
}
LOG_INFO("server.loading", "[BotLevelBrackets] Final Range {}: {}-{}, Alliance Desired: {}%, Horde Desired: {}%",
i + 1,
g_AllianceLevelRanges[i].lower,
g_AllianceLevelRanges[i].upper,
g_AllianceLevelRanges[i].desiredPercent,
g_HordeLevelRanges[i].desiredPercent);
}
}
}
uint32 totalAllianceBots = 0;
std::vector<int> allianceActualCounts(g_NumRanges, 0);
std::vector< std::vector<Player*> > allianceBotsByRange(g_NumRanges);