From e7ddd74e0cf5d7e64efd75e612a1f359a95b32a2 Mon Sep 17 00:00:00 2001 From: Rov Date: Fri, 24 Jul 2026 21:56:48 +0930 Subject: [PATCH] Initial commit: Guilded playerbots will now make diposits of crafting mats into the guild bank if available --- conf/playerbots.conf.dist | 10 ++ src/Ai/Base/ActionContext.h | 3 + .../Actions/DepositGuildMaterialsAction.cpp | 107 ++++++++++++ .../Actions/DepositGuildMaterialsAction.h | 29 ++++ src/Ai/Base/Actions/GuildBankAction.cpp | 71 +++++--- src/Ai/Base/Actions/GuildBankAction.h | 8 +- src/Bot/RandomPlayerbotMgr.cpp | 156 +++++++++++++++--- src/Bot/RandomPlayerbotMgr.h | 5 +- src/Mgr/Travel/TravelMgr.cpp | 98 ++++++++++- src/Mgr/Travel/TravelMgr.h | 2 + src/PlayerbotAIConfig.cpp | 4 + src/PlayerbotAIConfig.h | 2 + 12 files changed, 452 insertions(+), 43 deletions(-) create mode 100644 src/Ai/Base/Actions/DepositGuildMaterialsAction.cpp create mode 100644 src/Ai/Base/Actions/DepositGuildMaterialsAction.h diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 9befb8ad31d..4f2f0fbe1d9 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -1224,6 +1224,16 @@ AiPlayerbot.RandomBotMaps = 0,1,530,571 # Default: 0.25 AiPlayerbot.ProbTeleToBankers = 0.25 +# Let guilded randombots use their normal city-banker visits to deposit surplus crafting materials into their guild bank. +# The bot must have a depositable material, a guild-bank tab, and permission to deposit into it. +# Default: 1 (enabled) +AiPlayerbot.EnableRandomBotGuildBankDeposits = 1 + +# Minimum seconds between successful automatic guild-bank material deposits per randombot. +# Set to 0 to allow a deposit on every eligible city visit. +# Default: 14400 (4 hours) +AiPlayerbot.RandomBotGuildBankDepositCooldown = 14400 + # Control probability weights for bots teleporting to Capital city bankers # Sum of weights need not be 100. Set to 0 to disable teleporting to the city. AiPlayerbot.EnableWeightTeleToCityBankers = 1 diff --git a/src/Ai/Base/ActionContext.h b/src/Ai/Base/ActionContext.h index 229f1b2aeb3..dbcbed50ba1 100644 --- a/src/Ai/Base/ActionContext.h +++ b/src/Ai/Base/ActionContext.h @@ -25,6 +25,7 @@ #include "ChooseTravelTargetAction.h" #include "CombatActions.h" #include "DelayAction.h" +#include "DepositGuildMaterialsAction.h" #include "DestroyItemAction.h" #include "EmoteAction.h" #include "FollowActions.h" @@ -161,6 +162,7 @@ class ActionContext : public NamedObjectContext creators["outfit"] = &ActionContext::outfit; creators["random bot update"] = &ActionContext::random_bot_update; creators["delay"] = &ActionContext::delay; + creators["deposit guild materials"] = &ActionContext::deposit_guild_materials; creators["greet"] = &ActionContext::greet; creators["check values"] = &ActionContext::check_values; creators["ra"] = &ActionContext::ra; @@ -372,6 +374,7 @@ class ActionContext : public NamedObjectContext static Action* outfit(PlayerbotAI* botAI) { return new OutfitAction(botAI); } static Action* random_bot_update(PlayerbotAI* botAI) { return new RandomBotUpdateAction(botAI); } static Action* delay(PlayerbotAI* botAI) { return new DelayAction(botAI); } + static Action* deposit_guild_materials(PlayerbotAI* botAI) { return new DepositGuildMaterialsAction(botAI); } static Action* apply_poison(PlayerbotAI* botAI) { return new ImbueWithPoisonAction(botAI); } static Action* apply_oil(PlayerbotAI* botAI) { return new ImbueWithOilAction(botAI); } diff --git a/src/Ai/Base/Actions/DepositGuildMaterialsAction.cpp b/src/Ai/Base/Actions/DepositGuildMaterialsAction.cpp new file mode 100644 index 00000000000..055b2fb34de --- /dev/null +++ b/src/Ai/Base/Actions/DepositGuildMaterialsAction.cpp @@ -0,0 +1,107 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "DepositGuildMaterialsAction.h" + +#include "AiObjectContext.h" +#include "ItemUsageValue.h" +#include "ObjectMgr.h" +#include "Player.h" +#include "QuestDef.h" + +bool DepositGuildMaterialsAction::Execute(Event /*event*/) +{ + if (!bot->IsAlive() || !bot->IsInWorld() || bot->IsBeingTeleported() || bot->IsInCombat() || + !CanDepositToFirstTab()) + return false; + + GameObject* bank = GetNearbyGuildBank(); + if (!bank) + return false; + + CollectItemsVisitor visitor; + IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); + + bool deposited = false; + for (Item* item : visitor.items) + { + if (!IsDepositCandidate(item)) + continue; + + // Stop after the first failed transfer. This includes an unpurchased or full first tab, + // and avoids sending an inventory error for every remaining stack. + if (!MoveFromCharToBank(item, bank, false)) + break; + + deposited = true; + } + + return deposited; +} + +bool DepositGuildMaterialsAction::isPossible() +{ + // This intentionally does not require a nearby guild bank. RandomPlayerbotMgr uses it as a + // location-independent preflight before selecting a city guild-bank visit. + return CanDepositToFirstTab() && HasDepositCandidate(); +} + +bool DepositGuildMaterialsAction::HasDepositCandidate() +{ + CollectItemsVisitor visitor; + IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); + + for (Item* item : visitor.items) + if (IsDepositCandidate(item)) + return true; + + return false; +} + +bool DepositGuildMaterialsAction::IsDepositCandidate(Item* item) +{ + if (!item || item->IsSoulBound() || !item->CanBeTraded()) + return false; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto || proto->Class != ITEM_CLASS_TRADE_GOODS || proto->Duration > 0 || + proto->Bonding == BIND_WHEN_PICKED_UP || proto->Bonding == BIND_QUEST_ITEM || + proto->Bonding == BIND_QUEST_ITEM1 || proto->HasFlag(ITEM_FLAG_HAS_QUEST_GLOW)) + return false; + + if (IsQuestItem(proto)) + return false; + + // ItemUsage already knows which reagents and reserve stacks the bot should retain for its + // own professions, as well as active guild-task items. Only donate items the existing usage + // rules consider disposable. + ItemUsage const usage = context->GetValue("item usage", item->GetEntry())->Get(); + return usage == ITEM_USAGE_NONE || usage == ITEM_USAGE_AH || usage == ITEM_USAGE_VENDOR; +} + +bool DepositGuildMaterialsAction::IsQuestItem(ItemTemplate const* proto) const +{ + auto isRequiredForActiveQuest = [proto](Player* player) + { + if (!player) + return false; + + for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) + { + Quest const* quest = sObjectMgr->GetQuestTemplate(player->GetQuestSlotQuestId(slot)); + if (!quest) + continue; + + for (uint8 itemSlot = 0; itemSlot < 4; ++itemSlot) + if (quest->RequiredItemId[itemSlot] == proto->ItemId) + return true; + } + + return false; + }; + + return isRequiredForActiveQuest(bot) || isRequiredForActiveQuest(GetMaster()); +} diff --git a/src/Ai/Base/Actions/DepositGuildMaterialsAction.h b/src/Ai/Base/Actions/DepositGuildMaterialsAction.h new file mode 100644 index 00000000000..31fb282722f --- /dev/null +++ b/src/Ai/Base/Actions/DepositGuildMaterialsAction.h @@ -0,0 +1,29 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DEPOSITGUILDMATERIALSACTION_H +#define PLAYERBOTS_DEPOSITGUILDMATERIALSACTION_H + +#include "GuildBankAction.h" + +class Item; +struct ItemTemplate; + +class DepositGuildMaterialsAction : public GuildBankAction +{ +public: + DepositGuildMaterialsAction(PlayerbotAI* botAI) : GuildBankAction(botAI, "deposit guild materials") {} + + bool Execute(Event event) override; + bool isPossible() override; + +private: + bool HasDepositCandidate(); + bool IsDepositCandidate(Item* item); + bool IsQuestItem(ItemTemplate const* proto) const; +}; + +#endif diff --git a/src/Ai/Base/Actions/GuildBankAction.cpp b/src/Ai/Base/Actions/GuildBankAction.cpp index 89087579756..5a9e8255edf 100644 --- a/src/Ai/Base/Actions/GuildBankAction.cpp +++ b/src/Ai/Base/Actions/GuildBankAction.cpp @@ -6,9 +6,10 @@ #include "GuildBankAction.h" -#include "GuildMgr.h" -#include "PlayerbotAI.h" #include "AiObjectContext.h" +#include "GameObject.h" +#include "Guild.h" +#include "PlayerbotAI.h" bool GuildBankAction::Execute(Event event) { @@ -22,18 +23,33 @@ bool GuildBankAction::Execute(Event event) return false; } - GuidVector gos = *botAI->GetAiObjectContext()->GetValue("nearest game objects"); - for (GuidVector::iterator i = gos.begin(); i != gos.end(); ++i) + if (GameObject* bank = GetNearbyGuildBank()) + return Execute(text, bank); + + botAI->TellMaster("Cannot find the guild bank nearby"); + return false; +} + +GameObject* GuildBankAction::GetNearbyGuildBank() const +{ + GuidVector const gos = *botAI->GetAiObjectContext()->GetValue("nearest game objects"); + for (ObjectGuid const& guid : gos) { - GameObject* go = botAI->GetGameObject(*i); - if (!go || !bot->GetGameObjectIfCanInteractWith(go->GetGUID(), GAMEOBJECT_TYPE_GUILD_BANK)) + GameObject* go = botAI->GetGameObject(guid); + if (!go) continue; - return Execute(text, go); + if (GameObject* bank = bot->GetGameObjectIfCanInteractWith(go->GetGUID(), GAMEOBJECT_TYPE_GUILD_BANK)) + return bank; } - botAI->TellMaster("Cannot find the guild bank nearby"); - return false; + return nullptr; +} + +bool GuildBankAction::CanDepositToFirstTab() const +{ + Guild* guild = bot->GetGuild(); + return guild && guild->MemberHasTabRights(bot->GetGUID(), 0, GUILD_BANK_RIGHT_DEPOSIT_ITEM); } bool GuildBankAction::Execute(std::string const text, GameObject* bank) @@ -54,27 +70,44 @@ bool GuildBankAction::Execute(std::string const text, GameObject* bank) return result; } -bool GuildBankAction::MoveFromCharToBank(Item* item, GameObject* /*bank*/) +bool GuildBankAction::MoveFromCharToBank(Item* item, GameObject* bank, bool report) { + if (!item || !bank || !bot->GetGameObjectIfCanInteractWith(bank->GetGUID(), GAMEOBJECT_TYPE_GUILD_BANK)) + return false; + uint32 playerSlot = item->GetSlot(); uint32 playerBag = item->GetBagSlot(); + ObjectGuid const itemGuid = item->GetGUID(); + std::string const itemText = chat->FormatItem(item->GetTemplate()); std::ostringstream out; - Guild* guild = sGuildMgr->GetGuildById(bot->GetGuildId()); + Guild* guild = bot->GetGuild(); // guild->SwapItems(bot, 0, playerSlot, 0, INVENTORY_SLOT_BAG_0, 0); // check source pos rights (item moved to bank) - if (!guild->MemberHasTabRights(bot->GetGUID(), 0, GUILD_BANK_RIGHT_DEPOSIT_ITEM)) - out << "I can't put " << chat->FormatItem(item->GetTemplate()) - << " to guild bank. I have no rights to put items in the first guild bank tab"; - else + if (!guild || !guild->MemberHasTabRights(bot->GetGUID(), 0, GUILD_BANK_RIGHT_DEPOSIT_ITEM)) { - out << chat->FormatItem(item->GetTemplate()) << " put to guild bank"; - guild->SwapItemsWithInventory(bot, false, 0, 255, playerBag, playerSlot, 0); + out << "I can't put " << itemText + << " to guild bank. I have no rights to put items in the first guild bank tab"; + if (report) + botAI->TellMaster(out); + return false; } - botAI->TellMaster(out); + guild->SwapItemsWithInventory(bot, false, 0, NULL_SLOT, playerBag, playerSlot, 0); + + Item* sourceItem = bot->GetItemByPos(playerBag, playerSlot); + bool const moved = !sourceItem || sourceItem->GetGUID() != itemGuid; + if (report) + { + if (moved) + out << itemText << " put to guild bank"; + else + out << "I can't put " << itemText << " to guild bank"; + + botAI->TellMaster(out); + } - return true; + return moved; } diff --git a/src/Ai/Base/Actions/GuildBankAction.h b/src/Ai/Base/Actions/GuildBankAction.h index b3bb89c352a..20c63549eec 100644 --- a/src/Ai/Base/Actions/GuildBankAction.h +++ b/src/Ai/Base/Actions/GuildBankAction.h @@ -16,13 +16,17 @@ class PlayerbotAI; class GuildBankAction : public InventoryAction { public: - GuildBankAction(PlayerbotAI* botAI) : InventoryAction(botAI, "guild bank") {} + GuildBankAction(PlayerbotAI* botAI, std::string const name = "guild bank") : InventoryAction(botAI, name) {} bool Execute(Event event) override; +protected: + GameObject* GetNearbyGuildBank() const; + bool CanDepositToFirstTab() const; + bool MoveFromCharToBank(Item* item, GameObject* bank, bool report = true); + private: bool Execute(std::string const text, GameObject* bank); - bool MoveFromCharToBank(Item* item, GameObject* bank); }; #endif diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index 6abae472ab7..1013f5cf27c 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -16,6 +16,8 @@ #include #include "AiFactory.h" +#include "AiObjectContext.h" +#include "Action.h" #include "Battleground.h" #include "BattlegroundMgr.h" #include "ChannelMgr.h" @@ -30,6 +32,7 @@ #include "NewRpgInfo.h" #include "NewRpgStrategy.h" #include "ObjectGuid.h" +#include "ObjectAccessor.h" #include "PerfMonitor.h" #include "Player.h" #include "PlayerbotAI.h" @@ -51,6 +54,23 @@ #include "CellImpl.h" #include "GridNotifiersImpl.h" +namespace +{ +constexpr char GUILD_BANK_DEPOSIT_PENDING_EVENT[] = "guild_bank_deposit_pending"; +constexpr char GUILD_BANK_DEPOSIT_COOLDOWN_EVENT[] = "guild_bank_deposit_cooldown"; +constexpr uint32 GUILD_BANK_DEPOSIT_PENDING_TIMEOUT = MINUTE; +constexpr uint32 GUILD_BANK_DEPOSIT_DISPATCH_DELAY_MS = 5 * IN_MILLISECONDS; + +bool CanDepositGuildMaterials(PlayerbotAI* botAI) +{ + if (!botAI || !botAI->GetAiObjectContext()) + return false; + + Action* action = botAI->GetAiObjectContext()->GetAction("deposit guild materials"); + return action && action->isPossible(); +} +} // namespace + struct GuidClassRaceInfo { ObjectGuid::LowType guid; @@ -1333,6 +1353,80 @@ void RandomPlayerbotMgr::ScheduleChangeStrategy(uint32 bot, uint32 time) SetEventValue(bot, "change_strategy", 1, time); } +void RandomPlayerbotMgr::ScheduleGuildBankDeposit(Player* bot) +{ + if (!bot || !sPlayerbotAIConfig.enableRandomBotGuildBankDeposits) + return; + + uint32 botId = bot->GetGUID().GetCounter(); + SetEventValue(botId, GUILD_BANK_DEPOSIT_PENDING_EVENT, 1, GUILD_BANK_DEPOSIT_PENDING_TIMEOUT); + + ScheduleGuildBankDepositAttempt(bot); +} + +void RandomPlayerbotMgr::ScheduleGuildBankDepositAttempt(Player* bot) +{ + if (!bot || !GetEventValue(bot->GetGUID().GetCounter(), GUILD_BANK_DEPOSIT_PENDING_EVENT)) + return; + + if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot)) + { + ObjectGuid const guid = bot->GetGUID(); + botAI->AddTimedEvent( + [guid]() + { + if (Player* pendingBot = ObjectAccessor::FindPlayer(guid)) + { + sRandomPlayerbotMgr.ProcessPendingGuildBankDeposit(pendingBot); + sRandomPlayerbotMgr.ScheduleGuildBankDepositAttempt(pendingBot); + } + }, + GUILD_BANK_DEPOSIT_DISPATCH_DELAY_MS); + } +} + +bool RandomPlayerbotMgr::ProcessPendingGuildBankDeposit(Player* bot) +{ + if (!bot) + return false; + + uint32 botId = bot->GetGUID().GetCounter(); + if (!GetEventValue(botId, GUILD_BANK_DEPOSIT_PENDING_EVENT)) + return false; + + // The callback normally runs after the teleport acknowledgement. Keep the short-lived pending event intact while + // the server is still relocating the bot; the timed callback retries independently of the random-bot update rate. + if (!bot->IsInWorld() || bot->IsBeingTeleported()) + return true; + + if (!sPlayerbotAIConfig.enableRandomBotGuildBankDeposits || !bot->GetGuildId() || bot->isDead() || + bot->IsInCombat()) + { + SetEventValue(botId, GUILD_BANK_DEPOSIT_PENDING_EVENT, 0, 0); + return true; + } + + PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); + if (!botAI || !CanDepositGuildMaterials(botAI)) + { + SetEventValue(botId, GUILD_BANK_DEPOSIT_PENDING_EVENT, 0, 0); + return true; + } + + if (botAI->DoSpecificAction("deposit guild materials", Event(), true)) + { + if (sPlayerbotAIConfig.randomBotGuildBankDepositCooldown) + { + SetEventValue(botId, GUILD_BANK_DEPOSIT_COOLDOWN_EVENT, 1, + sPlayerbotAIConfig.randomBotGuildBankDepositCooldown); + } + + SetEventValue(botId, GUILD_BANK_DEPOSIT_PENDING_EVENT, 0, 0); + } + + return true; +} + bool RandomPlayerbotMgr::ProcessBot(uint32 bot) { ObjectGuid botGUID = ObjectGuid::Create(bot); @@ -1456,7 +1550,10 @@ bool RandomPlayerbotMgr::ProcessBot(Player* bot) if (bot->InBattlegroundQueue()) return false; - uint32 botId = bot->GetGUID().GetCounter(); + uint32 botId = bot->GetGUID().GetCounter(); + + if (ProcessPendingGuildBankDeposit(bot)) + return true; // if death revive if (bot->isDead()) @@ -1575,27 +1672,27 @@ void RandomPlayerbotMgr::Revive(Player* player) RandomTeleportGrindForLevel(player); } -void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& locs, bool hearth) +bool RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& locs, bool hearth) { // ignore when alrdy teleported or not in the world yet. if (bot->IsBeingTeleported() || !bot->IsInWorld()) - return; + return false; // no teleport / movement update when rooted. if (bot->IsRooted()) - return; + return false; // ignore when in queue for battle grounds. if (bot->InBattlegroundQueue()) - return; + return false; // ignore when in battle grounds or arena. if (bot->InBattleground() || bot->InArena()) - return; + return false; // ignore when in group (e.g. world, dungeons, raids) and leader is not a player. if (bot->GetGroup() && !bot->GetGroup()->IsLeader(bot->GetGUID())) - return; + return false; PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); if (botAI) @@ -1603,16 +1700,19 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& // ignore when in when taxi with boat/zeppelin and has players nearby if (bot->HasUnitMovementFlag(MOVEMENTFLAG_ONTRANSPORT) && bot->HasUnitState(UNIT_STATE_IGNORE_PATHFINDING) && botAI->HasPlayerNearby()) - return; + return false; } + if (!botAI) + return false; + // if (sPlayerbotAIConfig.randomBotRpgChance < 0) // return; if (locs.empty()) { LOG_DEBUG("playerbots", "Cannot teleport bot {} - no locations available", bot->GetName().c_str()); - return; + return false; } std::vector tlocs; @@ -1631,7 +1731,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& if (tlocs.empty()) { LOG_DEBUG("playerbots", "Cannot teleport bot {} - all locations removed by filter", bot->GetName().c_str()); - return; + return false; } PerfMonitorOperation* pmo = sPerfMonitor.start(PERF_MON_RNDBOT, "RandomTeleportByLocations"); @@ -1687,11 +1787,6 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& zone->area_name[locale], area->ID, area->area_name[locale], zone->area_level, area->area_level, x, y, z, i + 1, tlocs.size()); - if (hearth) - { - bot->SetHomebind(loc, zone->ID); - } - // Prevent blink to be detected by visible real players if (botAI->HasPlayerNearby(150.0f)) { @@ -1699,17 +1794,20 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& } bot->GetMotionMaster()->Clear(); - PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); - if (botAI) - botAI->Reset(true); + botAI->Reset(true); bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP); - bot->TeleportTo(loc.GetMapId(), x, y, z, 0); + if (!bot->TeleportTo(loc.GetMapId(), x, y, z, 0)) + continue; + + if (hearth) + bot->SetHomebind(loc, zone->ID); + bot->SendMovementFlagUpdate(); if (pmo) pmo->finish(); - return; + return true; } if (pmo) @@ -1717,6 +1815,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& // LOG_ERROR("playerbots", "Cannot teleport bot {} - no locations available ({} locations)", bot->GetName().c_str(), // tlocs.size()); + return false; } void RandomPlayerbotMgr::PrepareAddclassCache() @@ -1772,6 +1871,23 @@ void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot) if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) { + uint32 botId = bot->GetGUID().GetCounter(); + PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); + bool canDeposit = sPlayerbotAIConfig.enableRandomBotGuildBankDeposits && bot->GetGuildId() && + (!sPlayerbotAIConfig.randomBotGuildBankDepositCooldown || + !GetEventValue(botId, GUILD_BANK_DEPOSIT_COOLDOWN_EVENT)) && + CanDepositGuildMaterials(botAI); + + if (canDeposit) + { + std::vector guildBankLocs = sTravelMgr.GetCityGuildBankLocations(bot); + if (!guildBankLocs.empty() && RandomTeleport(bot, guildBankLocs, true)) + { + ScheduleGuildBankDeposit(bot); + return; + } + } + std::vector locs = sTravelMgr.GetCityLocations(bot); if (!locs.empty()) { diff --git a/src/Bot/RandomPlayerbotMgr.h b/src/Bot/RandomPlayerbotMgr.h index 4e167dd2a51..577d43511c3 100644 --- a/src/Bot/RandomPlayerbotMgr.h +++ b/src/Bot/RandomPlayerbotMgr.h @@ -235,9 +235,12 @@ class RandomPlayerbotMgr : public PlayerbotHolder time_t printStatsTimer; uint32 AddRandomBots(); bool ProcessBot(uint32 bot); + bool ProcessPendingGuildBankDeposit(Player* bot); + void ScheduleGuildBankDeposit(Player* bot); + void ScheduleGuildBankDepositAttempt(Player* bot); void ScheduleRandomize(uint32 bot, uint32 time); void RandomTeleport(Player* bot); - void RandomTeleport(Player* bot, std::vector& locs, bool hearth = false); + bool RandomTeleport(Player* bot, std::vector& locs, bool hearth = false); uint32 GetZoneLevel(uint16 mapId, float teleX, float teleY, float teleZ); typedef void (RandomPlayerbotMgr::*ConsoleCommandHandler)(Player*); std::vector players; diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index e6a618ca546..3088c26bc01 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -4556,6 +4556,58 @@ std::vector TravelMgr::GetCityLocations(Player* bot) return fallbackLocations; } +std::vector TravelMgr::GetCityGuildBankLocations(Player* bot) +{ + if (!bot) + return {}; + + std::vector fallbackLocations; + std::vector validGuildBankCities; + TeamId botTeamId = bot->GetTeamId(); + std::unordered_set levelAppropriateCities; + + for (BankerLocation const& bankerLocation : bankerLocsPerLevelCache[bot->GetLevel()]) + { + Capital const* capital = FindCapitalByBanker(bankerLocation.entry); + if (capital && (capital->team == botTeamId || capital->team == TEAM_NEUTRAL)) + levelAppropriateCities.insert(capital->zoneId); + } + + for (auto const& [zoneId, locations] : guildBankLocsByCity) + { + Capital const* capital = FindCapitalByZone(zoneId); + if (!capital || locations.empty() || levelAppropriateCities.find(zoneId) == levelAppropriateCities.end()) + continue; + + fallbackLocations.insert(fallbackLocations.end(), locations.begin(), locations.end()); + validGuildBankCities.push_back(zoneId); + } + + if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers || validGuildBankCities.empty()) + return fallbackLocations; + + std::vector weightedCities; + for (uint32 zoneId : validGuildBankCities) + { + int weight = GetCityWeight(zoneId); + if (weight <= 0) + continue; + + for (int i = 0; i < weight; ++i) + weightedCities.push_back(zoneId); + } + + if (weightedCities.empty()) + return fallbackLocations; + + uint32 selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; + auto const locations = guildBankLocsByCity.find(selectedCity); + if (locations == guildBankLocsByCity.end() || locations->second.empty()) + return fallbackLocations; + + return {locations->second[urand(0, locations->second.size() - 1)]}; +} + void TravelMgr::PrepareZone2LevelBracket() { // Classic WoW - starter zones @@ -4642,6 +4694,9 @@ void TravelMgr::PrepareDestinationCache() uint32 flightMastersCount = 0; uint32 innkeepersCount = 0; uint32 bankerCount = 0; + uint32 guildBankCount = 0; + + guildBankLocsByCity.clear(); LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel); // Temporary map to group creatures by entry and area @@ -4809,6 +4864,44 @@ void TravelMgr::PrepareDestinationCache() } } + constexpr float guildBankInteractionOffset = 6.0f; + for (auto const& gameObjectDataPair : sObjectMgr->GetAllGOData()) + { + GameObjectData const& gameObjectData = gameObjectDataPair.second; + GameObjectTemplate const* gameObjectTemplate = sObjectMgr->GetGameObjectTemplate(gameObjectData.id); + if (!gameObjectTemplate || gameObjectTemplate->type != GAMEOBJECT_TYPE_GUILD_BANK || + !(gameObjectData.phaseMask & PHASEMASK_NORMAL)) + continue; + + uint16 mapId = gameObjectData.mapid; + if (std::find(sPlayerbotAIConfig.randomBotMaps.begin(), sPlayerbotAIConfig.randomBotMaps.end(), mapId) == + sPlayerbotAIConfig.randomBotMaps.end()) + continue; + + Map* map = sMapMgr->FindMap(mapId, 0); + if (!map) + continue; + + float x = gameObjectData.posX; + float y = gameObjectData.posY; + float z = gameObjectData.posZ; + float orient = gameObjectData.orientation; + + AreaTableEntry const* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z)); + if (!area) + continue; + + uint32 zoneId = area->zone ? area->zone : area->ID; + if (!FindCapitalByZone(zoneId)) + continue; + + // Do not target the object center: it may be inside its collision model. The same six-yard, front-facing + // offset used for banker travel keeps the bot within the guild-bank interaction range while on walkable ground. + guildBankLocsByCity[zoneId].emplace_back(mapId, x + cos(orient) * guildBankInteractionOffset, + y + sin(orient) * guildBankInteractionOffset, z + 0.5f, orient + M_PI); + ++guildBankCount; + } + // Process temporary caches for (auto const& [gridTuple, creatureDataList] : tempLocsCache) { @@ -4871,5 +4964,8 @@ void TravelMgr::PrepareDestinationCache() break; } } - LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount); + LOG_INFO("playerbots", + ">> {} flight masters and {} innkeepers and {} banker locations and {} guild bank locations for level " + "collected.", + flightMastersCount, innkeepersCount, bankerCount, guildBankCount); } diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index cb24cf0539a..a848c1906ac 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -880,6 +880,7 @@ class TravelMgr const std::vector GetTeleportLocations(Player* bot); const std::vector GetTravelHubs(Player* bot); std::vector GetCityLocations(Player* bot); + std::vector GetCityGuildBankLocations(Player* bot); std::vector GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode = 0) const; bool SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer); const std::vector& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; } @@ -999,6 +1000,7 @@ class TravelMgr std::map> hordeHubsPerLevelCache; std::map> bankerLocsPerLevelCache; std::unordered_map bankerEntryToLocation; + std::map> guildBankLocsByCity; std::map> locsPerLevelCache; std::unordered_map> creatureSpawnsByTemplate; std::map zone2LevelBracket; diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index eef9bfc50e1..e06b31ab82a 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -174,6 +174,10 @@ bool PlayerbotAIConfig::Initialize() LoadList>(randomBotMapsAsString, randomBotMaps); probTeleToBankers = sConfigMgr->GetOption("AiPlayerbot.ProbTeleToBankers", 0.25f); enableWeightTeleToCityBankers = sConfigMgr->GetOption("AiPlayerbot.EnableWeightTeleToCityBankers", false); + enableRandomBotGuildBankDeposits = + sConfigMgr->GetOption("AiPlayerbot.EnableRandomBotGuildBankDeposits", true); + randomBotGuildBankDepositCooldown = + sConfigMgr->GetOption("AiPlayerbot.RandomBotGuildBankDepositCooldown", 4 * HOUR); weightTeleToStormwind = sConfigMgr->GetOption("AiPlayerbot.TeleToStormwindWeight", 2); weightTeleToIronforge = sConfigMgr->GetOption("AiPlayerbot.TeleToIronforgeWeight", 1); weightTeleToDarnassus = sConfigMgr->GetOption("AiPlayerbot.TeleToDarnassusWeight", 1); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index b01b5fdaad1..95bb3038428 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -127,6 +127,8 @@ class PlayerbotAIConfig std::string randomBotMapsAsString; float probTeleToBankers; bool enableWeightTeleToCityBankers; + bool enableRandomBotGuildBankDeposits; + uint32 randomBotGuildBankDepositCooldown; int weightTeleToStormwind; int weightTeleToIronforge; int weightTeleToDarnassus;