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
This commit is contained in:
NoxMax
2025-07-27 00:13:20 -06:00
committed by GitHub
parent 1e33b28abe
commit db7a17ffde
7 changed files with 449 additions and 127 deletions

View File

@@ -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';

View File

@@ -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';

View File

@@ -118,7 +118,6 @@ bool PlayerbotAIConfig::Initialize()
tellWhenAvoidAoe = sConfigMgr->GetOption<bool>("AiPlayerbot.TellWhenAvoidAoe", false); tellWhenAvoidAoe = sConfigMgr->GetOption<bool>("AiPlayerbot.TellWhenAvoidAoe", false);
randomGearLoweringChance = sConfigMgr->GetOption<float>("AiPlayerbot.RandomGearLoweringChance", 0.0f); randomGearLoweringChance = sConfigMgr->GetOption<float>("AiPlayerbot.RandomGearLoweringChance", 0.0f);
incrementalGearInit = sConfigMgr->GetOption<bool>("AiPlayerbot.IncrementalGearInit", true); incrementalGearInit = sConfigMgr->GetOption<bool>("AiPlayerbot.IncrementalGearInit", true);
randomGearQualityLimit = sConfigMgr->GetOption<int32>("AiPlayerbot.RandomGearQualityLimit", 3); randomGearQualityLimit = sConfigMgr->GetOption<int32>("AiPlayerbot.RandomGearQualityLimit", 3);
randomGearScoreLimit = sConfigMgr->GetOption<int32>("AiPlayerbot.RandomGearScoreLimit", 0); randomGearScoreLimit = sConfigMgr->GetOption<int32>("AiPlayerbot.RandomGearScoreLimit", 0);
@@ -618,6 +617,9 @@ bool PlayerbotAIConfig::Initialize()
return true; return true;
} }
// Assign account types after accounts are created
sRandomPlayerbotMgr->AssignAccountTypes();
if (sPlayerbotAIConfig->enabled) if (sPlayerbotAIConfig->enabled)
{ {
sRandomPlayerbotMgr->Init(); sRandomPlayerbotMgr->Init();

View File

@@ -584,8 +584,8 @@ void PlayerbotHolder::OnBotLogin(Player* const bot)
} }
bot->SaveToDB(false, false); bot->SaveToDB(false, false);
bool addClassBot = sRandomPlayerbotMgr->IsAddclassBot(bot->GetGUID().GetCounter()); bool addClassBot = sRandomPlayerbotMgr->IsAccountType(accountId, 2);
if (addClassBot && master && isRandomAccount && abs((int)master->GetLevel() - (int)bot->GetLevel()) > 3) if (addClassBot && master && abs((int)master->GetLevel() - (int)bot->GetLevel()) > 3)
{ {
// PlayerbotFactory factory(bot, master->GetLevel()); // PlayerbotFactory factory(bot, master->GetLevel());
// factory.Randomize(false); // factory.Randomize(false);

View File

@@ -393,37 +393,118 @@ std::string const RandomPlayerbotFactory::CreateRandomBotName(NameRaceAndGender
return std::move(botName); 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() uint32 RandomPlayerbotFactory::CalculateTotalAccountCount()
{ {
// Calculates the total number of required accounts, either using the specified randomBotAccountCount // Reset account types if features are disabled
// or determining it dynamically based on the WOTLK condition, max random bots, rotation pool size, // Reset is done here to precede needed accounts calculations
// and additional class account pool size. 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<uint64>() == 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. // Checks if randomBotAccountCount is set, otherwise calculate it dynamically.
if (sPlayerbotAIConfig->randomBotAccountCount > 0) if (sPlayerbotAIConfig->randomBotAccountCount > 0)
return sPlayerbotAIConfig->randomBotAccountCount; return sPlayerbotAIConfig->randomBotAccountCount;
// Avoid creating accounts if both maxRandom & ClassBots are set to zero. // Check existing account types
if (sPlayerbotAIConfig->maxRandomBots == 0 && uint32 existingRndBotAccounts = 0;
sPlayerbotAIConfig->addClassAccountPoolSize == 0) uint32 existingAddClassAccounts = 0;
return 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<uint8>();
uint32 count = fields[1].Get<uint32>();
// 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(); int divisor = CalculateAvailableCharsPerAccount();
// Calculate max bots // Calculate max bots
int maxBots = sPlayerbotAIConfig->maxRandomBots; int maxBots = sPlayerbotAIConfig->maxRandomBots;
// Take perodic online - offline into account // Take periodic online - offline into account
if (sPlayerbotAIConfig->enablePeriodicOnlineOffline) if (sPlayerbotAIConfig->enablePeriodicOnlineOffline)
{ {
maxBots *= sPlayerbotAIConfig->periodicOnlineOfflineRatio; maxBots *= sPlayerbotAIConfig->periodicOnlineOfflineRatio;
} }
// Calculate base accounts, add class account pool size, and add 1 as a fixed offset // Calculate number of accounts needed for RNDbots
uint32 baseAccounts = maxBots / divisor; // Result is rounded up for maxBots not cleanly divisible by the divisor
return baseAccounts + sPlayerbotAIConfig->addClassAccountPoolSize + 1; 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() uint32 RandomPlayerbotFactory::CalculateAvailableCharsPerAccount()
@@ -475,8 +556,9 @@ void RandomPlayerbotFactory::CreateRandomBots()
LOG_INFO("playerbots", "Deleting all random bot characters and accounts..."); LOG_INFO("playerbots", "Deleting all random bot characters and accounts...");
// First execute all the cleanup SQL commands // 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_random_bots");
PlayerbotsDatabase.Execute("DELETE FROM playerbots_account_type");
// Get the database names dynamically // Get the database names dynamically
std::string loginDBName = LoginDatabase.GetConnectionInfo()->database; std::string loginDBName = LoginDatabase.GetConnectionInfo()->database;

View File

@@ -515,15 +515,174 @@ void RandomPlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/)
// setActivityPercentage(activityPercentage); // 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<uint32> 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<uint32>();
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<uint32, uint8> currentAssignments;
if (existingAssignments)
{
do
{
Field* fields = existingAssignments->Fetch();
uint32 accountId = fields[0].Get<uint32>();
uint8 accountType = fields[1].Get<uint8>();
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 RandomPlayerbotMgr::AddRandomBots()
{ {
uint32 maxAllowedBotCount = GetEventValue(0, "bot_count"); uint32 maxAllowedBotCount = GetEventValue(0, "bot_count");
static time_t missingBotsTimer = 0;
if (currentBots.size() < maxAllowedBotCount) if (currentBots.size() < maxAllowedBotCount)
{ {
// Calculate how many bots to add
maxAllowedBotCount -= currentBots.size(); maxAllowedBotCount -= currentBots.size();
maxAllowedBotCount = std::min(sPlayerbotAIConfig->randomBotsPerInterval, maxAllowedBotCount); 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 totalRatio = sPlayerbotAIConfig->randomBotAllianceRatio + sPlayerbotAIConfig->randomBotHordeRatio;
uint32 allowedAllianceCount = maxAllowedBotCount * (sPlayerbotAIConfig->randomBotAllianceRatio) / totalRatio; uint32 allowedAllianceCount = maxAllowedBotCount * (sPlayerbotAIConfig->randomBotAllianceRatio) / totalRatio;
@@ -535,26 +694,42 @@ uint32 RandomPlayerbotMgr::AddRandomBots()
allowedAllianceCount++; allowedAllianceCount++;
} }
uint32 allowedHordeCount = maxAllowedBotCount - allowedAllianceCount; // Determine which accounts to use based on EnablePeriodicOnlineOffline
std::vector<uint32> accountsToUse;
for (std::vector<uint32>::iterator i = sPlayerbotAIConfig->randomBotAccounts.begin(); if (sPlayerbotAIConfig->enablePeriodicOnlineOffline)
i != sPlayerbotAIConfig->randomBotAccounts.end(); i++)
{ {
uint32 accountId = *i;
if (sPlayerbotAIConfig->enablePeriodicOnlineOffline)
{
// minus addclass bots account
int32 baseAccount =
RandomPlayerbotFactory::CalculateTotalAccountCount() - sPlayerbotAIConfig->addClassAccountPoolSize;
if (baseAccount <= 0 || baseAccount > sPlayerbotAIConfig->randomBotAccounts.size()) // Calculate how many accounts can be used
{ // With enablePeriodicOnlineOffline, don't use all of rndBotTypeAccounts right away. Fraction results are rounded up
LOG_ERROR("playerbots", "Account calculation error with PeriodicOnlineOffline"); uint32 accountsToUseCount = (rndBotTypeAccounts.size() + sPlayerbotAIConfig->periodicOnlineOfflineRatio - 1)
return 0; / sPlayerbotAIConfig->periodicOnlineOfflineRatio;
}
uint32 index = urand(0, baseAccount - 1); // Randomly select accounts
accountId = sPlayerbotAIConfig->randomBotAccounts[index]; std::vector<uint32> 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<CharacterInfo> allCharacters;
for (uint32 accountId : accountsToUse)
{
CharacterDatabasePreparedStatement* stmt = CharacterDatabasePreparedStatement* stmt =
CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARS_BY_ACCOUNT_ID); CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARS_BY_ACCOUNT_ID);
stmt->SetData(0, accountId); stmt->SetData(0, accountId);
@@ -562,87 +737,115 @@ uint32 RandomPlayerbotMgr::AddRandomBots()
if (!result) if (!result)
continue; continue;
std::vector<GuidClassRaceInfo> allGuidInfos;
do do
{ {
Field* fields = result->Fetch(); Field* fields = result->Fetch();
GuidClassRaceInfo info; CharacterInfo info;
info.guid = fields[0].Get<uint32>(); info.guid = fields[0].Get<uint32>();
info.rClass = fields[1].Get<uint8>(); info.rClass = fields[1].Get<uint8>();
info.rRace = fields[2].Get<uint8>(); info.rRace = fields[2].Get<uint8>();
allGuidInfos.push_back(info); info.accountId = accountId;
allCharacters.push_back(info);
} while (result->NextRow()); } while (result->NextRow());
// random shuffle for class balance
std::mt19937 rnd(time(0));
std::shuffle(allGuidInfos.begin(), allGuidInfos.end(), rnd);
std::vector<uint32> 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<CharacterInfo> allianceChars;
std::vector<CharacterInfo> 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) if (maxAllowedBotCount)
LOG_ERROR("playerbots", {
"Not enough random bot accounts available. Try to increase RandomBotAccountCount " if (missingBotsTimer == 0)
"in your conf file", missingBotsTimer = time(nullptr);
ceil(maxAllowedBotCount / 10));
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(); return currentBots.size();
@@ -1165,7 +1368,6 @@ void RandomPlayerbotMgr::ScheduleChangeStrategy(uint32 bot, uint32 time)
bool RandomPlayerbotMgr::ProcessBot(uint32 bot) bool RandomPlayerbotMgr::ProcessBot(uint32 bot)
{ {
ObjectGuid botGUID = ObjectGuid::Create<HighGuid::Player>(bot); ObjectGuid botGUID = ObjectGuid::Create<HighGuid::Player>(bot);
Player* player = GetPlayerBot(botGUID); Player* player = GetPlayerBot(botGUID);
PlayerbotAI* botAI = player ? GET_PLAYERBOT_AI(player) : nullptr; PlayerbotAI* botAI = player ? GET_PLAYERBOT_AI(player) : nullptr;
@@ -1875,24 +2077,21 @@ void RandomPlayerbotMgr::PrepareTeleportCache()
void RandomPlayerbotMgr::PrepareAddclassCache() void RandomPlayerbotMgr::PrepareAddclassCache()
{ {
/// @FIXME: Modifying RandomBotAccountCount may cause the original addclass bots to be converted into rndbots, // Using accounts marked as type 2 (AddClass)
// 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;
int32 collected = 0; 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++) for (uint8 claz = CLASS_WARRIOR; claz <= CLASS_DRUID; claz++)
{ {
if (claz == 10) if (claz == 10)
continue; continue;
QueryResult results = CharacterDatabase.Query( QueryResult results = CharacterDatabase.Query(
"SELECT guid, race FROM characters " "SELECT guid, race FROM characters "
"WHERE account = {} AND class = '{}' AND online = 0 " "WHERE account = {} AND class = '{}' AND online = 0",
"ORDER BY account DESC", accountId, claz);
sPlayerbotAIConfig->randomBotAccounts[i], claz);
if (results) if (results)
{ {
do 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() void RandomPlayerbotMgr::Init()
@@ -2286,10 +2486,13 @@ bool RandomPlayerbotMgr::IsRandomBot(ObjectGuid::LowType bot)
ObjectGuid guid = ObjectGuid::Create<HighGuid::Player>(bot); ObjectGuid guid = ObjectGuid::Create<HighGuid::Player>(bot);
if (!sPlayerbotAIConfig->IsInRandomAccountList(sCharacterCache->GetCharacterAccountIdByGuid(guid))) if (!sPlayerbotAIConfig->IsInRandomAccountList(sCharacterCache->GetCharacterAccountIdByGuid(guid)))
return false; return false;
if (std::find(currentBots.begin(), currentBots.end(), bot) != currentBots.end()) if (std::find(currentBots.begin(), currentBots.end(), bot) != currentBots.end())
return true; return true;
return false; return false;
} }
bool RandomPlayerbotMgr::IsAddclassBot(Player* bot) bool RandomPlayerbotMgr::IsAddclassBot(Player* bot)
{ {
if (bot && GET_PLAYERBOT_AI(bot)) if (bot && GET_PLAYERBOT_AI(bot))
@@ -2301,23 +2504,37 @@ bool RandomPlayerbotMgr::IsAddclassBot(Player* bot)
{ {
return IsAddclassBot(bot->GetGUID().GetCounter()); return IsAddclassBot(bot->GetGUID().GetCounter());
} }
return false; return false;
} }
bool RandomPlayerbotMgr::IsAddclassBot(ObjectGuid::LowType bot) bool RandomPlayerbotMgr::IsAddclassBot(ObjectGuid::LowType bot)
{ {
ObjectGuid guid = ObjectGuid::Create<HighGuid::Player>(bot); ObjectGuid guid = ObjectGuid::Create<HighGuid::Player>(bot);
// Check the cache with faction considerations
for (uint8 claz = CLASS_WARRIOR; claz <= CLASS_DRUID; claz++) for (uint8 claz = CLASS_WARRIOR; claz <= CLASS_DRUID; claz++)
{ {
if (claz == 10) if (claz == 10)
continue; continue;
for (uint8 isAlliance = 0; isAlliance <= 1; isAlliance++) for (uint8 isAlliance = 0; isAlliance <= 1; isAlliance++)
{ {
if (addclassCache[GetTeamClassIdx(isAlliance, claz)].find(guid) != if (addclassCache[GetTeamClassIdx(isAlliance, claz)].find(guid) !=
addclassCache[GetTeamClassIdx(isAlliance, claz)].end()) addclassCache[GetTeamClassIdx(isAlliance, claz)].end())
{
return true; 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; return false;
} }

View File

@@ -60,7 +60,6 @@ public:
bool IsEmpty() { return !lastChangeTime; } bool IsEmpty() { return !lastChangeTime; }
public:
uint32 value; uint32 value;
uint32 lastChangeTime; uint32 lastChangeTime;
uint32 validIn; uint32 validIn;
@@ -104,10 +103,6 @@ public:
void LogPlayerLocation(); void LogPlayerLocation();
void UpdateAIInternal(uint32 elapsed, bool minimal = false) override; void UpdateAIInternal(uint32 elapsed, bool minimal = false) override;
private:
//void ScaleBotActivity();
public:
uint32 activeBots = 0; uint32 activeBots = 0;
static bool HandlePlayerbotConsoleCommand(ChatHandler* handler, char const* args); static bool HandlePlayerbotConsoleCommand(ChatHandler* handler, char const* args);
bool IsRandomBot(Player* bot); bool IsRandomBot(Player* bot);
@@ -189,6 +184,11 @@ public:
}; };
std::map<uint32, LevelBracket> zone2LevelBracket; std::map<uint32, LevelBracket> zone2LevelBracket;
std::map<uint8, std::vector<WorldLocation>> bankerLocsPerLevelCache; std::map<uint8, std::vector<WorldLocation>> bankerLocsPerLevelCache;
// Account type management
void AssignAccountTypes();
bool IsAccountType(uint32 accountId, uint8 accountType);
protected: protected:
void OnBotLoginInternal(Player* const bot) override; void OnBotLoginInternal(Player* const bot) override;
@@ -218,11 +218,9 @@ private:
void RandomTeleport(Player* bot, std::vector<WorldLocation>& locs, bool hearth = false); void RandomTeleport(Player* bot, std::vector<WorldLocation>& locs, bool hearth = false);
uint32 GetZoneLevel(uint16 mapId, float teleX, float teleY, float teleZ); uint32 GetZoneLevel(uint16 mapId, float teleX, float teleY, float teleZ);
typedef void (RandomPlayerbotMgr::*ConsoleCommandHandler)(Player*); typedef void (RandomPlayerbotMgr::*ConsoleCommandHandler)(Player*);
std::vector<Player*> players; std::vector<Player*> players;
uint32 processTicks; uint32 processTicks;
// std::map<uint32, std::vector<WorldLocation>> rpgLocsCache; // std::map<uint32, std::vector<WorldLocation>> rpgLocsCache;
std::map<uint32, std::map<uint32, std::vector<WorldLocation>>> rpgLocsCacheLevel; std::map<uint32, std::map<uint32, std::vector<WorldLocation>>> rpgLocsCacheLevel;
std::map<TeamId, std::map<BattlegroundTypeId, std::vector<uint32>>> BattleMastersCache; std::map<TeamId, std::map<BattlegroundTypeId, std::vector<uint32>>> BattleMastersCache;
@@ -230,6 +228,12 @@ private:
std::list<uint32> currentBots; std::list<uint32> currentBots;
uint32 bgBotsCount; uint32 bgBotsCount;
uint32 playersLevel; uint32 playersLevel;
// Account lists
std::vector<uint32> rndBotTypeAccounts; // Accounts marked as RNDbot (type 1)
std::vector<uint32> addClassTypeAccounts; // Accounts marked as AddClass (type 2)
//void ScaleBotActivity(); // Deprecated function
}; };
#define sRandomPlayerbotMgr RandomPlayerbotMgr::instance() #define sRandomPlayerbotMgr RandomPlayerbotMgr::instance()