mod-playerbots/src/Bot/PlayerbotMgr.cpp
Mat ff001afd46
Reset instance ID via existing cmd refresh=raid for alt bots (#2422)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
Alt bots can now use refresh=raid cmd if cfg is set to 1.
`AiPlayerbot.ResetInstanceIdForAltBots = 1`


## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.



## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->
Add alt bot, use .playerbots bot refresh=raid and it should say ok after
reseting instance.


## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [ ] No
    - - [x] Yes (**explain why**)
Alt bots will now reset instance ID if cfg is set to 1.


- Does this change add new decision branches or increase maintenance
complexity?
    - - [ ] No
    - - [x] Yes (**explain below**)
It will check if cfg is set to 1 to enable alt bots to reset instance
id.


## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->



<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->

---------

Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
2026-05-30 11:12:54 -07:00

1939 lines
64 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "PlayerbotMgr.h"
#include <cstdio>
#include <cstring>
#include <string>
#include <unordered_set>
#include <openssl/sha.h>
#include <iomanip>
#include <algorithm>
#include "ChannelMgr.h"
#include "CharacterCache.h"
#include "CharacterPackets.h"
#include "Common.h"
#include "Define.h"
#include "Group.h"
#include "GuildMgr.h"
#include "ObjectAccessor.h"
#include "ObjectGuid.h"
#include "ObjectMgr.h"
#include "PlayerbotAIConfig.h"
#include "PlayerbotRepository.h"
#include "PlayerbotFactory.h"
#include "PlayerbotOperations.h"
#include "PlayerbotSecurity.h"
#include "PlayerbotTextMgr.h"
#include "PlayerbotWorldThreadProcessor.h"
#include "Playerbots.h"
#include "PlayerbotGuildMgr.h"
#include "RandomPlayerbotMgr.h"
#include "SharedDefines.h"
#include "WorldSession.h"
#include "BroadcastHelper.h"
#include "WorldSessionMgr.h"
#include "DatabaseEnv.h"
class BotInitGuard
{
public:
BotInitGuard(ObjectGuid guid) : guid(guid), active(false)
{
if (!botsBeingInitialized.contains(guid))
{
botsBeingInitialized.insert(guid);
active = true;
}
}
~BotInitGuard()
{
if (active)
botsBeingInitialized.erase(guid);
}
bool IsLocked() const { return !active; }
private:
ObjectGuid guid;
bool active;
static std::unordered_set<ObjectGuid> botsBeingInitialized;
};
std::unordered_set<ObjectGuid> BotInitGuard::botsBeingInitialized;
std::unordered_map<ObjectGuid, uint32> PlayerbotHolder::botLoading;
PlayerbotHolder::PlayerbotHolder() : PlayerbotAIBase(false) {}
class PlayerbotLoginQueryHolder : public LoginQueryHolder
{
private:
uint32 masterAccountId;
public:
PlayerbotLoginQueryHolder(uint32 masterAccount, uint32 accountId, ObjectGuid guid)
: LoginQueryHolder(accountId, guid), masterAccountId(masterAccount)
{
}
uint32 GetMasterAccountId() const { return masterAccountId; }
};
void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId)
{
if (botLoading.find(playerGuid) != botLoading.end())
return;
// has bot already been added?
Player* bot = ObjectAccessor::FindConnectedPlayer(playerGuid);
if (bot && bot->IsInWorld())
return;
uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(playerGuid);
if (!accountId)
return;
WorldSession* masterSession = masterAccountId ? sWorldSessionMgr->FindSession(masterAccountId) : nullptr;
Player* masterPlayer = masterSession ? masterSession->GetPlayer() : nullptr;
bool isRndbot = !masterAccountId;
bool sameAccount = sPlayerbotAIConfig.allowAccountBots && accountId == masterAccountId;
Guild* guild = masterPlayer ? sGuildMgr->GetGuildById(masterPlayer->GetGuildId()) : nullptr;
bool sameGuild = sPlayerbotAIConfig.allowGuildBots && guild && guild->GetMember(playerGuid);
bool addClassBot = sRandomPlayerbotMgr.IsAddclassBot(playerGuid.GetCounter());
bool linkedAccount = sPlayerbotAIConfig.allowTrustedAccountBots && IsAccountLinked(accountId, masterAccountId);
bool allowed = true;
std::ostringstream out;
std::string botName;
sCharacterCache->GetCharacterNameByGuid(playerGuid, botName);
if (!isRndbot && !sameAccount && !sameGuild && !addClassBot && !linkedAccount)
{
allowed = false;
out << "Failure: You are not allowed to control bot " << botName.c_str();
}
if (masterAccountId && masterPlayer)
{
PlayerbotMgr* mgr = GET_PLAYERBOT_MGR(masterPlayer);
if (!mgr)
{
LOG_DEBUG("playerbots", "PlayerbotMgr not found for master player with GUID: {}", masterPlayer->GetGUID().GetRawValue());
return;
}
uint32 loadingForMaster = 0;
for (auto const& [guid, acctId] : botLoading)
{
if (acctId == masterAccountId)
++loadingForMaster;
}
uint32 count = mgr->GetPlayerbotsCount() + loadingForMaster;
if (count >= PlayerbotAIConfig::instance().maxAddedBots)
{
allowed = false;
out << "Failure: You have added too many bots (more than " << sPlayerbotAIConfig.maxAddedBots << ")";
}
}
if (!allowed)
{
if (masterSession)
{
ChatHandler ch(masterSession);
ch.SendSysMessage(out.str());
}
return;
}
std::shared_ptr<PlayerbotLoginQueryHolder> holder =
std::make_shared<PlayerbotLoginQueryHolder>(masterAccountId, accountId, playerGuid);
if (!holder->Initialize())
{
return;
}
botLoading.emplace(playerGuid, masterAccountId);
// Always login in with world session to avoid race condition
sWorld->AddQueryHolderCallback(CharacterDatabase.DelayQueryHolder(holder))
.AfterComplete(
[](SQLQueryHolderBase const& queryHolder)
{
PlayerbotLoginQueryHolder const& holder = static_cast<PlayerbotLoginQueryHolder const&>(queryHolder);
uint32 masterAccountId = holder.GetMasterAccountId();
if (masterAccountId)
{
// verify and find current world session of master
WorldSession* masterSession = sWorldSessionMgr->FindSession(masterAccountId);
Player* masterPlayer = masterSession ? masterSession->GetPlayer() : nullptr;
if (masterPlayer)
{
PlayerbotHolder* mgr = PlayerbotsMgr::instance().GetPlayerbotMgr(masterPlayer);
if (mgr != nullptr)
{
mgr->HandlePlayerBotLoginCallback(holder);
return;
}
PlayerbotHolder::botLoading.erase(holder.GetGuid());
return;
}
}
RandomPlayerbotMgr ::instance().HandlePlayerBotLoginCallback(holder);
});
}
bool PlayerbotHolder::IsAccountLinked(uint32 accountId, uint32 linkedAccountId)
{
QueryResult result = PlayerbotsDatabase.Query(
"SELECT 1 FROM playerbots_account_links WHERE account_id = {} AND linked_account_id = {}", accountId, linkedAccountId);
return result != nullptr;
}
void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder const& holder)
{
uint32 botAccountId = holder.GetAccountId();
// At login DBC locale should be what the server is set to use by default (as spells etc are hardcoded to ENUS this
// allows channels to work as intended)
WorldSession* botSession =
new WorldSession(botAccountId, "", 0x0, nullptr, SEC_PLAYER, EXPANSION_WRATH_OF_THE_LICH_KING, time_t(0),
sWorld->GetDefaultDbcLocale(), 0, false, false, 0, true);
botSession->HandlePlayerLoginFromDB(holder); // will delete lqh
Player* bot = botSession->GetPlayer();
if (!bot)
{
// Debug log
LOG_DEBUG("mod-playerbots", "Bot player could not be loaded for account ID: {}", botAccountId);
botSession->LogoutPlayer(true);
delete botSession;
PlayerbotHolder::botLoading.erase(holder.GetGuid());
return;
}
uint32 masterAccountId = holder.GetMasterAccountId();
WorldSession* masterSession = masterAccountId ? sWorldSessionMgr->FindSession(masterAccountId) : nullptr;
// Check if masterSession->GetPlayer() is valid
Player* masterPlayer = masterSession ? masterSession->GetPlayer() : nullptr;
if (masterSession && !masterPlayer)
{
LOG_DEBUG("mod-playerbots", "Master session found but no player is associated for master account ID: {}",
masterAccountId);
}
sRandomPlayerbotMgr.OnPlayerLogin(bot);
auto op = std::make_unique<OnBotLoginOperation>(bot->GetGUID(), masterAccountId);
PlayerbotWorldThreadProcessor::instance().QueueOperation(std::move(op));
PlayerbotHolder::botLoading.erase(holder.GetGuid());
}
void PlayerbotHolder::UpdateSessions()
{
for (PlayerBotMap::const_iterator itr = GetPlayerBotsBegin(); itr != GetPlayerBotsEnd(); ++itr)
{
Player* const bot = itr->second;
if (bot->IsBeingTeleported())
{
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (botAI)
{
botAI->HandleTeleportAck();
}
}
else if (bot->IsInWorld())
{
HandleBotPackets(bot->GetSession());
}
}
}
void PlayerbotHolder::HandleBotPackets(WorldSession* session)
{
WorldPacket* packet;
while (session->GetPacketQueue().next(packet))
{
OpcodeClient opcode = static_cast<OpcodeClient>(packet->GetOpcode());
ClientOpcodeHandler const* opHandle = opcodeTable[opcode];
if (!opHandle)
{
LOG_ERROR("playerbots", "Unhandled opcode {} queued for bot session {}. Packet dropped.", static_cast<uint32>(opcode), session->GetAccountId());
delete packet;
continue;
}
opHandle->Call(session, *packet);
delete packet;
}
}
void PlayerbotHolder::LogoutAllBots()
{
/*
while (true)
{
PlayerBotMap::const_iterator itr = GetPlayerBotsBegin();
if (itr == GetPlayerBotsEnd())
break;
Player* bot= itr->second;
if (!GET_PLAYERBOT_AI(bot)->IsRealPlayer())
LogoutPlayerBot(bot->GetGUID());
}
*/
PlayerBotMap bots = playerBots;
for (auto& itr : bots)
{
Player* bot = itr.second;
if (!bot)
continue;
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (!botAI || botAI->IsRealPlayer())
continue;
LogoutPlayerBot(bot->GetGUID());
}
}
void PlayerbotMgr::CancelLogout()
{
Player* master = GetMaster();
if (!master)
return;
for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (!botAI || botAI->IsRealPlayer())
continue;
if (bot->GetSession()->isLogingOut())
{
WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL);
bot->GetSession()->HandleLogoutCancelOpcode(data);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"logout_cancel", "Logout cancelled!", {}));
}
}
for (PlayerBotMap::const_iterator it = sRandomPlayerbotMgr.GetPlayerBotsBegin();
it != sRandomPlayerbotMgr.GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (!botAI || botAI->IsRealPlayer())
continue;
if (botAI->GetMaster() != master)
continue;
if (bot->GetSession()->isLogingOut())
{
WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL);
bot->GetSession()->HandleLogoutCancelOpcode(data);
}
}
}
void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid)
{
if (Player* bot = GetPlayerBot(guid))
{
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (!botAI)
return;
// Queue group cleanup operation for world thread
auto cleanupOp = std::make_unique<BotLogoutGroupCleanupOperation>(guid);
PlayerbotWorldThreadProcessor::instance().QueueOperation(std::move(cleanupOp));
LOG_DEBUG("playerbots", "Bot {} logging out", bot->GetName().c_str());
bot->SaveToDB(false, false);
WorldSession* botWorldSessionPtr = bot->GetSession();
WorldSession* masterWorldSessionPtr = nullptr;
if (botWorldSessionPtr->isLogingOut())
return;
Player* master = botAI->GetMaster();
if (master)
masterWorldSessionPtr = master->GetSession();
// TODO: Review whether or not to implement timed logout.
// Unused block. Useful only for timed logout.
/*
// check for instant logout
bool logout = botWorldSessionPtr->ShouldLogOut(time(nullptr));
if (masterWorldSessionPtr && masterWorldSessionPtr->ShouldLogOut(time(nullptr)))
logout = true;
if (masterWorldSessionPtr && !masterWorldSessionPtr->GetPlayer())
logout = true;
if (bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || bot->HasUnitState(UNIT_STATE_IN_FLIGHT) ||
botWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT))
logout = true;
if (master &&
(master->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || master->HasUnitState(UNIT_STATE_IN_FLIGHT) ||
(masterWorldSessionPtr &&
masterWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT))))
logout = true;
*/
// Instant logout (the only option right now)
{
std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"goodbye", "Goodbye!", {});
botAI->TellMaster(message);
RemoveFromPlayerbotsMap(guid); // deletes bot player ptr inside this WorldSession PlayerBotMap
botWorldSessionPtr->LogoutPlayer(true); // this will delete the bot Player object and PlayerbotAI object
delete botWorldSessionPtr; // finally delete the bot's WorldSession
}
}
}
void PlayerbotHolder::DisablePlayerBot(ObjectGuid guid)
{
if (Player* bot = GetPlayerBot(guid))
{
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (!botAI)
{
return;
}
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"goodbye", "Goodbye!", {}));
bot->StopMoving();
bot->GetMotionMaster()->Clear();
Group* group = bot->GetGroup();
if (group && !bot->InBattleground() && !bot->InBattlegroundQueue() && botAI->HasActivePlayerMaster())
{
PlayerbotRepository::instance().Save(botAI);
}
LOG_DEBUG("playerbots", "Bot {} logged out", bot->GetName().c_str());
bot->SaveToDB(false, false);
if (botAI->GetAiObjectContext()) // Maybe some day re-write to delate all pointer values.
{
TravelTarget* target = botAI->GetAiObjectContext()->GetValue<TravelTarget*>("travel target")->Get();
if (target)
delete target;
}
RemoveFromPlayerbotsMap(guid); // deletes bot player ptr inside this WorldSession PlayerBotMap
delete botAI;
}
}
void PlayerbotHolder::RemoveFromPlayerbotsMap(ObjectGuid guid)
{
playerBots.erase(guid);
}
Player* PlayerbotHolder::GetPlayerBot(ObjectGuid playerGuid) const
{
PlayerBotMap::const_iterator it = playerBots.find(playerGuid);
return (it == playerBots.end()) ? 0 : it->second;
}
Player* PlayerbotHolder::GetPlayerBot(ObjectGuid::LowType lowGuid) const
{
ObjectGuid playerGuid = ObjectGuid::Create<HighGuid::Player>(lowGuid);
PlayerBotMap::const_iterator it = playerBots.find(playerGuid);
return (it == playerBots.end()) ? 0 : it->second;
}
void PlayerbotHolder::OnBotLogin(Player* const bot)
{
// Prevent duplicate login
if (playerBots.find(bot->GetGUID()) != playerBots.end())
{
return;
}
PlayerbotsMgr::instance().AddPlayerbotData(bot, true);
playerBots[bot->GetGUID()] = bot;
OnBotLoginInternal(bot);
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (!botAI)
{
// Log a warning here to indicate that the botAI is null
LOG_DEBUG("mod-playerbots", "PlayerbotAI is null for bot with GUID: {}", bot->GetGUID().GetRawValue());
return;
}
Player* master = botAI->GetMaster();
Group* group = bot->GetGroup();
if (group)
{
bool groupValid = false;
Group::MemberSlotList const& slots = group->GetMemberSlots();
for (Group::MemberSlotList::const_iterator i = slots.begin(); i != slots.end(); ++i)
{
ObjectGuid member = i->guid;
if (master)
{
if (master->GetGUID() == member)
{
groupValid = true;
break;
}
}
// Don't disband alt groups when master goes away
// Controlled by config
if (sPlayerbotAIConfig.KeepAltsInGroup())
{
uint32 account = sCharacterCache->GetCharacterAccountIdByGuid(member);
if (!sPlayerbotAIConfig.IsInRandomAccountList(account))
{
groupValid = true;
break;
}
}
}
if (!groupValid)
{
botAI->LeaveOrDisbandGroup();
}
}
group = bot->GetGroup();
if (group)
{
botAI->ResetStrategies();
}
else
{
botAI->ResetStrategies(!sRandomPlayerbotMgr.IsRandomBot(bot));
}
PlayerbotRepository::instance().Load(botAI);
if (master && !master->HasUnitState(UNIT_STATE_IN_FLIGHT))
{
bot->GetMotionMaster()->MovementExpired();
bot->CleanupAfterTaxiFlight();
}
// check activity
botAI->AllowActivity(ALL_ACTIVITY, true);
// set delay on login
botAI->SetNextCheckDelay(urand(2000, 4000));
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"hello", "Hello!", {}), PLAYERBOT_SECURITY_TALK);
// Queue group operations for world thread
if (master && master->GetGroup() && !group)
{
Group* mgroup = master->GetGroup();
if (mgroup->GetMembersCount() >= 5)
{
if (!mgroup->isRaidGroup() && !mgroup->isLFGGroup() && !mgroup->isBGGroup() && !mgroup->isBFGroup())
{
// Queue ConvertToRaid operation
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(master->GetGUID());
PlayerbotWorldThreadProcessor::instance().QueueOperation(std::move(convertOp));
}
if (mgroup->isRaidGroup())
{
// Queue AddMember operation
auto addOp = std::make_unique<GroupInviteOperation>(master->GetGUID(), bot->GetGUID());
PlayerbotWorldThreadProcessor::instance().QueueOperation(std::move(addOp));
}
}
else
{
// Queue AddMember operation
auto addOp = std::make_unique<GroupInviteOperation>(master->GetGUID(), bot->GetGUID());
PlayerbotWorldThreadProcessor::instance().QueueOperation(std::move(addOp));
}
}
else if (master && !group)
{
// Queue group creation and AddMember operation
auto inviteOp = std::make_unique<GroupInviteOperation>(master->GetGUID(), bot->GetGUID());
PlayerbotWorldThreadProcessor::instance().QueueOperation(std::move(inviteOp));
}
// if (master)
// {
// // bot->TeleportTo(master);
// }
uint32 accountId = bot->GetSession()->GetAccountId();
bool isRandomAccount = sPlayerbotAIConfig.IsInRandomAccountList(accountId);
if (isRandomAccount && sPlayerbotAIConfig.randomBotFixedLevel)
{
bot->SetPlayerFlag(PLAYER_FLAGS_NO_XP_GAIN);
}
else if (isRandomAccount && !sPlayerbotAIConfig.randomBotFixedLevel)
{
bot->RemovePlayerFlag(PLAYER_FLAGS_NO_XP_GAIN);
}
bot->SaveToDB(false, false);
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);
uint32 mixedGearScore =
PlayerbotAI::GetMixedGearScore(master, true, false, 12) * sPlayerbotAIConfig.autoInitEquipLevelLimitRatio;
// work around: distinguish from 0 if no gear
if (mixedGearScore == 0)
mixedGearScore = 1;
PlayerbotFactory factory(bot, master->GetLevel(), ITEM_QUALITY_LEGENDARY, mixedGearScore);
factory.Randomize(false);
}
// bots join World chat if not solo oriented
if (bot->GetLevel() >= 10 && sRandomPlayerbotMgr.IsRandomBot(bot) && GET_PLAYERBOT_AI(bot) &&
GET_PLAYERBOT_AI(bot)->GetGrouperType() != GrouperType::SOLO)
{
// TODO make action/config
// Make the bot join the world channel for chat
WorldPacket pkt(CMSG_JOIN_CHANNEL);
pkt << uint32(0) << uint8(0) << uint8(0);
pkt << std::string("World");
pkt << ""; // Pass
bot->GetSession()->HandleJoinChannel(pkt);
}
// join standard channels
uint8 locale = BroadcastHelper::GetLocale();
AreaTableEntry const* current_zone = GET_PLAYERBOT_AI(bot)->GetCurrentZone();
ChannelMgr* cMgr = ChannelMgr::forTeam(bot->GetTeamId());
std::string current_zone_name = current_zone ? GET_PLAYERBOT_AI(bot)->GetLocalizedAreaName(current_zone) : "";
if (current_zone && cMgr)
{
for (uint32 i = 0; i < sChatChannelsStore.GetNumRows(); ++i)
{
ChatChannelsEntry const* channel = sChatChannelsStore.LookupEntry(i);
if (!channel)
continue;
Channel* new_channel = nullptr;
switch (channel->ChannelID)
{
case ChatChannelId::GENERAL:
case ChatChannelId::LOCAL_DEFENSE:
{
char new_channel_name_buf[100];
snprintf(new_channel_name_buf, 100, channel->pattern[locale], current_zone_name.c_str());
new_channel = cMgr->GetJoinChannel(new_channel_name_buf, channel->ChannelID);
break;
}
case ChatChannelId::TRADE:
case ChatChannelId::GUILD_RECRUITMENT:
{
char new_channel_name_buf[100];
//3459 is ID for a zone named "City" (only exists for the sake of using its name)
//Currently in magons TBC, if you switch zones, then you join "Trade - <zone>" and "GuildRecruitment - <zone>"
//which is a core bug, should be "Trade - City" and "GuildRecruitment - City" in both 1.12 and TBC
//but if you (actual player) logout in a city and log back in - you join "City" versions
snprintf(new_channel_name_buf, 100, channel->pattern[locale], GET_PLAYERBOT_AI(bot)->GetLocalizedAreaName(GetAreaEntryByAreaID(3459)).c_str());
new_channel = cMgr->GetJoinChannel(new_channel_name_buf, channel->ChannelID);
break;
}
case ChatChannelId::LOOKING_FOR_GROUP:
case ChatChannelId::WORLD_DEFENSE:
{
new_channel = cMgr->GetJoinChannel(channel->pattern[locale], channel->ChannelID);
break;
}
default:
break;
}
if (new_channel)
new_channel->JoinChannel(bot, "");
}
}
}
std::string const PlayerbotHolder::ProcessBotCommand(std::string const cmd, ObjectGuid guid, ObjectGuid masterguid,
bool admin, uint32 masterAccountId, uint32)
{
if (!sPlayerbotAIConfig.enabled || guid.IsEmpty())
return "bot system is disabled";
//bool isRandomBot = sRandomPlayerbotMgr.IsRandomBot(guid.GetCounter()); //not used, line marked for removal.
//bool isRandomAccount = sPlayerbotAIConfig.IsInRandomAccountList(botAccount); //not used, shadowed, line marked for removal.
//bool isMasterAccount = (masterAccountId == botAccount); //not used, line marked for removal.
if (cmd == "add" || cmd == "addaccount" || cmd == "login")
{
if (ObjectAccessor::FindPlayer(guid))
return "player already logged in";
// For addaccount command, verify it's an account name
if (cmd == "addaccount")
{
uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(guid);
if (!accountId)
{
return "character not found";
}
if (!sPlayerbotAIConfig.allowAccountBots && accountId != masterAccountId &&
!(sPlayerbotAIConfig.allowTrustedAccountBots && IsAccountLinked(accountId, masterAccountId)))
{
return "you can only add bots from your own account or linked accounts";
}
}
AddPlayerBot(guid, masterAccountId);
return "ok";
}
else if (cmd == "remove" || cmd == "logout" || cmd == "rm")
{
if (!ObjectAccessor::FindPlayer(guid))
return "player is offline";
if (!GetPlayerBot(guid))
return "not your bot";
LogoutPlayerBot(guid);
return "ok";
}
// if (admin)
// {
Player* bot = GetPlayerBot(guid);
if (!bot)
bot = sRandomPlayerbotMgr.GetPlayerBot(guid);
if (!bot)
return "bot not found";
bool addClassBot = sRandomPlayerbotMgr.IsAddclassBot(guid.GetCounter());
if (!addClassBot)
{
if (!(cmd == "refresh=raid" && sPlayerbotAIConfig.resetInstanceIdForAltBots))
return "ERROR: You can only use this command on addclass bots.";
}
if (!admin)
{
Player* master = ObjectAccessor::FindConnectedPlayer(masterguid);
if (master && (master->IsInCombat() || bot->IsInCombat()))
{
return "ERROR: You can not use this command during combat.";
}
}
if (GET_PLAYERBOT_AI(bot))
{
if (Player* master = GET_PLAYERBOT_AI(bot)->GetMaster())
{
if (master->GetSession()->GetSecurity() <= SEC_PLAYER && sPlayerbotAIConfig.autoInitOnly &&
cmd != "init=auto")
{
return "The command is not allowed, use init=auto instead.";
}
// Use boot guard
BotInitGuard guard(bot->GetGUID());
if (guard.IsLocked())
{
return "Initialization already in progress, please wait.";
}
int gs;
if (cmd == "init=white" || cmd == "init=common")
{
PlayerbotFactory factory(bot, master->GetLevel(), ITEM_QUALITY_NORMAL);
factory.Randomize(false);
return "ok";
}
else if (cmd == "init=green" || cmd == "init=uncommon")
{
PlayerbotFactory factory(bot, master->GetLevel(), ITEM_QUALITY_UNCOMMON);
factory.Randomize(false);
return "ok";
}
else if (cmd == "init=blue" || cmd == "init=rare")
{
PlayerbotFactory factory(bot, master->GetLevel(), ITEM_QUALITY_RARE);
factory.Randomize(false);
return "ok";
}
else if (cmd == "init=epic" || cmd == "init=purple")
{
PlayerbotFactory factory(bot, master->GetLevel(), ITEM_QUALITY_EPIC);
factory.Randomize(false);
return "ok";
}
else if (cmd == "init=legendary" || cmd == "init=yellow")
{
PlayerbotFactory factory(bot, master->GetLevel(), ITEM_QUALITY_LEGENDARY);
factory.Randomize(false);
return "ok";
}
else if (cmd == "init=auto")
{
uint32 mixedGearScore = PlayerbotAI::GetMixedGearScore(master, true, false, 12) *
sPlayerbotAIConfig.autoInitEquipLevelLimitRatio;
// work around: distinguish from 0 if no gear
if (mixedGearScore == 0)
mixedGearScore = 1;
PlayerbotFactory factory(bot, master->GetLevel(), ITEM_QUALITY_LEGENDARY, mixedGearScore);
factory.Randomize(false);
return "ok, gear score limit: " + std::to_string(mixedGearScore / PlayerbotAI::GetItemScoreMultiplier(ItemQualities(ITEM_QUALITY_EPIC))) +
"(for epic)";
}
else if (cmd.starts_with("init=") && sscanf(cmd.c_str(), "init=%d", &gs) != -1)
{
PlayerbotFactory factory(bot, master->GetLevel(), ITEM_QUALITY_LEGENDARY, gs);
factory.Randomize(false);
return "ok, gear score limit: " + std::to_string(gs / PlayerbotAI::GetItemScoreMultiplier(ItemQualities(ITEM_QUALITY_EPIC))) + "(for epic)";
}
}
if (cmd == "refresh=raid")
{ // TODO: This function is not perfect yet. If you are already in a raid,
// after the command is executed, the AI needs to go back online or exit the raid and re-enter.
PlayerbotFactory factory(bot, bot->GetLevel());
factory.UnbindInstance();
return "ok";
}
}
if (cmd == "levelup" || cmd == "level")
{
PlayerbotFactory factory(bot, bot->GetLevel());
factory.Randomize(true);
return "ok";
}
else if (cmd == "refresh")
{
PlayerbotFactory factory(bot, bot->GetLevel());
factory.Refresh();
return "ok";
}
else if (cmd == "random")
{
sRandomPlayerbotMgr.Randomize(bot);
return "ok";
}
else if (cmd == "quests")
{
PlayerbotFactory factory(bot, bot->GetLevel());
factory.InitInstanceQuests();
return "Initialization quests";
}
// }
return "unknown command";
}
// Added for gender choice : Returns the gender of an offline character: 0 = male, 1 = female.
static uint8 GetOfflinePlayerGender(ObjectGuid guid)
{
QueryResult result = CharacterDatabase.Query(
"SELECT gender FROM characters WHERE guid = {}", guid.GetCounter());
if (result)
return (*result)[0].Get<uint8>(); // 0 = male, 1 = female
return GENDER_MALE; // fallback value
}
bool PlayerbotMgr::HandlePlayerbotMgrCommand(ChatHandler* handler, char const* args)
{
if (!sPlayerbotAIConfig.enabled)
{
handler->PSendSysMessage("|cffff0000Playerbot system is currently disabled!");
return false;
}
WorldSession* m_session = handler->GetSession();
if (!m_session)
{
handler->PSendSysMessage("You may only add bots from an active session");
return false;
}
Player* player = m_session->GetPlayer();
PlayerbotMgr* mgr = GET_PLAYERBOT_MGR(player);
if (!mgr)
{
handler->PSendSysMessage("You cannot control bots yet");
return false;
}
std::vector<std::string> messages = mgr->HandlePlayerbotCommand(args, player);
if (messages.empty())
return true;
for (std::vector<std::string>::iterator i = messages.begin(); i != messages.end(); ++i)
{
handler->PSendSysMessage("{}", i->c_str());
}
return true;
}
std::vector<std::string> PlayerbotHolder::HandlePlayerbotCommand(char const* args, Player* master)
{
std::vector<std::string> messages;
if (!*args)
{
messages.push_back("usage: list/reload/tweak/self or add/addaccount/init/remove PLAYERNAME\n");
messages.push_back("usage: addclass CLASSNAME [male|female|0|1]");
return messages;
}
char* cmd = strtok((char*)args, " ");
char* charname = strtok(nullptr, " ");
char* genderArg = strtok(nullptr, " "); // Added for gender choice [male|female|0|1] optionnel
if (!cmd)
{
messages.push_back("usage: list/reload/tweak/self or add/init/remove PLAYERNAME or addclass CLASSNAME [male|female]");
return messages;
}
if (!strcmp(cmd, "initself"))
{
if (master->CanBeGameMaster())
{
// OnBotLogin(master);
PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_EPIC);
factory.Randomize(false);
messages.push_back("initself ok");
return messages;
}
else
{
messages.push_back("ERROR: Only GM can use this command.");
return messages;
}
}
if (!strncmp(cmd, "initself=", 9))
{
if (!strcmp(cmd, "initself=uncommon"))
{
if (master->CanBeGameMaster())
{
// OnBotLogin(master);
PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_UNCOMMON);
factory.Randomize(false);
messages.push_back("initself ok");
return messages;
}
else
{
messages.push_back("ERROR: Only GM can use this command.");
return messages;
}
}
if (!strcmp(cmd, "initself=rare"))
{
if (master->CanBeGameMaster())
{
// OnBotLogin(master);
PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_RARE);
factory.Randomize(false);
messages.push_back("initself ok");
return messages;
}
else
{
messages.push_back("ERROR: Only GM can use this command.");
return messages;
}
}
if (!strcmp(cmd, "initself=epic"))
{
if (master->CanBeGameMaster())
{
// OnBotLogin(master);
PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_EPIC);
factory.Randomize(false);
messages.push_back("initself ok");
return messages;
}
else
{
messages.push_back("ERROR: Only GM can use this command.");
return messages;
}
}
if (!strcmp(cmd, "initself=legendary"))
{
if (master->CanBeGameMaster())
{
// OnBotLogin(master);
PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_LEGENDARY);
factory.Randomize(false);
messages.push_back("initself ok");
return messages;
}
else
{
messages.push_back("ERROR: Only GM can use this command.");
return messages;
}
}
int32 gs;
if (sscanf(cmd, "initself=%d", &gs) != -1)
{
if (master->CanBeGameMaster())
{
// OnBotLogin(master);
PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_LEGENDARY, gs);
factory.Randomize(false);
messages.push_back("initself ok, gs = " + std::to_string(gs));
return messages;
}
else
{
messages.push_back("ERROR: Only GM can use this command.");
return messages;
}
}
}
if (!strcmp(cmd, "list"))
{
messages.push_back(ListBots(master));
return messages;
}
if (!strcmp(cmd, "reload"))
{
if (master->CanBeGameMaster())
{
sPlayerbotAIConfig.Initialize();
messages.push_back("Config reloaded.");
return messages;
}
else
{
messages.push_back("ERROR: Only GM can use this command.");
return messages;
}
}
if (!strcmp(cmd, "tweak"))
{
sPlayerbotAIConfig.tweakValue = sPlayerbotAIConfig.tweakValue++;
if (sPlayerbotAIConfig.tweakValue > 2)
sPlayerbotAIConfig.tweakValue = 0;
messages.push_back("Set tweakvalue to " + std::to_string(sPlayerbotAIConfig.tweakValue));
return messages;
}
if (!strcmp(cmd, "self"))
{
if (GET_PLAYERBOT_AI(master))
{
messages.push_back("Disable player botAI");
delete GET_PLAYERBOT_AI(master);
}
else if (sPlayerbotAIConfig.selfBotLevel == 0)
messages.push_back("Self-bot is disabled");
else if (sPlayerbotAIConfig.selfBotLevel == 1 && !master->CanBeGameMaster())
messages.push_back("You do not have permission to enable player botAI");
else
{
messages.push_back("Enable player botAI");
PlayerbotsMgr::instance().AddPlayerbotData(master, true);
GET_PLAYERBOT_AI(master)->SetMaster(master);
}
return messages;
}
if (!strcmp(cmd, "lookup"))
{
messages.push_back(LookupBots(master));
return messages;
}
if (!strcmp(cmd, "addclass"))
{
if (sPlayerbotAIConfig.addClassCommand == 0 && !master->CanBeGameMaster())
{
messages.push_back("You do not have permission to create bot by addclass command");
return messages;
}
if (!charname)
{
messages.push_back(
"addclass: invalid CLASSNAME(warrior/paladin/hunter/rogue/priest/shaman/mage/warlock/druid/dk)");
return messages;
}
uint8 claz;
if (!strcmp(charname, "warrior"))
{
claz = 1;
}
else if (!strcmp(charname, "paladin"))
{
claz = 2;
}
else if (!strcmp(charname, "hunter"))
{
claz = 3;
}
else if (!strcmp(charname, "rogue"))
{
claz = 4;
}
else if (!strcmp(charname, "priest"))
{
claz = 5;
}
else if (!strcmp(charname, "shaman"))
{
claz = 7;
}
else if (!strcmp(charname, "mage"))
{
claz = 8;
}
else if (!strcmp(charname, "warlock"))
{
claz = 9;
}
else if (!strcmp(charname, "druid"))
{
claz = 11;
}
else if (!strcmp(charname, "dk"))
{
claz = 6;
}
else
{
messages.push_back("Error: Invalid Class. Try again.");
return messages;
}
// Added for gender choice : Parsing gender
int8 gender = -1; // -1 = gender will be random
if (genderArg)
{
std::string g = genderArg;
std::transform(g.begin(), g.end(), g.begin(), ::tolower);
if (g == "male" || g == "0")
gender = GENDER_MALE; // 0
else if (g == "female" || g == "1")
gender = GENDER_FEMALE; // 1
else
{
messages.push_back("Unknown gender : " + g + " (male/female/0/1)");
return messages;
}
} //end
if (claz == 6 && master->GetLevel() < sWorld->getIntConfig(CONFIG_START_HEROIC_PLAYER_LEVEL))
{
messages.push_back("Your level is too low to summon Deathknight");
return messages;
}
uint8 teamId = master->GetTeamId(true);
const std::unordered_set<ObjectGuid> &guidCache = sRandomPlayerbotMgr.addclassCache[RandomPlayerbotMgr::GetTeamClassIdx(teamId == TEAM_ALLIANCE, claz)];
for (const ObjectGuid &guid: guidCache)
{
// If the user requested a specific gender, skip any character that doesn't match.
if (gender != -1 && GetOfflinePlayerGender(guid) != gender)
continue;
if (botLoading.find(guid) != botLoading.end())
continue;
if (ObjectAccessor::FindConnectedPlayer(guid))
continue;
uint32 guildId = sCharacterCache->GetCharacterGuildIdByGuid(guid);
if (guildId && PlayerbotGuildMgr::instance().IsRealGuild(guildId))
continue;
AddPlayerBot(guid, master->GetSession()->GetAccountId());
messages.push_back("Add class " + std::string(charname));
return messages;
}
messages.push_back("Add class failed, no available characters!");
return messages;
}
std::string charnameStr;
if (!charname)
{
std::string name;
bool isPlayer = sCharacterCache->GetCharacterNameByGuid(master->GetTarget(), name);
// Player* tPlayer = ObjectAccessor::FindConnectedPlayer(master->GetTarget());
if (isPlayer)
{
charnameStr = name;
}
else
{
messages.push_back("usage: list/reload/tweak/self or add/init/remove PLAYERNAME");
return messages;
}
}
else
{
charnameStr = charname;
}
std::string const cmdStr = cmd;
std::unordered_set<std::string> bots;
if (charnameStr == "*" && master)
{
Group* group = master->GetGroup();
if (!group)
{
messages.push_back("you must be in group");
return messages;
}
Group::MemberSlotList slots = group->GetMemberSlots();
for (Group::member_citerator i = slots.begin(); i != slots.end(); i++)
{
ObjectGuid member = i->guid;
if (member == master->GetGUID())
continue;
std::string bot;
if (sCharacterCache->GetCharacterNameByGuid(member, bot))
bots.insert(bot);
}
}
if (charnameStr == "!" && master && master->GetSession()->GetSecurity() > SEC_GAMEMASTER)
{
for (PlayerBotMap::const_iterator i = GetPlayerBotsBegin(); i != GetPlayerBotsEnd(); ++i)
{
if (Player* bot = i->second)
if (bot->IsInWorld())
bots.insert(bot->GetName());
}
}
std::vector<std::string> chars = split(charnameStr, ',');
for (std::vector<std::string>::iterator i = chars.begin(); i != chars.end(); i++)
{
std::string s = *i;
if (!strcmp(cmd, "addaccount"))
{
// When using addaccount, first try to get account ID directly
uint32 accountId = GetAccountId(s);
if (!accountId)
{
// If not found, try to get account ID from character name
std::string charName = s;
if (!normalizePlayerName(charName))
{
messages.push_back("Neither account nor character '" + s + "' found");
continue;
}
ObjectGuid charGuid = sCharacterCache->GetCharacterGuidByName(charName);
if (!charGuid)
{
messages.push_back("Neither account nor character '" + s + "' found");
continue;
}
accountId = sCharacterCache->GetCharacterAccountIdByGuid(charGuid);
if (!accountId)
{
messages.push_back("Could not find account for character '" + s + "'");
continue;
}
}
QueryResult results = CharacterDatabase.Query("SELECT name FROM characters WHERE account = {}", accountId);
if (results)
{
do
{
Field* fields = results->Fetch();
std::string const charName = fields[0].Get<std::string>();
bots.insert(charName);
} while (results->NextRow());
}
}
else
{
// For regular add command, only add the specific character
if (!normalizePlayerName(s))
{
messages.push_back("Character '" + *i + "' not found");
continue;
}
ObjectGuid charGuid = sCharacterCache->GetCharacterGuidByName(s);
if (!charGuid)
{
messages.push_back("Character '" + s + "' not found");
continue;
}
bots.insert(s);
}
}
for (auto i = bots.begin(); i != bots.end(); ++i)
{
std::string const bot = *i;
std::ostringstream out;
out << cmdStr << ": " << bot << " - ";
ObjectGuid member = sCharacterCache->GetCharacterGuidByName(bot);
if (!member)
{
out << "character not found";
}
else if (master && member != master->GetGUID())
{
out << ProcessBotCommand(cmdStr, member, master->GetGUID(),
master->CanBeGameMaster(),
master->GetSession()->GetAccountId(), master->GetGuildId());
}
else if (!master)
{
out << ProcessBotCommand(cmdStr, member, ObjectGuid::Empty, true, -1, -1);
}
messages.push_back(out.str());
}
return messages;
}
uint32 PlayerbotHolder::GetAccountId(std::string const name) { return AccountMgr::GetId(name); }
uint32 PlayerbotHolder::GetAccountId(ObjectGuid guid)
{
if (!guid.IsPlayer())
return 0;
// prevent DB access for online player
if (Player* player = ObjectAccessor::FindConnectedPlayer(guid))
return player->GetSession()->GetAccountId();
ObjectGuid::LowType lowguid = guid.GetCounter();
if (QueryResult result = CharacterDatabase.Query("SELECT account FROM characters WHERE guid = {}", lowguid))
{
uint32 acc = (*result)[0].Get<uint32>();
return acc;
}
return 0;
}
std::string const PlayerbotHolder::ListBots(Player* master)
{
std::set<std::string> bots;
std::map<uint8, std::string> classNames;
classNames[CLASS_DEATH_KNIGHT] = "Death Knight";
classNames[CLASS_DRUID] = "Druid";
classNames[CLASS_HUNTER] = "Hunter";
classNames[CLASS_MAGE] = "Mage";
classNames[CLASS_PALADIN] = "Paladin";
classNames[CLASS_PRIEST] = "Priest";
classNames[CLASS_ROGUE] = "Rogue";
classNames[CLASS_SHAMAN] = "Shaman";
classNames[CLASS_WARLOCK] = "Warlock";
classNames[CLASS_WARRIOR] = "Warrior";
classNames[CLASS_DEATH_KNIGHT] = "DeathKnight";
std::map<std::string, std::string> online;
std::vector<std::string> names;
std::map<std::string, std::string> classes;
for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
std::string const name = bot->GetName();
bots.insert(name);
names.push_back(name);
online[name] = "+";
classes[name] = classNames[bot->getClass()];
}
if (master)
{
QueryResult results = CharacterDatabase.Query("SELECT class, name FROM characters WHERE account = {}",
master->GetSession()->GetAccountId());
if (results)
{
do
{
Field* fields = results->Fetch();
uint8 cls = fields[0].Get<uint8>();
std::string const name = fields[1].Get<std::string>();
if (bots.find(name) == bots.end() && name != master->GetSession()->GetPlayerName())
{
names.push_back(name);
online[name] = "-";
classes[name] = classNames[cls];
}
} while (results->NextRow());
}
}
std::sort(names.begin(), names.end());
if (Group* group = master->GetGroup())
{
Group::MemberSlotList const& groupSlot = group->GetMemberSlots();
for (Group::member_citerator itr = groupSlot.begin(); itr != groupSlot.end(); itr++)
{
Player* member = ObjectAccessor::FindPlayer(itr->guid);
if (member && sRandomPlayerbotMgr.IsRandomBot(member))
{
std::string const name = member->GetName();
names.push_back(name);
online[name] = "+";
classes[name] = classNames[member->getClass()];
}
}
}
std::ostringstream out;
bool first = true;
out << "Bot roster: ";
for (std::vector<std::string>::iterator i = names.begin(); i != names.end(); ++i)
{
if (first)
first = false;
else
out << ", ";
std::string const name = *i;
out << online[name] << name << " " << classes[name];
}
return out.str();
}
std::string const PlayerbotHolder::LookupBots(Player*)
{
std::list<std::string> messages;
messages.push_back("Classes Available:");
messages.push_back("|TInterface\\icons\\INV_Sword_27.png:25:25:0:-1|t Warrior");
messages.push_back("|TInterface\\icons\\INV_Hammer_01.png:25:25:0:-1|t Paladin");
messages.push_back("|TInterface\\icons\\INV_Weapon_Bow_07.png:25:25:0:-1|t Hunter");
messages.push_back("|TInterface\\icons\\INV_ThrowingKnife_04.png:25:25:0:-1|t Rogue");
messages.push_back("|TInterface\\icons\\INV_Staff_30.png:25:25:0:-1|t Priest");
messages.push_back("|TInterface\\icons\\inv_jewelry_talisman_04.png:25:25:0:-1|t Shaman");
messages.push_back("|TInterface\\icons\\INV_staff_30.png:25:25:0:-1|t Mage");
messages.push_back("|TInterface\\icons\\INV_staff_30.png:25:25:0:-1|t Warlock");
messages.push_back("|TInterface\\icons\\Ability_Druid_Maul.png:25:25:0:-1|t Druid");
messages.push_back("DK");
messages.push_back("(Usage: .bot lookup CLASS)");
std::string ret_msg;
for (std::string msg : messages)
{
ret_msg += msg + "\n";
}
return ret_msg;
}
uint32 PlayerbotHolder::GetPlayerbotsCountByClass(uint32 cls)
{
uint32 count = 0;
for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
if (bot && bot->IsInWorld() && bot->getClass() == cls)
{
count++;
}
}
return count;
}
PlayerbotMgr::PlayerbotMgr(Player* const master) : PlayerbotHolder(), master(master), lastErrorTell(0) {}
PlayerbotMgr::~PlayerbotMgr()
{
if (master)
PlayerbotsMgr::instance().RemovePlayerBotData(master->GetGUID(), false);
}
void PlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/)
{
SetNextCheckDelay(sPlayerbotAIConfig.reactDelay);
CheckTellErrors(elapsed);
}
void PlayerbotMgr::HandleCommand(uint32 type, std::string const text)
{
Player* master = GetMaster();
if (!master)
return;
if (text.find(sPlayerbotAIConfig.commandSeparator) != std::string::npos)
{
std::vector<std::string> commands;
split(commands, text, sPlayerbotAIConfig.commandSeparator.c_str());
for (std::vector<std::string>::iterator i = commands.begin(); i != commands.end(); ++i)
{
HandleCommand(type, *i);
}
return;
}
for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (botAI)
botAI->HandleCommand(type, text, master);
}
for (PlayerBotMap::const_iterator it = sRandomPlayerbotMgr.GetPlayerBotsBegin();
it != sRandomPlayerbotMgr.GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (botAI && botAI->GetMaster() == master)
botAI->HandleCommand(type, text, master);
}
}
void PlayerbotMgr::HandleMasterIncomingPacket(WorldPacket const& packet)
{
for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
if (!bot)
continue;
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (botAI)
botAI->HandleMasterIncomingPacket(packet);
}
for (PlayerBotMap::const_iterator it = sRandomPlayerbotMgr.GetPlayerBotsBegin();
it != sRandomPlayerbotMgr.GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (botAI && botAI->GetMaster() == GetMaster())
botAI->HandleMasterIncomingPacket(packet);
}
switch (packet.GetOpcode())
{
// if master is logging out, log out all bots
case CMSG_LOGOUT_REQUEST:
{
Player* master = GetMaster();
if (master)
{
// Replicate the AFK logout prevention checks from WorldSession::HandleLogoutRequestOpcode
// so bots are not logged out when the master's own logout is going to be prevented.
AreaTableEntry const* areaEntry = sAreaTableStore.LookupEntry(master->GetAreaId());
bool preventAfkSanctuaryLogout = sWorld->getIntConfig(CONFIG_AFK_PREVENT_LOGOUT) == 1
&& master->isAFK() && areaEntry && areaEntry->IsSanctuary();
bool preventAfkLogout = sWorld->getIntConfig(CONFIG_AFK_PREVENT_LOGOUT) == 2
&& master->isAFK();
if (preventAfkSanctuaryLogout || preventAfkLogout)
{
break;
}
}
LogoutAllBots();
break;
}
// if master cancelled logout, cancel too
case CMSG_LOGOUT_CANCEL:
{
CancelLogout();
break;
}
}
}
void PlayerbotMgr::HandleMasterOutgoingPacket(WorldPacket const& packet)
{
for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (botAI)
botAI->HandleMasterOutgoingPacket(packet);
}
for (PlayerBotMap::const_iterator it = sRandomPlayerbotMgr.GetPlayerBotsBegin();
it != sRandomPlayerbotMgr.GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (botAI && botAI->GetMaster() == GetMaster())
botAI->HandleMasterOutgoingPacket(packet);
}
}
void PlayerbotMgr::SaveToDB()
{
for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
bot->SaveToDB(false, false);
}
for (PlayerBotMap::const_iterator it = sRandomPlayerbotMgr.GetPlayerBotsBegin();
it != sRandomPlayerbotMgr.GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
if (GET_PLAYERBOT_AI(bot) && GET_PLAYERBOT_AI(bot)->GetMaster() == GetMaster())
bot->SaveToDB(false, false);
}
}
void PlayerbotMgr::OnBotLoginInternal(Player* const bot)
{
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (!botAI)
{
return;
}
botAI->SetMaster(master);
botAI->ResetStrategies();
LOG_INFO("playerbots", "Bot {} logged in", bot->GetName().c_str());
}
void PlayerbotMgr::OnPlayerLogin(Player* player)
{
if (!player)
return;
WorldSession* session = player->GetSession();
if (!session)
{
LOG_WARN("playerbots", "Unable to register locale priority for player {} because the session is missing", player->GetName());
return;
}
// DB locale (source of bot text translation)
LocaleConstant const databaseLocale = session->GetSessionDbLocaleIndex();
// For bot texts (DB-driven), prefer the database locale with a safe fallback.
LocaleConstant usedLocale = databaseLocale;
if (usedLocale >= TOTAL_LOCALES)
usedLocale = LOCALE_enUS; // fallback
// set locale priority for bot texts
PlayerbotTextMgr::instance().AddLocalePriority(usedLocale);
if (sPlayerbotAIConfig.selfBotLevel > 2)
HandlePlayerbotCommand("self", player);
if (!sPlayerbotAIConfig.botAutologin)
return;
uint32 accountId = session->GetAccountId();
QueryResult results = CharacterDatabase.Query("SELECT name FROM characters WHERE account = {}", accountId);
if (results)
{
std::ostringstream out;
out << "add ";
bool first = true;
do
{
Field* fields = results->Fetch();
if (first)
first = false;
else
out << ",";
out << fields[0].Get<std::string>();
} while (results->NextRow());
HandlePlayerbotCommand(out.str().c_str(), player);
}
}
void PlayerbotMgr::TellError(std::string const botName, std::string const text)
{
std::set<std::string> names = errors[text];
if (names.find(botName) == names.end())
{
names.insert(botName);
}
errors[text] = names;
}
void PlayerbotMgr::CheckTellErrors(uint32 /*elapsed*/)
{
time_t now = time(nullptr);
if ((now - lastErrorTell) < sPlayerbotAIConfig.errorDelay / 1000)
return;
lastErrorTell = now;
for (PlayerBotErrorMap::iterator i = errors.begin(); i != errors.end(); ++i)
{
std::string const text = i->first;
std::set<std::string> names = i->second;
std::ostringstream out;
bool first = true;
for (std::set<std::string>::iterator j = names.begin(); j != names.end(); ++j)
{
if (!first)
out << ", ";
else
first = false;
out << *j;
}
out << "|cfff00000: " << text;
ChatHandler(master->GetSession()).PSendSysMessage(out.str().c_str());
}
errors.clear();
}
void PlayerbotsMgr::AddPlayerbotData(Player* player, bool isBotAI)
{
if (!player)
{
return;
}
// If the guid already exists in the map, remove it
if (!isBotAI)
{
std::unordered_map<ObjectGuid, PlayerbotAIBase*>::iterator itr = _playerbotsMgrMap.find(player->GetGUID());
if (itr != _playerbotsMgrMap.end())
{
_playerbotsMgrMap.erase(itr);
}
PlayerbotMgr* playerbotMgr = new PlayerbotMgr(player);
ASSERT(_playerbotsMgrMap.emplace(player->GetGUID(), playerbotMgr).second);
playerbotMgr->OnPlayerLogin(player);
}
else
{
std::unordered_map<ObjectGuid, PlayerbotAIBase*>::iterator itr = _playerbotsAIMap.find(player->GetGUID());
if (itr != _playerbotsAIMap.end())
{
_playerbotsAIMap.erase(itr);
}
PlayerbotAI* botAI = new PlayerbotAI(player);
ASSERT(_playerbotsAIMap.emplace(player->GetGUID(), botAI).second);
}
}
void PlayerbotsMgr::RemovePlayerBotData(ObjectGuid const& guid, bool is_AI)
{
if (is_AI)
{
std::unordered_map<ObjectGuid, PlayerbotAIBase*>::iterator itr = _playerbotsAIMap.find(guid);
if (itr != _playerbotsAIMap.end())
{
_playerbotsAIMap.erase(itr);
}
}
else
{
std::unordered_map<ObjectGuid, PlayerbotAIBase*>::iterator itr = _playerbotsMgrMap.find(guid);
if (itr != _playerbotsMgrMap.end())
{
_playerbotsMgrMap.erase(itr);
}
}
}
PlayerbotAI* PlayerbotsMgr::GetPlayerbotAI(Player* player)
{
if (!(sPlayerbotAIConfig.enabled) || !player)
{
return nullptr;
}
// if (player->GetSession()->isLogingOut() || player->IsDuringRemoveFromWorld())
// {
// return nullptr;
// }
auto itr = _playerbotsAIMap.find(player->GetGUID());
if (itr != _playerbotsAIMap.end())
{
if (itr->second->IsBotAI())
return dynamic_cast<PlayerbotAI*>(itr->second);
}
return nullptr;
}
PlayerbotMgr* PlayerbotsMgr::GetPlayerbotMgr(Player* player)
{
if (!(sPlayerbotAIConfig.enabled) || !player)
{
return nullptr;
}
auto itr = _playerbotsMgrMap.find(player->GetGUID());
if (itr != _playerbotsMgrMap.end())
{
if (!itr->second->IsBotAI())
return dynamic_cast<PlayerbotMgr*>(itr->second);
}
return nullptr;
}
void PlayerbotMgr::HandleSetSecurityKeyCommand(Player* player, const std::string& key)
{
uint32 accountId = player->GetSession()->GetAccountId();
// Hash the security key using SHA-256
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256((unsigned char*)key.c_str(), key.size(), hash);
// Convert the hash to a hexadecimal string
std::ostringstream hashedKey;
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i)
hashedKey << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i];
// Store the hashed key in the database
PlayerbotsDatabase.Execute(
"REPLACE INTO playerbots_account_keys (account_id, security_key) VALUES ({}, '{}')",
accountId, hashedKey.str());
ChatHandler(player->GetSession()).PSendSysMessage("Security key set successfully.");
}
void PlayerbotMgr::HandleLinkAccountCommand(Player* player, const std::string& accountName, const std::string& key)
{
QueryResult result = LoginDatabase.Query("SELECT id FROM account WHERE username = '{}'", accountName);
if (!result)
{
ChatHandler(player->GetSession()).PSendSysMessage("Account not found.");
return;
}
Field* fields = result->Fetch();
uint32 linkedAccountId = fields[0].Get<uint32>();
result = PlayerbotsDatabase.Query("SELECT security_key FROM playerbots_account_keys WHERE account_id = {}", linkedAccountId);
if (!result)
{
ChatHandler(player->GetSession()).PSendSysMessage("Invalid security key.");
return;
}
// Hash the provided key
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256((unsigned char*)key.c_str(), key.size(), hash);
// Convert the hash to a hexadecimal string
std::ostringstream hashedKey;
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i)
hashedKey << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i];
// Compare the hashed key with the stored hashed key
std::string storedKey = result->Fetch()->Get<std::string>();
if (hashedKey.str() != storedKey)
{
ChatHandler(player->GetSession()).PSendSysMessage("Invalid security key.");
return;
}
uint32 accountId = player->GetSession()->GetAccountId();
PlayerbotsDatabase.Execute(
"INSERT IGNORE INTO playerbots_account_links (account_id, linked_account_id) VALUES ({}, {})",
accountId, linkedAccountId);
PlayerbotsDatabase.Execute(
"INSERT IGNORE INTO playerbots_account_links (account_id, linked_account_id) VALUES ({}, {})",
linkedAccountId, accountId);
ChatHandler(player->GetSession()).PSendSysMessage("Account linked successfully.");
}
void PlayerbotMgr::HandleViewLinkedAccountsCommand(Player* player)
{
uint32 accountId = player->GetSession()->GetAccountId();
QueryResult result = PlayerbotsDatabase.Query("SELECT linked_account_id FROM playerbots_account_links WHERE account_id = {}", accountId);
if (!result)
{
ChatHandler(player->GetSession()).PSendSysMessage("No linked accounts.");
return;
}
ChatHandler(player->GetSession()).PSendSysMessage("Linked accounts:");
do
{
Field* fields = result->Fetch();
uint32 linkedAccountId = fields[0].Get<uint32>();
QueryResult accountResult = LoginDatabase.Query("SELECT username FROM account WHERE id = {}", linkedAccountId);
if (accountResult)
{
Field* accountFields = accountResult->Fetch();
std::string username = accountFields[0].Get<std::string>();
ChatHandler(player->GetSession()).PSendSysMessage("- {}", username.c_str());
}
else
{
ChatHandler(player->GetSession()).PSendSysMessage("- Unknown account");
}
} while (result->NextRow());
}
void PlayerbotMgr::HandleUnlinkAccountCommand(Player* player, const std::string& accountName)
{
QueryResult result = LoginDatabase.Query("SELECT id FROM account WHERE username = '{}'", accountName);
if (!result)
{
ChatHandler(player->GetSession()).PSendSysMessage("Account not found.");
return;
}
Field* fields = result->Fetch();
uint32 linkedAccountId = fields[0].Get<uint32>();
uint32 accountId = player->GetSession()->GetAccountId();
PlayerbotsDatabase.Execute("DELETE FROM playerbots_account_links WHERE (account_id = {} AND linked_account_id = {}) OR (account_id = {} AND linked_account_id = {})",
accountId, linkedAccountId, linkedAccountId, accountId);
ChatHandler(player->GetSession()).PSendSysMessage("Account unlinked successfully.");
}