From db7a17ffde8fcf43aca03aaff7661ececfb00991 Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Sun, 27 Jul 2025 00:13:20 -0600 Subject: [PATCH] Fix: Properly track RNDbot and AddClass accounts, and login faction balance issue (#1434) * AssignAccountTypes & AddRandomBots Fix: Properly track RNDbot and AddClass accounts, and login faction balance issue * code style edits * fix addclass init on first build of playerbots_account_type --- .../base/playerbots_account_type.sql | 8 + .../2025_07_01_00_account_type.sql | 9 + src/PlayerbotAIConfig.cpp | 10 +- src/PlayerbotMgr.cpp | 4 +- src/RandomPlayerbotFactory.cpp | 110 ++++- src/RandomPlayerbotMgr.cpp | 417 +++++++++++++----- src/RandomPlayerbotMgr.h | 18 +- 7 files changed, 449 insertions(+), 127 deletions(-) create mode 100644 data/sql/playerbots/base/playerbots_account_type.sql create mode 100644 data/sql/playerbots/updates/db_playerbots/2025_07_01_00_account_type.sql diff --git a/data/sql/playerbots/base/playerbots_account_type.sql b/data/sql/playerbots/base/playerbots_account_type.sql new file mode 100644 index 00000000..d9ce9074 --- /dev/null +++ b/data/sql/playerbots/base/playerbots_account_type.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS `playerbots_account_type`; +CREATE TABLE `playerbots_account_type` ( + `account_id` int unsigned NOT NULL, + `account_type` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0 = unassigned, 1 = RNDbot, 2 = AddClass', + `assignment_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`account_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Playerbot account type assignments'; + diff --git a/data/sql/playerbots/updates/db_playerbots/2025_07_01_00_account_type.sql b/data/sql/playerbots/updates/db_playerbots/2025_07_01_00_account_type.sql new file mode 100644 index 00000000..1bee566f --- /dev/null +++ b/data/sql/playerbots/updates/db_playerbots/2025_07_01_00_account_type.sql @@ -0,0 +1,9 @@ +-- Create playerbots_account_type table for tracking accounts assignments +DROP TABLE IF EXISTS `playerbots_account_type`; +CREATE TABLE `playerbots_account_type` ( + `account_id` int unsigned NOT NULL, + `account_type` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0 = unassigned, 1 = RNDbot, 2 = AddClass', + `assignment_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`account_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Playerbot account type assignments'; + diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 7899de84..86bf734b 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -118,7 +118,6 @@ bool PlayerbotAIConfig::Initialize() tellWhenAvoidAoe = sConfigMgr->GetOption("AiPlayerbot.TellWhenAvoidAoe", false); randomGearLoweringChance = sConfigMgr->GetOption("AiPlayerbot.RandomGearLoweringChance", 0.0f); - incrementalGearInit = sConfigMgr->GetOption("AiPlayerbot.IncrementalGearInit", true); randomGearQualityLimit = sConfigMgr->GetOption("AiPlayerbot.RandomGearQualityLimit", 3); randomGearScoreLimit = sConfigMgr->GetOption("AiPlayerbot.RandomGearScoreLimit", 0); @@ -157,7 +156,7 @@ bool PlayerbotAIConfig::Initialize() LoadList>( sConfigMgr->GetOption("AiPlayerbot.RandomBotQuestIds", "7848,3802,5505,6502,7761"), randomBotQuestIds); - + LoadSet>(sConfigMgr->GetOption("AiPlayerbot.DisallowedGameObjects", "176213,17155"), disallowedGameObjects); botAutologin = sConfigMgr->GetOption("AiPlayerbot.BotAutologin", false); @@ -190,7 +189,7 @@ bool PlayerbotAIConfig::Initialize() maxRandomBotsPriceChangeInterval = sConfigMgr->GetOption("AiPlayerbot.MaxRandomBotsPriceChangeInterval", 48 * HOUR); randomBotJoinLfg = sConfigMgr->GetOption("AiPlayerbot.RandomBotJoinLfg", true); - + //////////////////////////// ICC EnableICCBuffs = sConfigMgr->GetOption("AiPlayerbot.EnableICCBuffs", true); @@ -346,7 +345,7 @@ bool PlayerbotAIConfig::Initialize() { std::string setting = "AiPlayerbot.ZoneBracket." + std::to_string(zoneId); std::string value = sConfigMgr->GetOption(setting, ""); - + if (!value.empty()) { size_t commaPos = value.find(','); @@ -618,6 +617,9 @@ bool PlayerbotAIConfig::Initialize() return true; } + // Assign account types after accounts are created + sRandomPlayerbotMgr->AssignAccountTypes(); + if (sPlayerbotAIConfig->enabled) { sRandomPlayerbotMgr->Init(); diff --git a/src/PlayerbotMgr.cpp b/src/PlayerbotMgr.cpp index 72489431..ee6c6926 100644 --- a/src/PlayerbotMgr.cpp +++ b/src/PlayerbotMgr.cpp @@ -584,8 +584,8 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) } bot->SaveToDB(false, false); - bool addClassBot = sRandomPlayerbotMgr->IsAddclassBot(bot->GetGUID().GetCounter()); - if (addClassBot && master && isRandomAccount && abs((int)master->GetLevel() - (int)bot->GetLevel()) > 3) + bool addClassBot = sRandomPlayerbotMgr->IsAccountType(accountId, 2); + if (addClassBot && master && abs((int)master->GetLevel() - (int)bot->GetLevel()) > 3) { // PlayerbotFactory factory(bot, master->GetLevel()); // factory.Randomize(false); diff --git a/src/RandomPlayerbotFactory.cpp b/src/RandomPlayerbotFactory.cpp index d411ffc9..a3d6a2c7 100644 --- a/src/RandomPlayerbotFactory.cpp +++ b/src/RandomPlayerbotFactory.cpp @@ -393,37 +393,118 @@ std::string const RandomPlayerbotFactory::CreateRandomBotName(NameRaceAndGender return std::move(botName); } +// Calculates the total number of required accounts, either using the specified randomBotAccountCount +// or determining it dynamically based on MaxRandomBots, EnablePeriodicOnlineOffline and its ratio, +// and AddClassAccountPoolSize. The system also factors in the types of existing account, as assigned by +// AssignAccountTypes() uint32 RandomPlayerbotFactory::CalculateTotalAccountCount() { - // Calculates the total number of required accounts, either using the specified randomBotAccountCount - // or determining it dynamically based on the WOTLK condition, max random bots, rotation pool size, - // and additional class account pool size. + // Reset account types if features are disabled + // Reset is done here to precede needed accounts calculations + if (sPlayerbotAIConfig->maxRandomBots == 0 || sPlayerbotAIConfig->addClassAccountPoolSize == 0) + { + if (sPlayerbotAIConfig->maxRandomBots == 0) + { + PlayerbotsDatabase.Execute("UPDATE playerbots_account_type SET account_type = 0 WHERE account_type = 1"); + LOG_INFO("playerbots", "MaxRandomBots set to 0, any RNDbot accounts (type 1) will be unassigned (type 0)"); + } + if (sPlayerbotAIConfig->addClassAccountPoolSize == 0) + { + PlayerbotsDatabase.Execute("UPDATE playerbots_account_type SET account_type = 0 WHERE account_type = 2"); + LOG_INFO("playerbots", "AddClassAccountPoolSize set to 0, any AddClass accounts (type 2) will be unassigned (type 0)"); + } + + // Wait for DB to reflect the change, up to 1 second max. This is needed to make sure other logs don't show wrong info + for (int waited = 0; waited < 1000; waited += 50) + { + QueryResult res = PlayerbotsDatabase.Query("SELECT COUNT(*) FROM playerbots_account_type WHERE account_type IN ({}, {})", + sPlayerbotAIConfig->maxRandomBots == 0 ? 1 : -1, + sPlayerbotAIConfig->addClassAccountPoolSize == 0 ? 2 : -1); + + if (!res || res->Fetch()[0].Get() == 0) + { + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); // Extra 50ms fixed delay for safety. + } + } // Checks if randomBotAccountCount is set, otherwise calculate it dynamically. if (sPlayerbotAIConfig->randomBotAccountCount > 0) return sPlayerbotAIConfig->randomBotAccountCount; - // Avoid creating accounts if both maxRandom & ClassBots are set to zero. - if (sPlayerbotAIConfig->maxRandomBots == 0 && - sPlayerbotAIConfig->addClassAccountPoolSize == 0) - return 0; + // Check existing account types + uint32 existingRndBotAccounts = 0; + uint32 existingAddClassAccounts = 0; + uint32 existingUnassignedAccounts = 0; - //bool isWOTLK = sWorld->getIntConfig(CONFIG_EXPANSION) == EXPANSION_WRATH_OF_THE_LICH_KING; //not used, line marked for removal. + QueryResult typeCheck = PlayerbotsDatabase.Query("SELECT account_type, COUNT(*) FROM playerbots_account_type GROUP BY account_type"); + if (typeCheck) + { + do + { + Field* fields = typeCheck->Fetch(); + uint8 accountType = fields[0].Get(); + uint32 count = fields[1].Get(); - // Determine divisor based on WOTLK condition + if (accountType == 0) existingUnassignedAccounts = count; + else if (accountType == 1) existingRndBotAccounts = count; + else if (accountType == 2) existingAddClassAccounts = count; + } while (typeCheck->NextRow()); + } + + // Determine divisor based on Death Knight login eligibility and requested A&H faction ratio int divisor = CalculateAvailableCharsPerAccount(); // Calculate max bots int maxBots = sPlayerbotAIConfig->maxRandomBots; - // Take perodic online - offline into account + // Take periodic online - offline into account if (sPlayerbotAIConfig->enablePeriodicOnlineOffline) { maxBots *= sPlayerbotAIConfig->periodicOnlineOfflineRatio; } - // Calculate base accounts, add class account pool size, and add 1 as a fixed offset - uint32 baseAccounts = maxBots / divisor; - return baseAccounts + sPlayerbotAIConfig->addClassAccountPoolSize + 1; + // Calculate number of accounts needed for RNDbots + // Result is rounded up for maxBots not cleanly divisible by the divisor + uint32 neededRndBotAccounts = (maxBots + divisor - 1) / divisor; + uint32 neededAddClassAccounts = sPlayerbotAIConfig->addClassAccountPoolSize; + + // Start with existing total + uint32 existingTotal = existingRndBotAccounts + existingAddClassAccounts + existingUnassignedAccounts; + + // Calculate shortfalls after using unassigned accounts + uint32 availableUnassigned = existingUnassignedAccounts; + uint32 additionalAccountsNeeded = 0; + + // Check RNDbot needs + if (neededRndBotAccounts > existingRndBotAccounts) + { + uint32 rndBotShortfall = neededRndBotAccounts - existingRndBotAccounts; + if (rndBotShortfall <= availableUnassigned) + availableUnassigned -= rndBotShortfall; + else + { + additionalAccountsNeeded += (rndBotShortfall - availableUnassigned); + availableUnassigned = 0; + } + } + + // Check AddClass needs + if (neededAddClassAccounts > existingAddClassAccounts) + { + uint32 addClassShortfall = neededAddClassAccounts - existingAddClassAccounts; + if (addClassShortfall <= availableUnassigned) + availableUnassigned -= addClassShortfall; + else + { + additionalAccountsNeeded += (addClassShortfall - availableUnassigned); + availableUnassigned = 0; + } + } + + // Return existing total plus any additional accounts needed + return existingTotal + additionalAccountsNeeded; } uint32 RandomPlayerbotFactory::CalculateAvailableCharsPerAccount() @@ -475,8 +556,9 @@ void RandomPlayerbotFactory::CreateRandomBots() LOG_INFO("playerbots", "Deleting all random bot characters and accounts..."); // First execute all the cleanup SQL commands - // Clear playerbots_random_bots table + // Clear playerbots_random_bots and playerbots_account_type PlayerbotsDatabase.Execute("DELETE FROM playerbots_random_bots"); + PlayerbotsDatabase.Execute("DELETE FROM playerbots_account_type"); // Get the database names dynamically std::string loginDBName = LoginDatabase.GetConnectionInfo()->database; diff --git a/src/RandomPlayerbotMgr.cpp b/src/RandomPlayerbotMgr.cpp index fb9504a9..fd29b1df 100644 --- a/src/RandomPlayerbotMgr.cpp +++ b/src/RandomPlayerbotMgr.cpp @@ -515,15 +515,174 @@ void RandomPlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/) // setActivityPercentage(activityPercentage); // } +// Assigns accounts as RNDbot accounts (type 1) based on MaxRandomBots and EnablePeriodicOnlineOffline and its ratio, +// and assigns accounts as AddClass accounts (type 2) based AddClassAccountPoolSize. Type 1 and 2 assignments are +// permenant, unless MaxRandomBots or AddClassAccountPoolSize are set to 0. If so, their associated accounts will +// be unassigned (type 0) +void RandomPlayerbotMgr::AssignAccountTypes() +{ + LOG_INFO("playerbots", "Assigning account types for random bot accounts..."); + + // Clear existing filtered lists + rndBotTypeAccounts.clear(); + addClassTypeAccounts.clear(); + + // First, get ALL randombot accounts from the database + std::vector allRandomBotAccounts; + QueryResult allAccounts = LoginDatabase.Query( + "SELECT id FROM account WHERE username LIKE '{}%%' ORDER BY id", + sPlayerbotAIConfig->randomBotAccountPrefix.c_str()); + + if (allAccounts) + { + do + { + Field* fields = allAccounts->Fetch(); + uint32 accountId = fields[0].Get(); + allRandomBotAccounts.push_back(accountId); + } while (allAccounts->NextRow()); + } + + LOG_INFO("playerbots", "Found {} total randombot accounts in database", allRandomBotAccounts.size()); + + // Check existing assignments + QueryResult existingAssignments = PlayerbotsDatabase.Query("SELECT account_id, account_type FROM playerbots_account_type"); + std::map currentAssignments; + + if (existingAssignments) + { + do + { + Field* fields = existingAssignments->Fetch(); + uint32 accountId = fields[0].Get(); + uint8 accountType = fields[1].Get(); + currentAssignments[accountId] = accountType; + } while (existingAssignments->NextRow()); + } + + // Mark ALL randombot accounts as unassigned if not already assigned + for (uint32 accountId : allRandomBotAccounts) + { + if (currentAssignments.find(accountId) == currentAssignments.end()) + { + PlayerbotsDatabase.Execute("INSERT INTO playerbots_account_type (account_id, account_type) VALUES ({}, 0) ON DUPLICATE KEY UPDATE account_type = account_type", accountId); + currentAssignments[accountId] = 0; + } + } + + // Calculate needed RNDbot accounts + uint32 neededRndBotAccounts = 0; + if (sPlayerbotAIConfig->maxRandomBots > 0) + { + int divisor = RandomPlayerbotFactory::CalculateAvailableCharsPerAccount(); + int maxBots = sPlayerbotAIConfig->maxRandomBots; + + // Take periodic online-offline into account + if (sPlayerbotAIConfig->enablePeriodicOnlineOffline) + { + maxBots *= sPlayerbotAIConfig->periodicOnlineOfflineRatio; + } + + // Calculate base accounts needed for RNDbots, ensuring round up for maxBots not cleanly divisible by the divisor + neededRndBotAccounts = (maxBots + divisor - 1) / divisor; + } + + // Count existing assigned accounts + uint32 existingRndBotAccounts = 0; + uint32 existingAddClassAccounts = 0; + + for (const auto& [accountId, accountType] : currentAssignments) + { + if (accountType == 1) existingRndBotAccounts++; + else if (accountType == 2) existingAddClassAccounts++; + } + + // Assign RNDbot accounts from lowest position if needed + if (existingRndBotAccounts < neededRndBotAccounts) + { + uint32 toAssign = neededRndBotAccounts - existingRndBotAccounts; + uint32 assigned = 0; + + for (uint32 i = 0; i < allRandomBotAccounts.size() && assigned < toAssign; i++) + { + uint32 accountId = allRandomBotAccounts[i]; + if (currentAssignments[accountId] == 0) // Unassigned + { + PlayerbotsDatabase.Execute("UPDATE playerbots_account_type SET account_type = 1, assignment_date = NOW() WHERE account_id = {}", accountId); + currentAssignments[accountId] = 1; + assigned++; + } + } + + if (assigned < toAssign) + { + LOG_ERROR("playerbots", "Not enough unassigned accounts to fulfill RNDbot requirements. Need {} more accounts.", toAssign - assigned); + } + } + + // Assign AddClass accounts from highest position if needed + uint32 neededAddClassAccounts = sPlayerbotAIConfig->addClassAccountPoolSize; + + if (existingAddClassAccounts < neededAddClassAccounts) + { + uint32 toAssign = neededAddClassAccounts - existingAddClassAccounts; + uint32 assigned = 0; + + for (int i = allRandomBotAccounts.size() - 1; i >= 0 && assigned < toAssign; i--) + { + uint32 accountId = allRandomBotAccounts[i]; + if (currentAssignments[accountId] == 0) // Unassigned + { + PlayerbotsDatabase.Execute("UPDATE playerbots_account_type SET account_type = 2, assignment_date = NOW() WHERE account_id = {}", accountId); + currentAssignments[accountId] = 2; + assigned++; + } + } + + if (assigned < toAssign) + { + LOG_ERROR("playerbots", "Not enough unassigned accounts to fulfill AddClass requirements. Need {} more accounts.", toAssign - assigned); + } + } + + // Populate filtered account lists with ALL accounts of each type + for (const auto& [accountId, accountType] : currentAssignments) + { + if (accountType == 1) rndBotTypeAccounts.push_back(accountId); + else if (accountType == 2) addClassTypeAccounts.push_back(accountId); + } + + LOG_INFO("playerbots", "Account type assignment complete: {} RNDbot accounts, {} AddClass accounts, {} unassigned", + rndBotTypeAccounts.size(), addClassTypeAccounts.size(), + currentAssignments.size() - rndBotTypeAccounts.size() - addClassTypeAccounts.size()); +} + +bool RandomPlayerbotMgr::IsAccountType(uint32 accountId, uint8 accountType) +{ + QueryResult result = PlayerbotsDatabase.Query("SELECT 1 FROM playerbots_account_type WHERE account_id = {} AND account_type = {}", accountId, accountType); + return result != nullptr; +} + +// Logs-in bots in 4 phases. Phase 1 logs Alliance bots up to how much is expected according to the faction ratio, +// and Phase 2 logs-in the remainder Horde bots to reach the total maxAllowedBotCount. If maxAllowedBotCount is not +// reached after Phase 2, the function goes back to log-in Alliance bots and reach maxAllowedBotCount. This is done +// because not every account is guaranteed 5A/5H bots, so the true ratio might be skewed by few percentages. Finally, +// Phase 4 is reached if and only if the value of RandomBotAccountCount is lower than it should. uint32 RandomPlayerbotMgr::AddRandomBots() { uint32 maxAllowedBotCount = GetEventValue(0, "bot_count"); + static time_t missingBotsTimer = 0; if (currentBots.size() < maxAllowedBotCount) { + // Calculate how many bots to add maxAllowedBotCount -= currentBots.size(); maxAllowedBotCount = std::min(sPlayerbotAIConfig->randomBotsPerInterval, maxAllowedBotCount); + // Single RNG instance for all shuffling + std::mt19937 rng(std::chrono::steady_clock::now().time_since_epoch().count()); + + // Only need to track the Alliance count, as it's in Phase 1 uint32 totalRatio = sPlayerbotAIConfig->randomBotAllianceRatio + sPlayerbotAIConfig->randomBotHordeRatio; uint32 allowedAllianceCount = maxAllowedBotCount * (sPlayerbotAIConfig->randomBotAllianceRatio) / totalRatio; @@ -535,26 +694,42 @@ uint32 RandomPlayerbotMgr::AddRandomBots() allowedAllianceCount++; } - uint32 allowedHordeCount = maxAllowedBotCount - allowedAllianceCount; - - for (std::vector::iterator i = sPlayerbotAIConfig->randomBotAccounts.begin(); - i != sPlayerbotAIConfig->randomBotAccounts.end(); i++) + // Determine which accounts to use based on EnablePeriodicOnlineOffline + std::vector accountsToUse; + if (sPlayerbotAIConfig->enablePeriodicOnlineOffline) { - uint32 accountId = *i; - if (sPlayerbotAIConfig->enablePeriodicOnlineOffline) - { - // minus addclass bots account - int32 baseAccount = - RandomPlayerbotFactory::CalculateTotalAccountCount() - sPlayerbotAIConfig->addClassAccountPoolSize; - if (baseAccount <= 0 || baseAccount > sPlayerbotAIConfig->randomBotAccounts.size()) - { - LOG_ERROR("playerbots", "Account calculation error with PeriodicOnlineOffline"); - return 0; - } - uint32 index = urand(0, baseAccount - 1); - accountId = sPlayerbotAIConfig->randomBotAccounts[index]; + // Calculate how many accounts can be used + // With enablePeriodicOnlineOffline, don't use all of rndBotTypeAccounts right away. Fraction results are rounded up + uint32 accountsToUseCount = (rndBotTypeAccounts.size() + sPlayerbotAIConfig->periodicOnlineOfflineRatio - 1) + / sPlayerbotAIConfig->periodicOnlineOfflineRatio; + + // Randomly select accounts + std::vector shuffledAccounts = rndBotTypeAccounts; + std::shuffle(shuffledAccounts.begin(), shuffledAccounts.end(), rng); + + for (uint32 i = 0; i < accountsToUseCount && i < shuffledAccounts.size(); i++) + { + accountsToUse.push_back(shuffledAccounts[i]); } + } + else + { + accountsToUse = rndBotTypeAccounts; + } + + // Pre-map all characters from selected accounts + struct CharacterInfo + { + uint32 guid; + uint8 rClass; + uint8 rRace; + uint32 accountId; + }; + std::vector allCharacters; + + for (uint32 accountId : accountsToUse) + { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARS_BY_ACCOUNT_ID); stmt->SetData(0, accountId); @@ -562,87 +737,115 @@ uint32 RandomPlayerbotMgr::AddRandomBots() if (!result) continue; - std::vector allGuidInfos; - do { Field* fields = result->Fetch(); - GuidClassRaceInfo info; + CharacterInfo info; info.guid = fields[0].Get(); info.rClass = fields[1].Get(); info.rRace = fields[2].Get(); - allGuidInfos.push_back(info); + info.accountId = accountId; + allCharacters.push_back(info); } while (result->NextRow()); - - // random shuffle for class balance - std::mt19937 rnd(time(0)); - std::shuffle(allGuidInfos.begin(), allGuidInfos.end(), rnd); - - std::vector guids; - for (const auto& info : allGuidInfos) - { - ObjectGuid::LowType guid = info.guid; - uint32 rClass = info.rClass; - uint32 rRace = info.rRace; - - if (GetEventValue(guid, "add")) - continue; - - if (GetEventValue(guid, "logout")) - continue; - - if (GetPlayerBot(guid)) - continue; - - if (std::find(currentBots.begin(), currentBots.end(), guid) != currentBots.end()) - continue; - - if (sPlayerbotAIConfig->disableDeathKnightLogin) - { - if (rClass == CLASS_DEATH_KNIGHT) - { - continue; - } - } - - uint32 isAlliance = IsAlliance(rRace); - bool factionNotAllowed = (!allowedAllianceCount && isAlliance) || (!allowedHordeCount && !isAlliance); - - if (factionNotAllowed) - continue; - - if (isAlliance) - { - allowedAllianceCount--; - } - else - { - allowedHordeCount--; - } - - uint32 add_time = sPlayerbotAIConfig->enablePeriodicOnlineOffline - ? urand(sPlayerbotAIConfig->minRandomBotInWorldTime, - sPlayerbotAIConfig->maxRandomBotInWorldTime) - : sPlayerbotAIConfig->permanantlyInWorldTime; - - SetEventValue(guid, "add", 1, add_time); - SetEventValue(guid, "logout", 0, 0); - currentBots.push_back(guid); - - maxAllowedBotCount--; - if (!maxAllowedBotCount) - break; - } - - if (!maxAllowedBotCount) - break; } + // Shuffle for class balance + std::shuffle(allCharacters.begin(), allCharacters.end(), rng); + + // Separate characters by faction for phased login + std::vector allianceChars; + std::vector hordeChars; + + for (const auto& charInfo : allCharacters) + { + if (IsAlliance(charInfo.rRace)) + allianceChars.push_back(charInfo); + + else + hordeChars.push_back(charInfo); + } + + // Lambda to handle bot login logic + auto tryLoginBot = [&](const CharacterInfo& charInfo) -> bool + { + if (GetEventValue(charInfo.guid, "add") || + GetEventValue(charInfo.guid, "logout") || + GetPlayerBot(charInfo.guid) || + std::find(currentBots.begin(), currentBots.end(), charInfo.guid) != currentBots.end() || + (sPlayerbotAIConfig->disableDeathKnightLogin && charInfo.rClass == CLASS_DEATH_KNIGHT)) + { + return false; + } + + uint32 add_time = sPlayerbotAIConfig->enablePeriodicOnlineOffline + ? urand(sPlayerbotAIConfig->minRandomBotInWorldTime, + sPlayerbotAIConfig->maxRandomBotInWorldTime) + : sPlayerbotAIConfig->permanantlyInWorldTime; + + SetEventValue(charInfo.guid, "add", 1, add_time); + SetEventValue(charInfo.guid, "logout", 0, 0); + currentBots.push_back(charInfo.guid); + + return true; + }; + + // PHASE 1: Log-in Alliance bots up to allowedAllianceCount + for (const auto& charInfo : allianceChars) + { + if (!allowedAllianceCount) + break; + + if (tryLoginBot(charInfo)) + { + maxAllowedBotCount--; + allowedAllianceCount--; + } + } + + // PHASE 2: Log-in Horde bots up to maxAllowedBotCount + for (const auto& charInfo : hordeChars) + { + if (!maxAllowedBotCount) + break; + + if (tryLoginBot(charInfo)) + maxAllowedBotCount--; + } + + // PHASE 3: If maxAllowedBotCount wasn't reached, log-in more Alliance bots + for (const auto& charInfo : allianceChars) + { + if (!maxAllowedBotCount) + break; + + if (tryLoginBot(charInfo)) + maxAllowedBotCount--; + } + + // PHASE 4: An error is given if maxAllowedBotCount is still not reached if (maxAllowedBotCount) - LOG_ERROR("playerbots", - "Not enough random bot accounts available. Try to increase RandomBotAccountCount " - "in your conf file", - ceil(maxAllowedBotCount / 10)); + { + if (missingBotsTimer == 0) + missingBotsTimer = time(nullptr); + + if (time(nullptr) - missingBotsTimer >= 10) + { + int divisor = RandomPlayerbotFactory::CalculateAvailableCharsPerAccount(); + uint32 moreAccountsNeeded = (maxAllowedBotCount + divisor - 1) / divisor; + LOG_ERROR("playerbots", + "Can't log-in all the requested bots. Try increasing RandomBotAccountCount in your conf file.\n" + "{} more accounts needed.", moreAccountsNeeded); + missingBotsTimer = 0; // Reset timer so error is not spammed every tick + } + } + else + { + missingBotsTimer = 0; // Reset timer if logins for this interval were successful + } + } + else + { + missingBotsTimer = 0; // Reset timer if there's enough bots } return currentBots.size(); @@ -1165,7 +1368,6 @@ void RandomPlayerbotMgr::ScheduleChangeStrategy(uint32 bot, uint32 time) bool RandomPlayerbotMgr::ProcessBot(uint32 bot) { ObjectGuid botGUID = ObjectGuid::Create(bot); - Player* player = GetPlayerBot(botGUID); PlayerbotAI* botAI = player ? GET_PLAYERBOT_AI(player) : nullptr; @@ -1875,24 +2077,21 @@ void RandomPlayerbotMgr::PrepareTeleportCache() void RandomPlayerbotMgr::PrepareAddclassCache() { - /// @FIXME: Modifying RandomBotAccountCount may cause the original addclass bots to be converted into rndbots, - // which needs to be fixed by separating the two accounts in implementation - size_t poolSize = sPlayerbotAIConfig->addClassAccountPoolSize; - size_t start = sPlayerbotAIConfig->randomBotAccounts.size() > poolSize - ? sPlayerbotAIConfig->randomBotAccounts.size() - poolSize - : 0; + // Using accounts marked as type 2 (AddClass) int32 collected = 0; - for (size_t i = start; i < sPlayerbotAIConfig->randomBotAccounts.size(); i++) + + for (uint32 accountId : addClassTypeAccounts) { for (uint8 claz = CLASS_WARRIOR; claz <= CLASS_DRUID; claz++) { if (claz == 10) continue; + QueryResult results = CharacterDatabase.Query( "SELECT guid, race FROM characters " - "WHERE account = {} AND class = '{}' AND online = 0 " - "ORDER BY account DESC", - sPlayerbotAIConfig->randomBotAccounts[i], claz); + "WHERE account = {} AND class = '{}' AND online = 0", + accountId, claz); + if (results) { do @@ -1907,7 +2106,8 @@ void RandomPlayerbotMgr::PrepareAddclassCache() } } } - LOG_INFO("playerbots", ">> {} characters collected for addclass command.", collected); + + LOG_INFO("playerbots", ">> {} characters collected for addclass command from {} AddClass accounts.", collected, addClassTypeAccounts.size()); } void RandomPlayerbotMgr::Init() @@ -2286,10 +2486,13 @@ bool RandomPlayerbotMgr::IsRandomBot(ObjectGuid::LowType bot) ObjectGuid guid = ObjectGuid::Create(bot); if (!sPlayerbotAIConfig->IsInRandomAccountList(sCharacterCache->GetCharacterAccountIdByGuid(guid))) return false; + if (std::find(currentBots.begin(), currentBots.end(), bot) != currentBots.end()) return true; + return false; } + bool RandomPlayerbotMgr::IsAddclassBot(Player* bot) { if (bot && GET_PLAYERBOT_AI(bot)) @@ -2301,23 +2504,37 @@ bool RandomPlayerbotMgr::IsAddclassBot(Player* bot) { return IsAddclassBot(bot->GetGUID().GetCounter()); } + return false; } bool RandomPlayerbotMgr::IsAddclassBot(ObjectGuid::LowType bot) { ObjectGuid guid = ObjectGuid::Create(bot); + + // Check the cache with faction considerations for (uint8 claz = CLASS_WARRIOR; claz <= CLASS_DRUID; claz++) { if (claz == 10) continue; + for (uint8 isAlliance = 0; isAlliance <= 1; isAlliance++) { if (addclassCache[GetTeamClassIdx(isAlliance, claz)].find(guid) != addclassCache[GetTeamClassIdx(isAlliance, claz)].end()) + { return true; + } } } + + // If not in cache, check the account type + uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(guid); + if (accountId && IsAccountType(accountId, 2)) // Type 2 = AddClass + { + return true; + } + return false; } diff --git a/src/RandomPlayerbotMgr.h b/src/RandomPlayerbotMgr.h index 02d0ff9d..6a62a68b 100644 --- a/src/RandomPlayerbotMgr.h +++ b/src/RandomPlayerbotMgr.h @@ -60,7 +60,6 @@ public: bool IsEmpty() { return !lastChangeTime; } -public: uint32 value; uint32 lastChangeTime; uint32 validIn; @@ -104,10 +103,6 @@ public: void LogPlayerLocation(); void UpdateAIInternal(uint32 elapsed, bool minimal = false) override; -private: - //void ScaleBotActivity(); - -public: uint32 activeBots = 0; static bool HandlePlayerbotConsoleCommand(ChatHandler* handler, char const* args); bool IsRandomBot(Player* bot); @@ -189,6 +184,11 @@ public: }; std::map zone2LevelBracket; std::map> bankerLocsPerLevelCache; + + // Account type management + void AssignAccountTypes(); + bool IsAccountType(uint32 accountId, uint8 accountType); + protected: void OnBotLoginInternal(Player* const bot) override; @@ -218,10 +218,8 @@ private: void RandomTeleport(Player* bot, std::vector& locs, bool hearth = false); uint32 GetZoneLevel(uint16 mapId, float teleX, float teleY, float teleZ); typedef void (RandomPlayerbotMgr::*ConsoleCommandHandler)(Player*); - std::vector players; uint32 processTicks; - // std::map> rpgLocsCache; std::map>> rpgLocsCacheLevel; @@ -230,6 +228,12 @@ private: std::list currentBots; uint32 bgBotsCount; uint32 playersLevel; + + // Account lists + std::vector rndBotTypeAccounts; // Accounts marked as RNDbot (type 1) + std::vector addClassTypeAccounts; // Accounts marked as AddClass (type 2) + + //void ScaleBotActivity(); // Deprecated function }; #define sRandomPlayerbotMgr RandomPlayerbotMgr::instance()