diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 1bf88955..c66711ee 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -569,10 +569,21 @@ AiPlayerbot.RandomBotRandomPassword = 0 # Prefix for account names to create for randombots AiPlayerbot.RandomBotAccountPrefix = "rndbot" -# Minimum and maximum levels for randombots +# Minimum and maximum initialization levels for randombots AiPlayerbot.RandomBotMinLevel = 1 AiPlayerbot.RandomBotMaxLevel = 80 +# Minimum and maximum level range for randombots allowed to login +# If level filtration is used, minRandomBots and maxRandomBots might be automatically adjusted to lower values, +# depending on available eligible bots in the database +# Default Min,Max: 1,80 (no level filtration) +AiPlayerbot.RandomBotMinLoginLevel = 1 +AiPlayerbot.RandomBotMaxLoginLevel = 80 + +# log-out randombots if they they level outside the allowed login range +# Default: 0 (disabled) +AiPlayerbot.RandomBotLogoutOutsideLoginRange = 0 + # Sync max randombot level with max level of online players # Default: 0 (disabled) AiPlayerbot.SyncLevelWithPlayers = 0 diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 4eed4223..80cdeb97 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -86,6 +86,9 @@ bool PlayerbotAIConfig::Initialize() rpWarningCooldown = sConfigMgr->GetOption("AiPlayerbot.RPWarningCooldown", 30); disabledWithoutRealPlayerLoginDelay = sConfigMgr->GetOption("AiPlayerbot.DisabledWithoutRealPlayerLoginDelay", 30); disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300); + randomBotMinLoginLevel = sConfigMgr->GetOption("AiPlayerbot.RandomBotMinLoginLevel", 1); + randomBotMaxLoginLevel = sConfigMgr->GetOption("AiPlayerbot.RandomBotMaxLoginLevel", 80); + randomBotLogoutOutsideLoginRange = sConfigMgr->GetOption("AiPlayerbot.RandomBotLogoutOutsideLoginRange", false); farDistance = sConfigMgr->GetOption("AiPlayerbot.FarDistance", 20.0f); sightDistance = sConfigMgr->GetOption("AiPlayerbot.SightDistance", 75.0f); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 25cabcb7..e7b404d8 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -247,6 +247,8 @@ public: std::string randomBotCombatStrategies, randomBotNonCombatStrategies; bool applyInstanceStrategies; uint32 randomBotMinLevel, randomBotMaxLevel; + uint32 randomBotMinLoginLevel, randomBotMaxLoginLevel; + bool randomBotLogoutOutsideLoginRange; float randomChangeMultiplier; // std::string premadeLevelSpec[MAX_CLASSES][10][91]; //lvl 10 - 100 diff --git a/src/Playerbots.cpp b/src/Playerbots.cpp index b5334ba7..926e1f4e 100644 --- a/src/Playerbots.cpp +++ b/src/Playerbots.cpp @@ -87,7 +87,8 @@ public: PLAYERHOOK_ON_BEFORE_CRITERIA_PROGRESS, PLAYERHOOK_ON_BEFORE_ACHI_COMPLETE, PLAYERHOOK_CAN_PLAYER_USE_PRIVATE_CHAT, - PLAYERHOOK_ON_GIVE_EXP + PLAYERHOOK_ON_GIVE_EXP, + PLAYERHOOK_ON_LEVEL_CHANGED }) {} void OnPlayerLogin(Player* player) override @@ -121,6 +122,33 @@ public: } } + void OnPlayerLevelChanged(Player* player, uint8 oldLevel) override + { + // Check if feature is enabled and required objects are valid + if (!sPlayerbotAIConfig || !sPlayerbotAIConfig->randomBotLogoutOutsideLoginRange || !sRandomPlayerbotMgr) + return; + + // Only apply to bots from rndBotTypeAccounts (type 1) + uint32 accountId = player->GetSession()->GetAccountId(); + if (!sRandomPlayerbotMgr->IsAccountType(accountId, 1)) + return; + + uint32 newLevel = player->GetLevel(); + + // Check if the new level is outside the allowed login range + if (newLevel < sPlayerbotAIConfig->randomBotMinLoginLevel || + newLevel > sPlayerbotAIConfig->randomBotMaxLoginLevel) + { + LOG_INFO("playerbots", "Bot {} changed levels from {} to {}, outside login range ({}-{}). Marking for logout", + player->GetName(), oldLevel, newLevel, + sPlayerbotAIConfig->randomBotMinLoginLevel, sPlayerbotAIConfig->randomBotMaxLoginLevel); + + // Mark the bot for removal in the next update cycle + sRandomPlayerbotMgr->MarkBotForLogout(player->GetGUID().GetCounter()); + sRandomPlayerbotMgr->ForceRecount(); + } + } + void OnPlayerAfterUpdate(Player* player, uint32 diff) override { if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(player)) diff --git a/src/RandomPlayerbotMgr.cpp b/src/RandomPlayerbotMgr.cpp index ab28c25d..557450d0 100644 --- a/src/RandomPlayerbotMgr.cpp +++ b/src/RandomPlayerbotMgr.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "AccountMgr.h" #include "AiFactory.h" @@ -374,13 +375,76 @@ void RandomPlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/) }*/ uint32 maxAllowedBotCount = GetEventValue(0, "bot_count"); - if (!maxAllowedBotCount || (maxAllowedBotCount < sPlayerbotAIConfig->minRandomBots || - maxAllowedBotCount > sPlayerbotAIConfig->maxRandomBots)) + + // Check if level filtering is active and populate eligible bots + if (!levelFilterAdjusted && IsLevelFilterActive()) { - maxAllowedBotCount = urand(sPlayerbotAIConfig->minRandomBots, sPlayerbotAIConfig->maxRandomBots); - SetEventValue(0, "bot_count", maxAllowedBotCount, - urand(sPlayerbotAIConfig->randomBotCountChangeMinInterval, - sPlayerbotAIConfig->randomBotCountChangeMaxInterval)); + PopulateEligibleBots(); + + // Count total eligible bots from RNDbot accounts + uint32 eligibleBotCount = 0; + + for (uint32 accountId : rndBotTypeAccounts) + { + QueryResult result = CharacterDatabase.Query( + "SELECT COUNT(*) FROM characters WHERE account = {} AND level >= {} AND level <= {}", + accountId, sPlayerbotAIConfig->randomBotMinLoginLevel, sPlayerbotAIConfig->randomBotMaxLoginLevel + ); + + if (result) + { + Field* fields = result->Fetch(); + eligibleBotCount += fields[0].Get(); + } + } + + if (eligibleBotCount > 0) + { + // Cap eligible bots by maxRandomBots + uint32 effectiveMaxBots = std::min(eligibleBotCount, sPlayerbotAIConfig->maxRandomBots); + + LOG_INFO("playerbots", "Level filter active: {} eligible bots found in range {}-{}. Effective maximum: {} bots.", + eligibleBotCount, sPlayerbotAIConfig->randomBotMinLoginLevel, + sPlayerbotAIConfig->randomBotMaxLoginLevel, effectiveMaxBots); + + if (effectiveMaxBots >= sPlayerbotAIConfig->minRandomBots) + { + maxAllowedBotCount = urand(sPlayerbotAIConfig->minRandomBots, effectiveMaxBots); + } + else + { + maxAllowedBotCount = effectiveMaxBots; + } + + SetEventValue(0, "bot_count", maxAllowedBotCount, + urand(sPlayerbotAIConfig->randomBotCountChangeMinInterval, + sPlayerbotAIConfig->randomBotCountChangeMaxInterval)); + + currentBots.clear(); + levelFilterAdjusted = true; + } + else + { + LOG_ERROR("playerbots", "No eligible bots found with level filter {}-{}. Change the level range.", + sPlayerbotAIConfig->randomBotMinLoginLevel, sPlayerbotAIConfig->randomBotMaxLoginLevel); + SetEventValue(0, "bot_count", 0, INT_MAX); + maxAllowedBotCount = 0; + currentBots.clear(); + levelFilterAdjusted = true; + } + } + else if (!levelFilterAdjusted) + { + // Normal bot count logic (no level filtering) + if (!maxAllowedBotCount || (maxAllowedBotCount < sPlayerbotAIConfig->minRandomBots || + maxAllowedBotCount > sPlayerbotAIConfig->maxRandomBots)) + { + maxAllowedBotCount = urand(sPlayerbotAIConfig->minRandomBots, sPlayerbotAIConfig->maxRandomBots); + SetEventValue(0, "bot_count", maxAllowedBotCount, + urand(sPlayerbotAIConfig->randomBotCountChangeMinInterval, + sPlayerbotAIConfig->randomBotCountChangeMaxInterval)); + } + levelFilterAdjusted = true; } GetBots(); @@ -772,23 +836,41 @@ uint32 RandomPlayerbotMgr::AddRandomBots() for (uint32 accountId : accountsToUse) { - CharacterDatabasePreparedStatement* stmt = - CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARS_BY_ACCOUNT_ID); - stmt->SetData(0, accountId); - PreparedQueryResult result = CharacterDatabase.Query(stmt); - if (!result) - continue; - - do + // Lambda to process results regardless of query type + auto processResults = [&](auto result) { - Field* fields = result->Fetch(); - CharacterInfo info; - info.guid = fields[0].Get(); - info.rClass = fields[1].Get(); - info.rRace = fields[2].Get(); - info.accountId = accountId; - allCharacters.push_back(info); - } while (result->NextRow()); + if (!result) + return; + + do + { + Field* fields = result->Fetch(); + CharacterInfo info; + info.guid = fields[0].Get(); + info.rClass = fields[1].Get(); + info.rRace = fields[2].Get(); + info.accountId = accountId; + allCharacters.push_back(info); + } while (result->NextRow()); + }; + + if (IsLevelFilterActive()) + { + // Custom query with level filtering + auto result = CharacterDatabase.Query( + "SELECT guid, class, race FROM characters WHERE account = {} AND level >= {} AND level <= {}", + accountId, sPlayerbotAIConfig->randomBotMinLoginLevel, sPlayerbotAIConfig->randomBotMaxLoginLevel + ); + processResults(result); + } + else + { + CharacterDatabasePreparedStatement* stmt = + CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARS_BY_ACCOUNT_ID); + stmt->SetData(0, accountId); + auto result = CharacterDatabase.Query(stmt); + processResults(result); + } } // Shuffle for class balance @@ -2670,17 +2752,55 @@ void RandomPlayerbotMgr::GetBots() stmt->SetData(0, 0); stmt->SetData(1, "add"); uint32 maxAllowedBotCount = GetEventValue(0, "bot_count"); + if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt)) { do { Field* fields = result->Fetch(); uint32 bot = fields[0].Get(); + if (GetEventValue(bot, "add")) + { + // Single query to get both account and level + QueryResult charResult = CharacterDatabase.Query("SELECT account, level FROM characters WHERE guid = {}", bot); + if (!charResult) + continue; + + Field* charFields = charResult->Fetch(); + uint32 botAccountId = charFields[0].Get(); + uint32 botLevel = charFields[1].Get(); + + // Skip if not an RNDbot account + bool isRndBotAccount = false; + for (uint32 accountId : rndBotTypeAccounts) + { + if (accountId == botAccountId) + { + isRndBotAccount = true; + break; + } + } + + if (!isRndBotAccount) + continue; + + // If level filtering is active, check bot level + if (IsLevelFilterActive()) + { + // Skip bots outside the allowed level range + if (botLevel < sPlayerbotAIConfig->randomBotMinLoginLevel || + botLevel > sPlayerbotAIConfig->randomBotMaxLoginLevel) + { + continue; + } + } + currentBots.push_back(bot); - if (currentBots.size() >= maxAllowedBotCount) - break; + if (currentBots.size() >= maxAllowedBotCount) + break; + } } while (result->NextRow()); } } @@ -2708,6 +2828,51 @@ std::vector RandomPlayerbotMgr::GetBgBots(uint32 bracket) return std::move(BgBots); } +void RandomPlayerbotMgr::PopulateEligibleBots() +{ + if (!sPlayerbotAIConfig || !(sPlayerbotAIConfig->randomBotMinLoginLevel > 1 || + sPlayerbotAIConfig->randomBotMaxLoginLevel < 80)) + return; + + LOG_INFO("playerbots", "Populating eligible bots for level filter ({}-{})...", + sPlayerbotAIConfig->randomBotMinLoginLevel, sPlayerbotAIConfig->randomBotMaxLoginLevel); + + bool botsAdded = false; + + // Use only RNDbot type accounts (type 1) + for (uint32 accountId : rndBotTypeAccounts) + { + QueryResult result = CharacterDatabase.Query( + "SELECT guid, level, online FROM characters WHERE account = {} AND level >= {} AND level <= {}", + accountId, sPlayerbotAIConfig->randomBotMinLoginLevel, sPlayerbotAIConfig->randomBotMaxLoginLevel + ); + + if (result) + { + do + { + Field* fields = result->Fetch(); + uint32 botGuid = fields[0].Get(); + bool isOnline = fields[2].Get(); + + // Skip bots that are already online or already have "add" event + if (isOnline || GetEventValue(botGuid, "add")) + continue; + + uint32 add_time = urand(sPlayerbotAIConfig->minRandomBotInWorldTime, + sPlayerbotAIConfig->maxRandomBotInWorldTime); + SetEventValue(botGuid, "add", 1, add_time); + botsAdded = true; + } while (result->NextRow()); + } + } + + if (botsAdded) + { + currentBots.clear(); // Force reload + } +} + uint32 RandomPlayerbotMgr::GetEventValue(uint32 bot, std::string const event) { // load all events at once on first event load @@ -2735,23 +2900,13 @@ uint32 RandomPlayerbotMgr::GetEventValue(uint32 bot, std::string const event) } CachedEvent& e = eventCache[bot][event]; - /*if (e.IsEmpty()) - { - QueryResult results = PlayerbotsDatabase.Query("SELECT `value`, `time`, validIn, `data` FROM - playerbots_random_bots WHERE owner = 0 AND bot = {} AND event = {}", bot, event.c_str()); - if (results) - { - Field* fields = results->Fetch(); - e.value = fields[0].Get(); - e.lastChangeTime = fields[1].Get(); - e.validIn = fields[2].Get(); - e.data = fields[3].Get(); - } - } - */ + bool shouldExpire = (time(0) - e.lastChangeTime) >= e.validIn && + event != "specNo" && + event != "specLink" && + !(event == "bot_count" && IsLevelFilterActive()); // Don't expire bot_count when level filtering is active - if ((time(0) - e.lastChangeTime) >= e.validIn && event != "specNo" && event != "specLink") + if (shouldExpire) e.value = 0; return e.value; diff --git a/src/RandomPlayerbotMgr.h b/src/RandomPlayerbotMgr.h index be624328..0906ef54 100644 --- a/src/RandomPlayerbotMgr.h +++ b/src/RandomPlayerbotMgr.h @@ -193,6 +193,14 @@ public: void AssignAccountTypes(); bool IsAccountType(uint32 accountId, uint8 accountType); + // Allowed login range management + void ForceRecount() { SetEventValue(0, "bot_count", 0, 0); } + void MarkBotForLogout(uint32 bot) + { + SetEventValue(bot, "add", 0, 0); // Clear the "add" event to trigger logout + SetEventValue(bot, "logout", 1, 1); // Also set logout for clarity + } + protected: void OnBotLoginInternal(Player* const bot) override; @@ -237,6 +245,16 @@ private: std::vector rndBotTypeAccounts; // Accounts marked as RNDbot (type 1) std::vector addClassTypeAccounts; // Accounts marked as AddClass (type 2) + // Login level filtering + bool levelFilterAdjusted = false; + void PopulateEligibleBots(); + bool IsLevelFilterActive() const + { + return sPlayerbotAIConfig && + (sPlayerbotAIConfig->randomBotMinLoginLevel > 1 || + sPlayerbotAIConfig->randomBotMaxLoginLevel < 80); + } + //void ScaleBotActivity(); // Deprecated function };