/* * Copyright (C) 2016+ AzerothCore , 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 "PlayerbotAI.h" #include #include #include #include #include "AiFactory.h" #include "BudgetValues.h" #include "ChannelMgr.h" #include "CharacterPackets.h" #include "Common.h" #include "CreatureAIImpl.h" #include "CreatureData.h" #include "EmoteAction.h" #include "Engine.h" #include "EventProcessor.h" #include "ExternalEventHelper.h" #include "GameObjectData.h" #include "GameTime.h" #include "GuildMgr.h" #include "GuildTaskMgr.h" #include "LFGMgr.h" #include "LastMovementValue.h" #include "LastSpellCastValue.h" #include "LogLevelAction.h" #include "LootObjectStack.h" #include "MapMgr.h" #include "MotionMaster.h" #include "MoveSpline.h" #include "MoveSplineInit.h" #include "NewRpgStrategy.h" #include "ObjectGuid.h" #include "ObjectMgr.h" #include "PerformanceMonitor.h" #include "Player.h" #include "PlayerbotAIConfig.h" #include "PlayerbotDbStore.h" #include "PlayerbotMgr.h" #include "Playerbots.h" #include "PointMovementGenerator.h" #include "PositionValue.h" #include "RandomPlayerbotMgr.h" #include "SayAction.h" #include "ScriptMgr.h" #include "ServerFacade.h" #include "SharedDefines.h" #include "SocialMgr.h" #include "SpellAuraEffects.h" #include "SpellInfo.h" #include "Transport.h" #include "Unit.h" #include "UpdateTime.h" #include "Vehicle.h" const int SPELL_TITAN_GRIP = 49152; std::vector PlayerbotAI::dispel_whitelist = { "mutating injection", "frostbolt", }; std::vector& split(std::string const s, char delim, std::vector& elems); std::vector split(std::string const s, char delim); char* strstri(char const* str1, char const* str2); std::string& trim(std::string& s); std::set PlayerbotAI::unsecuredCommands; PlayerbotChatHandler::PlayerbotChatHandler(Player* pMasterPlayer) : ChatHandler(pMasterPlayer->GetSession()) {} uint32 PlayerbotChatHandler::extractQuestId(std::string const str) { char* source = (char*)str.c_str(); char* cId = extractKeyFromLink(source, "Hquest"); return cId ? atol(cId) : 0; } void PacketHandlingHelper::AddHandler(uint16 opcode, std::string const handler) { handlers[opcode] = handler; } void PacketHandlingHelper::Handle(ExternalEventHelper& helper) { while (!queue.empty()) { helper.HandlePacket(handlers, queue.top()); queue.pop(); } } void PacketHandlingHelper::AddPacket(WorldPacket const& packet) { if (packet.empty()) return; // assert(handlers); // assert(packet); // assert(packet.GetOpcode()); if (handlers.find(packet.GetOpcode()) != handlers.end()) queue.push(WorldPacket(packet)); } PlayerbotAI::PlayerbotAI() : PlayerbotAIBase(true), bot(nullptr), aiObjectContext(nullptr), currentEngine(nullptr), chatHelper(this), chatFilter(this), accountId(0), security(nullptr), master(nullptr), currentState(BOT_STATE_NON_COMBAT) { for (uint8 i = 0; i < BOT_STATE_MAX; i++) engines[i] = nullptr; for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++) { allowActiveCheckTimer[i] = time(nullptr); allowActive[i] = false; } } PlayerbotAI::PlayerbotAI(Player* bot) : PlayerbotAIBase(true), bot(bot), chatHelper(this), chatFilter(this), master(nullptr), security(bot) // reorder args - whipowill { if (!bot->isTaxiCheater() && HasCheat((BotCheatMask::taxi))) bot->SetTaxiCheater(true); for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++) { allowActiveCheckTimer[i] = time(nullptr); allowActive[i] = false; } accountId = bot->GetSession()->GetAccountId(); aiObjectContext = AiFactory::createAiObjectContext(bot, this); engines[BOT_STATE_COMBAT] = AiFactory::createCombatEngine(bot, this, aiObjectContext); engines[BOT_STATE_NON_COMBAT] = AiFactory::createNonCombatEngine(bot, this, aiObjectContext); engines[BOT_STATE_DEAD] = AiFactory::createDeadEngine(bot, this, aiObjectContext); if (sPlayerbotAIConfig->applyInstanceStrategies) ApplyInstanceStrategies(bot->GetMapId()); currentEngine = engines[BOT_STATE_NON_COMBAT]; currentState = BOT_STATE_NON_COMBAT; masterIncomingPacketHandlers.AddHandler(CMSG_GAMEOBJ_USE, "use game object"); masterIncomingPacketHandlers.AddHandler(CMSG_AREATRIGGER, "area trigger"); // masterIncomingPacketHandlers.AddHandler(CMSG_GAMEOBJ_USE, "use game object"); // masterIncomingPacketHandlers.AddHandler(CMSG_LOOT_ROLL, "loot roll"); masterIncomingPacketHandlers.AddHandler(CMSG_GOSSIP_HELLO, "gossip hello"); masterIncomingPacketHandlers.AddHandler(CMSG_QUESTGIVER_HELLO, "gossip hello"); masterIncomingPacketHandlers.AddHandler(CMSG_ACTIVATETAXI, "activate taxi"); masterIncomingPacketHandlers.AddHandler(CMSG_ACTIVATETAXIEXPRESS, "activate taxi"); masterIncomingPacketHandlers.AddHandler(CMSG_TAXICLEARALLNODES, "taxi done"); masterIncomingPacketHandlers.AddHandler(CMSG_TAXICLEARNODE, "taxi done"); masterIncomingPacketHandlers.AddHandler(CMSG_GROUP_UNINVITE, "uninvite"); masterIncomingPacketHandlers.AddHandler(CMSG_GROUP_UNINVITE_GUID, "uninvite guid"); masterIncomingPacketHandlers.AddHandler(CMSG_LFG_TELEPORT, "lfg teleport"); masterIncomingPacketHandlers.AddHandler(CMSG_CAST_SPELL, "see spell"); masterIncomingPacketHandlers.AddHandler(CMSG_REPOP_REQUEST, "release spirit"); masterIncomingPacketHandlers.AddHandler(CMSG_RECLAIM_CORPSE, "revive from corpse"); botOutgoingPacketHandlers.AddHandler(SMSG_PETITION_SHOW_SIGNATURES, "petition offer"); botOutgoingPacketHandlers.AddHandler(SMSG_GROUP_INVITE, "group invite"); botOutgoingPacketHandlers.AddHandler(SMSG_GUILD_INVITE, "guild invite"); botOutgoingPacketHandlers.AddHandler(BUY_ERR_NOT_ENOUGHT_MONEY, "not enough money"); botOutgoingPacketHandlers.AddHandler(BUY_ERR_REPUTATION_REQUIRE, "not enough reputation"); botOutgoingPacketHandlers.AddHandler(SMSG_GROUP_SET_LEADER, "group set leader"); botOutgoingPacketHandlers.AddHandler(SMSG_FORCE_RUN_SPEED_CHANGE, "check mount state"); botOutgoingPacketHandlers.AddHandler(SMSG_RESURRECT_REQUEST, "resurrect request"); botOutgoingPacketHandlers.AddHandler(SMSG_INVENTORY_CHANGE_FAILURE, "cannot equip"); botOutgoingPacketHandlers.AddHandler(SMSG_TRADE_STATUS, "trade status"); botOutgoingPacketHandlers.AddHandler(SMSG_TRADE_STATUS_EXTENDED, "trade status extended"); botOutgoingPacketHandlers.AddHandler(SMSG_LOOT_RESPONSE, "loot response"); botOutgoingPacketHandlers.AddHandler(SMSG_ITEM_PUSH_RESULT, "item push result"); botOutgoingPacketHandlers.AddHandler(SMSG_PARTY_COMMAND_RESULT, "party command"); botOutgoingPacketHandlers.AddHandler(SMSG_LEVELUP_INFO, "levelup"); botOutgoingPacketHandlers.AddHandler(SMSG_LOG_XPGAIN, "xpgain"); botOutgoingPacketHandlers.AddHandler(SMSG_CAST_FAILED, "cast failed"); botOutgoingPacketHandlers.AddHandler(SMSG_DUEL_REQUESTED, "duel requested"); botOutgoingPacketHandlers.AddHandler(SMSG_INVENTORY_CHANGE_FAILURE, "inventory change failure"); botOutgoingPacketHandlers.AddHandler(SMSG_BATTLEFIELD_STATUS, "bg status"); botOutgoingPacketHandlers.AddHandler(SMSG_LFG_ROLE_CHECK_UPDATE, "lfg role check"); botOutgoingPacketHandlers.AddHandler(SMSG_LFG_PROPOSAL_UPDATE, "lfg proposal"); botOutgoingPacketHandlers.AddHandler(SMSG_TEXT_EMOTE, "receive text emote"); botOutgoingPacketHandlers.AddHandler(SMSG_EMOTE, "receive emote"); botOutgoingPacketHandlers.AddHandler(SMSG_LOOT_START_ROLL, "master loot roll"); botOutgoingPacketHandlers.AddHandler(SMSG_ARENA_TEAM_INVITE, "arena team invite"); botOutgoingPacketHandlers.AddHandler(SMSG_GROUP_DESTROYED, "group destroyed"); botOutgoingPacketHandlers.AddHandler(SMSG_GROUP_LIST, "group list"); masterOutgoingPacketHandlers.AddHandler(SMSG_PARTY_COMMAND_RESULT, "party command"); masterOutgoingPacketHandlers.AddHandler(MSG_RAID_READY_CHECK, "ready check"); masterOutgoingPacketHandlers.AddHandler(MSG_RAID_READY_CHECK_FINISHED, "ready check finished"); masterOutgoingPacketHandlers.AddHandler(SMSG_QUESTGIVER_OFFER_REWARD, "questgiver quest details"); // quest packet masterIncomingPacketHandlers.AddHandler(CMSG_QUESTGIVER_COMPLETE_QUEST, "complete quest"); masterIncomingPacketHandlers.AddHandler(CMSG_QUESTGIVER_ACCEPT_QUEST, "accept quest"); masterIncomingPacketHandlers.AddHandler(CMSG_QUEST_CONFIRM_ACCEPT, "confirm quest"); masterIncomingPacketHandlers.AddHandler(CMSG_PUSHQUESTTOPARTY, "quest share"); botOutgoingPacketHandlers.AddHandler(SMSG_QUESTUPDATE_COMPLETE, "quest update complete"); botOutgoingPacketHandlers.AddHandler(SMSG_QUESTUPDATE_ADD_KILL, "quest update add kill"); // SMSG_QUESTUPDATE_ADD_ITEM no longer used // botOutgoingPacketHandlers.AddHandler(SMSG_QUESTUPDATE_ADD_ITEM, "quest update add item"); botOutgoingPacketHandlers.AddHandler(SMSG_QUEST_CONFIRM_ACCEPT, "confirm quest"); } PlayerbotAI::~PlayerbotAI() { for (uint8 i = 0; i < BOT_STATE_MAX; i++) { if (engines[i]) delete engines[i]; } if (aiObjectContext) delete aiObjectContext; if (bot) sPlayerbotsMgr->RemovePlayerBotData(bot->GetGUID(), true); } void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) { // Handle the AI check delay if (nextAICheckDelay > elapsed) nextAICheckDelay -= elapsed; else nextAICheckDelay = 0; // Early return if bot is in invalid state if (!bot || !bot->IsInWorld() || !bot->GetSession() || bot->GetSession()->isLogingOut() || bot->IsDuringRemoveFromWorld()) return; // Handle cheat options (set bot health and power if cheats are enabled) if (bot->IsAlive() && (static_cast(GetCheat()) > 0 || static_cast(sPlayerbotAIConfig->botCheatMask) > 0)) { if (HasCheat(BotCheatMask::health)) bot->SetFullHealth(); if (HasCheat(BotCheatMask::mana) && bot->getPowerType() == POWER_MANA) bot->SetPower(POWER_MANA, bot->GetMaxPower(POWER_MANA)); if (HasCheat(BotCheatMask::power) && bot->getPowerType() != POWER_MANA) bot->SetPower(bot->getPowerType(), bot->GetMaxPower(bot->getPowerType())); } AllowActivity(); if (!CanUpdateAI()) return; // Handle the current spell Spell* currentSpell = bot->GetCurrentSpell(CURRENT_GENERIC_SPELL); if (!currentSpell) currentSpell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL); if (currentSpell) { const SpellInfo* spellInfo = currentSpell->GetSpellInfo(); if (spellInfo && currentSpell->getState() == SPELL_STATE_PREPARING) { Unit* spellTarget = currentSpell->m_targets.GetUnitTarget(); // Interrupt if target is dead or spell can't target dead units if (spellTarget && !spellTarget->IsAlive() && !spellInfo->IsAllowingDeadTarget()) { InterruptSpell(); YieldThread(GetReactDelay()); return; } bool isHeal = false; bool isSingleTarget = true; for (uint8 i = 0; i < 3; ++i) { if (!spellInfo->Effects[i].Effect) continue; // Check if spell is a heal if (spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL || spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL_MAX_HEALTH || spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL_MECHANICAL) isHeal = true; // Check if spell is single-target if ((spellInfo->Effects[i].TargetA.GetTarget() && spellInfo->Effects[i].TargetA.GetTarget() != TARGET_UNIT_TARGET_ALLY) || (spellInfo->Effects[i].TargetB.GetTarget() && spellInfo->Effects[i].TargetB.GetTarget() != TARGET_UNIT_TARGET_ALLY)) { isSingleTarget = false; } } // Interrupt if target ally has full health (heal by other member) if (isHeal && isSingleTarget && spellTarget && spellTarget->IsFullHealth()) { InterruptSpell(); YieldThread(GetReactDelay()); return; } // Ensure bot is facing target if necessary if (spellTarget && !bot->HasInArc(CAST_ANGLE_IN_FRONT, spellTarget) && (spellInfo->FacingCasterFlags & SPELL_FACING_FLAG_INFRONT)) { sServerFacade->SetFacingTo(bot, spellTarget); } // Wait for spell cast YieldThread(GetReactDelay()); return; } } // Handle transport check delay if (nextTransportCheck > elapsed) nextTransportCheck -= elapsed; else nextTransportCheck = 0; if (!nextTransportCheck) { nextTransportCheck = 1000; Transport* newTransport = bot->GetMap()->GetTransportForPos(bot->GetPhaseMask(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot); if (newTransport != bot->GetTransport()) { LOG_DEBUG("playerbots", "Bot {} is on a transport", bot->GetName()); if (bot->GetTransport()) bot->GetTransport()->RemovePassenger(bot, true); if (newTransport) newTransport->AddPassenger(bot, true); bot->StopMovingOnCurrentPos(); } } // Update the bot's group status (moved to helper function) UpdateAIGroupAndMaster(); // Update internal AI UpdateAIInternal(elapsed, minimal); YieldThread(GetReactDelay()); } // Helper function for UpdateAI to check group membership and handle removal if necessary void PlayerbotAI::UpdateAIGroupAndMaster() { if (!bot) return; PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); if (!botAI) return; Group* group = bot->GetGroup(); bool IsRandomBot = sRandomPlayerbotMgr->IsRandomBot(bot); // If bot is not in group verify that for is RandomBot before clearing master and resetting. if (!group) { if (master && IsRandomBot) { SetMaster(nullptr); Reset(true); ResetStrategies(); } return; } // Bot in BG, but master no longer part of a group: release master // Exclude alt and addclass bots as they rely on current (real player) master, security-wise. if (bot->InBattleground() && IsRandomBot && master && !master->GetGroup()) SetMaster(nullptr); PlayerbotAI* masterBotAI = nullptr; if (master) masterBotAI = GET_PLAYERBOT_AI(master); if (!master || (masterBotAI && !masterBotAI->IsRealPlayer())) { Player* newMaster = FindNewMaster(); if (newMaster) { master = newMaster; botAI->SetMaster(newMaster); botAI->ResetStrategies(); if (!bot->InBattleground()) { botAI->ChangeStrategy("+follow", BOT_STATE_NON_COMBAT); if (botAI->GetMaster() == botAI->GetGroupMaster()) botAI->TellMaster("Hello, I follow you!"); else botAI->TellMaster(!urand(0, 2) ? "Hello!" : "Hi!"); } else { // we're in a battleground, stay with the pack and focus on objective botAI->ChangeStrategy("-follow", BOT_STATE_NON_COMBAT); } } else if (!newMaster && !bot->InBattleground()) LeaveOrDisbandGroup(); } } void PlayerbotAI::UpdateAIInternal([[maybe_unused]] uint32 elapsed, bool minimal) { if (bot->IsBeingTeleported() || !bot->IsInWorld()) return; std::string const mapString = WorldPosition(bot).isOverworld() ? std::to_string(bot->GetMapId()) : "I"; PerformanceMonitorOperation* pmo = sPerformanceMonitor->start(PERF_MON_TOTAL, "PlayerbotAI::UpdateAIInternal " + mapString); ExternalEventHelper helper(aiObjectContext); // chat replies for (auto it = chatReplies.begin(); it != chatReplies.end();) { time_t checkTime = it->m_time; if (checkTime && time(0) < checkTime) { ++it; continue; } ChatReplyAction::ChatReplyDo(bot, it->m_type, it->m_guid1, it->m_guid2, it->m_msg, it->m_chanName, it->m_name); it = chatReplies.erase(it); } HandleCommands(); // logout if logout timer is ready or if instant logout is possible if (bot->GetSession()->isLogingOut()) { WorldSession* botWorldSessionPtr = bot->GetSession(); bool logout = botWorldSessionPtr->ShouldLogOut(time(nullptr)); if (!master || !master->GetSession()->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) || (master->GetSession() && master->GetSession()->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)))) { logout = true; } if (logout) { PlayerbotMgr* masterBotMgr = nullptr; if (master) masterBotMgr = GET_PLAYERBOT_MGR(master); if (masterBotMgr) { masterBotMgr->LogoutPlayerBot(bot->GetGUID()); } else { sRandomPlayerbotMgr->LogoutPlayerBot(bot->GetGUID()); } return; } SetNextCheckDelay(sPlayerbotAIConfig->reactDelay); return; } botOutgoingPacketHandlers.Handle(helper); masterIncomingPacketHandlers.Handle(helper); masterOutgoingPacketHandlers.Handle(helper); DoNextAction(minimal); if (pmo) pmo->finish(); } void PlayerbotAI::HandleCommands() { ExternalEventHelper helper(aiObjectContext); for (auto it = chatCommands.begin(); it != chatCommands.end();) { time_t& checkTime = it->GetTime(); if (checkTime && time(0) < checkTime) { ++it; continue; } const std::string& command = it->GetCommand(); Player* owner = it->GetOwner(); if (!helper.ParseChatCommand(command, owner) && it->GetType() == CHAT_MSG_WHISPER) { // ostringstream out; out << "Unknown command " << command; // TellPlayer(out); // helper.ParseChatCommand("help"); } it = chatCommands.erase(it); } } std::map chatMap; void PlayerbotAI::HandleCommand(uint32 type, const std::string& text, Player& fromPlayer, const uint32 lang) { std::string filtered = text; if (!IsAllowedCommand(filtered) && !GetSecurity()->CheckLevelFor(PlayerbotSecurityLevel::PLAYERBOT_SECURITY_INVITE, type != CHAT_MSG_WHISPER, &fromPlayer)) return; if (type == CHAT_MSG_ADDON) return; if (filtered.find("BOT\t") == 0) // Mangosbot has BOT prefix so we remove that. filtered = filtered.substr(4); else if (lang == LANG_ADDON) // Other addon messages should not command bots. return; if (type == CHAT_MSG_SYSTEM) return; if (filtered.find(sPlayerbotAIConfig->commandSeparator) != std::string::npos) { std::vector commands; split(commands, filtered, sPlayerbotAIConfig->commandSeparator.c_str()); for (std::vector::iterator i = commands.begin(); i != commands.end(); ++i) { HandleCommand(type, *i, fromPlayer); } return; } if (!sPlayerbotAIConfig->commandPrefix.empty()) { if (filtered.find(sPlayerbotAIConfig->commandPrefix) != 0) return; filtered = filtered.substr(sPlayerbotAIConfig->commandPrefix.size()); } if (chatMap.empty()) { chatMap["#w "] = CHAT_MSG_WHISPER; chatMap["#p "] = CHAT_MSG_PARTY; chatMap["#r "] = CHAT_MSG_RAID; chatMap["#a "] = CHAT_MSG_ADDON; chatMap["#g "] = CHAT_MSG_GUILD; } currentChat = std::pair(CHAT_MSG_WHISPER, 0); for (std::map::iterator i = chatMap.begin(); i != chatMap.end(); ++i) { if (filtered.find(i->first) == 0) { filtered = filtered.substr(3); currentChat = std::pair(i->second, time(0) + 2); break; } } filtered = chatFilter.Filter(trim((std::string&)filtered)); if (filtered.empty()) return; if (filtered.substr(0, 6) == "debug ") { std::string response = HandleRemoteCommand(filtered.substr(6)); WorldPacket data; ChatHandler::BuildChatPacket(data, CHAT_MSG_ADDON, response.c_str(), LANG_ADDON, CHAT_TAG_NONE, bot->GetGUID(), bot->GetName()); sServerFacade->SendPacket(&fromPlayer, &data); return; } if (!IsAllowedCommand(filtered) && !GetSecurity()->CheckLevelFor(PlayerbotSecurityLevel::PLAYERBOT_SECURITY_ALLOW_ALL, type != CHAT_MSG_WHISPER, &fromPlayer)) return; if (type == CHAT_MSG_RAID_WARNING && filtered.find(bot->GetName()) != std::string::npos && filtered.find("award") == std::string::npos) { chatCommands.push_back(ChatCommandHolder("warning", &fromPlayer, type)); return; } if ((filtered.size() > 2 && filtered.substr(0, 2) == "d ") || (filtered.size() > 3 && filtered.substr(0, 3) == "do ")) { Event event("do", "", &fromPlayer); std::string action = filtered.substr(filtered.find(" ") + 1); DoSpecificAction(action, event); } if (ChatHelper::parseValue("command", filtered).substr(0, 3) == "do ") { Event event("do", "", &fromPlayer); std::string action = ChatHelper::parseValue("command", filtered); action = action.substr(3); DoSpecificAction(action, event); } else if (type != CHAT_MSG_WHISPER && filtered.size() > 6 && filtered.substr(0, 6) == "queue ") { std::string remaining = filtered.substr(filtered.find(" ") + 1); int index = 1; Group* group = bot->GetGroup(); if (group) { for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { if (ref->GetSource() == master) continue; if (ref->GetSource() == bot) break; index++; } } chatCommands.push_back(ChatCommandHolder(remaining, &fromPlayer, type, time(0) + index)); } else if (filtered == "reset") { Reset(true); } // TODO: missing implementation to port /*else if (filtered == "logout") { if (!(bot->IsStunnedByLogout() || bot->GetSession()->isLogingOut())) { if (type == CHAT_MSG_WHISPER) TellPlayer(&fromPlayer, BOT_TEXT("logout_start")); if (master && master->GetPlayerbotMgr()) SetShouldLogOut(true); } } else if (filtered == "logout cancel") { if (bot->IsStunnedByLogout() || bot->GetSession()->isLogingOut()) { if (type == CHAT_MSG_WHISPER) TellPlayer(&fromPlayer, BOT_TEXT("logout_cancel")); WorldPacket p; bot->GetSession()->HandleLogoutCancelOpcode(p); SetShouldLogOut(false); } } else if ((filtered.size() > 5) && (filtered.substr(0, 5) == "wait ") && (filtered.find("wait for attack") == std::string::npos)) { std::string remaining = filtered.substr(filtered.find(" ") + 1); uint32 delay = atof(remaining.c_str()) * IN_MILLISECONDS; if (delay > 20000) { bot->TellMaster(&fromPlayer, "Max wait time is 20 seconds!"); return; } IncreaseAIInternalUpdateDelay(delay); isWaiting = true; TellPlayer(&fromPlayer, "Waiting for " + remaining + " seconds!"); return; }*/ else { chatCommands.push_back(ChatCommandHolder(filtered, &fromPlayer, type)); } } void PlayerbotAI::HandleTeleportAck() { if (IsRealPlayer()) return; bot->GetMotionMaster()->Clear(true); bot->StopMoving(); if (bot->IsBeingTeleportedNear()) { // Temporary fix for instance can not enter if (!bot->IsInWorld()) { bot->GetMap()->AddPlayerToMap(bot); } while (bot->IsInWorld() && bot->IsBeingTeleportedNear()) { Player* plMover = bot->m_mover->ToPlayer(); if (!plMover) return; WorldPacket p = WorldPacket(MSG_MOVE_TELEPORT_ACK, 20); p << plMover->GetPackGUID(); p << (uint32)0; // supposed to be flags? not used currently p << (uint32)0; // time - not currently used bot->GetSession()->HandleMoveTeleportAck(p); }; } if (bot->IsBeingTeleportedFar()) { while (bot->IsBeingTeleportedFar()) { bot->GetSession()->HandleMoveWorldportAck(); } // SetNextCheckDelay(urand(2000, 5000)); if (sPlayerbotAIConfig->applyInstanceStrategies) ApplyInstanceStrategies(bot->GetMapId(), true); if (sPlayerbotAIConfig->restrictHealerDPS) EvaluateHealerDpsStrategy(); Reset(true); } SetNextCheckDelay(sPlayerbotAIConfig->globalCoolDown); } void PlayerbotAI::Reset(bool full) { if (bot->HasUnitState(UNIT_STATE_IN_FLIGHT)) return; WorldSession* botWorldSessionPtr = bot->GetSession(); bool logout = botWorldSessionPtr->ShouldLogOut(time(nullptr)); // cancel logout if (!logout && bot->GetSession()->isLogingOut()) { WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL); bot->GetSession()->HandleLogoutCancelOpcode(data); TellMaster("Logout cancelled!"); } currentEngine = engines[BOT_STATE_NON_COMBAT]; currentState = BOT_STATE_NON_COMBAT; nextAICheckDelay = 0; whispers.clear(); aiObjectContext->GetValue("old target")->Set(nullptr); aiObjectContext->GetValue("current target")->Set(nullptr); aiObjectContext->GetValue("prioritized targets")->Reset(); aiObjectContext->GetValue("pull target")->Set(ObjectGuid::Empty); aiObjectContext->GetValue("rpg target")->Set(GuidPosition()); aiObjectContext->GetValue("loot target")->Set(LootObject()); aiObjectContext->GetValue("lfg proposal")->Set(0); bot->SetTarget(); LastSpellCast& lastSpell = aiObjectContext->GetValue("last spell cast")->Get(); lastSpell.Reset(); if (full) { aiObjectContext->GetValue("last movement")->Get().Set(nullptr); aiObjectContext->GetValue("last area trigger")->Get().Set(nullptr); aiObjectContext->GetValue("last taxi")->Get().Set(nullptr); aiObjectContext->GetValue("travel target") ->Get() ->setTarget(sTravelMgr->nullTravelDestination, sTravelMgr->nullWorldPosition, true); aiObjectContext->GetValue("travel target")->Get()->setStatus(TRAVEL_STATUS_EXPIRED); aiObjectContext->GetValue("travel target")->Get()->setExpireIn(1000); rpgInfo = NewRpgInfo(); } aiObjectContext->GetValue("ignore rpg target")->Get().clear(); bot->GetMotionMaster()->Clear(); InterruptSpell(); if (full) { for (uint8 i = 0; i < BOT_STATE_MAX; i++) { engines[i]->Init(); } } } void PlayerbotAI::LeaveOrDisbandGroup() { if (!bot || !bot->GetGroup() || IsRealPlayer()) return; WorldPacket* packet = new WorldPacket(CMSG_GROUP_DISBAND); bot->GetSession()->QueuePacket(packet); } bool PlayerbotAI::IsAllowedCommand(std::string const text) { if (unsecuredCommands.empty()) { unsecuredCommands.insert("who"); unsecuredCommands.insert("wts"); unsecuredCommands.insert("sendmail"); unsecuredCommands.insert("invite"); unsecuredCommands.insert("leave"); unsecuredCommands.insert("lfg"); unsecuredCommands.insert("rpg status"); } for (std::set::iterator i = unsecuredCommands.begin(); i != unsecuredCommands.end(); ++i) { if (text.find(*i) == 0) { return true; } } return false; } void PlayerbotAI::HandleCommand(uint32 type, std::string const text, Player* fromPlayer) { if (!GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, type != CHAT_MSG_WHISPER, fromPlayer)) return; if (type == CHAT_MSG_ADDON) return; if (type == CHAT_MSG_SYSTEM) return; if (text.find(sPlayerbotAIConfig->commandSeparator) != std::string::npos) { std::vector commands; split(commands, text, sPlayerbotAIConfig->commandSeparator.c_str()); for (std::vector::iterator i = commands.begin(); i != commands.end(); ++i) { HandleCommand(type, *i, fromPlayer); } return; } std::string filtered = text; if (!sPlayerbotAIConfig->commandPrefix.empty()) { if (filtered.find(sPlayerbotAIConfig->commandPrefix) != 0) return; filtered = filtered.substr(sPlayerbotAIConfig->commandPrefix.size()); } if (chatMap.empty()) { chatMap["#w "] = CHAT_MSG_WHISPER; chatMap["#p "] = CHAT_MSG_PARTY; chatMap["#r "] = CHAT_MSG_RAID; chatMap["#a "] = CHAT_MSG_ADDON; chatMap["#g "] = CHAT_MSG_GUILD; } currentChat = std::pair(CHAT_MSG_WHISPER, 0); for (std::map::iterator i = chatMap.begin(); i != chatMap.end(); ++i) { if (filtered.find(i->first) == 0) { filtered = filtered.substr(3); currentChat = std::pair(i->second, time(nullptr) + 2); break; } } filtered = chatFilter.Filter(trim(filtered)); if (filtered.empty()) return; if (filtered.substr(0, 6) == "debug ") { std::string const response = HandleRemoteCommand(filtered.substr(6)); WorldPacket data; ChatHandler::BuildChatPacket(data, (ChatMsg)type, type == CHAT_MSG_ADDON ? LANG_ADDON : LANG_UNIVERSAL, bot, nullptr, response.c_str()); fromPlayer->SendDirectMessage(&data); return; } if (!IsAllowedCommand(filtered) && (!GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_ALLOW_ALL, type != CHAT_MSG_WHISPER, fromPlayer))) return; if (type == CHAT_MSG_RAID_WARNING && filtered.find(bot->GetName()) != std::string::npos && filtered.find("award") == std::string::npos) { chatCommands.push_back(ChatCommandHolder("warning", fromPlayer, type)); return; } if ((filtered.size() > 2 && filtered.substr(0, 2) == "d ") || (filtered.size() > 3 && filtered.substr(0, 3) == "do ")) { std::string const action = filtered.substr(filtered.find(" ") + 1); DoSpecificAction(action); } else if (type != CHAT_MSG_WHISPER && filtered.size() > 6 && filtered.substr(0, 6) == "queue ") { std::string const remaining = filtered.substr(filtered.find(" ") + 1); uint32 index = 1; if (Group* group = bot->GetGroup()) { for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { if (ref->GetSource() == master) continue; if (ref->GetSource() == bot) break; ++index; } } chatCommands.push_back(ChatCommandHolder(remaining, fromPlayer, type, time(nullptr) + index)); } else if (filtered == "reset") { Reset(true); } else if (filtered == "logout") { if (!bot->GetSession()->isLogingOut()) { if (type == CHAT_MSG_WHISPER) TellMaster("I'm logging out!"); PlayerbotMgr* masterBotMgr = nullptr; if (master) masterBotMgr = GET_PLAYERBOT_MGR(master); if (masterBotMgr) masterBotMgr->LogoutPlayerBot(bot->GetGUID()); } } else if (filtered == "logout cancel") { if (bot->GetSession()->isLogingOut()) { if (type == CHAT_MSG_WHISPER) TellMaster("Logout cancelled!"); WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL); bot->GetSession()->HandleLogoutCancelOpcode(data); } } else { chatCommands.push_back(ChatCommandHolder(filtered, fromPlayer, type)); } } void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) { if (packet.empty()) return; if (!bot || !bot->IsInWorld() || bot->IsDuringRemoveFromWorld()) { return; } switch (packet.GetOpcode()) { case SMSG_SPELL_FAILURE: { WorldPacket p(packet); p.rpos(0); ObjectGuid casterGuid; p >> casterGuid.ReadAsPacked(); if (casterGuid != bot->GetGUID()) return; uint8 count, result; uint32 spellId; p >> count >> spellId >> result; SpellInterrupted(spellId); return; } case SMSG_SPELL_DELAYED: { WorldPacket p(packet); p.rpos(0); ObjectGuid casterGuid; p >> casterGuid.ReadAsPacked(); if (casterGuid != bot->GetGUID()) return; uint32 delaytime; p >> delaytime; if (delaytime <= 1000) IncreaseNextCheckDelay(delaytime); return; } case SMSG_EMOTE: // do not react to NPC emotes { WorldPacket p(packet); ObjectGuid source; uint32 emoteId; p.rpos(0); p >> emoteId >> source; if (source.IsPlayer()) botOutgoingPacketHandlers.AddPacket(packet); return; } case SMSG_MESSAGECHAT: // do not react to self or if not ready to reply { if (!sPlayerbotAIConfig->randomBotTalk) return; if (!AllowActivity()) return; WorldPacket p(packet); if (!p.empty() && (p.GetOpcode() == SMSG_MESSAGECHAT || p.GetOpcode() == SMSG_GM_MESSAGECHAT)) { p.rpos(0); uint8 msgtype, chatTag; uint32 lang, textLen, unused; ObjectGuid guid1, guid2; std::string name = ""; std::string chanName = ""; std::string message = ""; p >> msgtype >> lang; p >> guid1 >> unused; if (guid1.IsEmpty() || p.size() > p.DEFAULT_SIZE) return; if (p.GetOpcode() == SMSG_GM_MESSAGECHAT) { p >> textLen; p >> name; } switch (msgtype) { case CHAT_MSG_CHANNEL: p >> chanName; [[fallthrough]]; case CHAT_MSG_SAY: case CHAT_MSG_PARTY: case CHAT_MSG_YELL: case CHAT_MSG_WHISPER: case CHAT_MSG_GUILD: p >> guid2; p >> textLen >> message >> chatTag; break; default: return; } if (chanName == "World") return; // do not reply to self but always try to reply to real player if (guid1 != bot->GetGUID()) { time_t lastChat = GetAiObjectContext()->GetValue("last said", "chat")->Get(); bool isPaused = time(0) < lastChat; bool isFromFreeBot = false; sCharacterCache->GetCharacterNameByGuid(guid1, name); uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(guid1); isFromFreeBot = sPlayerbotAIConfig->IsInRandomAccountList(accountId); bool isMentioned = message.find(bot->GetName()) != std::string::npos; // ChatChannelSource chatChannelSource = GetChatChannelSource(bot, msgtype, chanName); // random bot speaks, chat CD if (isFromFreeBot && isPaused) return; // BG: react only if mentioned or if not channel and real player spoke if (bot->InBattleground() && !(isMentioned || (msgtype != CHAT_MSG_CHANNEL && !isFromFreeBot))) return; if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID()) return; if (lang == LANG_ADDON) return; if (message.starts_with(sPlayerbotAIConfig->toxicLinksPrefix) && (GetChatHelper()->ExtractAllItemIds(message).size() > 0 || GetChatHelper()->ExtractAllQuestIds(message).size() > 0) && sPlayerbotAIConfig->toxicLinksRepliesChance) { if (urand(0, 50) > 0 || urand(1, 100) > sPlayerbotAIConfig->toxicLinksRepliesChance) { return; } } else if ((GetChatHelper()->ExtractAllItemIds(message).count(19019) && sPlayerbotAIConfig->thunderfuryRepliesChance)) { if (urand(0, 60) > 0 || urand(1, 100) > sPlayerbotAIConfig->thunderfuryRepliesChance) { return; } } else { if (isFromFreeBot && urand(0, 20)) return; // if (msgtype == CHAT_MSG_GUILD && (!sPlayerbotAIConfig->guildRepliesRate || urand(1, 100) >= // sPlayerbotAIConfig->guildRepliesRate)) return; if (!isFromFreeBot) { if (!isMentioned && urand(0, 4)) return; } else { if (urand(0, 20 + 10 * isMentioned)) return; } } QueueChatResponse(ChatQueuedReply{msgtype, guid1.GetCounter(), guid2.GetCounter(), message, chanName, name, time(nullptr) + urand(inCombat ? 10 : 5, inCombat ? 25 : 15)}); GetAiObjectContext()->GetValue("last said", "chat")->Set(time(0) + urand(5, 25)); return; } } return; } case SMSG_MOVE_KNOCK_BACK: // handle knockbacks { WorldPacket p(packet); p.rpos(0); ObjectGuid guid; uint32 counter; float vcos, vsin, horizontalSpeed, verticalSpeed = 0.f; p >> guid.ReadAsPacked() >> counter >> vcos >> vsin >> horizontalSpeed >> verticalSpeed; if (horizontalSpeed <= 0.1f) { horizontalSpeed = 0.11f; } verticalSpeed = -verticalSpeed; // high vertical may result in stuck as bot can not handle gravity if (verticalSpeed > 35.0f) break; // stop casting InterruptSpell(); // stop movement bot->StopMoving(); bot->GetMotionMaster()->Clear(); // Unit* currentTarget = GetAiObjectContext()->GetValue("current target")->Get(); bot->GetMotionMaster()->MoveKnockbackFromForPlayer(bot->GetPositionX() - vcos, bot->GetPositionY() - vsin, horizontalSpeed, verticalSpeed); // bot->AddUnitMovementFlag(MOVEMENTFLAG_FALLING); // bot->AddUnitMovementFlag(MOVEMENTFLAG_FORWARD); // bot->m_movementInfo.AddMovementFlag(MOVEMENTFLAG_PENDING_STOP); // if (bot->m_movementInfo.HasMovementFlag(MOVEMENTFLAG_SPLINE_ELEVATION)) // bot->m_movementInfo.RemoveMovementFlag(MOVEMENTFLAG_SPLINE_ELEVATION); // bot->GetMotionMaster()->MoveIdle(); // Position dest = bot->GetPosition(); // float moveTimeHalf = verticalSpeed / Movement::gravity; // float dist = 2 * moveTimeHalf * horizontalSpeed; // float max_height = -Movement::computeFallElevation(moveTimeHalf, false, -verticalSpeed); // Use a mmap raycast to get a valid destination. // bot->GetMotionMaster()->MoveKnockbackFrom(fx, fy, horizontalSpeed, verticalSpeed); // // set delay based on actual distance // float newdis = sqrt(sServerFacade->GetDistance2d(bot, fx, fy)); // SetNextCheckDelay((uint32)((newdis / dis) * moveTimeHalf * 4 * IN_MILLISECONDS)); // // add moveflags // // copy MovementInfo // MovementInfo movementInfo = bot->m_movementInfo; // // send ack // WorldPacket ack(CMSG_MOVE_KNOCK_BACK_ACK); // // movementInfo.jump.cosAngle = vcos; // // movementInfo.jump.sinAngle = vsin; // // movementInfo.jump.zspeed = -verticalSpeed; // // movementInfo.jump.xyspeed = horizontalSpeed; // ack << bot->GetGUID().WriteAsPacked(); // // bot->m_mover->BuildMovementPacket(&ack); // ack << (uint32)0; // bot->BuildMovementPacket(&ack); // // ack << movementInfo.jump.sinAngle; // // ack << movementInfo.jump.cosAngle; // // ack << movementInfo.jump.xyspeed; // // ack << movementInfo.jump.zspeed; // bot->GetSession()->HandleMoveKnockBackAck(ack); // // // set jump destination for MSG_LAND packet // SetJumpDestination(Position(x, y, z, bot->GetOrientation())); // bot->Heart(); // */ return; } default: botOutgoingPacketHandlers.AddPacket(packet); } } void PlayerbotAI::SpellInterrupted(uint32 spellid) { for (uint8 type = CURRENT_MELEE_SPELL; type <= CURRENT_CHANNELED_SPELL; type++) { Spell* spell = bot->GetCurrentSpell((CurrentSpellTypes)type); if (!spell) continue; if (spell->GetSpellInfo()->Id == spellid) bot->InterruptSpell((CurrentSpellTypes)type); } // LastSpellCast& lastSpell = aiObjectContext->GetValue("last spell cast")->Get(); // lastSpell.id = 0; } int32 PlayerbotAI::CalculateGlobalCooldown(uint32 spellid) { if (!spellid) return 0; if (bot->HasSpellCooldown(spellid)) return sPlayerbotAIConfig->globalCoolDown; return sPlayerbotAIConfig->reactDelay; } void PlayerbotAI::HandleMasterIncomingPacket(WorldPacket const& packet) { masterIncomingPacketHandlers.AddPacket(packet); } void PlayerbotAI::HandleMasterOutgoingPacket(WorldPacket const& packet) { masterOutgoingPacketHandlers.AddPacket(packet); } void PlayerbotAI::ChangeEngine(BotState type) { Engine* engine = engines[type]; if (currentEngine != engine) { currentEngine = engine; currentState = type; ReInitCurrentEngine(); switch (type) { case BOT_STATE_COMBAT: ChangeEngineOnCombat(); break; case BOT_STATE_NON_COMBAT: ChangeEngineOnNonCombat(); break; case BOT_STATE_DEAD: // LOG_DEBUG("playerbots", "=== {} DEAD ===", bot->GetName().c_str()); break; default: break; } } } void PlayerbotAI::ChangeEngineOnCombat() { if (HasStrategy("stay", BOT_STATE_COMBAT)) { aiObjectContext->GetValue("pos", "stay") ->Set(PositionInfo(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId())); } } void PlayerbotAI::ChangeEngineOnNonCombat() { if (HasStrategy("stay", BOT_STATE_NON_COMBAT)) { aiObjectContext->GetValue("pos", "stay")->Reset(); } } void PlayerbotAI::DoNextAction(bool min) { if (!bot->IsInWorld() || bot->IsBeingTeleported() || (GetMaster() && GetMaster()->IsBeingTeleported())) { SetNextCheckDelay(sPlayerbotAIConfig->globalCoolDown); return; } // Change engine if just died bool isBotAlive = bot->IsAlive(); if (currentEngine != engines[BOT_STATE_DEAD] && !isBotAlive) { bot->StopMoving(); bot->GetMotionMaster()->Clear(); bot->GetMotionMaster()->MoveIdle(); // Death Count to prevent skeleton piles // Player* master = GetMaster(); // warning here - whipowill if (!HasActivePlayerMaster() && !bot->InBattleground()) { uint32 dCount = aiObjectContext->GetValue("death count")->Get(); aiObjectContext->GetValue("death count")->Set(++dCount); } aiObjectContext->GetValue("current target")->Set(nullptr); aiObjectContext->GetValue("enemy player target")->Set(nullptr); aiObjectContext->GetValue("pull target")->Set(ObjectGuid::Empty); aiObjectContext->GetValue("loot target")->Set(LootObject()); ChangeEngine(BOT_STATE_DEAD); return; } // Change engine if just ressed if (currentEngine == engines[BOT_STATE_DEAD] && isBotAlive) { ChangeEngine(BOT_STATE_NON_COMBAT); return; } // Clear targets if in combat but sticking with old data if (currentEngine == engines[BOT_STATE_NON_COMBAT] && bot->IsInCombat()) { Unit* currentTarget = aiObjectContext->GetValue("current target")->Get(); if (currentTarget != nullptr) { aiObjectContext->GetValue("current target")->Set(nullptr); } } bool minimal = !AllowActivity(); currentEngine->DoNextAction(nullptr, 0, (minimal || min)); if (minimal) { if (!bot->isAFK() && !bot->InBattleground() && !HasRealPlayerMaster()) bot->ToggleAFK(); SetNextCheckDelay(sPlayerbotAIConfig->passiveDelay); return; } else if (bot->isAFK()) bot->ToggleAFK(); Group* group = bot->GetGroup(); PlayerbotAI* masterBotAI = nullptr; if (master && master->IsInWorld()) { float distance = sServerFacade->GetDistance2d(bot, master); if (master->m_movementInfo.HasMovementFlag(MOVEMENTFLAG_WALKING) && distance < 20.0f) bot->m_movementInfo.AddMovementFlag(MOVEMENTFLAG_WALKING); else bot->m_movementInfo.RemoveMovementFlag(MOVEMENTFLAG_WALKING); if (master->IsSitState() && nextAICheckDelay < 1000) { if (!bot->isMoving() && distance < 10.0f) bot->SetStandState(UNIT_STAND_STATE_SIT); } else if (nextAICheckDelay < 1000) bot->SetStandState(UNIT_STAND_STATE_STAND); } else if (bot->m_movementInfo.HasMovementFlag(MOVEMENTFLAG_WALKING)) bot->m_movementInfo.RemoveMovementFlag(MOVEMENTFLAG_WALKING); else if ((nextAICheckDelay < 1000) && bot->IsSitState()) bot->SetStandState(UNIT_STAND_STATE_STAND); bool hasMountAura = bot->HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED) || bot->HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED); if (hasMountAura && !bot->IsMounted()) { bot->RemoveAurasByType(SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED); bot->RemoveAurasByType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED); } } void PlayerbotAI::ReInitCurrentEngine() { // InterruptSpell(); currentEngine->Init(); } void PlayerbotAI::ChangeStrategy(std::string const names, BotState type) { Engine* e = engines[type]; if (!e) return; e->ChangeStrategy(names); } void PlayerbotAI::ClearStrategies(BotState type) { Engine* e = engines[type]; if (!e) return; e->removeAllStrategies(); } std::vector PlayerbotAI::GetStrategies(BotState type) { Engine* e = engines[type]; if (!e) return std::vector(); return e->GetStrategies(); } void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) { std::string strategyName; switch (mapId) { case 249: strategyName = "onyxia"; // Onyxia's Lair break; case 409: strategyName = "mc"; // Molten Core break; case 469: strategyName = "bwl"; // Blackwing Lair break; case 509: strategyName = "aq20"; // Ruins of Ahn'Qiraj break; case 532: strategyName = "karazhan"; // Karazhan break; case 533: strategyName = "naxx"; // Naxxramas break; case 544: strategyName = "magtheridon"; // Magtheridon's Lair case 565: strategyName = "gruulslair"; // Gruul's Lair break; case 574: strategyName = "wotlk-uk"; // Utgarde Keep break; case 575: strategyName = "wotlk-up"; // Utgarde Pinnacle break; case 576: strategyName = "wotlk-nex"; // The Nexus break; case 578: strategyName = "wotlk-occ"; // The Oculus break; case 595: strategyName = "wotlk-cos"; // The Culling of Stratholme break; case 599: strategyName = "wotlk-hos"; // Halls of Stone break; case 600: strategyName = "wotlk-dtk"; // Drak'Tharon Keep break; case 601: strategyName = "wotlk-an"; // Azjol-Nerub break; case 602: strategyName = "wotlk-hol"; // Halls of Lightning break; case 603: strategyName = "uld"; // Ulduar break; case 604: strategyName = "wotlk-gd"; // Gundrak break; case 608: strategyName = "wotlk-vh"; // Violet Hold break; case 615: strategyName = "wotlk-os"; // Obsidian Sanctum break; case 616: strategyName = "wotlk-eoe"; // Eye Of Eternity break; case 619: strategyName = "wotlk-ok"; // Ahn'kahet: The Old Kingdom break; case 624: strategyName = "voa"; // Vault of Archavon break; case 631: strategyName = "icc"; // Icecrown Citadel break; case 632: strategyName = "wotlk-fos"; // The Forge of Souls break; case 650: strategyName = "wotlk-toc"; // Trial of the Champion break; case 658: strategyName = "wotlk-pos"; // Pit of Saron break; case 668: strategyName = "wotlk-hor"; // Halls of Reflection break; default: break; } if (strategyName.empty()) return; engines[BOT_STATE_COMBAT]->addStrategy(strategyName); engines[BOT_STATE_NON_COMBAT]->addStrategy(strategyName); if (tellMaster && !strategyName.empty()) { std::ostringstream out; out << "Added " << strategyName << " instance strategy"; TellMasterNoFacing(out.str()); } } bool PlayerbotAI::DoSpecificAction(std::string const name, Event event, bool silent, std::string const qualifier) { std::ostringstream out; for (uint8 i = 0; i < BOT_STATE_MAX; i++) { ActionResult res = engines[i]->ExecuteAction(name, event, qualifier); switch (res) { case ACTION_RESULT_UNKNOWN: continue; case ACTION_RESULT_OK: if (!silent) { PlaySound(TEXT_EMOTE_NOD); } return true; case ACTION_RESULT_IMPOSSIBLE: out << name << ": impossible"; if (!silent) { TellError(out.str()); PlaySound(TEXT_EMOTE_NO); } return false; case ACTION_RESULT_USELESS: out << name << ": useless"; if (!silent) { TellError(out.str()); PlaySound(TEXT_EMOTE_NO); } return false; case ACTION_RESULT_FAILED: if (!silent) { out << name << ": failed"; TellError(out.str()); } return false; } } if (!silent) { out << name << ": unknown action"; TellError(out.str()); } return false; } bool PlayerbotAI::PlaySound(uint32 emote) { if (EmotesTextSoundEntry const* soundEntry = FindTextSoundEmoteFor(emote, bot->getRace(), bot->getGender())) { bot->PlayDistanceSound(soundEntry->SoundId); return true; } return false; } bool PlayerbotAI::PlayEmote(uint32 emote) { WorldPacket data(SMSG_TEXT_EMOTE); data << (TextEmotes)emote; data << EmoteAction::GetNumberOfEmoteVariants((TextEmotes)emote, bot->getRace(), bot->getGender()); data << ((master && (sServerFacade->GetDistance2d(bot, master) < 30.0f) && urand(0, 1)) ? master->GetGUID() : (bot->GetTarget() && urand(0, 1)) ? bot->GetTarget() : ObjectGuid::Empty); bot->GetSession()->HandleTextEmoteOpcode(data); return false; } bool PlayerbotAI::ContainsStrategy(StrategyType type) { for (uint8 i = 0; i < BOT_STATE_MAX; i++) { if (engines[i]->HasStrategyType(type)) return true; } return false; } bool PlayerbotAI::HasStrategy(std::string const name, BotState type) { return engines[type]->HasStrategy(name); } void PlayerbotAI::ResetStrategies(bool load) { for (uint8 i = 0; i < BOT_STATE_MAX; i++) engines[i]->removeAllStrategies(); AiFactory::AddDefaultCombatStrategies(bot, this, engines[BOT_STATE_COMBAT]); AiFactory::AddDefaultNonCombatStrategies(bot, this, engines[BOT_STATE_NON_COMBAT]); AiFactory::AddDefaultDeadStrategies(bot, this, engines[BOT_STATE_DEAD]); if (sPlayerbotAIConfig->applyInstanceStrategies) ApplyInstanceStrategies(bot->GetMapId()); for (uint8 i = 0; i < BOT_STATE_MAX; i++) engines[i]->Init(); // if (load) // sPlayerbotDbStore->Load(this); } bool PlayerbotAI::IsRanged(Player* player, bool bySpec) { PlayerbotAI* botAi = GET_PLAYERBOT_AI(player); if (!bySpec && botAi) return botAi->ContainsStrategy(STRATEGY_TYPE_RANGED); int tab = AiFactory::GetPlayerSpecTab(player); switch (player->getClass()) { case CLASS_DEATH_KNIGHT: case CLASS_WARRIOR: case CLASS_ROGUE: return false; break; case CLASS_DRUID: if (tab == 1) { return false; } break; case CLASS_PALADIN: if (tab != 0) { return false; } break; case CLASS_SHAMAN: if (tab == 1) { return false; } break; } return true; } bool PlayerbotAI::IsMelee(Player* player, bool bySpec) { return !IsRanged(player, bySpec); } bool PlayerbotAI::IsCaster(Player* player, bool bySpec) { return IsRanged(player, bySpec) && player->getClass() != CLASS_HUNTER; } bool PlayerbotAI::IsCombo(Player* player) { // int tab = AiFactory::GetPlayerSpecTab(player); return player->getClass() == CLASS_ROGUE || (player->getClass() == CLASS_DRUID && player->HasAura(768)); // cat druid } bool PlayerbotAI::IsRangedDps(Player* player, bool bySpec) { return IsRanged(player, bySpec) && IsDps(player, bySpec); } bool PlayerbotAI::IsHealAssistantOfIndex(Player* player, int index) { Group* group = player->GetGroup(); if (!group) { return false; } int counter = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (IsHeal(member)) // Check if the member is a healer { bool isAssistant = group->IsAssistant(member->GetGUID()); // Check if the index matches for both assistant and non-assistant healers if ((isAssistant && index == counter) || (!isAssistant && index == counter)) { return player == member; } counter++; } } return false; } bool PlayerbotAI::IsRangedDpsAssistantOfIndex(Player* player, int index) { Group* group = player->GetGroup(); if (!group) { return false; } int counter = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (IsRangedDps(member)) // Check if the member is a ranged DPS { bool isAssistant = group->IsAssistant(member->GetGUID()); // Check the index for both assistant and non-assistant ranges if ((isAssistant && index == counter) || (!isAssistant && index == counter)) { return player == member; } counter++; } } return false; } bool PlayerbotAI::HasAggro(Unit* unit) { if (!unit) { return false; } bool isMT = IsMainTank(bot); Unit* victim = unit->GetVictim(); if (victim && (victim->GetGUID() == bot->GetGUID() || (!isMT && victim->ToPlayer() && IsTank(victim->ToPlayer())))) { return true; } return false; } int32 PlayerbotAI::GetAssistTankIndex(Player* player) { Group* group = player->GetGroup(); if (!group) { return -1; } int counter = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (player == member) { return counter; } if (IsTank(member, true) && group->IsAssistant(member->GetGUID())) { counter++; } } return 0; } int32 PlayerbotAI::GetGroupSlotIndex(Player* player) { Group* group = bot->GetGroup(); if (!group) { return -1; } int counter = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (player == member) { return counter; } counter++; } return 0; } int32 PlayerbotAI::GetRangedIndex(Player* player) { if (!IsRanged(player)) { return -1; } Group* group = bot->GetGroup(); if (!group) { return -1; } int counter = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (player == member) { return counter; } if (IsRanged(member)) { counter++; } } return 0; } int32 PlayerbotAI::GetClassIndex(Player* player, uint8 cls) { if (player->getClass() != cls) { return -1; } Group* group = bot->GetGroup(); if (!group) { return -1; } int counter = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (player == member) { return counter; } if (member->getClass() == cls) { counter++; } } return 0; } int32 PlayerbotAI::GetRangedDpsIndex(Player* player) { if (!IsRangedDps(player)) { return -1; } Group* group = bot->GetGroup(); if (!group) { return -1; } int counter = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (player == member) { return counter; } if (IsRangedDps(member)) { counter++; } } return 0; } int32 PlayerbotAI::GetMeleeIndex(Player* player) { if (IsRanged(player)) { return -1; } Group* group = bot->GetGroup(); if (!group) { return -1; } int counter = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (player == member) { return counter; } if (!IsRanged(member)) { counter++; } } return 0; } bool PlayerbotAI::IsTank(Player* player, bool bySpec) { PlayerbotAI* botAi = GET_PLAYERBOT_AI(player); if (!bySpec && botAi) return botAi->ContainsStrategy(STRATEGY_TYPE_TANK); int tab = AiFactory::GetPlayerSpecTab(player); switch (player->getClass()) { case CLASS_DEATH_KNIGHT: if (tab == DEATHKNIGHT_TAB_BLOOD) { return true; } break; case CLASS_PALADIN: if (tab == PALADIN_TAB_PROTECTION) { return true; } break; case CLASS_WARRIOR: if (tab == WARRIOR_TAB_PROTECTION) { return true; } break; case CLASS_DRUID: if (tab == DRUID_TAB_FERAL && (player->GetShapeshiftForm() == FORM_BEAR || player->GetShapeshiftForm() == FORM_DIREBEAR || player->HasAura(16931))) { return true; } break; } return false; } bool PlayerbotAI::IsHeal(Player* player, bool bySpec) { PlayerbotAI* botAi = GET_PLAYERBOT_AI(player); if (!bySpec && botAi) return botAi->ContainsStrategy(STRATEGY_TYPE_HEAL); int tab = AiFactory::GetPlayerSpecTab(player); switch (player->getClass()) { case CLASS_PRIEST: if (tab == PRIEST_TAB_DISCIPLINE || tab == PRIEST_TAB_HOLY) { return true; } break; case CLASS_DRUID: if (tab == DRUID_TAB_RESTORATION) { return true; } break; case CLASS_SHAMAN: if (tab == SHAMAN_TAB_RESTORATION) { return true; } break; case CLASS_PALADIN: if (tab == PALADIN_TAB_HOLY) { return true; } break; } return false; } bool PlayerbotAI::IsDps(Player* player, bool bySpec) { PlayerbotAI* botAi = GET_PLAYERBOT_AI(player); if (!bySpec && botAi) return botAi->ContainsStrategy(STRATEGY_TYPE_DPS); int tab = AiFactory::GetPlayerSpecTab(player); switch (player->getClass()) { case CLASS_MAGE: case CLASS_WARLOCK: case CLASS_HUNTER: case CLASS_ROGUE: return true; case CLASS_PRIEST: if (tab == PRIEST_TAB_SHADOW) { return true; } break; case CLASS_DRUID: if (tab == DRUID_TAB_BALANCE) { return true; } if (tab == DRUID_TAB_FERAL && !IsTank(player, bySpec)) { return true; } break; case CLASS_SHAMAN: if (tab != SHAMAN_TAB_RESTORATION) { return true; } break; case CLASS_PALADIN: if (tab == PALADIN_TAB_RETRIBUTION) { return true; } break; case CLASS_DEATH_KNIGHT: if (tab != DEATHKNIGHT_TAB_BLOOD) { return true; } break; case CLASS_WARRIOR: if (tab != WARRIOR_TAB_PROTECTION) { return true; } break; } return false; } bool PlayerbotAI::IsMainTank(Player* player) { Group* group = player->GetGroup(); if (!group) { return IsTank(player); } ObjectGuid mainTank = ObjectGuid(); Group::MemberSlotList const& slots = group->GetMemberSlots(); for (Group::member_citerator itr = slots.begin(); itr != slots.end(); ++itr) { if (itr->flags & MEMBER_FLAG_MAINTANK) { mainTank = itr->guid; break; } } if (mainTank != ObjectGuid::Empty) { return player->GetGUID() == mainTank; } for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (IsTank(member) && member->IsAlive()) { return player->GetGUID() == member->GetGUID(); } } return false; } bool PlayerbotAI::IsBotMainTank(Player* player) { if (!player->GetSession()->IsBot() || !IsTank(player)) { return false; } if (IsMainTank(player)) { return true; } Group* group = player->GetGroup(); if (!group) { return true; // If no group, consider the bot as main tank } uint32 botAssistTankIndex = GetAssistTankIndex(player); if (botAssistTankIndex == -1) { return false; } for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* member = gref->GetSource(); if (!member) { continue; } uint32 memberAssistTankIndex = GetAssistTankIndex(member); if (memberAssistTankIndex == -1) { continue; } if (memberAssistTankIndex == botAssistTankIndex && player == member) { return true; } if (memberAssistTankIndex < botAssistTankIndex && member->GetSession()->IsBot()) { return false; } return false; } return false; } uint32 PlayerbotAI::GetGroupTankNum(Player* player) { Group* group = player->GetGroup(); if (!group) { return 0; } uint32 result = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (IsTank(member) && member->IsAlive()) { result++; } } return result; } bool PlayerbotAI::IsAssistTank(Player* player) { return IsTank(player) && !IsMainTank(player); } bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index) { Group* group = player->GetGroup(); if (!group) { return false; } int counter = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) { if (index == counter) { return player == member; } counter++; } } // not enough for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (!group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) { if (index == counter) { return player == member; } counter++; } } return false; } namespace acore { class UnitByGuidInRangeCheck { public: UnitByGuidInRangeCheck(WorldObject const* obj, ObjectGuid guid, float range) : i_obj(obj), i_range(range), i_guid(guid) { } WorldObject const& GetFocusObject() const { return *i_obj; } bool operator()(Unit* u) { return u->GetGUID() == i_guid && i_obj->IsWithinDistInMap(u, i_range); } private: WorldObject const* i_obj; float i_range; ObjectGuid i_guid; }; class GameObjectByGuidInRangeCheck { public: GameObjectByGuidInRangeCheck(WorldObject const* obj, ObjectGuid guid, float range) : i_obj(obj), i_range(range), i_guid(guid) { } WorldObject const& GetFocusObject() const { return *i_obj; } bool operator()(GameObject* u) { if (u && i_obj->IsWithinDistInMap(u, i_range) && u->isSpawned() && u->GetGOInfo() && u->GetGUID() == i_guid) return true; return false; } private: WorldObject const* i_obj; float i_range; ObjectGuid i_guid; }; }; // namespace acore Unit* PlayerbotAI::GetUnit(ObjectGuid guid) { if (!guid) return nullptr; return ObjectAccessor::GetUnit(*bot, guid); } Player* PlayerbotAI::GetPlayer(ObjectGuid guid) { Unit* unit = GetUnit(guid); return unit ? unit->ToPlayer() : nullptr; } uint32 GetCreatureIdForCreatureTemplateId(uint32 creatureTemplateId) { QueryResult results = WorldDatabase.Query("SELECT guid FROM `creature` WHERE id1 = {} LIMIT 1;", creatureTemplateId); if (results) { Field* fields = results->Fetch(); return fields[0].Get(); } return 0; } Unit* PlayerbotAI::GetUnit(CreatureData const* creatureData) { if (!creatureData) return nullptr; Map* map = sMapMgr->FindMap(creatureData->mapid, 0); if (!map) return nullptr; uint32 spawnId = creatureData->spawnId; if (!spawnId) // workaround for CreatureData with missing spawnId (this just uses first matching creatureId in DB, // but thats ok this method is only used for battlemasters and theres only 1 of each type) spawnId = GetCreatureIdForCreatureTemplateId(creatureData->id1); auto creatureBounds = map->GetCreatureBySpawnIdStore().equal_range(spawnId); if (creatureBounds.first == creatureBounds.second) return nullptr; return creatureBounds.first->second; } Creature* PlayerbotAI::GetCreature(ObjectGuid guid) { if (!guid) return nullptr; return ObjectAccessor::GetCreature(*bot, guid); } GameObject* PlayerbotAI::GetGameObject(ObjectGuid guid) { if (!guid) return nullptr; return ObjectAccessor::GetGameObject(*bot, guid); } // GameObject* PlayerbotAI::GetGameObject(GameObjectData const* gameObjectData) // { // if (!gameObjectData) // return nullptr; // Map* map = sMapMgr->FindMap(gameObjectData->mapid, 0); // if (!map) // return nullptr; // auto gameobjectBounds = map->GetGameObjectBySpawnIdStore().equal_range(gameObjectData->spawnId); // if (gameobjectBounds.first == gameobjectBounds.second) // return nullptr; // return gameobjectBounds.first->second; // } WorldObject* PlayerbotAI::GetWorldObject(ObjectGuid guid) { if (!guid) return nullptr; return ObjectAccessor::GetWorldObject(*bot, guid); } const AreaTableEntry* PlayerbotAI::GetCurrentArea() { return sAreaTableStore.LookupEntry( bot->GetMap()->GetAreaId(bot->GetPhaseMask(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ())); } const AreaTableEntry* PlayerbotAI::GetCurrentZone() { return sAreaTableStore.LookupEntry( bot->GetMap()->GetZoneId(bot->GetPhaseMask(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ())); } std::string PlayerbotAI::GetLocalizedAreaName(const AreaTableEntry* entry) { std::string name; if (entry) { name = entry->area_name[sWorld->GetDefaultDbcLocale()]; if (name.empty()) name = entry->area_name[LOCALE_enUS]; } return name; } std::string PlayerbotAI::GetLocalizedCreatureName(uint32 entry) { std::string name; const CreatureLocale* cl = sObjectMgr->GetCreatureLocale(entry); if (cl) ObjectMgr::GetLocaleString(cl->Name, sWorld->GetDefaultDbcLocale(), name); if (name.empty()) { CreatureTemplate const* ct = sObjectMgr->GetCreatureTemplate(entry); if (ct) name = ct->Name; } return name; } std::string PlayerbotAI::GetLocalizedGameObjectName(uint32 entry) { std::string name; const GameObjectLocale* gl = sObjectMgr->GetGameObjectLocale(entry); if (gl) ObjectMgr::GetLocaleString(gl->Name, sWorld->GetDefaultDbcLocale(), name); if (name.empty()) { GameObjectTemplate const* gt = sObjectMgr->GetGameObjectTemplate(entry); if (gt) name = gt->name; } return name; } std::vector PlayerbotAI::GetPlayersInGroup() { std::vector members; Group* group = bot->GetGroup(); if (!group) return members; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) { continue; } if (GET_PLAYERBOT_AI(member) && !GET_PLAYERBOT_AI(member)->IsRealPlayer()) continue; members.push_back(ref->GetSource()); } return members; } bool PlayerbotAI::SayToGuild(const std::string& msg) { if (msg.empty()) { return false; } if (bot->GetGuildId()) { if (Guild* guild = sGuildMgr->GetGuildById(bot->GetGuildId())) { if (!guild->HasRankRight(bot, GR_RIGHT_GCHATSPEAK)) { return false; } guild->BroadcastToGuild(bot->GetSession(), false, msg.c_str(), LANG_UNIVERSAL); return true; } } return false; } bool PlayerbotAI::SayToWorld(const std::string& msg) { if (msg.empty()) { return false; } ChannelMgr* cMgr = ChannelMgr::forTeam(bot->GetTeamId()); if (!cMgr) return false; // no zone if (Channel* worldChannel = cMgr->GetChannel("World", bot)) { worldChannel->Say(bot->GetGUID(), msg.c_str(), LANG_UNIVERSAL); return true; } return false; } bool PlayerbotAI::SayToChannel(const std::string& msg, const ChatChannelId& chanId) { // Checks whether the message or ChannelMgr is valid if (msg.empty()) return false; ChannelMgr* cMgr = ChannelMgr::forTeam(bot->GetTeamId()); if (!cMgr) return false; AreaTableEntry const* current_zone = GetCurrentZone(); if (!current_zone) return false; const auto current_str_zone = GetLocalizedAreaName(current_zone); std::mutex socialMutex; std::lock_guard lock(socialMutex); // Blocking for thread safety when accessing SocialMgr for (auto const& [key, channel] : cMgr->GetChannels()) { // Checks if the channel pointer is valid if (!channel) continue; // Checks if the channel matches the specified ChatChannelId if (channel->GetChannelId() == chanId) { // If the channel name is empty, skip it to avoid access problems if (channel->GetName().empty()) continue; // Checks if the channel name contains the current zone const auto does_contains = channel->GetName().find(current_str_zone) != std::string::npos; if (chanId != ChatChannelId::LOOKING_FOR_GROUP && chanId != ChatChannelId::WORLD_DEFENSE && !does_contains) { continue; } else if (chanId == ChatChannelId::LOOKING_FOR_GROUP || chanId == ChatChannelId::WORLD_DEFENSE) { // Here you can add the capital check if necessary } // Final check to ensure the channel is correct before trying to say something if (channel) { channel->Say(bot->GetGUID(), msg.c_str(), LANG_UNIVERSAL); return true; } } } return false; } bool PlayerbotAI::SayToParty(const std::string& msg) { if (!bot->GetGroup()) return false; WorldPacket data; ChatHandler::BuildChatPacket(data, CHAT_MSG_PARTY, msg.c_str(), LANG_UNIVERSAL, CHAT_TAG_NONE, bot->GetGUID(), bot->GetName()); for (auto reciever : GetPlayersInGroup()) { sServerFacade->SendPacket(reciever, &data); } return true; } bool PlayerbotAI::SayToRaid(const std::string& msg) { if (!bot->GetGroup() || bot->GetGroup()->isRaidGroup()) return false; WorldPacket data; ChatHandler::BuildChatPacket(data, CHAT_MSG_RAID, msg.c_str(), LANG_UNIVERSAL, CHAT_TAG_NONE, bot->GetGUID(), bot->GetName()); for (auto reciever : GetPlayersInGroup()) { sServerFacade->SendPacket(reciever, &data); } return true; } bool PlayerbotAI::Yell(const std::string& msg) { if (bot->GetTeamId() == TeamId::TEAM_ALLIANCE) { bot->Yell(msg, LANG_COMMON); } else { bot->Yell(msg, LANG_ORCISH); } return true; } bool PlayerbotAI::Say(const std::string& msg) { if (bot->GetTeamId() == TeamId::TEAM_ALLIANCE) { bot->Say(msg, LANG_COMMON); } else { bot->Say(msg, LANG_ORCISH); } return true; } bool PlayerbotAI::Whisper(const std::string& msg, const std::string& receiverName) { const auto receiver = ObjectAccessor::FindPlayerByName(receiverName); if (!receiver) { return false; } if (bot->GetTeamId() == TeamId::TEAM_ALLIANCE) { bot->Whisper(msg, LANG_COMMON, receiver); } else { bot->Whisper(msg, LANG_ORCISH, receiver); } return true; } bool PlayerbotAI::TellMasterNoFacing(std::ostringstream& stream, PlayerbotSecurityLevel securityLevel) { return TellMasterNoFacing(stream.str(), securityLevel); } bool PlayerbotAI::TellMasterNoFacing(std::string const text, PlayerbotSecurityLevel securityLevel) { Player* master = GetMaster(); PlayerbotAI* masterBotAI = nullptr; if (master) masterBotAI = GET_PLAYERBOT_AI(master); if ((!master || (masterBotAI && !masterBotAI->IsRealPlayer())) && (sPlayerbotAIConfig->randomBotSayWithoutMaster || HasStrategy("debug", BOT_STATE_NON_COMBAT))) { bot->Say(text, (bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH)); return true; } if (!IsTellAllowed(securityLevel)) return false; time_t lastSaid = whispers[text]; if (!lastSaid || (time(nullptr) - lastSaid) >= sPlayerbotAIConfig->repeatDelay / 1000) { whispers[text] = time(nullptr); ChatMsg type = CHAT_MSG_WHISPER; if (currentChat.second - time(nullptr) >= 1) type = currentChat.first; WorldPacket data; ChatHandler::BuildChatPacket(data, type == CHAT_MSG_ADDON ? CHAT_MSG_PARTY : type, type == CHAT_MSG_ADDON ? LANG_ADDON : LANG_UNIVERSAL, bot, nullptr, text.c_str()); master->SendDirectMessage(&data); } return true; } bool PlayerbotAI::TellError(std::string const text, PlayerbotSecurityLevel securityLevel) { Player* master = GetMaster(); if (!IsTellAllowed(securityLevel) || !master || GET_PLAYERBOT_AI(master)) return false; if (PlayerbotMgr* mgr = GET_PLAYERBOT_MGR(master)) mgr->TellError(bot->GetName(), text); return false; } bool PlayerbotAI::IsTellAllowed(PlayerbotSecurityLevel securityLevel) { Player* master = GetMaster(); if (!master || master->IsBeingTeleported()) return false; if (!GetSecurity()->CheckLevelFor(securityLevel, true, master)) return false; if (sPlayerbotAIConfig->whisperDistance && !bot->GetGroup() && sRandomPlayerbotMgr->IsRandomBot(bot) && master->GetSession()->GetSecurity() < SEC_GAMEMASTER && (bot->GetMapId() != master->GetMapId() || sServerFacade->GetDistance2d(bot, master) > sPlayerbotAIConfig->whisperDistance)) return false; return true; } bool PlayerbotAI::TellMaster(std::ostringstream& stream, PlayerbotSecurityLevel securityLevel) { return TellMaster(stream.str(), securityLevel); } bool PlayerbotAI::TellMaster(std::string const text, PlayerbotSecurityLevel securityLevel) { if (!master || !TellMasterNoFacing(text, securityLevel)) return false; if (!bot->isMoving() && !bot->IsInCombat() && bot->GetMapId() == master->GetMapId() && !bot->HasUnitState(UNIT_STATE_IN_FLIGHT) && !bot->IsFlying()) { if (!bot->HasInArc(EMOTE_ANGLE_IN_FRONT, master, sPlayerbotAIConfig->sightDistance)) bot->SetFacingToObject(master); bot->HandleEmoteCommand(EMOTE_ONESHOT_TALK); } return true; } bool IsRealAura(Player* bot, AuraEffect const* aurEff, Unit const* unit) { if (!aurEff) return false; if (!unit->IsHostileTo(bot)) return true; SpellInfo const* spellInfo = aurEff->GetSpellInfo(); uint32 stacks = aurEff->GetBase()->GetStackAmount(); if (stacks >= spellInfo->StackAmount) return true; if (aurEff->GetCaster() == bot || spellInfo->IsPositive() || spellInfo->Effects[aurEff->GetEffIndex()].IsAreaAuraEffect()) return true; return false; } bool PlayerbotAI::HasAura(std::string const name, Unit* unit, bool maxStack, bool checkIsOwner, int maxAuraAmount, bool checkDuration) { if (!unit) return false; std::wstring wnamepart; if (!Utf8toWStr(name, wnamepart)) return false; wstrToLower(wnamepart); int auraAmount = 0; // Iterate through all aura types for (uint32 auraType = SPELL_AURA_BIND_SIGHT; auraType < TOTAL_AURAS; auraType++) { Unit::AuraEffectList const& auras = unit->GetAuraEffectsByType((AuraType)auraType); if (auras.empty()) continue; // Iterate through each aura effect for (AuraEffect const* aurEff : auras) { if (!aurEff) continue; SpellInfo const* spellInfo = aurEff->GetSpellInfo(); if (!spellInfo) continue; // Check if the aura name matches std::string_view const auraName = spellInfo->SpellName[0]; if (auraName.empty() || auraName.length() != wnamepart.length() || !Utf8FitTo(auraName, wnamepart)) continue; // Check if this is a valid aura for the bot if (IsRealAura(bot, aurEff, unit)) { // Check caster if necessary if (checkIsOwner && aurEff->GetCasterGUID() != bot->GetGUID()) continue; // Check aura duration if necessary if (checkDuration && aurEff->GetBase()->GetDuration() == -1) continue; // Count stacks and charges uint32 maxStackAmount = spellInfo->StackAmount; uint32 maxProcCharges = spellInfo->ProcCharges; // Count the aura based on max stack and proc charges if (maxStack) { if (maxStackAmount && aurEff->GetBase()->GetStackAmount() >= maxStackAmount) auraAmount++; if (maxProcCharges && aurEff->GetBase()->GetCharges() >= maxProcCharges) auraAmount++; } else { auraAmount++; } // Early exit if maxAuraAmount is reached if (maxAuraAmount < 0 && auraAmount > 0) return true; } } } // Return based on the maximum aura amount conditions if (maxAuraAmount >= 0) { return auraAmount == maxAuraAmount || (auraAmount > 0 && auraAmount <= maxAuraAmount); } return false; } bool PlayerbotAI::HasAura(uint32 spellId, Unit const* unit) { if (!spellId || !unit) return false; return unit->HasAura(spellId); // for (uint8 effect = EFFECT_0; effect <= EFFECT_2; effect++) // { // AuraEffect const* aurEff = unit->GetAuraEffect(spellId, effect); // if (IsRealAura(bot, aurEff, unit)) // return true; // } // return false; } Aura* PlayerbotAI::GetAura(std::string const name, Unit* unit, bool checkIsOwner, bool checkDuration, int checkStack) { if (!unit) return nullptr; std::wstring wnamepart; if (!Utf8toWStr(name, wnamepart)) return nullptr; wstrToLower(wnamepart); for (uint32 auraType = SPELL_AURA_BIND_SIGHT; auraType < TOTAL_AURAS; ++auraType) { Unit::AuraEffectList const& auras = unit->GetAuraEffectsByType((AuraType)auraType); if (auras.empty()) continue; for (AuraEffect const* aurEff : auras) { SpellInfo const* spellInfo = aurEff->GetSpellInfo(); std::string const& auraName = spellInfo->SpellName[0]; // Directly skip if name mismatch (both length and content) if (auraName.empty() || auraName.length() != wnamepart.length() || !Utf8FitTo(auraName, wnamepart)) continue; if (!IsRealAura(bot, aurEff, unit)) continue; // Check owner if necessary if (checkIsOwner && aurEff->GetCasterGUID() != bot->GetGUID()) continue; // Check duration if necessary if (checkDuration && aurEff->GetBase()->GetDuration() == -1) continue; // Check stack if necessary if (checkStack != -1 && aurEff->GetBase()->GetStackAmount() < checkStack) continue; return aurEff->GetBase(); } } return nullptr; } bool PlayerbotAI::HasAnyAuraOf(Unit* player, ...) { if (!player) return false; va_list vl; va_start(vl, player); const char* cur; while ((cur = va_arg(vl, const char*)) != nullptr) { if (HasAura(cur, player)) { va_end(vl); return true; } } va_end(vl); return false; } bool PlayerbotAI::CanCastSpell(std::string const name, Unit* target, Item* itemTarget) { return CanCastSpell(aiObjectContext->GetValue("spell id", name)->Get(), target, true, itemTarget); } bool PlayerbotAI::CanCastSpell(uint32 spellid, Unit* target, bool checkHasSpell, Item* itemTarget, Item* castItem) { if (!spellid) { if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) { LOG_DEBUG("playerbots", "Can cast spell failed. No spellid. - spellid: {}, bot name: {}", spellid, bot->GetName()); } return false; } if (bot->HasUnitState(UNIT_STATE_LOST_CONTROL)) { if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) { LOG_DEBUG("playerbots", "Can cast spell failed. Unit state lost control. - spellid: {}, bot name: {}", spellid, bot->GetName()); } return false; } if (!target) target = bot; if (Pet* pet = bot->GetPet()) if (pet->HasSpell(spellid)) return true; if (checkHasSpell && !bot->HasSpell(spellid)) { if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) { LOG_DEBUG("playerbots", "Can cast spell failed. Bot not has spell. - target name: {}, spellid: {}, bot name: {}", target->GetName(), spellid, bot->GetName()); } return false; } if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL) != nullptr) { if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) { LOG_DEBUG( "playerbots", "CanCastSpell() target name: {}, spellid: {}, bot name: {}, failed because has current channeled spell", target->GetName(), spellid, bot->GetName()); } return false; } if (bot->HasSpellCooldown(spellid)) { if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) { LOG_DEBUG("playerbots", "Can cast spell failed. Spell not has cooldown. - target name: {}, spellid: {}, bot name: {}", target->GetName(), spellid, bot->GetName()); } return false; } SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellid); if (!spellInfo) { if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) { LOG_DEBUG("playerbots", "Can cast spell failed. No spellInfo. - target name: {}, spellid: {}, bot name: {}", target->GetName(), spellid, bot->GetName()); } return false; } if ((bot->GetShapeshiftForm() == FORM_FLIGHT || bot->GetShapeshiftForm() == FORM_FLIGHT_EPIC) && !bot->IsInCombat()) { if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) { LOG_DEBUG( "playerbots", "Can cast spell failed. In flight form (not in combat). - target name: {}, spellid: {}, bot name: {}", target->GetName(), spellid, bot->GetName()); } return false; } uint32 CastingTime = !spellInfo->IsChanneled() ? spellInfo->CalcCastTime(bot) : spellInfo->GetDuration(); // bool interruptOnMove = spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT; if ((CastingTime || spellInfo->IsAutoRepeatRangedSpell()) && bot->isMoving()) { if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) { LOG_DEBUG("playerbots", "Casting time and bot is moving - target name: {}, spellid: {}, bot name: {}", target->GetName(), spellid, bot->GetName()); } return false; } if (!itemTarget) { // Exception for Deep Freeze (44572) - allow cast for damage on immune targets (e.g., bosses) if (target->IsImmunedToSpell(spellInfo)) { if (spellid != 44572) // Deep Freeze { if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) { LOG_DEBUG("playerbots", "target is immuned to spell - target name: {}, spellid: {}, bot name: {}", target->GetName(), spellid, bot->GetName()); } return false; } // Otherwise, allow Deep Freeze even if immune } if (bot != target && sServerFacade->GetDistance2d(bot, target) > sPlayerbotAIConfig->sightDistance) { if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) { LOG_DEBUG("playerbots", "target is out of sight distance - target name: {}, spellid: {}, bot name: {}", target->GetName(), spellid, bot->GetName()); } return false; } } Unit* oldSel = bot->GetSelectedUnit(); // TRIGGERED_IGNORE_POWER_AND_REAGENT_COST flag for not calling CheckPower in check // which avoids buff charge to be ineffectively reduced (e.g. dk freezing fog for howling blast) /// @TODO: Fix all calls to ApplySpellMod Spell* spell = new Spell(bot, spellInfo, TRIGGERED_IGNORE_POWER_AND_REAGENT_COST); spell->m_targets.SetUnitTarget(target); spell->m_CastItem = castItem; if (itemTarget == nullptr) { itemTarget = aiObjectContext->GetValue("item for spell", spellid)->Get(); ; } spell->m_targets.SetItemTarget(itemTarget); SpellCastResult result = spell->CheckCast(true); delete spell; // if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) // { // if (result != SPELL_FAILED_NOT_READY && result != SPELL_CAST_OK) // { // LOG_DEBUG("playerbots", "CanCastSpell - target name: {}, spellid: {}, bot name: {}, result: {}", // target->GetName(), spellid, bot->GetName(), result); // } // } if (oldSel) bot->SetSelection(oldSel->GetGUID()); switch (result) { case SPELL_FAILED_NOT_INFRONT: case SPELL_FAILED_NOT_STANDING: case SPELL_FAILED_UNIT_NOT_INFRONT: case SPELL_FAILED_MOVING: case SPELL_FAILED_TRY_AGAIN: case SPELL_CAST_OK: case SPELL_FAILED_NOT_SHAPESHIFT: case SPELL_FAILED_OUT_OF_RANGE: return true; default: if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) { LOG_DEBUG("playerbots", "CanCastSpell Check Failed. - target name: {}, spellid: {}, bot name: {}, result: {}", target->GetName(), spellid, bot->GetName(), result); } return false; } } bool PlayerbotAI::CanCastSpell(uint32 spellid, GameObject* goTarget, bool checkHasSpell) { if (!spellid) return false; if (bot->HasUnitState(UNIT_STATE_LOST_CONTROL)) return false; Pet* pet = bot->GetPet(); if (pet && pet->HasSpell(spellid)) return true; if (checkHasSpell && !bot->HasSpell(spellid)) return false; if (bot->HasSpellCooldown(spellid)) return false; SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellid); if (!spellInfo) return false; int32 CastingTime = !spellInfo->IsChanneled() ? spellInfo->CalcCastTime(bot) : spellInfo->GetDuration(); if (CastingTime > 0 && bot->isMoving()) return false; if (sServerFacade->GetDistance2d(bot, goTarget) > sPlayerbotAIConfig->sightDistance) return false; // ObjectGuid oldSel = bot->GetTarget(); // bot->SetTarget(goTarget->GetGUID()); Spell* spell = new Spell(bot, spellInfo, TRIGGERED_NONE); spell->m_targets.SetGOTarget(goTarget); Item* item = aiObjectContext->GetValue("item for spell", spellid)->Get(); spell->m_targets.SetItemTarget(item); SpellCastResult result = spell->CheckCast(true); delete spell; // if (oldSel) // bot->SetTarget(oldSel); switch (result) { case SPELL_FAILED_NOT_INFRONT: case SPELL_FAILED_NOT_STANDING: case SPELL_FAILED_UNIT_NOT_INFRONT: case SPELL_FAILED_MOVING: case SPELL_FAILED_TRY_AGAIN: case SPELL_CAST_OK: return true; default: break; } return false; } bool PlayerbotAI::CanCastSpell(uint32 spellid, float x, float y, float z, bool checkHasSpell, Item* itemTarget) { if (!spellid) return false; Pet* pet = bot->GetPet(); if (pet && pet->HasSpell(spellid)) return true; if (checkHasSpell && !bot->HasSpell(spellid)) return false; if (bot->HasSpellCooldown(spellid)) return false; SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellid); if (!spellInfo) return false; if (!itemTarget) { if (bot->GetDistance(x, y, z) > sPlayerbotAIConfig->sightDistance) return false; } Spell* spell = new Spell(bot, spellInfo, TRIGGERED_NONE); spell->m_targets.SetDst(x, y, z, 0.f); Item* item = itemTarget ? itemTarget : aiObjectContext->GetValue("item for spell", spellid)->Get(); spell->m_targets.SetItemTarget(item); SpellCastResult result = spell->CheckCast(true); delete spell; switch (result) { case SPELL_FAILED_NOT_INFRONT: case SPELL_FAILED_NOT_STANDING: case SPELL_FAILED_UNIT_NOT_INFRONT: case SPELL_FAILED_MOVING: case SPELL_FAILED_TRY_AGAIN: case SPELL_CAST_OK: return true; default: return false; } } bool PlayerbotAI::CastSpell(std::string const name, Unit* target, Item* itemTarget) { bool result = CastSpell(aiObjectContext->GetValue("spell id", name)->Get(), target, itemTarget); if (result) { aiObjectContext->GetValue("last spell cast time", name)->Set(time(nullptr)); } return result; } bool PlayerbotAI::CastSpell(uint32 spellId, Unit* target, Item* itemTarget) { if (!spellId) { return false; } if (!target) target = bot; Pet* pet = bot->GetPet(); SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); if (pet && pet->HasSpell(spellId)) { // List of spell IDs for which we do NOT want to toggle auto-cast or send message // We are excluding Spell Lock and Devour Magic because they are casted in the GenericWarlockStrategy // Without this exclusion, the skill would be togged for auto-cast and the player would // be spammed with messages about enabling/disabling auto-cast switch (spellId) { case 19244: // Spell Lock rank 1 case 19647: // Spell Lock rank 2 case 19505: // Devour Magic rank 1 case 19731: // Devour Magic rank 2 case 19734: // Devour Magic rank 3 case 19736: // Devour Magic rank 4 case 27276: // Devour Magic rank 5 case 27277: // Devour Magic rank 6 case 48011: // Devour Magic rank 7 // No message - just break out of the switch and let normal cast logic continue break; default: bool autocast = false; for (unsigned int& m_autospell : pet->m_autospells) { if (m_autospell == spellId) { autocast = true; break; } } pet->ToggleAutocast(spellInfo, !autocast); std::ostringstream out; out << (autocast ? "|cffff0000|Disabling" : "|cFF00ff00|Enabling") << " pet auto-cast for "; out << chatHelper.FormatSpell(spellInfo); TellMaster(out); return true; } } // aiObjectContext->GetValue("last movement")->Get().Set(nullptr); // aiObjectContext->GetValue("stay time")->Set(0); if (bot->IsFlying() || bot->HasUnitState(UNIT_STATE_IN_FLIGHT)) { // if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) // { // LOG_DEBUG("playerbots", "Spell cast is flying - target name: {}, spellid: {}, bot name: {}}", // target->GetName(), spellId, bot->GetName()); // } return false; } // bot->ClearUnitState(UNIT_STATE_CHASE); // bot->ClearUnitState(UNIT_STATE_FOLLOW); bool failWithDelay = false; if (!bot->IsStandState()) { bot->SetStandState(UNIT_STAND_STATE_STAND); failWithDelay = true; } ObjectGuid oldSel = bot->GetSelectedUnit() ? bot->GetSelectedUnit()->GetGUID() : ObjectGuid(); bot->SetSelection(target->GetGUID()); WorldObject* faceTo = target; if (!bot->HasInArc(CAST_ANGLE_IN_FRONT, faceTo) && (spellInfo->FacingCasterFlags & SPELL_FACING_FLAG_INFRONT)) { sServerFacade->SetFacingTo(bot, faceTo); // failWithDelay = true; } if (failWithDelay) { SetNextCheckDelay(sPlayerbotAIConfig->reactDelay); return false; } Spell* spell = new Spell(bot, spellInfo, TRIGGERED_NONE); SpellCastTargets targets; if (spellInfo->Effects[0].Effect != SPELL_EFFECT_OPEN_LOCK && (spellInfo->Targets & TARGET_FLAG_ITEM || spellInfo->Targets & TARGET_FLAG_GAMEOBJECT_ITEM)) { Item* item = itemTarget ? itemTarget : aiObjectContext->GetValue("item for spell", spellId)->Get(); targets.SetItemTarget(item); if (bot->GetTradeData()) { bot->GetTradeData()->SetSpell(spellId); delete spell; // if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) // { // LOG_DEBUG("playerbots", "Spell cast no item - target name: {}, spellid: {}, bot name: {}", // target->GetName(), spellId, bot->GetName()); // } return true; } } else if (spellInfo->Targets & TARGET_FLAG_DEST_LOCATION) { // WorldLocation aoe = aiObjectContext->GetValue("aoe position")->Get(); // targets.SetDst(aoe); targets.SetDst(*target); } else if (spellInfo->Targets & TARGET_FLAG_SOURCE_LOCATION) { targets.SetDst(*bot); } else { targets.SetUnitTarget(target); } if (spellInfo->Effects[0].Effect == SPELL_EFFECT_OPEN_LOCK || spellInfo->Effects[0].Effect == SPELL_EFFECT_SKINNING) { LootObject loot = *aiObjectContext->GetValue("loot target"); GameObject* go = GetGameObject(loot.guid); if (go && go->isSpawned()) { WorldPacket packetgouse(CMSG_GAMEOBJ_USE, 8); packetgouse << loot.guid; bot->GetSession()->HandleGameObjectUseOpcode(packetgouse); targets.SetGOTarget(go); faceTo = go; } else if (itemTarget) { Player* trader = bot->GetTrader(); if (trader) { targets.SetTradeItemTarget(bot); targets.SetUnitTarget(bot); faceTo = trader; } else { targets.SetItemTarget(itemTarget); } } else { if (Unit* creature = GetUnit(loot.guid)) { targets.SetUnitTarget(creature); faceTo = creature; } } } if (bot->isMoving() && spell->GetCastTime()) { // bot->StopMoving(); SetNextCheckDelay(sPlayerbotAIConfig->reactDelay); spell->cancel(); delete spell; return false; } // spell->m_targets.SetUnitTarget(target); // SpellCastResult spellSuccess = spell->CheckCast(true); // if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) // { // LOG_DEBUG("playerbots", "Spell cast result - target name: {}, spellid: {}, bot name: {}, result: {}", // target->GetName(), spellId, bot->GetName(), spellSuccess); // } // if (spellSuccess != SPELL_CAST_OK) // return false; SpellCastResult result = spell->prepare(&targets); if (result != SPELL_CAST_OK) { // if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) // { // LOG_DEBUG("playerbots", "Spell cast failed. - target name: {}, spellid: {}, bot name: {}, result: {}", // target->GetName(), spellId, bot->GetName(), result); // } if (HasStrategy("debug spell", BOT_STATE_NON_COMBAT)) { std::ostringstream out; out << "Spell cast failed - "; out << "Spell ID: " << spellId << " (" << ChatHelper::FormatSpell(spellInfo) << "), "; out << "Error Code: " << static_cast(result) << " (0x" << std::hex << static_cast(result) << std::dec << "), "; out << "Bot: " << bot->GetName() << ", "; // Check spell target type if (targets.GetUnitTarget()) { out << "Target: Unit (" << targets.GetUnitTarget()->GetName() << ", Low GUID: " << targets.GetUnitTarget()->GetGUID().GetCounter() << ", High GUID: " << static_cast(targets.GetUnitTarget()->GetGUID().GetHigh()) << "), "; } if (targets.GetGOTarget()) { out << "Target: GameObject (Low GUID: " << targets.GetGOTarget()->GetGUID().GetCounter() << ", High GUID: " << static_cast(targets.GetGOTarget()->GetGUID().GetHigh()) << "), "; } if (targets.GetItemTarget()) { out << "Target: Item (Low GUID: " << targets.GetItemTarget()->GetGUID().GetCounter() << ", High GUID: " << static_cast(targets.GetItemTarget()->GetGUID().GetHigh()) << "), "; } // Check if bot is in trade mode if (bot->GetTradeData()) { out << "Trade Mode: Active, "; Item* tradeItem = bot->GetTradeData()->GetTraderData()->GetItem(TRADE_SLOT_NONTRADED); if (tradeItem) { out << "Trade Item: " << tradeItem->GetEntry() << " (Low GUID: " << tradeItem->GetGUID().GetCounter() << ", High GUID: " << static_cast(tradeItem->GetGUID().GetHigh()) << "), "; } else { out << "Trade Item: None, "; } } else { out << "Trade Mode: Inactive, "; } TellMasterNoFacing(out); } return false; } // if (spellInfo->Effects[0].Effect == SPELL_EFFECT_OPEN_LOCK || spellInfo->Effects[0].Effect == // SPELL_EFFECT_SKINNING) // { // LootObject loot = *aiObjectContext->GetValue("loot target"); // if (!loot.IsLootPossible(bot)) // { // spell->cancel(); // delete spell; // if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) // { // LOG_DEBUG("playerbots", "Spell cast loot - target name: {}, spellid: {}, bot name: {}", // target->GetName(), spellId, bot->GetName()); // } // return false; // } // } // WaitForSpellCast(spell); aiObjectContext->GetValue("last spell cast")->Get().Set(spellId, target->GetGUID(), time(nullptr)); aiObjectContext->GetValue("position")->Get()["random"].Reset(); if (oldSel) bot->SetSelection(oldSel); if (HasStrategy("debug spell", BOT_STATE_NON_COMBAT)) { std::ostringstream out; out << "Casting " << ChatHelper::FormatSpell(spellInfo); TellMasterNoFacing(out); } return true; } bool PlayerbotAI::CastSpell(uint32 spellId, float x, float y, float z, Item* itemTarget) { if (!spellId) return false; Pet* pet = bot->GetPet(); SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); if (pet && pet->HasSpell(spellId)) { bool autocast = false; for (unsigned int& m_autospell : pet->m_autospells) { if (m_autospell == spellId) { autocast = true; break; } } pet->ToggleAutocast(spellInfo, !autocast); std::ostringstream out; out << (autocast ? "|cffff0000|Disabling" : "|cFF00ff00|Enabling") << " pet auto-cast for "; out << chatHelper.FormatSpell(spellInfo); TellMaster(out); return true; } // aiObjectContext->GetValue("last movement")->Get().Set(nullptr); // aiObjectContext->GetValue("stay time")->Set(0); // MotionMaster& mm = *bot->GetMotionMaster(); if (bot->IsFlying() || bot->HasUnitState(UNIT_STATE_IN_FLIGHT)) return false; // bot->ClearUnitState(UNIT_STATE_CHASE); // bot->ClearUnitState(UNIT_STATE_FOLLOW); bool failWithDelay = false; if (!bot->IsStandState()) { bot->SetStandState(UNIT_STAND_STATE_STAND); failWithDelay = true; } ObjectGuid oldSel = bot->GetSelectedUnit() ? bot->GetSelectedUnit()->GetGUID() : ObjectGuid(); if (!bot->isMoving()) bot->SetFacingTo(bot->GetAngle(x, y)); if (failWithDelay) { SetNextCheckDelay(sPlayerbotAIConfig->globalCoolDown); return false; } Spell* spell = new Spell(bot, spellInfo, TRIGGERED_NONE); SpellCastTargets targets; if (spellInfo->Targets & TARGET_FLAG_ITEM || spellInfo->Targets & TARGET_FLAG_GAMEOBJECT_ITEM) { Item* item = itemTarget ? itemTarget : aiObjectContext->GetValue("item for spell", spellId)->Get(); targets.SetItemTarget(item); if (bot->GetTradeData()) { bot->GetTradeData()->SetSpell(spellId); delete spell; return true; } } else if (spellInfo->Targets & TARGET_FLAG_DEST_LOCATION) { // WorldLocation aoe = aiObjectContext->GetValue("aoe position")->Get(); targets.SetDst(x, y, z, 0.f); } else if (spellInfo->Targets & TARGET_FLAG_SOURCE_LOCATION) { targets.SetDst(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), 0.f); } else { return false; } if (spellInfo->Effects[0].Effect == SPELL_EFFECT_OPEN_LOCK || spellInfo->Effects[0].Effect == SPELL_EFFECT_SKINNING) { return false; } spell->prepare(&targets); if (bot->isMoving() && spell->GetCastTime()) { // bot->StopMoving(); SetNextCheckDelay(sPlayerbotAIConfig->reactDelay); spell->cancel(); delete spell; return false; } if (spellInfo->Effects[0].Effect == SPELL_EFFECT_OPEN_LOCK || spellInfo->Effects[0].Effect == SPELL_EFFECT_SKINNING) { LootObject loot = *aiObjectContext->GetValue("loot target"); if (!loot.IsLootPossible(bot)) { spell->cancel(); delete spell; return false; } } // WaitForSpellCast(spell); aiObjectContext->GetValue("last spell cast")->Get().Set(spellId, bot->GetGUID(), time(nullptr)); aiObjectContext->GetValue("position")->Get()["random"].Reset(); if (oldSel) bot->SetSelection(oldSel); if (HasStrategy("debug spell", BOT_STATE_NON_COMBAT)) { std::ostringstream out; out << "Casting " << ChatHelper::FormatSpell(spellInfo); TellMasterNoFacing(out); } return true; } bool PlayerbotAI::CanCastVehicleSpell(uint32 spellId, Unit* target) { if (!spellId) return false; Vehicle* vehicle = bot->GetVehicle(); if (!vehicle) return false; // do not allow if no spells VehicleSeatEntry const* seat = vehicle->GetSeatForPassenger(bot); if (!seat || !(seat->m_flags & VEHICLE_SEAT_FLAG_CAN_CAST)) return false; Unit* vehicleBase = vehicle->GetBase(); Unit* spellTarget = target; if (!spellTarget) spellTarget = vehicleBase; if (!spellTarget) return false; if (vehicleBase->HasSpellCooldown(spellId)) return false; SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); if (!spellInfo) return false; // check BG siege position set in BG Tactics PositionInfo siegePos = GetAiObjectContext()->GetValue("position")->Get()["bg siege"]; // do not cast spell on self if spell is location based if (!(siegePos.isSet() || spellTarget != vehicleBase) && spellInfo->Targets & TARGET_FLAG_DEST_LOCATION) return false; uint32 CastingTime = !spellInfo->IsChanneled() ? spellInfo->CalcCastTime(vehicleBase) : spellInfo->GetDuration(); if (CastingTime && vehicleBase->isMoving()) return false; if (vehicleBase != spellTarget && sServerFacade->GetDistance2d(vehicleBase, spellTarget) > 120.0f) return false; if (!target && siegePos.isSet()) { if (sServerFacade->GetDistance2d(vehicleBase, siegePos.x, siegePos.y) > 120.0f) return false; } Spell* spell = new Spell(vehicleBase, spellInfo, TRIGGERED_NONE); WorldLocation dest; if (siegePos.isSet()) dest = WorldLocation(bot->GetMapId(), siegePos.x, siegePos.y, siegePos.z, 0); else if (spellTarget != vehicleBase) dest = WorldLocation(spellTarget->GetMapId(), spellTarget->GetPosition()); if (spellInfo->Targets & TARGET_FLAG_DEST_LOCATION) spell->m_targets.SetDst(dest); else if (spellTarget != vehicleBase) spell->m_targets.SetUnitTarget(spellTarget); SpellCastResult result = spell->CheckCast(true); delete spell; switch (result) { case SPELL_FAILED_NOT_INFRONT: case SPELL_FAILED_NOT_STANDING: case SPELL_FAILED_UNIT_NOT_INFRONT: case SPELL_FAILED_MOVING: case SPELL_FAILED_TRY_AGAIN: case SPELL_CAST_OK: return true; default: return false; } return false; } bool PlayerbotAI::CastVehicleSpell(uint32 spellId, Unit* target) { if (!spellId) return false; Vehicle* vehicle = bot->GetVehicle(); if (!vehicle) return false; // do not allow if no spells VehicleSeatEntry const* seat = vehicle->GetSeatForPassenger(bot); if (!seat || !(seat->m_flags & VEHICLE_SEAT_FLAG_CAN_CAST)) return false; Unit* vehicleBase = vehicle->GetBase(); Unit* spellTarget = target; if (!spellTarget) spellTarget = vehicleBase; if (!spellTarget) return false; SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); if (!spellInfo) return false; // check BG siege position set in BG Tactics PositionInfo siegePos = GetAiObjectContext()->GetValue("position")->Get()["bg siege"]; if (!target && siegePos.isSet()) { if (sServerFacade->GetDistance2d(vehicleBase, siegePos.x, siegePos.y) > 120.0f) return false; } // do not cast spell on self if spell is location based if (!(siegePos.isSet() || spellTarget != vehicleBase) && (spellInfo->Targets & TARGET_FLAG_DEST_LOCATION) != 0) return false; if (seat->CanControl()) { // aiObjectContext->GetValue("last movement")->Get().Set(nullptr); // aiObjectContext->GetValue("stay time")->Set(0); } // bot->clearUnitState(UNIT_STAT_CHASE); // bot->clearUnitState(UNIT_STAT_FOLLOW); // ObjectGuid oldSel = bot->GetSelectionGuid(); // bot->SetSelectionGuid(target->GetGUID()); // turn vehicle if target is not in front bool failWithDelay = false; if (spellTarget != vehicleBase && (seat->CanControl() || (seat->m_flags & VEHICLE_SEAT_FLAG_ALLOW_TURNING))) { if (!vehicleBase->HasInArc(CAST_ANGLE_IN_FRONT, spellTarget, 100.0f)) { vehicleBase->SetFacingToObject(spellTarget); failWithDelay = true; } } if (siegePos.isSet() && (seat->CanControl() || (seat->m_flags & VEHICLE_SEAT_FLAG_ALLOW_TURNING))) { vehicleBase->SetFacingTo(vehicleBase->GetAngle(siegePos.x, siegePos.y)); } if (failWithDelay) { SetNextCheckDelay(sPlayerbotAIConfig->reactDelay); return false; } Spell* spell = new Spell(vehicleBase, spellInfo, TRIGGERED_NONE); SpellCastTargets targets; if ((spellTarget != vehicleBase || siegePos.isSet()) && (spellInfo->Targets & TARGET_FLAG_DEST_LOCATION)) { WorldLocation dest; if (spellTarget != vehicleBase) dest = WorldLocation(spellTarget->GetMapId(), spellTarget->GetPosition()); else if (siegePos.isSet()) dest = WorldLocation(bot->GetMapId(), siegePos.x + frand(-5.0f, 5.0f), siegePos.y + frand(-5.0f, 5.0f), siegePos.z, 0.0f); else return false; targets.SetDst(dest); targets.SetSpeed(30.0f); float dist = vehicleBase->GetPosition().GetExactDist(dest); // very much an approximation of the real projectile arc float elev = dist >= 110.0f ? 1.0f : pow(((dist + 10.0f) / 120.0f), 2.0f); targets.SetElevation(elev); } if (spellInfo->Targets & TARGET_FLAG_SOURCE_LOCATION) { targets.SetSrc(vehicleBase->GetPositionX(), vehicleBase->GetPositionY(), vehicleBase->GetPositionZ()); } if (target && !(spellInfo->Targets & TARGET_FLAG_DEST_LOCATION)) { targets.SetUnitTarget(spellTarget); } spell->prepare(&targets); if (seat->CanControl() && vehicleBase->isMoving() && spell->GetCastTime()) { vehicleBase->StopMoving(); SetNextCheckDelay(sPlayerbotAIConfig->globalCoolDown); spell->cancel(); // delete spell; return false; } // WaitForSpellCast(spell); // aiObjectContext->GetValue("last spell cast")->Get().Set(spellId, target->GetGUID(), time(0)); // aiObjectContext->GetValue("position")->Get()["random"].Reset(); if (HasStrategy("debug spell", BOT_STATE_NON_COMBAT)) { std::ostringstream out; out << "Casting Vehicle Spell" << ChatHelper::FormatSpell(spellInfo); TellMasterNoFacing(out); } return true; } bool PlayerbotAI::IsInVehicle(bool canControl, bool canCast, bool canAttack, bool canTurn, bool fixed) { Vehicle* vehicle = bot->GetVehicle(); if (!vehicle) return false; // get vehicle Unit* vehicleBase = vehicle->GetBase(); if (!vehicleBase || !vehicleBase->IsAlive()) return false; if (!vehicle->GetVehicleInfo()) return false; // get seat VehicleSeatEntry const* seat = vehicle->GetSeatForPassenger(bot); if (!seat) return false; if (!(canControl || canCast || canAttack || canTurn || fixed)) return true; if (canControl) return seat->CanControl() && !(vehicle->GetVehicleInfo()->m_flags & VEHICLE_FLAG_FIXED_POSITION); if (canCast) return (seat->m_flags & VEHICLE_SEAT_FLAG_CAN_CAST) != 0; if (canAttack) return (seat->m_flags & VEHICLE_SEAT_FLAG_CAN_ATTACK) != 0; if (canTurn) return (seat->m_flags & VEHICLE_SEAT_FLAG_ALLOW_TURNING) != 0; if (fixed) return (vehicle->GetVehicleInfo()->m_flags & VEHICLE_FLAG_FIXED_POSITION) != 0; return false; } void PlayerbotAI::WaitForSpellCast(Spell* spell) { SpellInfo const* spellInfo = spell->GetSpellInfo(); uint32 castTime = spell->GetCastTime(); if (spellInfo->IsChanneled()) { int32 duration = spellInfo->GetDuration(); bot->ApplySpellMod(spellInfo->Id, SPELLMOD_DURATION, duration); if (duration > 0) castTime += duration; } SetNextCheckDelay(castTime + sPlayerbotAIConfig->reactDelay); } void PlayerbotAI::InterruptSpell() { for (uint8 type = CURRENT_MELEE_SPELL; type <= CURRENT_CHANNELED_SPELL; type++) { Spell* spell = bot->GetCurrentSpell((CurrentSpellTypes)type); if (!spell) continue; bot->InterruptSpell((CurrentSpellTypes)type); WorldPacket data(SMSG_SPELL_FAILURE, 8 + 1 + 4 + 1); data << bot->GetPackGUID(); data << uint8(1); data << uint32(spell->m_spellInfo->Id); data << uint8(0); bot->SendMessageToSet(&data, true); data.Initialize(SMSG_SPELL_FAILED_OTHER, 8 + 1 + 4 + 1); data << bot->GetPackGUID(); data << uint8(1); data << uint32(spell->m_spellInfo->Id); data << uint8(0); bot->SendMessageToSet(&data, true); SpellInterrupted(spell->m_spellInfo->Id); } } void PlayerbotAI::RemoveAura(std::string const name) { uint32 spellid = aiObjectContext->GetValue("spell id", name)->Get(); if (spellid && HasAura(spellid, bot)) bot->RemoveAurasDueToSpell(spellid); } bool PlayerbotAI::IsInterruptableSpellCasting(Unit* target, std::string const spell) { uint32 spellid = aiObjectContext->GetValue("spell id", spell)->Get(); if (!spellid || !target->IsNonMeleeSpellCast(true)) return false; SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellid); if (!spellInfo) return false; for (uint8 i = EFFECT_0; i <= EFFECT_2; i++) { if ((spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_INTERRUPT) && spellInfo->PreventionType == SPELL_PREVENTION_TYPE_SILENCE) return true; if (spellInfo->Effects[i].Effect == SPELL_EFFECT_INTERRUPT_CAST && !target->IsImmunedToSpellEffect(spellInfo, i)) return true; if ((spellInfo->Effects[i].Effect == SPELL_EFFECT_APPLY_AURA) && spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_MOD_SILENCE) return true; } return false; } bool PlayerbotAI::HasAuraToDispel(Unit* target, uint32 dispelType) { if (!target->IsInWorld()) { return false; } bool isFriend = bot->IsFriendlyTo(target); Unit::VisibleAuraMap const* visibleAuras = target->GetVisibleAuras(); for (Unit::VisibleAuraMap::const_iterator itr = visibleAuras->begin(); itr != visibleAuras->end(); ++itr) { Aura* aura = itr->second->GetBase(); if (aura->IsPassive()) continue; if (sPlayerbotAIConfig->dispelAuraDuration && aura->GetDuration() && aura->GetDuration() < (int32)sPlayerbotAIConfig->dispelAuraDuration) continue; SpellInfo const* spellInfo = aura->GetSpellInfo(); bool isPositiveSpell = spellInfo->IsPositive(); if (isPositiveSpell && isFriend) continue; if (!isPositiveSpell && !isFriend) continue; if (canDispel(spellInfo, dispelType)) return true; } return false; } #ifndef WIN32 inline int strcmpi(char const* s1, char const* s2) { for (; *s1 && *s2 && (toupper(*s1) == toupper(*s2)); ++s1, ++s2) { } return *s1 - *s2; } #endif bool PlayerbotAI::canDispel(SpellInfo const* spellInfo, uint32 dispelType) { if (spellInfo->Dispel != dispelType) return false; if (!spellInfo->SpellName[0]) { return true; } for (std::string& wl : dispel_whitelist) { if (strcmpi((const char*)spellInfo->SpellName[0], wl.c_str()) == 0) { return false; } } return !spellInfo->SpellName[0] || (strcmpi((const char*)spellInfo->SpellName[0], "demon skin") && strcmpi((const char*)spellInfo->SpellName[0], "mage armor") && strcmpi((const char*)spellInfo->SpellName[0], "frost armor") && strcmpi((const char*)spellInfo->SpellName[0], "wavering will") && strcmpi((const char*)spellInfo->SpellName[0], "chilled") && strcmpi((const char*)spellInfo->SpellName[0], "mana tap") && strcmpi((const char*)spellInfo->SpellName[0], "ice armor")); } bool IsAlliance(uint8 race) { return race == RACE_HUMAN || race == RACE_DWARF || race == RACE_NIGHTELF || race == RACE_GNOME || race == RACE_DRAENEI; } Player* PlayerbotAI::FindNewMaster() { // Ideally we want to have the leader as master. Group* group = bot->GetGroup(); // Only allow real players as masters unless in battleground. if (!group) return nullptr; Player* groupLeader = GetGroupMaster(); PlayerbotAI* leaderBotAI = GET_PLAYERBOT_AI(groupLeader); if (!leaderBotAI || leaderBotAI->IsRealPlayer()) return groupLeader; // Find the real player in group for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* member = gref->GetSource(); if (!member || member == bot || !member->IsInWorld() || !member->IsInSameRaidWith(bot)) continue; PlayerbotAI* memberBotAI = GET_PLAYERBOT_AI(member); if ((!memberBotAI || memberBotAI->IsRealPlayer()) && !bot->InBattleground()) return member; if (bot->InBattleground() && bot->GetBattleground() && bot->GetBattleground()->GetBgTypeID() == BATTLEGROUND_AV && !GET_PLAYERBOT_AI(member) && member->InBattleground() && bot->GetMapId() == member->GetMapId()) { // Skip if same BG but same subgroup or lower level if (!group->SameSubGroup(bot, member) || member->GetLevel() < bot->GetLevel()) continue; // Follow real player only if higher honor points uint32 honorpts = member->GetHonorPoints(); if (bot->GetHonorPoints() && honorpts < bot->GetHonorPoints()) continue; return member; } } return nullptr; } bool PlayerbotAI::HasRealPlayerMaster() { if (master) { PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(master); return !masterBotAI || masterBotAI->IsRealPlayer(); } return false; } bool PlayerbotAI::HasActivePlayerMaster() { return master && !GET_PLAYERBOT_AI(master); } bool PlayerbotAI::IsAlt() { return HasRealPlayerMaster() && !sRandomPlayerbotMgr->IsRandomBot(bot); } Player* PlayerbotAI::GetGroupMaster() { if (!bot->InBattleground()) if (Group* group = bot->GetGroup()) if (Player* player = ObjectAccessor::FindPlayer(group->GetLeaderGUID())) return player; return master; } uint32 PlayerbotAI::GetFixedBotNumer(uint32 maxNum, float cyclePerMin) { uint32 randseed = rand32(); // Seed random number uint32 randnum = bot->GetGUID().GetCounter() + randseed; // Semi-random but fixed number for each bot. if (cyclePerMin > 0) { uint32 cycle = floor(getMSTime() / (1000)); // Semi-random number adds 1 each second. cycle = cycle * cyclePerMin / 60; // Cycles cyclePerMin per minute. randnum += cycle; // Make the random number cylce. } randnum = (randnum % (maxNum + 1)); // Loops the randomnumber at maxNum. Bassically removes all the numbers above 99. return randnum; // Now we have a number unique for each bot between 0 and maxNum that increases by cyclePerMin. } /* enum GrouperType { SOLO = 0, MEMBER = 1, LEADER_2 = 2, LEADER_3 = 3, LEADER_4 = 4, LEADER_5 = 5 }; */ GrouperType PlayerbotAI::GetGrouperType() { uint32 grouperNumber = GetFixedBotNumer(100, 0); if (grouperNumber < 20 && !HasRealPlayerMaster()) return GrouperType::SOLO; if (grouperNumber < 80) return GrouperType::MEMBER; if (grouperNumber < 85) return GrouperType::LEADER_2; if (grouperNumber < 90) return GrouperType::LEADER_3; if (grouperNumber < 95) return GrouperType::LEADER_4; return GrouperType::LEADER_5; } GuilderType PlayerbotAI::GetGuilderType() { uint32 grouperNumber = GetFixedBotNumer(100, 0); if (grouperNumber < 20 && !HasRealPlayerMaster()) return GuilderType::SOLO; if (grouperNumber < 30) return GuilderType::TINY; if (grouperNumber < 40) return GuilderType::SMALL; if (grouperNumber < 60) return GuilderType::MEDIUM; if (grouperNumber < 80) return GuilderType::LARGE; return GuilderType::VERY_LARGE; } bool PlayerbotAI::HasPlayerNearby(WorldPosition* pos, float range) { float sqRange = range * range; bool nearPlayer = false; for (auto& player : sRandomPlayerbotMgr->GetPlayers()) { if (!player->IsGameMaster() || player->isGMVisible()) { if (player->GetMapId() != bot->GetMapId()) continue; if (pos->sqDistance(WorldPosition(player)) < sqRange) nearPlayer = true; // if player is far check farsight/cinematic camera WorldObject* viewObj = player->GetViewpoint(); if (viewObj && viewObj != player) { if (pos->sqDistance(WorldPosition(viewObj)) < sqRange) nearPlayer = true; } } } return nearPlayer; } bool PlayerbotAI::HasPlayerNearby(float range) { WorldPosition botPos(bot); return HasPlayerNearby(&botPos, range); }; bool PlayerbotAI::HasManyPlayersNearby(uint32 trigerrValue, float range) { float sqRange = range * range; uint32 found = 0; for (auto& player : sRandomPlayerbotMgr->GetPlayers()) { if ((!player->IsGameMaster() || player->isGMVisible()) && sServerFacade->GetDistance2d(player, bot) < sqRange) { found++; if (found >= trigerrValue) return true; } } return false; } inline bool HasRealPlayers(Map* map) { Map::PlayerList const& players = map->GetPlayers(); if (players.IsEmpty()) { return false; } for (auto const& itr : players) { Player* player = itr.GetSource(); if (!player || !player->IsVisible()) { continue; } PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); if (!botAI || botAI->IsRealPlayer() || botAI->HasRealPlayerMaster()) { return true; } } return false; } inline bool ZoneHasRealPlayers(Player* bot) { Map* map = bot->GetMap(); if (!bot || !map) { return false; } for (Player* player : sRandomPlayerbotMgr->GetPlayers()) { if (player->GetMapId() != bot->GetMapId()) continue; if (player->IsGameMaster() && !player->IsVisible()) { continue; } if (player->GetZoneId() == bot->GetZoneId()) { PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); if (!botAI || botAI->IsRealPlayer() || botAI->HasRealPlayerMaster()) { return true; } } } return false; } bool PlayerbotAI::AllowActive(ActivityType activityType) { // when botActiveAlone is 100% and smartScale disabled if (sPlayerbotAIConfig->botActiveAlone >= 100 && !sPlayerbotAIConfig->botActiveAloneSmartScale) { return true; } // Is in combat. Always defend yourself. if (activityType != OUT_OF_PARTY_ACTIVITY && activityType != PACKET_ACTIVITY) { if (bot->IsInCombat()) { return true; } } // only keep updating till initializing time has completed, // which prevents unneeded expensive GameTime calls. if (_isBotInitializing) { _isBotInitializing = GameTime::GetUptime().count() < sPlayerbotAIConfig->maxRandomBots * 0.11; // no activity allowed during bot initialization if (_isBotInitializing) { return false; } } // General exceptions if (activityType == PACKET_ACTIVITY) { return true; } // bg, raid, dungeon if (!WorldPosition(bot).isOverworld()) { return true; } // bot map has active players. if (sPlayerbotAIConfig->BotActiveAloneForceWhenInMap) { if (HasRealPlayers(bot->GetMap())) { return true; } } // bot zone has active players. if (sPlayerbotAIConfig->BotActiveAloneForceWhenInZone) { if (ZoneHasRealPlayers(bot)) { return true; } } // when in real guild if (sPlayerbotAIConfig->BotActiveAloneForceWhenInGuild) { if (IsInRealGuild()) { return true; } } // Player is near. Always active. if (HasPlayerNearby(sPlayerbotAIConfig->BotActiveAloneForceWhenInRadius)) { return true; } // Has player master. Always active. if (GetMaster()) { PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(GetMaster()); if (!masterBotAI || masterBotAI->IsRealPlayer()) { return true; } } // if grouped up Group* group = bot->GetGroup(); if (group) { for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* member = gref->GetSource(); if ((!member || !member->IsInWorld()) && member->GetMapId() != bot->GetMapId()) { continue; } if (member == bot) { continue; } PlayerbotAI* memberBotAI = GET_PLAYERBOT_AI(member); { if (!memberBotAI || memberBotAI->HasRealPlayerMaster()) { return true; } } if (group->IsLeader(member->GetGUID())) { if (!memberBotAI->AllowActivity(PARTY_ACTIVITY)) { return false; } } } } // In bg queue. Speed up bg queue/join. if (bot->InBattlegroundQueue()) { return true; } bool isLFG = false; if (group) { if (sLFGMgr->GetState(group->GetGUID()) != lfg::LFG_STATE_NONE) { isLFG = true; } } if (sLFGMgr->GetState(bot->GetGUID()) != lfg::LFG_STATE_NONE) { isLFG = true; } if (isLFG) { return true; } // HasFriend if (sPlayerbotAIConfig->BotActiveAloneForceWhenIsFriend) { if (!bot || !bot->IsInWorld() || !bot->GetGUID()) return false; for (auto& player : sRandomPlayerbotMgr->GetPlayers()) { if (!player || !player->IsInWorld()) continue; Player* connectedPlayer = ObjectAccessor::FindPlayer(player->GetGUID()); if (!connectedPlayer) continue; PlayerSocial* social = player->GetSocial(); if (!social) continue; if (social->HasFriend(bot->GetGUID())) return true; } } // Force the bots to spread if (activityType == OUT_OF_PARTY_ACTIVITY || activityType == GRIND_ACTIVITY) { if (HasManyPlayersNearby(10, 40)) { return true; } } // Bots don't need to move using PathGenerator. if (activityType == DETAILED_MOVE_ACTIVITY) { return false; } if (sPlayerbotAIConfig->botActiveAlone <= 0) { return false; } // ####################################################################################### // All mandatory conditations are checked to be active or not, from here the remaining // situations are usable for scaling when enabled. // ####################################################################################### // Below is code to have a specified % of bots active at all times. // The default is 100%. With 1% of all bots going active or inactive each minute. uint32 mod = sPlayerbotAIConfig->botActiveAlone > 100 ? 100 : sPlayerbotAIConfig->botActiveAlone; if (sPlayerbotAIConfig->botActiveAloneSmartScale && bot->GetLevel() >= sPlayerbotAIConfig->botActiveAloneSmartScaleWhenMinLevel && bot->GetLevel() <= sPlayerbotAIConfig->botActiveAloneSmartScaleWhenMaxLevel) { mod = AutoScaleActivity(mod); } uint32 ActivityNumber = GetFixedBotNumer(100, sPlayerbotAIConfig->botActiveAlone * static_cast(mod) / 100 * 0.01f); return ActivityNumber <= (sPlayerbotAIConfig->botActiveAlone * mod) / 100; // The given percentage of bots should be active and rotate 1% of those active bots each minute. } bool PlayerbotAI::AllowActivity(ActivityType activityType, bool checkNow) { if (!allowActiveCheckTimer[activityType]) allowActiveCheckTimer[activityType] = time(nullptr); if (!checkNow && time(nullptr) < (allowActiveCheckTimer[activityType] + 5)) return allowActive[activityType]; bool allowed = AllowActive(activityType); allowActive[activityType] = allowed; allowActiveCheckTimer[activityType] = time(nullptr); return allowed; } uint32 PlayerbotAI::AutoScaleActivity(uint32 mod) { // Current max server update time (ms), and the configured floor/ceiling values for bot scaling uint32 maxDiff = sWorldUpdateTime.GetMaxUpdateTimeOfCurrentTable(); uint32 diffLimitFloor = sPlayerbotAIConfig->botActiveAloneSmartScaleDiffLimitfloor; uint32 diffLimitCeiling = sPlayerbotAIConfig->botActiveAloneSmartScaleDiffLimitCeiling; if (diffLimitCeiling <= diffLimitFloor) { // Perfrom binary decision if ceiling <= floor: Either all bots are active or none are return (maxDiff > diffLimitCeiling) ? 0 : mod; } if (maxDiff > diffLimitCeiling) return 0; if (maxDiff <= diffLimitFloor) return mod; // Calculate lag progress from floor to ceiling (0 to 1) double lagProgress = (maxDiff - diffLimitFloor) / (double)(diffLimitCeiling - diffLimitFloor); // Apply the percentage of active bots (the complement of lag progress) to the mod value return static_cast(mod * (1 - lagProgress)); } bool PlayerbotAI::IsOpposing(Player* player) { return IsOpposing(player->getRace(), bot->getRace()); } bool PlayerbotAI::IsOpposing(uint8 race1, uint8 race2) { return (IsAlliance(race1) && !IsAlliance(race2)) || (!IsAlliance(race1) && IsAlliance(race2)); } void PlayerbotAI::RemoveShapeshift() { RemoveAura("bear form"); RemoveAura("dire bear form"); RemoveAura("moonkin form"); RemoveAura("travel form"); RemoveAura("cat form"); RemoveAura("flight form"); bot->RemoveAura(33943); // The latter added for now as RemoveAura("flight form") currently does not work. RemoveAura("swift flight form"); RemoveAura("aquatic form"); RemoveAura("ghost wolf"); // RemoveAura("tree of life"); } // Mirrors Blizzard’s GetAverageItemLevel rules : // https://wowpedia.fandom.com/wiki/API_GetAverageItemLevel uint32 PlayerbotAI::GetEquipGearScore(Player* player) { constexpr uint8 TOTAL_SLOTS = 17; // every slot except Body & Tabard uint32 sumLevel = 0; /* ---------- 0. Detect “ignore off-hand” situations --------- */ Item* main = player->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); Item* off = player->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); bool ignoreOffhand = false; // true → divisor = 16 if (main) { bool twoHand = (main->GetTemplate()->InventoryType == INVTYPE_2HWEAPON); if (twoHand && !player->HasAura(SPELL_TITAN_GRIP)) ignoreOffhand = true; // classic 2-hander } else if (!off) // both hands empty ignoreOffhand = true; /* ---------- 1. Sum up item-levels -------------------------- */ for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot) { if (slot == EQUIPMENT_SLOT_BODY || slot == EQUIPMENT_SLOT_TABARD) continue; // Blizzard never counts these if (ignoreOffhand && slot == EQUIPMENT_SLOT_OFFHAND) continue; // skip off-hand in 2-H case if (Item* it = player->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) sumLevel += it->GetTemplate()->ItemLevel; // missing items add 0 } /* ---------- 2. Divide by 17 or 16 -------------------------- */ const uint8 divisor = ignoreOffhand ? TOTAL_SLOTS - 1 : TOTAL_SLOTS; // 16 or 17 return sumLevel / divisor; } // NOTE : function rewritten as flags "withBags" and "withBank" not used, and _fillGearScoreData sometimes attribute // one-hand/2H Weapon in wrong slots /*uint32 PlayerbotAI::GetEquipGearScore(Player* player) { // This function aims to calculate the equipped gear score uint32 sum = 0; uint8 count = EQUIPMENT_SLOT_END - 2; // ignore body and tabard slots uint8 mh_type = 0; for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) { Item* item =player->GetItemByPos(INVENTORY_SLOT_BAG_0, i); if (item && i != EQUIPMENT_SLOT_BODY && i != EQUIPMENT_SLOT_TABARD) { ItemTemplate const* proto = item->GetTemplate(); sum += proto->ItemLevel; // If character is not warfury and have 2 hand weapon equipped, main hand will be counted twice if (i == SLOT_MAIN_HAND) mh_type = item->GetTemplate()->InventoryType; if (!player->HasAura(SPELL_TITAN_GRIP) && mh_type == INVTYPE_2HWEAPON && i == SLOT_MAIN_HAND) sum += item->GetTemplate()->ItemLevel; } } uint32 gs = uint32(sum / count); return gs; }*/ /*uint32 PlayerbotAI::GetEquipGearScore(Player* player, bool withBags, bool withBank) { std::vector gearScore(EQUIPMENT_SLOT_END); uint32 twoHandScore = 0; for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) { if (Item* item = player->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) _fillGearScoreData(player, item, &gearScore, twoHandScore); } if (withBags) { // check inventory for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) { if (Item* item = player->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) _fillGearScoreData(player, item, &gearScore, twoHandScore); } // check bags for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) { if (Bag* pBag = (Bag*)player->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { for (uint32 j = 0; j < pBag->GetBagSize(); ++j) { if (Item* item2 = pBag->GetItemByPos(j)) _fillGearScoreData(player, item2, &gearScore, twoHandScore); } } } } if (withBank) { for (uint8 i = BANK_SLOT_ITEM_START; i < BANK_SLOT_ITEM_END; ++i) { if (Item* item = player->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) _fillGearScoreData(player, item, &gearScore, twoHandScore); } for (uint8 i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; ++i) { if (Item* item = player->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { if (item->IsBag()) { Bag* bag = (Bag*)item; for (uint8 j = 0; j < bag->GetBagSize(); ++j) { if (Item* item2 = bag->GetItemByPos(j)) _fillGearScoreData(player, item2, &gearScore, twoHandScore); } } } } } uint8 count = EQUIPMENT_SLOT_END - 2; // ignore body and tabard slots uint32 sum = 0; // check if 2h hand is higher level than main hand + off hand if (gearScore[EQUIPMENT_SLOT_MAINHAND] + gearScore[EQUIPMENT_SLOT_OFFHAND] < twoHandScore * 2) { gearScore[EQUIPMENT_SLOT_OFFHAND] = 0; // off hand is ignored in calculations if 2h weapon has higher score --count; gearScore[EQUIPMENT_SLOT_MAINHAND] = twoHandScore; } for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) { sum += gearScore[i]; } if (count) { uint32 res = uint32(sum / count); return res; } return 0; }*/ uint32 PlayerbotAI::GetMixedGearScore(Player* player, bool withBags, bool withBank, uint32 topN) { std::vector gearScore(EQUIPMENT_SLOT_END); uint32 twoHandScore = 0; for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) { if (Item* item = player->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) _fillGearScoreData(player, item, &gearScore, twoHandScore, true); } if (withBags) { // check inventory for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) { if (Item* item = player->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) _fillGearScoreData(player, item, &gearScore, twoHandScore, true); } // check bags for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) { if (Bag* pBag = (Bag*)player->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { for (uint32 j = 0; j < pBag->GetBagSize(); ++j) { if (Item* item2 = pBag->GetItemByPos(j)) _fillGearScoreData(player, item2, &gearScore, twoHandScore, true); } } } } if (withBank) { for (uint8 i = BANK_SLOT_ITEM_START; i < BANK_SLOT_ITEM_END; ++i) { if (Item* item = player->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) _fillGearScoreData(player, item, &gearScore, twoHandScore, true); } for (uint8 i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; ++i) { if (Item* item = player->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { if (item->IsBag()) { Bag* bag = (Bag*)item; for (uint8 j = 0; j < bag->GetBagSize(); ++j) { if (Item* item2 = bag->GetItemByPos(j)) _fillGearScoreData(player, item2, &gearScore, twoHandScore, true); } } } } } if (!topN) { uint8 count = EQUIPMENT_SLOT_END - 2; // ignore body and tabard slots uint32 sum = 0; // check if 2h hand is higher level than main hand + off hand if (gearScore[EQUIPMENT_SLOT_MAINHAND] + gearScore[EQUIPMENT_SLOT_OFFHAND] < twoHandScore * 2) { gearScore[EQUIPMENT_SLOT_OFFHAND] = 0; // off hand is ignored in calculations if 2h weapon has higher score --count; gearScore[EQUIPMENT_SLOT_MAINHAND] = twoHandScore; } for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) { sum += gearScore[i]; } if (count) { uint32 res = uint32(sum / count); return res; } return 0; } // topN != 0 if (gearScore[EQUIPMENT_SLOT_MAINHAND] + gearScore[EQUIPMENT_SLOT_OFFHAND] < twoHandScore * 2) { gearScore[EQUIPMENT_SLOT_OFFHAND] = twoHandScore; gearScore[EQUIPMENT_SLOT_MAINHAND] = twoHandScore; } std::vector topGearScore; for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) { topGearScore.push_back(gearScore[i]); } std::sort(topGearScore.begin(), topGearScore.end(), [&](const uint32 lhs, const uint32 rhs) { return lhs > rhs; }); uint32 sum = 0; for (uint32 i = 0; i < std::min((uint32)topGearScore.size(), topN); i++) { sum += topGearScore[i]; } return sum / topN; } void PlayerbotAI::_fillGearScoreData(Player* player, Item* item, std::vector* gearScore, uint32& twoHandScore, bool mixed) { if (!item) return; ItemTemplate const* proto = item->GetTemplate(); if (player->CanUseItem(proto) != EQUIP_ERR_OK) return; uint8 type = proto->InventoryType; uint32 level = mixed ? proto->ItemLevel * PlayerbotAI::GetItemScoreMultiplier(ItemQualities(proto->Quality)) : proto->ItemLevel; switch (type) { case INVTYPE_2HWEAPON: twoHandScore = std::max(twoHandScore, level); break; case INVTYPE_WEAPON: case INVTYPE_WEAPONMAINHAND: (*gearScore)[SLOT_MAIN_HAND] = std::max((*gearScore)[SLOT_MAIN_HAND], level); break; case INVTYPE_SHIELD: case INVTYPE_WEAPONOFFHAND: (*gearScore)[EQUIPMENT_SLOT_OFFHAND] = std::max((*gearScore)[EQUIPMENT_SLOT_OFFHAND], level); break; case INVTYPE_THROWN: case INVTYPE_RANGEDRIGHT: case INVTYPE_RANGED: case INVTYPE_QUIVER: case INVTYPE_RELIC: (*gearScore)[EQUIPMENT_SLOT_RANGED] = std::max((*gearScore)[EQUIPMENT_SLOT_RANGED], level); break; case INVTYPE_HEAD: (*gearScore)[EQUIPMENT_SLOT_HEAD] = std::max((*gearScore)[EQUIPMENT_SLOT_HEAD], level); break; case INVTYPE_NECK: (*gearScore)[EQUIPMENT_SLOT_NECK] = std::max((*gearScore)[EQUIPMENT_SLOT_NECK], level); break; case INVTYPE_SHOULDERS: (*gearScore)[EQUIPMENT_SLOT_SHOULDERS] = std::max((*gearScore)[EQUIPMENT_SLOT_SHOULDERS], level); break; case INVTYPE_BODY: // Shouldn't be considered when calculating average ilevel (*gearScore)[EQUIPMENT_SLOT_BODY] = std::max((*gearScore)[EQUIPMENT_SLOT_BODY], level); break; case INVTYPE_CHEST: (*gearScore)[EQUIPMENT_SLOT_CHEST] = std::max((*gearScore)[EQUIPMENT_SLOT_CHEST], level); break; case INVTYPE_WAIST: (*gearScore)[EQUIPMENT_SLOT_WAIST] = std::max((*gearScore)[EQUIPMENT_SLOT_WAIST], level); break; case INVTYPE_LEGS: (*gearScore)[EQUIPMENT_SLOT_LEGS] = std::max((*gearScore)[EQUIPMENT_SLOT_LEGS], level); break; case INVTYPE_FEET: (*gearScore)[EQUIPMENT_SLOT_FEET] = std::max((*gearScore)[EQUIPMENT_SLOT_FEET], level); break; case INVTYPE_WRISTS: (*gearScore)[EQUIPMENT_SLOT_WRISTS] = std::max((*gearScore)[EQUIPMENT_SLOT_WRISTS], level); break; case INVTYPE_HANDS: (*gearScore)[EQUIPMENT_SLOT_HANDS] = std::max((*gearScore)[EQUIPMENT_SLOT_HANDS], level); break; // equipped gear score check uses both rings and trinkets for calculation, assume that for bags/banks it is the // same with keeping second highest score at second slot case INVTYPE_FINGER: { if ((*gearScore)[EQUIPMENT_SLOT_FINGER1] < level) { (*gearScore)[EQUIPMENT_SLOT_FINGER2] = (*gearScore)[EQUIPMENT_SLOT_FINGER1]; (*gearScore)[EQUIPMENT_SLOT_FINGER1] = level; } else if ((*gearScore)[EQUIPMENT_SLOT_FINGER2] < level) (*gearScore)[EQUIPMENT_SLOT_FINGER2] = level; break; } case INVTYPE_TRINKET: { if ((*gearScore)[EQUIPMENT_SLOT_TRINKET1] < level) { (*gearScore)[EQUIPMENT_SLOT_TRINKET2] = (*gearScore)[EQUIPMENT_SLOT_TRINKET1]; (*gearScore)[EQUIPMENT_SLOT_TRINKET1] = level; } else if ((*gearScore)[EQUIPMENT_SLOT_TRINKET2] < level) (*gearScore)[EQUIPMENT_SLOT_TRINKET2] = level; break; } case INVTYPE_CLOAK: (*gearScore)[EQUIPMENT_SLOT_BACK] = std::max((*gearScore)[EQUIPMENT_SLOT_BACK], level); break; default: break; } } std::string const PlayerbotAI::HandleRemoteCommand(std::string const command) { if (command == "state") { switch (currentState) { case BOT_STATE_COMBAT: return "combat"; case BOT_STATE_DEAD: return "dead"; case BOT_STATE_NON_COMBAT: return "non-combat"; default: return "unknown"; } } else if (command == "position") { std::ostringstream out; out << bot->GetPositionX() << " " << bot->GetPositionY() << " " << bot->GetPositionZ() << " " << bot->GetMapId() << " " << bot->GetOrientation(); if (AreaTableEntry const* zoneEntry = sAreaTableStore.LookupEntry(bot->GetZoneId())) out << " |" << zoneEntry->area_name[0] << "|"; return out.str(); } else if (command == "tpos") { Unit* target = *GetAiObjectContext()->GetValue("current target"); if (!target) { return ""; } std::ostringstream out; out << target->GetPositionX() << " " << target->GetPositionY() << " " << target->GetPositionZ() << " " << target->GetMapId() << " " << target->GetOrientation(); return out.str(); } else if (command == "movement") { LastMovement& data = *GetAiObjectContext()->GetValue("last movement"); std::ostringstream out; out << data.lastMoveShort.getX() << " " << data.lastMoveShort.getY() << " " << data.lastMoveShort.getZ() << " " << data.lastMoveShort.getMapId() << " " << data.lastMoveShort.getO(); return out.str(); } else if (command == "target") { Unit* target = *GetAiObjectContext()->GetValue("current target"); if (!target) { return ""; } return target->GetName(); } else if (command == "hp") { uint32 pct = static_cast(bot->GetHealthPct()); std::ostringstream out; out << pct << "%"; Unit* target = *GetAiObjectContext()->GetValue("current target"); if (!target) { return out.str(); } pct = static_cast(target->GetHealthPct()); out << " / " << pct << "%"; return out.str(); } else if (command == "strategy") { return currentEngine->ListStrategies(); } else if (command == "action") { return currentEngine->GetLastAction(); } else if (command == "values") { return GetAiObjectContext()->FormatValues(); } else if (command == "travel") { std::ostringstream out; TravelTarget* target = GetAiObjectContext()->GetValue("travel target")->Get(); if (target->getDestination()) { out << "Destination = " << target->getDestination()->getName(); out << ": " << target->getDestination()->getTitle(); out << " v: " << target->getDestination()->getVisitors(); if (!(*target->getPosition() == WorldPosition())) { out << "(" << target->getPosition()->getAreaName() << ")"; out << " distance: " << target->getPosition()->distance(bot) << "y"; out << " v: " << target->getPosition()->getVisitors(); } } out << " Status = "; if (target->getStatus() == TRAVEL_STATUS_NONE) out << " none"; else if (target->getStatus() == TRAVEL_STATUS_PREPARE) out << " prepare"; else if (target->getStatus() == TRAVEL_STATUS_TRAVEL) out << " travel"; else if (target->getStatus() == TRAVEL_STATUS_WORK) out << " work"; else if (target->getStatus() == TRAVEL_STATUS_COOLDOWN) out << " cooldown"; else if (target->getStatus() == TRAVEL_STATUS_EXPIRED) out << " expired"; if (target->getStatus() != TRAVEL_STATUS_EXPIRED) out << " Expire in " << (target->getTimeLeft() / 1000) << "s"; out << " Retry " << target->getRetryCount(true) << "/" << target->getRetryCount(false); return out.str(); } else if (command == "budget") { std::ostringstream out; AiObjectContext* context = GetAiObjectContext(); out << "Current money: " << ChatHelper::formatMoney(bot->GetMoney()) << " free to use:" << ChatHelper::formatMoney(AI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::anything)) << "\n"; out << "Purpose | Available / Needed \n"; for (uint32 i = 1; i < (uint32)NeedMoneyFor::anything; i++) { NeedMoneyFor needMoneyFor = NeedMoneyFor(i); switch (needMoneyFor) { case NeedMoneyFor::none: out << "nothing"; break; case NeedMoneyFor::repair: out << "repair"; break; case NeedMoneyFor::ammo: out << "ammo"; break; case NeedMoneyFor::spells: out << "spells"; break; case NeedMoneyFor::travel: out << "travel"; break; case NeedMoneyFor::consumables: out << "consumables"; break; case NeedMoneyFor::gear: out << "gear"; break; case NeedMoneyFor::guild: out << "guild"; break; default: break; } out << " | " << ChatHelper::formatMoney(AI_VALUE2(uint32, "free money for", i)) << " / " << ChatHelper::formatMoney(AI_VALUE2(uint32, "money needed for", i)) << "\n"; } return out.str(); } std::ostringstream out; out << "invalid command: " << command; return out.str(); } bool PlayerbotAI::HasSkill(SkillType skill) { return bot->HasSkill(skill) && bot->GetSkillValue(skill) > 0; } float PlayerbotAI::GetRange(std::string const type) { float val = 0; if (aiObjectContext) val = aiObjectContext->GetValue("range", type)->Get(); if (abs(val) >= 0.1f) return val; if (type == "spell") return sPlayerbotAIConfig->spellDistance; if (type == "shoot") return sPlayerbotAIConfig->shootDistance; if (type == "flee") return sPlayerbotAIConfig->fleeDistance; if (type == "heal") return sPlayerbotAIConfig->healDistance; if (type == "melee") return sPlayerbotAIConfig->meleeDistance; return 0; } void PlayerbotAI::Ping(float x, float y) { WorldPacket data(MSG_MINIMAP_PING, (8 + 4 + 4)); data << bot->GetGUID(); data << x; data << y; if (bot->GetGroup()) { bot->GetGroup()->BroadcastPacket(&data, true, -1, bot->GetGUID()); } else { bot->GetSession()->SendPacket(&data); } } // Helper function to iterate through items in the inventory and bags Item* PlayerbotAI::FindItemInInventory(std::function checkItem) const { // List out items in the main backpack for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot) { if (Item* const pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) { ItemTemplate const* pItemProto = pItem->GetTemplate(); if (pItemProto && bot->CanUseItem(pItemProto) == EQUIP_ERR_OK && checkItem(pItemProto)) return pItem; } } // List out items in other removable backpacks for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) { if (Bag const* const pBag = (Bag*)bot->GetItemByPos(INVENTORY_SLOT_BAG_0, bag)) { for (uint8 slot = 0; slot < pBag->GetBagSize(); ++slot) { if (Item* const pItem = bot->GetItemByPos(bag, slot)) { ItemTemplate const* pItemProto = pItem->GetTemplate(); if (pItemProto && bot->CanUseItem(pItemProto) == EQUIP_ERR_OK && checkItem(pItemProto)) return pItem; } } } } return nullptr; } // Find Poison Item* PlayerbotAI::FindPoison() const { return FindItemInInventory([](ItemTemplate const* pItemProto) -> bool { return pItemProto->Class == ITEM_CLASS_CONSUMABLE && pItemProto->SubClass == 6; }); } // Find Ammo Item* PlayerbotAI::FindAmmo() const { // Get equipped ranged weapon if (Item* rangedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED)) { uint32 weaponSubClass = rangedWeapon->GetTemplate()->SubClass; uint32 requiredAmmoType = 0; // Determine the correct ammo type based on the weapon switch (weaponSubClass) { case ITEM_SUBCLASS_WEAPON_GUN: requiredAmmoType = ITEM_SUBCLASS_BULLET; break; case ITEM_SUBCLASS_WEAPON_BOW: case ITEM_SUBCLASS_WEAPON_CROSSBOW: requiredAmmoType = ITEM_SUBCLASS_ARROW; break; default: return nullptr; // Not a ranged weapon that requires ammo } // Search inventory for the correct ammo type return FindItemInInventory( [requiredAmmoType](ItemTemplate const* pItemProto) -> bool { return pItemProto->Class == ITEM_CLASS_PROJECTILE && pItemProto->SubClass == requiredAmmoType; }); } return nullptr; // No ranged weapon equipped } // Find Consumable Item* PlayerbotAI::FindConsumable(uint32 itemId) const { return FindItemInInventory( [itemId](ItemTemplate const* pItemProto) -> bool { return (pItemProto->Class == ITEM_CLASS_CONSUMABLE || pItemProto->Class == ITEM_CLASS_TRADE_GOODS) && pItemProto->ItemId == itemId; }); } // Find Bandage Item* PlayerbotAI::FindBandage() const { return FindItemInInventory( [](ItemTemplate const* pItemProto) -> bool { return pItemProto->Class == ITEM_CLASS_CONSUMABLE && pItemProto->SubClass == ITEM_SUBCLASS_BANDAGE; }); } Item* PlayerbotAI::FindOpenableItem() const { return FindItemInInventory( [this](ItemTemplate const* itemTemplate) -> bool { return itemTemplate->HasFlag(ITEM_FLAG_HAS_LOOT) && (itemTemplate->LockID == 0 || !this->bot->GetItemByEntry(itemTemplate->ItemId)->IsLocked()); }); } Item* PlayerbotAI::FindLockedItem() const { return FindItemInInventory( [this](ItemTemplate const* itemTemplate) -> bool { if (!this->bot->HasSkill(SKILL_LOCKPICKING)) // Ensure bot has Lockpicking skill return false; if (itemTemplate->LockID == 0) // Ensure the item is actually locked return false; Item* item = this->bot->GetItemByEntry(itemTemplate->ItemId); if (!item || !item->IsLocked()) // Ensure item instance is locked return false; // Check if bot has enough Lockpicking skill LockEntry const* lockInfo = sLockStore.LookupEntry(itemTemplate->LockID); if (!lockInfo) return false; for (uint8 j = 0; j < 8; ++j) { if (lockInfo->Type[j] == LOCK_KEY_SKILL) { uint32 skillId = SkillByLockType(LockType(lockInfo->Index[j])); if (skillId == SKILL_LOCKPICKING) { uint32 requiredSkill = lockInfo->Skill[j]; uint32 botSkill = this->bot->GetSkillValue(SKILL_LOCKPICKING); return botSkill >= requiredSkill; } } } return false; }); } Item* PlayerbotAI::FindStoneFor(Item* weapon) const { if (!weapon) return nullptr; const ItemTemplate* item_template = weapon->GetTemplate(); if (!item_template) return nullptr; static const std::vector uPrioritizedSharpStoneIds = { ADAMANTITE_SHARPENING_STONE, FEL_SHARPENING_STONE, ELEMENTAL_SHARPENING_STONE, DENSE_SHARPENING_STONE, SOLID_SHARPENING_STONE, HEAVY_SHARPENING_STONE, COARSE_SHARPENING_STONE, ROUGH_SHARPENING_STONE }; static const std::vector uPrioritizedWeightStoneIds = { ADAMANTITE_WEIGHTSTONE, FEL_WEIGHTSTONE, DENSE_WEIGHTSTONE, SOLID_WEIGHTSTONE, HEAVY_WEIGHTSTONE, COARSE_WEIGHTSTONE, ROUGH_WEIGHTSTONE }; Item* stone = nullptr; ItemTemplate const* pProto = weapon->GetTemplate(); if (pProto && (pProto->SubClass == ITEM_SUBCLASS_WEAPON_SWORD || pProto->SubClass == ITEM_SUBCLASS_WEAPON_SWORD2 || pProto->SubClass == ITEM_SUBCLASS_WEAPON_AXE || pProto->SubClass == ITEM_SUBCLASS_WEAPON_AXE2 || pProto->SubClass == ITEM_SUBCLASS_WEAPON_DAGGER || pProto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM)) { for (uint8 i = 0; i < std::size(uPrioritizedSharpStoneIds); ++i) { stone = FindConsumable(uPrioritizedSharpStoneIds[i]); if (stone) { return stone; } } } else if (pProto && (pProto->SubClass == ITEM_SUBCLASS_WEAPON_MACE || pProto->SubClass == ITEM_SUBCLASS_WEAPON_MACE2 || pProto->SubClass == ITEM_SUBCLASS_WEAPON_STAFF || pProto->SubClass == ITEM_SUBCLASS_WEAPON_FIST)) { for (uint8 i = 0; i < std::size(uPrioritizedWeightStoneIds); ++i) { stone = FindConsumable(uPrioritizedWeightStoneIds[i]); if (stone) { return stone; } } } return stone; } Item* PlayerbotAI::FindOilFor(Item* weapon) const { if (!weapon) return nullptr; const ItemTemplate* item_template = weapon->GetTemplate(); if (!item_template) return nullptr; static const std::vector uPrioritizedWizardOilIds = { BRILLIANT_WIZARD_OIL, SUPERIOR_WIZARD_OIL, WIZARD_OIL, LESSER_WIZARD_OIL, MINOR_WIZARD_OIL, BRILLIANT_MANA_OIL, SUPERIOR_MANA_OIL, LESSER_MANA_OIL, MINOR_MANA_OIL}; static const std::vector uPrioritizedManaOilIds = { BRILLIANT_MANA_OIL, SUPERIOR_MANA_OIL, LESSER_MANA_OIL, MINOR_MANA_OIL, BRILLIANT_WIZARD_OIL, SUPERIOR_WIZARD_OIL, WIZARD_OIL, LESSER_WIZARD_OIL, MINOR_WIZARD_OIL}; Item* oil = nullptr; int botClass = bot->getClass(); int specTab = AiFactory::GetPlayerSpecTab(bot); const std::vector* prioritizedOils = nullptr; switch (botClass) { case CLASS_PRIEST: prioritizedOils = (specTab == 2) ? &uPrioritizedWizardOilIds : &uPrioritizedManaOilIds; break; case CLASS_MAGE: prioritizedOils = &uPrioritizedWizardOilIds; break; case CLASS_DRUID: if (specTab == 0) // Balance prioritizedOils = &uPrioritizedWizardOilIds; else if (specTab == 1) // Feral prioritizedOils = nullptr; else // Restoration (specTab == 2) or any other/unspecified spec prioritizedOils = &uPrioritizedManaOilIds; break; case CLASS_HUNTER: prioritizedOils = &uPrioritizedManaOilIds; break; case CLASS_PALADIN: if (specTab == 1) // Protection prioritizedOils = &uPrioritizedWizardOilIds; else if (specTab == 2) // Retribution prioritizedOils = nullptr; else // Holy (specTab == 0) or any other/unspecified spec prioritizedOils = &uPrioritizedManaOilIds; break; default: prioritizedOils = &uPrioritizedManaOilIds; break; } if (prioritizedOils) { for (auto const& id : *prioritizedOils) { oil = FindConsumable(id); if (oil) { return oil; } } } return oil; } std::vector PlayerbotAI::GetInventoryAndEquippedItems() { std::vector items; for (int i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) { if (Bag* pBag = (Bag*)bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { for (uint32 j = 0; j < pBag->GetBagSize(); ++j) { if (Item* pItem = pBag->GetItemByPos(j)) { items.push_back(pItem); } } } } for (int i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) { if (Item* pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { items.push_back(pItem); } } for (int i = KEYRING_SLOT_START; i < KEYRING_SLOT_END; ++i) { if (Item* pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { items.push_back(pItem); } } for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; slot++) { if (Item* pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) { items.push_back(pItem); } } return items; } std::vector PlayerbotAI::GetInventoryItems() { std::vector items; for (int i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) { if (Bag* pBag = (Bag*)bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { for (uint32 j = 0; j < pBag->GetBagSize(); ++j) { if (Item* pItem = pBag->GetItemByPos(j)) { items.push_back(pItem); } } } } for (int i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) { if (Item* pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { items.push_back(pItem); } } for (int i = KEYRING_SLOT_START; i < KEYRING_SLOT_END; ++i) { if (Item* pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { items.push_back(pItem); } } return items; } uint32 PlayerbotAI::GetInventoryItemsCountWithId(uint32 itemId) { uint32 count = 0; for (int i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) { if (Bag* pBag = (Bag*)bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { for (uint32 j = 0; j < pBag->GetBagSize(); ++j) { if (Item* pItem = pBag->GetItemByPos(j)) { if (pItem->GetTemplate()->ItemId == itemId) { count += pItem->GetCount(); } } } } } for (int i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) { if (Item* pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { if (pItem->GetTemplate()->ItemId == itemId) { count += pItem->GetCount(); } } } for (int i = KEYRING_SLOT_START; i < KEYRING_SLOT_END; ++i) { if (Item* pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { if (pItem->GetTemplate()->ItemId == itemId) { count += pItem->GetCount(); } } } return count; } bool PlayerbotAI::HasItemInInventory(uint32 itemId) { for (int i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) { if (Bag* pBag = (Bag*)bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { for (uint32 j = 0; j < pBag->GetBagSize(); ++j) { if (Item* pItem = pBag->GetItemByPos(j)) { if (pItem->GetTemplate()->ItemId == itemId) { return true; } } } } } for (int i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) { if (Item* pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { if (pItem->GetTemplate()->ItemId == itemId) { return true; } } } for (int i = KEYRING_SLOT_START; i < KEYRING_SLOT_END; ++i) { if (Item* pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) { if (pItem->GetTemplate()->ItemId == itemId) { return true; } } } return false; } std::vector> PlayerbotAI::GetCurrentQuestsRequiringItemId(uint32 itemId) { std::vector> result; if (!itemId) { return result; } for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) { uint32 questId = bot->GetQuestSlotQuestId(slot); if (!questId) continue; // QuestStatus status = bot->GetQuestStatus(questId); const Quest* quest = sObjectMgr->GetQuestTemplate(questId); for (uint8 i = 0; i < std::size(quest->RequiredItemId); ++i) { if (quest->RequiredItemId[i] == itemId) { result.push_back(std::pair(quest, quest->RequiredItemId[i])); break; } } } return result; } // on self void PlayerbotAI::ImbueItem(Item* item) { ImbueItem(item, TARGET_FLAG_NONE, ObjectGuid::Empty); } // item on unit void PlayerbotAI::ImbueItem(Item* item, Unit* target) { if (!target) return; ImbueItem(item, TARGET_FLAG_UNIT, target->GetGUID()); } // item on equipped item void PlayerbotAI::ImbueItem(Item* item, uint8 targetInventorySlot) { if (targetInventorySlot >= EQUIPMENT_SLOT_END) return; Item* const targetItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, targetInventorySlot); if (!targetItem) return; ImbueItem(item, TARGET_FLAG_ITEM, targetItem->GetGUID()); } // generic item use method void PlayerbotAI::ImbueItem(Item* item, uint32 targetFlag, ObjectGuid targetGUID) { if (!item) return; uint32 glyphIndex = 0; uint8 castFlags = 0; uint8 bagIndex = item->GetBagSlot(); uint8 slot = item->GetSlot(); uint8 cast_count = 0; ObjectGuid item_guid = item->GetGUID(); uint32 spellId = 0; for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) { if (item->GetTemplate()->Spells[i].SpellId > 0) { spellId = item->GetTemplate()->Spells[i].SpellId; break; } } WorldPacket* packet = new WorldPacket(CMSG_USE_ITEM); *packet << bagIndex; *packet << slot; *packet << cast_count; *packet << spellId; *packet << item_guid; *packet << glyphIndex; *packet << castFlags; *packet << targetFlag; if (targetFlag & (TARGET_FLAG_UNIT | TARGET_FLAG_ITEM | TARGET_FLAG_GAMEOBJECT)) *packet << targetGUID.WriteAsPacked(); bot->GetSession()->QueuePacket(packet); } void PlayerbotAI::EnchantItemT(uint32 spellid, uint8 slot) { Item* pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); if (!pItem || !pItem->IsInWorld() || !pItem->GetOwner() || !pItem->GetOwner()->IsInWorld() || !pItem->GetOwner()->GetSession()) return; SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellid); if (!spellInfo) return; uint32 enchantid = spellInfo->Effects[0].MiscValue; if (!enchantid) { // LOG_ERROR("playerbots", "{}: Invalid enchantid ", enchantid, " report to devs", bot->GetName().c_str()); return; } if (!((1 << pItem->GetTemplate()->SubClass) & spellInfo->EquippedItemSubClassMask) && !((1 << pItem->GetTemplate()->InventoryType) & spellInfo->EquippedItemInventoryTypeMask)) { // LOG_ERROR("playerbots", "{}: items could not be enchanted, wrong item type equipped", // bot->GetName().c_str()); return; } bot->ApplyEnchantment(pItem, PERM_ENCHANTMENT_SLOT, false); pItem->SetEnchantment(PERM_ENCHANTMENT_SLOT, enchantid, 0, 0); bot->ApplyEnchantment(pItem, PERM_ENCHANTMENT_SLOT, true); LOG_INFO("playerbots", "{}: items was enchanted successfully!", bot->GetName().c_str()); } uint32 PlayerbotAI::GetBuffedCount(Player* player, std::string const spellname) { uint32 bcount = 0; if (Group* group = bot->GetGroup()) { for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* member = gref->GetSource(); if (!member || !member->IsInWorld()) continue; if (!member->IsInSameRaidWith(player)) continue; if (HasAura(spellname, member, true)) bcount++; } } return bcount; } int32 PlayerbotAI::GetNearGroupMemberCount(float dis) { int count = 1; // yourself if (Group* group = bot->GetGroup()) { for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* member = gref->GetSource(); if (member == bot) // calculated continue; if (!member || !member->IsInWorld()) continue; if (member->GetMapId() != bot->GetMapId()) continue; if (member->GetExactDist(bot) > dis) continue; count++; } } return count; } bool PlayerbotAI::CanMove() { // do not allow if not vehicle driver if (IsInVehicle() && !IsInVehicle(true)) return false; if (bot->isFrozen() || bot->IsPolymorphed() || (bot->isDead() && !bot->HasPlayerFlag(PLAYER_FLAGS_GHOST)) || bot->IsBeingTeleported() || bot->HasRootAura() || bot->HasSpiritOfRedemptionAura() || bot->HasConfuseAura() || bot->IsCharmed() || bot->HasStunAura() || bot->IsInFlight() || bot->HasUnitState(UNIT_STATE_LOST_CONTROL)) return false; return bot->GetMotionMaster()->GetCurrentMovementGeneratorType() != FLIGHT_MOTION_TYPE; } bool PlayerbotAI::IsRealGuild(uint32 guildId) { Guild* guild = sGuildMgr->GetGuildById(guildId); if (!guild) { return false; } uint32 leaderAccount = sCharacterCache->GetCharacterAccountIdByGuid(guild->GetLeaderGUID()); if (!leaderAccount) return false; return !(sPlayerbotAIConfig->IsInRandomAccountList(leaderAccount)); } bool PlayerbotAI::IsInRealGuild() { if (!bot->GetGuildId()) return false; return IsRealGuild(bot->GetGuildId()); } void PlayerbotAI::QueueChatResponse(const ChatQueuedReply chatReply) { chatReplies.push_back(std::move(chatReply)); } bool PlayerbotAI::EqualLowercaseName(std::string s1, std::string s2) { if (s1.length() != s2.length()) { return false; } for (std::string::size_type i = 0; i < s1.length(); i++) { if (tolower(s1[i]) != tolower(s2[i])) { return false; } } return true; } // A custom CanEquipItem (remove AutoUnequipOffhand in FindEquipSlot to prevent unequip on `item usage` calculation) InventoryResult PlayerbotAI::CanEquipItem(uint8 slot, uint16& dest, Item* pItem, bool swap, bool not_loading) const { dest = 0; if (pItem) { LOG_DEBUG("entities.player.items", "STORAGE: CanEquipItem slot = {}, item = {}, count = {}", slot, pItem->GetEntry(), pItem->GetCount()); ItemTemplate const* pProto = pItem->GetTemplate(); if (pProto) { if (!sScriptMgr->OnPlayerCanEquipItem(bot, slot, dest, pItem, swap, not_loading)) return EQUIP_ERR_CANT_DO_RIGHT_NOW; // item used if (pItem->m_lootGenerated) return EQUIP_ERR_ALREADY_LOOTED; if (pItem->IsBindedNotWith(bot)) return EQUIP_ERR_DONT_OWN_THAT_ITEM; InventoryResult res = bot->CanTakeMoreSimilarItems(pItem); if (res != EQUIP_ERR_OK) return res; ScalingStatDistributionEntry const* ssd = pProto->ScalingStatDistribution ? sScalingStatDistributionStore.LookupEntry(pProto->ScalingStatDistribution) : 0; // check allowed level (extend range to upper values if MaxLevel more or equal max player level, this let GM // set high level with 1...max range items) if (ssd && ssd->MaxLevel < DEFAULT_MAX_LEVEL && ssd->MaxLevel < bot->GetLevel()) return EQUIP_ERR_ITEM_CANT_BE_EQUIPPED; uint8 eslot = FindEquipSlot(pProto, slot, swap); if (eslot == NULL_SLOT) return EQUIP_ERR_ITEM_CANT_BE_EQUIPPED; // Xinef: dont allow to equip items on disarmed slot if (!bot->CanUseAttackType(bot->GetAttackBySlot(eslot))) return EQUIP_ERR_NOT_WHILE_DISARMED; res = bot->CanUseItem(pItem, not_loading); if (res != EQUIP_ERR_OK) return res; if (!swap && bot->GetItemByPos(INVENTORY_SLOT_BAG_0, eslot)) return EQUIP_ERR_NO_EQUIPMENT_SLOT_AVAILABLE; // if we are swapping 2 equiped items, CanEquipUniqueItem check // should ignore the item we are trying to swap, and not the // destination item. CanEquipUniqueItem should ignore destination // item only when we are swapping weapon from bag uint8 ignore = uint8(NULL_SLOT); switch (eslot) { case EQUIPMENT_SLOT_MAINHAND: ignore = EQUIPMENT_SLOT_OFFHAND; break; case EQUIPMENT_SLOT_OFFHAND: ignore = EQUIPMENT_SLOT_MAINHAND; break; case EQUIPMENT_SLOT_FINGER1: ignore = EQUIPMENT_SLOT_FINGER2; break; case EQUIPMENT_SLOT_FINGER2: ignore = EQUIPMENT_SLOT_FINGER1; break; case EQUIPMENT_SLOT_TRINKET1: ignore = EQUIPMENT_SLOT_TRINKET2; break; case EQUIPMENT_SLOT_TRINKET2: ignore = EQUIPMENT_SLOT_TRINKET1; break; } if (ignore == uint8(NULL_SLOT) || pItem != bot->GetItemByPos(INVENTORY_SLOT_BAG_0, ignore)) ignore = eslot; InventoryResult res2 = bot->CanEquipUniqueItem(pItem, swap ? ignore : uint8(NULL_SLOT)); if (res2 != EQUIP_ERR_OK) return res2; // check unique-equipped special item classes if (pProto->Class == ITEM_CLASS_QUIVER) for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) if (Item* pBag = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i)) if (pBag != pItem) if (ItemTemplate const* pBagProto = pBag->GetTemplate()) if (pBagProto->Class == pProto->Class && (!swap || pBag->GetSlot() != eslot)) return (pBagProto->SubClass == ITEM_SUBCLASS_AMMO_POUCH) ? EQUIP_ERR_CAN_EQUIP_ONLY1_AMMOPOUCH : EQUIP_ERR_CAN_EQUIP_ONLY1_QUIVER; uint32 type = pProto->InventoryType; if (eslot == EQUIPMENT_SLOT_OFFHAND) { // Do not allow polearm to be equipped in the offhand (rare case for the only 1h polearm 41750) // xinef: same for fishing poles if (type == INVTYPE_WEAPON && (pProto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM || pProto->SubClass == ITEM_SUBCLASS_WEAPON_FISHING_POLE)) return EQUIP_ERR_ITEM_DOESNT_GO_TO_SLOT; else if (type == INVTYPE_WEAPON || type == INVTYPE_WEAPONOFFHAND) { if (!bot->CanDualWield()) return EQUIP_ERR_CANT_DUAL_WIELD; } else if (type == INVTYPE_2HWEAPON) { if (!bot->CanDualWield() || !bot->CanTitanGrip()) return EQUIP_ERR_CANT_DUAL_WIELD; } if (bot->IsTwoHandUsed()) return EQUIP_ERR_CANT_EQUIP_WITH_TWOHANDED; } // equip two-hand weapon case (with possible unequip 2 items) if (type == INVTYPE_2HWEAPON) { if (eslot == EQUIPMENT_SLOT_OFFHAND) { if (!bot->CanTitanGrip()) return EQUIP_ERR_ITEM_CANT_BE_EQUIPPED; } else if (eslot != EQUIPMENT_SLOT_MAINHAND) return EQUIP_ERR_ITEM_CANT_BE_EQUIPPED; if (!bot->CanTitanGrip()) { // offhand item must can be stored in inventory for offhand item and it also must be unequipped Item* offItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); ItemPosCountVec off_dest; if (offItem && (!not_loading || bot->CanUnequipItem(uint16(INVENTORY_SLOT_BAG_0) << 8 | EQUIPMENT_SLOT_OFFHAND, false) != EQUIP_ERR_OK || bot->CanStoreItem(NULL_BAG, NULL_SLOT, off_dest, offItem, false) != EQUIP_ERR_OK)) return swap ? EQUIP_ERR_ITEMS_CANT_BE_SWAPPED : EQUIP_ERR_INVENTORY_FULL; } } dest = ((INVENTORY_SLOT_BAG_0 << 8) | eslot); return EQUIP_ERR_OK; } } return !swap ? EQUIP_ERR_ITEM_NOT_FOUND : EQUIP_ERR_ITEMS_CANT_BE_SWAPPED; } uint8 PlayerbotAI::FindEquipSlot(ItemTemplate const* proto, uint32 slot, bool swap) const { uint8 slots[4]; slots[0] = NULL_SLOT; slots[1] = NULL_SLOT; slots[2] = NULL_SLOT; slots[3] = NULL_SLOT; switch (proto->InventoryType) { case INVTYPE_HEAD: slots[0] = EQUIPMENT_SLOT_HEAD; break; case INVTYPE_NECK: slots[0] = EQUIPMENT_SLOT_NECK; break; case INVTYPE_SHOULDERS: slots[0] = EQUIPMENT_SLOT_SHOULDERS; break; case INVTYPE_BODY: slots[0] = EQUIPMENT_SLOT_BODY; break; case INVTYPE_CHEST: case INVTYPE_ROBE: slots[0] = EQUIPMENT_SLOT_CHEST; break; case INVTYPE_WAIST: slots[0] = EQUIPMENT_SLOT_WAIST; break; case INVTYPE_LEGS: slots[0] = EQUIPMENT_SLOT_LEGS; break; case INVTYPE_FEET: slots[0] = EQUIPMENT_SLOT_FEET; break; case INVTYPE_WRISTS: slots[0] = EQUIPMENT_SLOT_WRISTS; break; case INVTYPE_HANDS: slots[0] = EQUIPMENT_SLOT_HANDS; break; case INVTYPE_FINGER: slots[0] = EQUIPMENT_SLOT_FINGER1; slots[1] = EQUIPMENT_SLOT_FINGER2; break; case INVTYPE_TRINKET: slots[0] = EQUIPMENT_SLOT_TRINKET1; slots[1] = EQUIPMENT_SLOT_TRINKET2; break; case INVTYPE_CLOAK: slots[0] = EQUIPMENT_SLOT_BACK; break; case INVTYPE_WEAPON: { slots[0] = EQUIPMENT_SLOT_MAINHAND; // suggest offhand slot only if know dual wielding // (this will be replace mainhand weapon at auto equip instead unwonted "you don't known dual wielding" ... if (bot->CanDualWield()) slots[1] = EQUIPMENT_SLOT_OFFHAND; break; } case INVTYPE_SHIELD: case INVTYPE_WEAPONOFFHAND: case INVTYPE_HOLDABLE: slots[0] = EQUIPMENT_SLOT_OFFHAND; break; case INVTYPE_RANGED: case INVTYPE_RANGEDRIGHT: case INVTYPE_THROWN: slots[0] = EQUIPMENT_SLOT_RANGED; break; case INVTYPE_2HWEAPON: slots[0] = EQUIPMENT_SLOT_MAINHAND; if (Item* mhWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND)) { if (ItemTemplate const* mhWeaponProto = mhWeapon->GetTemplate()) { if (mhWeaponProto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM || mhWeaponProto->SubClass == ITEM_SUBCLASS_WEAPON_STAFF) { bot->AutoUnequipOffhandIfNeed(true); break; } } } if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND)) { if (proto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM || proto->SubClass == ITEM_SUBCLASS_WEAPON_STAFF) { break; } } if (bot->CanDualWield() && bot->CanTitanGrip() && proto->SubClass != ITEM_SUBCLASS_WEAPON_POLEARM && proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF && proto->SubClass != ITEM_SUBCLASS_WEAPON_FISHING_POLE) slots[1] = EQUIPMENT_SLOT_OFFHAND; break; case INVTYPE_TABARD: slots[0] = EQUIPMENT_SLOT_TABARD; break; case INVTYPE_WEAPONMAINHAND: slots[0] = EQUIPMENT_SLOT_MAINHAND; break; case INVTYPE_BAG: slots[0] = INVENTORY_SLOT_BAG_START + 0; slots[1] = INVENTORY_SLOT_BAG_START + 1; slots[2] = INVENTORY_SLOT_BAG_START + 2; slots[3] = INVENTORY_SLOT_BAG_START + 3; break; case INVTYPE_RELIC: { switch (proto->SubClass) { case ITEM_SUBCLASS_ARMOR_LIBRAM: if (bot->IsClass(CLASS_PALADIN, CLASS_CONTEXT_EQUIP_RELIC)) slots[0] = EQUIPMENT_SLOT_RANGED; break; case ITEM_SUBCLASS_ARMOR_IDOL: if (bot->IsClass(CLASS_DRUID, CLASS_CONTEXT_EQUIP_RELIC)) slots[0] = EQUIPMENT_SLOT_RANGED; break; case ITEM_SUBCLASS_ARMOR_TOTEM: if (bot->IsClass(CLASS_SHAMAN, CLASS_CONTEXT_EQUIP_RELIC)) slots[0] = EQUIPMENT_SLOT_RANGED; break; case ITEM_SUBCLASS_ARMOR_MISC: if (bot->IsClass(CLASS_WARLOCK, CLASS_CONTEXT_EQUIP_RELIC)) slots[0] = EQUIPMENT_SLOT_RANGED; break; case ITEM_SUBCLASS_ARMOR_SIGIL: if (bot->IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_EQUIP_RELIC)) slots[0] = EQUIPMENT_SLOT_RANGED; break; } break; } default: return NULL_SLOT; } if (slot != NULL_SLOT) { if (swap || !bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) for (uint8 i = 0; i < 4; ++i) if (slots[i] == slot) return slot; } else { // search free slot at first for (uint8 i = 0; i < 4; ++i) if (slots[i] != NULL_SLOT && !bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slots[i])) // in case 2hand equipped weapon (without titan grip) offhand slot empty but not free if (slots[i] != EQUIPMENT_SLOT_OFFHAND || !bot->IsTwoHandUsed()) return slots[i]; // if not found free and can swap return first appropriate from used for (uint8 i = 0; i < 4; ++i) if (slots[i] != NULL_SLOT && swap) return slots[i]; } // no free position return NULL_SLOT; } bool PlayerbotAI::IsSafe(Player* player) { return player && player->GetMapId() == bot->GetMapId() && player->GetInstanceId() == bot->GetInstanceId() && !player->IsBeingTeleported(); } bool PlayerbotAI::IsSafe(WorldObject* obj) { return obj && obj->GetMapId() == bot->GetMapId() && obj->GetInstanceId() == bot->GetInstanceId() && (!obj->IsPlayer() || !((Player*)obj)->IsBeingTeleported()); } ChatChannelSource PlayerbotAI::GetChatChannelSource(Player* bot, uint32 type, std::string channelName) { if (type == CHAT_MSG_CHANNEL) { if (channelName == "World") return ChatChannelSource::SRC_WORLD; else { ChannelMgr* cMgr = ChannelMgr::forTeam(bot->GetTeamId()); if (!cMgr) { return ChatChannelSource::SRC_UNDEFINED; } const Channel* channel = cMgr->GetChannel(channelName, bot); if (channel) { switch (channel->GetChannelId()) { case ChatChannelId::GENERAL: { return ChatChannelSource::SRC_GENERAL; } case ChatChannelId::TRADE: { return ChatChannelSource::SRC_TRADE; } case ChatChannelId::LOCAL_DEFENSE: { return ChatChannelSource::SRC_LOCAL_DEFENSE; } case ChatChannelId::WORLD_DEFENSE: { return ChatChannelSource::SRC_WORLD_DEFENSE; } case ChatChannelId::LOOKING_FOR_GROUP: { return ChatChannelSource::SRC_LOOKING_FOR_GROUP; } case ChatChannelId::GUILD_RECRUITMENT: { return ChatChannelSource::SRC_GUILD_RECRUITMENT; } default: { return ChatChannelSource::SRC_UNDEFINED; } } } } } else { switch (type) { case CHAT_MSG_WHISPER: { return ChatChannelSource::SRC_WHISPER; } case CHAT_MSG_SAY: { return ChatChannelSource::SRC_SAY; } case CHAT_MSG_YELL: { return ChatChannelSource::SRC_YELL; } case CHAT_MSG_GUILD: { return ChatChannelSource::SRC_GUILD; } case CHAT_MSG_PARTY: { return ChatChannelSource::SRC_PARTY; } case CHAT_MSG_RAID: { return ChatChannelSource::SRC_RAID; } case CHAT_MSG_EMOTE: { return ChatChannelSource::SRC_EMOTE; } case CHAT_MSG_TEXT_EMOTE: { return ChatChannelSource::SRC_TEXT_EMOTE; } default: { return ChatChannelSource::SRC_UNDEFINED; } } } return ChatChannelSource::SRC_UNDEFINED; } bool PlayerbotAI::CheckLocationDistanceByLevel(Player* player, const WorldLocation& loc, bool fromStartUp) { if (player->GetLevel() > 16) return true; float dis = 0.0f; if (fromStartUp) { PlayerInfo const* pInfo = sObjectMgr->GetPlayerInfo(player->getRace(true), player->getClass()); if (loc.GetMapId() != pInfo->mapId) return false; dis = loc.GetExactDist(pInfo->positionX, pInfo->positionY, pInfo->positionZ); } else { if (loc.GetMapId() != player->GetMapId()) return false; dis = loc.GetExactDist(player); } float bound = 10000.0f; if (player->GetLevel() <= 4) bound = 500.0f; else if (player->GetLevel() <= 10) bound = 2500.0f; return dis <= bound; } std::vector PlayerbotAI::GetAllCurrentQuests() { std::vector result; for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) { uint32 questId = bot->GetQuestSlotQuestId(slot); if (!questId) { continue; } result.push_back(sObjectMgr->GetQuestTemplate(questId)); } return result; } std::vector PlayerbotAI::GetCurrentIncompleteQuests() { std::vector result; for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) { uint32 questId = bot->GetQuestSlotQuestId(slot); if (!questId) { continue; } QuestStatus status = bot->GetQuestStatus(questId); if (status == QUEST_STATUS_INCOMPLETE || status == QUEST_STATUS_NONE) { result.push_back(sObjectMgr->GetQuestTemplate(questId)); } } return result; } std::set PlayerbotAI::GetAllCurrentQuestIds() { std::set result; for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) { uint32 questId = bot->GetQuestSlotQuestId(slot); if (!questId) { continue; } result.insert(questId); } return result; } std::set PlayerbotAI::GetCurrentIncompleteQuestIds() { std::set result; for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) { uint32 questId = bot->GetQuestSlotQuestId(slot); if (!questId) { continue; } QuestStatus status = bot->GetQuestStatus(questId); if (status == QUEST_STATUS_INCOMPLETE || status == QUEST_STATUS_NONE) { result.insert(questId); } } return result; } uint32 PlayerbotAI::GetReactDelay() { uint32 base = sPlayerbotAIConfig->reactDelay; // Default 100(ms) // If dynamic react delay is disabled, use a static calculation if (!sPlayerbotAIConfig->dynamicReactDelay) { if (HasRealPlayerMaster()) return base; bool inBG = bot->InBattleground() || bot->InArena(); if (sPlayerbotAIConfig->fastReactInBG && inBG) return base; bool inCombat = bot->IsInCombat(); if (!inCombat) return base * 10; else if (inCombat) return static_cast(base * 2.5f); return base; } // Dynamic react delay calculation: if (HasRealPlayerMaster()) return base; bool inBG = bot->InBattleground() || bot->InArena(); if (inBG) { if (bot->IsInCombat() || currentState == BOT_STATE_COMBAT) { return static_cast(base * (sPlayerbotAIConfig->fastReactInBG ? 2.5f : 5.0f)); } else { return static_cast(base * (sPlayerbotAIConfig->fastReactInBG ? 1.0f : 10.0f)); } } // When in combat, return 5 times the base if (bot->IsInCombat() || currentState == BOT_STATE_COMBAT) return base * 5; // When not resting, return 10-30 times the base if (!bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING)) return base * urand(10, 30); // In other cases, return 20-200 times the base return base * urand(20, 200); } void PlayerbotAI::PetFollow() { Pet* pet = bot->GetPet(); if (!pet) return; pet->AttackStop(); pet->InterruptNonMeleeSpells(false); pet->ClearInPetCombat(); pet->GetMotionMaster()->MoveFollow(bot, PET_FOLLOW_DIST, pet->GetFollowAngle()); if (pet->ToPet()) pet->ToPet()->ClearCastWhenWillAvailable(); CharmInfo* charmInfo = pet->GetCharmInfo(); if (!charmInfo) return; charmInfo->SetCommandState(COMMAND_FOLLOW); charmInfo->SetIsCommandAttack(false); charmInfo->SetIsAtStay(false); charmInfo->SetIsReturning(true); charmInfo->SetIsCommandFollow(true); charmInfo->SetIsFollowing(false); charmInfo->RemoveStayPosition(); charmInfo->SetForcedSpell(0); charmInfo->SetForcedTargetGUID(); } float PlayerbotAI::GetItemScoreMultiplier(ItemQualities quality) { switch (quality) { // each quality increase 1.1x case ITEM_QUALITY_POOR: return 1.0f; break; case ITEM_QUALITY_NORMAL: return 1.1f; break; case ITEM_QUALITY_UNCOMMON: return 1.21f; break; case ITEM_QUALITY_RARE: return 1.331f; break; case ITEM_QUALITY_EPIC: return 1.4641f; break; case ITEM_QUALITY_LEGENDARY: return 1.61051f; break; default: break; } return 1.0f; } bool PlayerbotAI::IsHealingSpell(uint32 spellFamilyName, flag96 spellFalimyFlags) { if (!spellFamilyName) return false; flag96 healingFlags; switch (spellFamilyName) { case SPELLFAMILY_DRUID: { uint32 healingFlagsA = 0x10 | 0x40 | 0x20 | 0x80; // rejuvenation | regrowth | healing touch | tranquility uint32 healingFlagsB = 0x4000000 | 0x2000000 | 0x2 | 0x10; // wild growth | nourish | swiftmend | lifebloom uint32 healingFlagsC = 0x0; healingFlags = {healingFlagsA, healingFlagsB, healingFlagsC}; break; } case SPELLFAMILY_PALADIN: { uint32 healingFlagsA = 0x80000000 | 0x40000000 | 0x8000 | 0x80000; // holy light | flash of light | lay on hands | judgement of light uint32 healingFlagsB = 0x10000; // holy shock uint32 healingFlagsC = 0x0; healingFlags = {healingFlagsA, healingFlagsB, healingFlagsC}; break; } case SPELLFAMILY_SHAMAN: { uint32 healingFlagsA = 0x80 | 0x40 | 0x100; // lesser healing wave | healing wave | chain heal uint32 healingFlagsB = 0x400; // earth shield uint32 healingFlagsC = 0x10; // riptide healingFlags = {healingFlagsA, healingFlagsB, healingFlagsC}; break; } case SPELLFAMILY_PRIEST: { uint32 healingFlagsA = 0x40 | 0x200 | 0x40000 | 0x1000 | 0x800 | 0x400 | 0x10000000; // renew | prayer of healing | lesser heal | greater heal | flash heal | // heal | circle of healing uint32 healingFlagsB = 0x800000 | 0x20 | 0x4; // penance | prayer of mending | binding heal uint32 healingFlagsC = 0x0; healingFlags = {healingFlagsA, healingFlagsB, healingFlagsC}; break; } default: break; } return spellFalimyFlags & healingFlags; } SpellFamilyNames PlayerbotAI::Class2SpellFamilyName(uint8 cls) { switch (cls) { case CLASS_WARRIOR: return SPELLFAMILY_WARRIOR; case CLASS_PALADIN: return SPELLFAMILY_PALADIN; case CLASS_HUNTER: return SPELLFAMILY_HUNTER; case CLASS_ROGUE: return SPELLFAMILY_ROGUE; case CLASS_PRIEST: return SPELLFAMILY_PRIEST; case CLASS_DEATH_KNIGHT: return SPELLFAMILY_DEATHKNIGHT; case CLASS_SHAMAN: return SPELLFAMILY_SHAMAN; case CLASS_MAGE: return SPELLFAMILY_MAGE; case CLASS_WARLOCK: return SPELLFAMILY_WARLOCK; case CLASS_DRUID: return SPELLFAMILY_DRUID; default: break; } return SPELLFAMILY_GENERIC; } void PlayerbotAI::AddTimedEvent(std::function callback, uint32 delayMs) { class LambdaEvent final : public BasicEvent { std::function _cb; public: explicit LambdaEvent(std::function cb) : _cb(std::move(cb)) {} bool Execute(uint64 /*execTime*/, uint32 /*diff*/) override { _cb(); return true; // remove after execution } }; // Every Player already owns an EventMap called m_Events bot->m_Events.AddEvent(new LambdaEvent(std::move(callback)), bot->m_Events.CalculateTime(delayMs)); } void PlayerbotAI::EvaluateHealerDpsStrategy() { if (!IsHeal(bot, true)) return; if (sPlayerbotAIConfig->IsRestrictedHealerDPSMap(bot->GetMapId())) ChangeStrategy("-healer dps", BOT_STATE_COMBAT); else ChangeStrategy("+healer dps", BOT_STATE_COMBAT); }