diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index 6820c6460..58846b949 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -231,7 +231,6 @@ bool NewRpgDoQuestAction::Execute(Event /*event*/) return false; auto& data = *dataPtr; uint32 questId = data.questId; - const Quest* quest = data.quest; uint8 questStatus = bot->GetQuestStatus(questId); switch (questStatus) { @@ -438,7 +437,7 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/) if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE) return MoveFarTo(flightMaster); - std::vector nodes = {data.fromNode, data.toNode}; + std::vector nodes = data.path; botAI->RemoveShapeshift(); if (bot->IsMounted()) @@ -447,7 +446,7 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/) if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0)) { LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(), - flightMaster->GetEntry(), nodes[0], nodes[1]); + flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]); botAI->rpgInfo.ChangeToIdle(); } return true; diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index 75392418b..b5156d6c1 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -3,7 +3,6 @@ #include "BroadcastHelper.h" #include "ChatHelper.h" #include "Creature.h" -#include "FlightMasterCache.h" #include "G3D/Vector2.h" #include "GameObject.h" #include "GossipDef.h" @@ -856,7 +855,7 @@ bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) { - const std::vector& locs = sRandomPlayerbotMgr.locsPerLevelCache[bot->GetLevel()]; + const std::vector& locs = sTravelMgr.GetLocsPerLevelCache(bot->GetLevel()); float hiRange = 500.0f; float loRange = 2500.0f; if (bot->GetLevel() < 5) @@ -914,9 +913,7 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) { - const std::vector& locs = IsAlliance(bot->getRace()) - ? sRandomPlayerbotMgr.allianceStarterPerLevelCache[bot->GetLevel()] - : sRandomPlayerbotMgr.hordeStarterPerLevelCache[bot->GetLevel()]; + const std::vector locs = sTravelMgr.GetTravelHubs(bot); bool inCity = false; @@ -957,70 +954,19 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) return dest; } -bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, uint32& fromNode, uint32& toNode) +bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path) { - Creature* nearestFlightMaster = FlightMasterCache::Instance().GetNearestFlightMaster(bot); - if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f) + flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot); + if (!flightMaster) return false; - fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(), - nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(), - bot->GetTeamId()); - - if (!fromNode) + std::vector> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot); + if (availablePaths.empty()) return false; - std::vector availableToNodes; - for (uint32 i = 1; i < sTaxiNodesStore.GetNumRows(); ++i) - { - if (fromNode == i) - continue; - - TaxiNodesEntry const* node = sTaxiNodesStore.LookupEntry(i); - - // check map - if (!node || node->map_id != bot->GetMapId() || - (!node->MountCreatureID[bot->GetTeamId() == TEAM_ALLIANCE ? 1 : 0])) // dk flight - continue; - - // check taxi node known - if (!bot->isTaxiCheater() && !bot->m_taxi.IsTaximaskNodeKnown(i)) - continue; - - // check distance by level - if (!botAI->CheckLocationDistanceByLevel(bot, WorldLocation(node->map_id, node->x, node->y, node->z), false)) - continue; - - // check path - uint32 path, cost; - sObjectMgr->GetTaxiPath(fromNode, i, path, cost); - if (!path) - continue; - - // check area level - uint32 nodeZoneId = bot->GetMap()->GetZoneId(bot->GetPhaseMask(), node->x, node->y, node->z); - bool capital = false; - if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(nodeZoneId)) - { - capital = zone->flags & AREA_FLAG_CAPITAL; - } - - auto itr = sRandomPlayerbotMgr.zone2LevelBracket.find(nodeZoneId); - if (!capital && itr == sRandomPlayerbotMgr.zone2LevelBracket.end()) - continue; - - if (!capital && (bot->GetLevel() < itr->second.low || bot->GetLevel() > itr->second.high)) - continue; - - availableToNodes.push_back(i); - } - if (availableToNodes.empty()) - return false; - - flightMaster = nearestFlightMaster->GetGUID(); - toNode = availableToNodes[urand(0, availableToNodes.size() - 1)]; + path = availablePaths[urand(0, availablePaths.size() - 1)]; LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)", - bot->GetName(), flightMaster.GetEntry(), fromNode, toNode, availableToNodes.size()); + bot->GetName(), flightMaster.GetEntry(), path[0], path[path.size() - 1], availablePaths.size()); return true; } @@ -1121,10 +1067,10 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta case RPG_TRAVEL_FLIGHT: { ObjectGuid flightMaster; - uint32 fromNode, toNode; - if (SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode)) + std::vector path; + if (SelectRandomFlightTaxiNode(flightMaster, path)) { - botAI->rpgInfo.ChangeToTravelFlight(flightMaster, fromNode, toNode); + botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path); return true; } return false; @@ -1197,8 +1143,8 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status) case RPG_TRAVEL_FLIGHT: { ObjectGuid flightMaster; - uint32 fromNode, toNode; - return SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode); + std::vector path; + return SelectRandomFlightTaxiNode(flightMaster, path); } default: return false; diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index 910e5f494..9cd939eb7 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -54,7 +54,7 @@ protected: bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector& poiInfo, bool toComplete = false); static WorldPosition SelectRandomGrindPos(Player* bot); static WorldPosition SelectRandomCampPos(Player* bot); - bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, uint32& fromNode, uint32& toNode); + bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path); bool RandomChangeStatus(std::vector candidateStatus); bool CheckRpgStatusAvailable(NewRpgStatus status); diff --git a/src/Ai/Base/Actions/RpgAction.cpp b/src/Ai/World/Rpg/Action/RpgAction.cpp similarity index 100% rename from src/Ai/Base/Actions/RpgAction.cpp rename to src/Ai/World/Rpg/Action/RpgAction.cpp diff --git a/src/Ai/Base/Actions/RpgAction.h b/src/Ai/World/Rpg/Action/RpgAction.h similarity index 100% rename from src/Ai/Base/Actions/RpgAction.h rename to src/Ai/World/Rpg/Action/RpgAction.h diff --git a/src/Ai/Base/Actions/RpgSubActions.cpp b/src/Ai/World/Rpg/Action/RpgSubActions.cpp similarity index 100% rename from src/Ai/Base/Actions/RpgSubActions.cpp rename to src/Ai/World/Rpg/Action/RpgSubActions.cpp diff --git a/src/Ai/Base/Actions/RpgSubActions.h b/src/Ai/World/Rpg/Action/RpgSubActions.h similarity index 100% rename from src/Ai/Base/Actions/RpgSubActions.h rename to src/Ai/World/Rpg/Action/RpgSubActions.h diff --git a/src/Ai/World/Rpg/NewRpgInfo.cpp b/src/Ai/World/Rpg/NewRpgInfo.cpp index 20db8b049..4e04ab086 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.cpp +++ b/src/Ai/World/Rpg/NewRpgInfo.cpp @@ -37,13 +37,12 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest) data = do_quest; } -void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode) +void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path) { startT = getMSTime(); TravelFlight flight; flight.fromFlightMaster = fromFlightMaster; - flight.fromNode = fromNode; - flight.toNode = toNode; + flight.path = std::move(path); flight.inFlight = false; data = flight; } @@ -150,8 +149,8 @@ std::string NewRpgInfo::ToString() { out << "TRAVEL_FLIGHT"; out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry(); - out << "\nfromNode: " << arg.fromNode; - out << "\ntoNode: " << arg.toNode; + out << "\nfromNode: " << arg.path[0]; + out << "\ntoNode: " << arg.path[arg.path.size() - 1]; out << "\ninFlight: " << arg.inFlight; } else diff --git a/src/Ai/World/Rpg/NewRpgInfo.h b/src/Ai/World/Rpg/NewRpgInfo.h index 5b6ae3cb9..c2349c14b 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.h +++ b/src/Ai/World/Rpg/NewRpgInfo.h @@ -50,8 +50,7 @@ struct NewRpgInfo struct TravelFlight { ObjectGuid fromFlightMaster{}; - uint32 fromNode{0}; - uint32 toNode{0}; + std::vector path; bool inFlight{false}; }; // RPG_REST @@ -91,7 +90,7 @@ struct NewRpgInfo void ChangeToWanderNpc(); void ChangeToWanderRandom(); void ChangeToDoQuest(uint32 questId, const Quest* quest); - void ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode); + void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path); void ChangeToRest(); void ChangeToIdle(); bool CanChangeTo(NewRpgStatus status); diff --git a/src/Ai/Base/Actions/RpgValues.h b/src/Ai/World/Rpg/RpgValues.h similarity index 100% rename from src/Ai/Base/Actions/RpgValues.h rename to src/Ai/World/Rpg/RpgValues.h diff --git a/src/Ai/Base/Strategy/RpgStrategy.cpp b/src/Ai/World/Rpg/Strategy/RpgStrategy.cpp similarity index 100% rename from src/Ai/Base/Strategy/RpgStrategy.cpp rename to src/Ai/World/Rpg/Strategy/RpgStrategy.cpp diff --git a/src/Ai/Base/Strategy/RpgStrategy.h b/src/Ai/World/Rpg/Strategy/RpgStrategy.h similarity index 100% rename from src/Ai/Base/Strategy/RpgStrategy.h rename to src/Ai/World/Rpg/Strategy/RpgStrategy.h diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 6c89400b1..800247c6f 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -6487,7 +6487,7 @@ ChatChannelSource PlayerbotAI::GetChatChannelSource(Player* bot, uint32 type, st return ChatChannelSource::SRC_UNDEFINED; } -bool PlayerbotAI::CheckLocationDistanceByLevel(Player* player, const WorldLocation& loc, bool fromStartUp) +bool PlayerbotAI::StarterLevelDistanceCheck(Player* player, const WorldLocation& loc, bool fromStartUp) { if (player->GetLevel() > 16) return true; diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index b3a51b15c..5d64bf159 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -556,7 +556,7 @@ public: bool IsSafe(WorldObject* obj); ChatChannelSource GetChatChannelSource(Player* bot, uint32 type, std::string channelName); - bool CheckLocationDistanceByLevel(Player* player, const WorldLocation &loc, bool fromStartUp = false); + bool StarterLevelDistanceCheck(Player* player, const WorldLocation &loc, bool fromStartUp = false); bool HasCheat(BotCheatMask mask) { diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index bc1ffaad1..3fea369a5 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -23,7 +23,6 @@ #include "DatabaseEnv.h" #include "Define.h" #include "FleeManager.h" -#include "FlightMasterCache.h" #include "GridNotifiers.h" #include "LFGMgr.h" #include "MapMgr.h" @@ -47,9 +46,7 @@ #include "World.h" #include "Cell.h" #include "GridNotifiers.h" -// Required for Cell because of poor AC implementation #include "CellImpl.h" -// Required for GridNotifiers because of poor AC implementation #include "GridNotifiersImpl.h" struct GuidClassRaceInfo @@ -59,48 +56,6 @@ struct GuidClassRaceInfo uint32 rRace; }; -enum class CityId : uint8 { - STORMWIND, IRONFORGE, DARNASSUS, EXODAR, - ORGRIMMAR, UNDERCITY, THUNDER_BLUFF, SILVERMOON_CITY, - SHATTRATH_CITY, DALARAN -}; - -enum class FactionId : uint8 { ALLIANCE, HORDE, NEUTRAL }; - -// Map of banker entry → city + faction -static const std::unordered_map> bankerToCity = { - {2455, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2456, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2457, {CityId::STORMWIND, FactionId::ALLIANCE}}, - {2460, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {2461, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {5099, {CityId::IRONFORGE, FactionId::ALLIANCE}}, - {4155, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4208, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4209, {CityId::DARNASSUS, FactionId::ALLIANCE}}, - {17773, {CityId::EXODAR, FactionId::ALLIANCE}}, {18350, {CityId::EXODAR, FactionId::ALLIANCE}}, {16710, {CityId::EXODAR, FactionId::ALLIANCE}}, - {3320, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3309, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3318, {CityId::ORGRIMMAR, FactionId::HORDE}}, - {4549, {CityId::UNDERCITY, FactionId::HORDE}}, {2459, {CityId::UNDERCITY, FactionId::HORDE}}, {2458, {CityId::UNDERCITY, FactionId::HORDE}}, {4550, {CityId::UNDERCITY, FactionId::HORDE}}, - {2996, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8356, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8357, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, - {17631, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17632, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17633, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, - {16615, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16616, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16617, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, - {19246, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, - {19034, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, - {30604, {CityId::DALARAN, FactionId::NEUTRAL}}, {30605, {CityId::DALARAN, FactionId::NEUTRAL}}, {30607, {CityId::DALARAN, FactionId::NEUTRAL}}, - {28675, {CityId::DALARAN, FactionId::NEUTRAL}}, {28676, {CityId::DALARAN, FactionId::NEUTRAL}}, {28677, {CityId::DALARAN, FactionId::NEUTRAL}} -}; - -// Map of city → available banker entries -static const std::unordered_map> cityToBankers = { - {CityId::STORMWIND, {2455, 2456, 2457}}, - {CityId::IRONFORGE, {2460, 2461, 5099}}, - {CityId::DARNASSUS, {4155, 4208, 4209}}, - {CityId::EXODAR, {17773, 18350, 16710}}, - {CityId::ORGRIMMAR, {3320, 3309, 3318}}, - {CityId::UNDERCITY, {4549, 2459, 2458, 4550}}, - {CityId::THUNDER_BLUFF, {2996, 8356, 8357}}, - {CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}}, - {CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}}, - {CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}} -}; - -// Quick lookup map: banker entry → location -static std::unordered_map bankerEntryToLocation; - void PrintStatsThread() { sRandomPlayerbotMgr.PrintStats(); } void activatePrintStatsThread() @@ -1718,7 +1673,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& z = 0.05f + ground; - if (!botAI->CheckLocationDistanceByLevel(bot, loc, true)) + if (!botAI->StarterLevelDistanceCheck(bot, loc, true)) continue; const LocaleConstant& locale = sWorld->GetDefaultDbcLocale(); @@ -1762,329 +1717,6 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& // tlocs.size()); } -void RandomPlayerbotMgr::PrepareZone2LevelBracket() -{ - // Classic WoW - Low - level zones - zone2LevelBracket[1] = {5, 12}; // Dun Morogh - zone2LevelBracket[12] = {5, 12}; // Elwynn Forest - zone2LevelBracket[14] = {5, 12}; // Durotar - zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades - zone2LevelBracket[141] = {5, 12}; // Teldrassil - zone2LevelBracket[215] = {5, 12}; // Mulgore - zone2LevelBracket[3430] = {5, 12}; // Eversong Woods - zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle - - // Classic WoW - Mid - level zones - zone2LevelBracket[17] = {10, 25}; // Barrens - zone2LevelBracket[38] = {10, 20}; // Loch Modan - zone2LevelBracket[40] = {10, 21}; // Westfall - zone2LevelBracket[130] = {10, 23}; // Silverpine Forest - zone2LevelBracket[148] = {10, 21}; // Darkshore - zone2LevelBracket[3433] = {10, 22}; // Ghostlands - zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle - - // Classic WoW - High - level zones - zone2LevelBracket[10] = {19, 33}; // Duskwood - zone2LevelBracket[11] = {21, 30}; // Wetlands - zone2LevelBracket[44] = {16, 28}; // Redridge Mountains - zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills - zone2LevelBracket[331] = {18, 33}; // Ashenvale - zone2LevelBracket[400] = {24, 36}; // Thousand Needles - zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains - - // Classic WoW - Higher - level zones - zone2LevelBracket[3] = {36, 46}; // Badlands - zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows - zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh - zone2LevelBracket[16] = {45, 52}; // Azshara - zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale - zone2LevelBracket[45] = {30, 42}; // Arathi Highlands - zone2LevelBracket[47] = {42, 51}; // Hinterlands - zone2LevelBracket[51] = {45, 51}; // Searing Gorge - zone2LevelBracket[357] = {40, 52}; // Feralas - zone2LevelBracket[405] = {30, 41}; // Desolace - zone2LevelBracket[440] = {41, 52}; // Tanaris - - // Classic WoW - Top - level zones - zone2LevelBracket[4] = {52, 57}; // Blasted Lands - zone2LevelBracket[28] = {50, 60}; // Western Plaguelands - zone2LevelBracket[46] = {51, 60}; // Burning Steppes - zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands - zone2LevelBracket[361] = {47, 57}; // Felwood - zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater - zone2LevelBracket[618] = {54, 61}; // Winterspring - zone2LevelBracket[1377] = {54, 63}; // Silithus - - // The Burning Crusade - Zones - zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula - zone2LevelBracket[3518] = {64, 70}; // Nagrand - zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest - zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley - zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh - zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains - zone2LevelBracket[3523] = {67, 73}; // Netherstorm - zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas - - // Wrath of the Lich King - Zones - zone2LevelBracket[65] = {71, 77}; // Dragonblight - zone2LevelBracket[66] = {74, 80}; // Zul'Drak - zone2LevelBracket[67] = {77, 80}; // Storm Peaks - zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier - zone2LevelBracket[394] = {72, 78}; // Grizzly Hills - zone2LevelBracket[495] = {68, 74}; // Howling Fjord - zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest - zone2LevelBracket[3537] = {68, 75}; // Borean Tundra - zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin - zone2LevelBracket[4197] = {79, 80}; // Wintergrasp - - // Override with values from config - for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets) - { - zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second}; - } -} - -void RandomPlayerbotMgr::PrepareTeleportCache() -{ - uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); - - LOG_INFO("playerbots", "Preparing random teleport caches for {} levels...", maxLevel); - - QueryResult results = WorldDatabase.Query( - "SELECT " - "g.map, " - "position_x, " - "position_y, " - "position_z, " - "t.minlevel, " - "t.maxlevel " - "FROM " - "(SELECT " - "map, " - "MIN( c.guid ) guid " - "FROM " - "creature c " - "INNER JOIN creature_template t ON c.id1 = t.entry " - "WHERE " - "t.npcflag = 0 " - "AND t.lootid != 0 " - "AND t.maxlevel - t.minlevel < 3 " - "AND map IN ({}) " - "AND t.entry not in (32820, 24196, 30627, 30617) " - "AND c.spawntimesecs < 1000 " - "AND t.faction not in (11, 71, 79, 85, 188, 1575) " - "AND (t.unit_flags & 256) = 0 " - "AND (t.unit_flags & 4096) = 0 " - "AND t.rank = 0 " - // "AND (t.flags_extra & 32768) = 0 " - "GROUP BY " - "map, " - "ROUND(position_x / 50), " - "ROUND(position_y / 50), " - "ROUND(position_z / 50) " - "HAVING " - "count(*) >= 2) " - "AS g " - "INNER JOIN creature c ON g.guid = c.guid " - "INNER JOIN creature_template t on c.id1 = t.entry " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - uint32 collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - uint32 min_level = fields[4].Get(); - uint32 max_level = fields[5].Get(); - uint32 level = (min_level + max_level + 1) / 2; - WorldLocation loc(mapId, x, y, z, 0); - collected_locs++; - for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; - l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) - { - if (l < 1 || l > maxLevel) - { - continue; - } - locsPerLevelCache[(uint8)l].push_back(loc); - } - } while (results->NextRow()); - } - LOG_INFO("playerbots", ">> {} locations for level collected.", collected_locs); - - if (sPlayerbotAIConfig.enableNewRpgStrategy) - { - PrepareZone2LevelBracket(); - LOG_INFO("playerbots", "Preparing innkeepers / flightmasters locations for level..."); - results = WorldDatabase.Query( - "SELECT " - "map, " - "position_x, " - "position_y, " - "position_z, " - "orientation, " - "t.faction, " - "t.entry, " - "t.npcflag, " - "c.guid " - "FROM " - "creature c " - "INNER JOIN creature_template t on c.id1 = t.entry " - "WHERE " - "t.npcflag & 73728 " - "AND map IN ({}) " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - float orient = fields[4].Get(); - uint32 faction = fields[5].Get(); - uint32 tEntry = fields[6].Get(); - uint32 tNpcflag = fields[7].Get(); - uint32 guid = fields[8].Get(); - - if (tEntry == 3838 || tEntry == 29480) - continue; - - const FactionTemplateEntry* entry = sFactionTemplateStore.LookupEntry(faction); - - WorldLocation loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); - collected_locs++; - Map* map = sMapMgr->FindMap(loc.GetMapId(), 0); - if (!map) - continue; - bool forHorde = !(entry->hostileMask & 4); - bool forAlliance = !(entry->hostileMask & 2); - if (tNpcflag & UNIT_NPC_FLAG_FLIGHTMASTER) - { - WorldPosition pos(mapId, x, y, z, orient); - if (forHorde) - FlightMasterCache::Instance().AddHordeFlightMaster(guid, pos); - - if (forAlliance) - FlightMasterCache::Instance().AddAllianceFlightMaster(guid, pos); - } - const AreaTableEntry* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z)); - uint32 zoneId = area->zone ? area->zone : area->ID; - if (zone2LevelBracket.find(zoneId) == zone2LevelBracket.end()) - continue; - LevelBracket bracket = zone2LevelBracket[zoneId]; - for (int i = bracket.low; i <= bracket.high; i++) - { - if (forHorde) - hordeStarterPerLevelCache[i].push_back(loc); - if (forAlliance) - allianceStarterPerLevelCache[i].push_back(loc); - } - - } while (results->NextRow()); - } - - // add all initial position - for (uint32 i = 1; i < sRaceMgr->GetMaxRaces(); i++) - { - for (uint32 j = 1; j < MAX_CLASSES; j++) - { - PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j); - - if (!info) - continue; - - WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation); - - for (int32 l = 1; l <= 5; l++) - { - if ((1 << (i - 1)) & sRaceMgr->GetAllianceRaceMask()) - allianceStarterPerLevelCache[(uint8)l].push_back(pos); - else - hordeStarterPerLevelCache[(uint8)l].push_back(pos); - } - break; - } - } - LOG_INFO("playerbots", ">> {} innkeepers locations for level collected.", collected_locs); - } - - results = WorldDatabase.Query( - "SELECT " - "map, " - "position_x, " - "position_y, " - "position_z, " - "orientation, " - "t.minlevel, " - "t.entry " - "FROM " - "creature c " - "INNER JOIN creature_template t on c.id1 = t.entry " - "WHERE " - "t.npcflag & 131072 " - "AND t.npcflag != 135298 " - "AND t.minlevel != 55 " - "AND t.minlevel != 65 " - "AND t.faction not in (35, 474, 69, 57) " - "AND t.entry not in (30606, 30608, 29282) " - "AND map IN ({}) " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - float orient = fields[4].Get(); - uint32 level = fields[5].Get(); - uint32 entry = fields[6].Get(); - BankerLocation bLoc; - bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI); - bLoc.entry = entry; - collected_locs++; - for (int32 l = 1; l <= maxLevel; l++) - { - // Bots 1-60 go to base game bankers (all have minlevel 30 or 45) - if (l <=60 && level > 45) - { - continue; - } - // Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70) - if ((l >=61 && l <=70) && (level < 60 || level > 70)) - { - continue; - } - // Bots 71+ go to Dalaran bankers (all have minlevel 75) - if ((l >=71) && level != 75) - { - continue; - } - bankerLocsPerLevelCache[(uint8)l].push_back(bLoc); - bankerEntryToLocation[bLoc.entry] = bLoc.loc; - } - } while (results->NextRow()); - } - LOG_INFO("playerbots", ">> {} banker locations for level collected.", collected_locs); -} - void RandomPlayerbotMgr::PrepareAddclassCache() { // Using accounts marked as type 2 (AddClass) @@ -2125,11 +1757,6 @@ void RandomPlayerbotMgr::Init() if (sPlayerbotAIConfig.addClassCommand) sRandomPlayerbotMgr.PrepareAddclassCache(); - if (sPlayerbotAIConfig.enabled) - { - sRandomPlayerbotMgr.PrepareTeleportCache(); - } - if (sPlayerbotAIConfig.randomBotJoinBG) sRandomPlayerbotMgr.LoadBattleMastersCache(); @@ -2141,103 +1768,17 @@ void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot) if (bot->InBattleground()) return; - uint32 level = bot->GetLevel(); - uint8 race = bot->getRace(); - std::vector* locs = nullptr; - if (sPlayerbotAIConfig.enableNewRpgStrategy) - locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level]; - else - locs = &locsPerLevelCache[level]; - if (level >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) + std::vector locs = sTravelMgr.GetCityLocations(bot); + if (!locs.empty()) { - std::vector fallbackLocs; - for (auto& bLoc : bankerLocsPerLevelCache[level]) - fallbackLocs.push_back(bLoc.loc); - - if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers) - { - RandomTeleport(bot, fallbackLocs, true); - return; - } - - // Collect valid cities based on bot faction. - std::unordered_set validBankerCities; - for (auto& loc : bankerLocsPerLevelCache[level]) - { - auto cityIt = bankerToCity.find(loc.entry); - if (cityIt == bankerToCity.end()) continue; - - CityId cityId = cityIt->second.first; - FactionId cityFactionId = cityIt->second.second; - - if ((IsAlliance(bot->getRace()) && cityFactionId == FactionId::ALLIANCE) || - (!IsAlliance(bot->getRace()) && cityFactionId == FactionId::HORDE) || - (cityFactionId == FactionId::NEUTRAL)) - { - validBankerCities.insert(cityId); - } - } - - // Fallback if no valid cities - if (validBankerCities.empty()) - { - RandomTeleport(bot, fallbackLocs, true); - return; - } - - // Apply weights to valid cities - std::vector weightedCities; - for (CityId city : validBankerCities) - { - int weight = 0; - switch (city) - { - case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break; - case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break; - case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break; - case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break; - case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break; - case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break; - case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break; - case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break; - case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break; - case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break; - default: weight = 0; break; - } - if (weight <= 0) continue; - - for (int i = 0; i < weight; ++i) - { - weightedCities.push_back(city); - } - } - - // Fallback if no valid cities - if (weightedCities.empty()) - { - RandomTeleport(bot, fallbackLocs, true); - return; - } - - // Pick a weighted city randomly, then a random banker in that city - // then teleport to that banker - CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; - auto const& bankers = cityToBankers.at(selectedCity); - uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)]; - auto locIt = bankerEntryToLocation.find(selectedBankerEntry); - if (locIt != bankerEntryToLocation.end()) - { - std::vector teleportTarget = { locIt->second }; - RandomTeleport(bot, teleportTarget, true); - return; - } - - // Fallback if something went wrong - RandomTeleport(bot, *locs); + RandomTeleport(bot, locs, true); + return; } - else + locs = sTravelMgr.GetTeleportLocations(bot); + if (!locs.empty()) { - RandomTeleport(bot, *locs); + RandomTeleport(bot, locs, false); + return; } } @@ -2246,17 +1787,11 @@ void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot) if (bot->InBattleground()) return; - uint32 level = bot->GetLevel(); - uint8 race = bot->getRace(); - std::vector* locs = nullptr; - if (sPlayerbotAIConfig.enableNewRpgStrategy) - locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level]; - else - locs = &locsPerLevelCache[level]; + std::vector locs = sTravelMgr.GetTeleportLocations(bot); LOG_DEBUG("playerbots", "Random teleporting bot {} for level {} ({} locations available)", bot->GetName().c_str(), - bot->GetLevel(), locs->size()); + bot->GetLevel(), locs.size()); - RandomTeleport(bot, *locs); + RandomTeleport(bot, locs); } void RandomPlayerbotMgr::RandomTeleport(Player* bot) diff --git a/src/Bot/RandomPlayerbotMgr.h b/src/Bot/RandomPlayerbotMgr.h index 94c0a0151..db74f2cbe 100644 --- a/src/Bot/RandomPlayerbotMgr.h +++ b/src/Bot/RandomPlayerbotMgr.h @@ -164,25 +164,8 @@ public: static uint8 GetTeamClassIdx(bool isAlliance, uint8 claz) { return isAlliance * 20 + claz; } void PrepareAddclassCache(); - void PrepareZone2LevelBracket(); - void PrepareTeleportCache(); void Init(); std::map> addclassCache; - std::map> locsPerLevelCache; - std::map> allianceStarterPerLevelCache; - std::map> hordeStarterPerLevelCache; - - struct LevelBracket { - uint32 low; - uint32 high; - bool InsideBracket(uint32 val) { return val >= low && val <= high; } - }; - std::map zone2LevelBracket; - struct BankerLocation { - WorldLocation loc; - uint32 entry; - }; - std::map> bankerLocsPerLevelCache; // Account type management void AssignAccountTypes(); diff --git a/src/Db/FlightMasterCache.cpp b/src/Db/FlightMasterCache.cpp deleted file mode 100644 index effe24993..000000000 --- a/src/Db/FlightMasterCache.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "FlightMasterCache.h" - -void FlightMasterCache::AddHordeFlightMaster(uint32 entry, WorldPosition pos) -{ - hordeFlightMasterCache[entry] = pos; -} - -void FlightMasterCache::AddAllianceFlightMaster(uint32 entry, WorldPosition pos) -{ - allianceFlightMasterCache[entry] = pos; -} - -Creature* FlightMasterCache::GetNearestFlightMaster(Player* bot) -{ - std::map& flightMasterCache = - (bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; - - Creature* nearestFlightMaster = nullptr; - float nearestDistance = std::numeric_limits::max(); - - for (auto const& [entry, pos] : flightMasterCache) - { - if (pos.GetMapId() == bot->GetMapId()) - { - float distance = bot->GetExactDist2dSq(pos); - if (distance < nearestDistance) - { - Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry); - if (flightMaster) - { - nearestDistance = distance; - nearestFlightMaster = flightMaster; - } - } - } - } - - return nearestFlightMaster; -} diff --git a/src/Db/FlightMasterCache.h b/src/Db/FlightMasterCache.h deleted file mode 100644 index 7f8b95310..000000000 --- a/src/Db/FlightMasterCache.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef _PLAYERBOT_FLIGHTMASTER_H -#define _PLAYERBOT_FLIGHTMASTER_H - -#include "Creature.h" -#include "Player.h" -#include "TravelMgr.h" - -class FlightMasterCache -{ -public: - static FlightMasterCache& Instance() - { - static FlightMasterCache instance; - - return instance; - } - - Creature* GetNearestFlightMaster(Player* bot); - void AddHordeFlightMaster(uint32 entry, WorldPosition pos); - void AddAllianceFlightMaster(uint32 entry, WorldPosition pos); - -private: - FlightMasterCache() = default; - ~FlightMasterCache() = default; - - FlightMasterCache(const FlightMasterCache&) = delete; - FlightMasterCache& operator=(const FlightMasterCache&) = delete; - - FlightMasterCache(FlightMasterCache&&) = delete; - FlightMasterCache& operator=(FlightMasterCache&&) = delete; - - std::map allianceFlightMasterCache; - std::map hordeFlightMasterCache; -}; - -#endif diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 703cca0cc..4ddba6d46 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -8,6 +8,10 @@ #include #include +#include "Creature.h" +#include "Log.h" +#include "ObjectAccessor.h" +#include "TravelNode.h" #include "Talentspec.h" #include "ChatHelper.h" #include "MMapFactory.h" @@ -22,6 +26,71 @@ #include "Corpse.h" #include "CellImpl.h" +// Navigation data + +enum class CityId : uint8 +{ + STORMWIND, + IRONFORGE, + DARNASSUS, + EXODAR, + ORGRIMMAR, + UNDERCITY, + THUNDER_BLUFF, + SILVERMOON_CITY, + SHATTRATH_CITY, + DALARAN +}; + +static const std::unordered_map> bankerToCity = { + {2455, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2456, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2457, {CityId::STORMWIND, TEAM_ALLIANCE}}, + {2460, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {2461, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {5099, {CityId::IRONFORGE, TEAM_ALLIANCE}}, + {4155, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4208, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4209, {CityId::DARNASSUS, TEAM_ALLIANCE}}, + {17773, {CityId::EXODAR, TEAM_ALLIANCE}}, {18350, {CityId::EXODAR, TEAM_ALLIANCE}}, {16710, {CityId::EXODAR, TEAM_ALLIANCE}}, + {3320, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3309, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3318, {CityId::ORGRIMMAR, TEAM_HORDE}}, + {4549, {CityId::UNDERCITY, TEAM_HORDE}}, {2459, {CityId::UNDERCITY, TEAM_HORDE}}, {2458, {CityId::UNDERCITY, TEAM_HORDE}}, {4550, {CityId::UNDERCITY, TEAM_HORDE}}, + {2996, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8356, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8357, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, + {17631, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17632, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17633, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, + {16615, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16616, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16617, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, + {19246, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, + {19034, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, + {30604, {CityId::DALARAN, TEAM_NEUTRAL}}, {30605, {CityId::DALARAN, TEAM_NEUTRAL}}, {30607, {CityId::DALARAN, TEAM_NEUTRAL}}, + {28675, {CityId::DALARAN, TEAM_NEUTRAL}}, {28676, {CityId::DALARAN, TEAM_NEUTRAL}}, {28677, {CityId::DALARAN, TEAM_NEUTRAL}} +}; + +static const std::unordered_map> cityToBankers = { + {CityId::STORMWIND, {2455, 2456, 2457}}, + {CityId::IRONFORGE, {2460, 2461, 5099}}, + {CityId::DARNASSUS, {4155, 4208, 4209}}, + {CityId::EXODAR, {17773, 18350, 16710}}, + {CityId::ORGRIMMAR, {3320, 3309, 3318}}, + {CityId::UNDERCITY, {4549, 2459, 2458, 4550}}, + {CityId::THUNDER_BLUFF, {2996, 8356, 8357}}, + {CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}}, + {CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}}, + {CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}} +}; + +static int GetCityWeight(CityId city) +{ + int weight = 0; + switch (city) + { + case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break; + case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break; + case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break; + case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break; + case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break; + case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break; + case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break; + case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break; + case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break; + case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break; + default: weight = 0; break; + } + return weight; +} + WorldPosition::WorldPosition(std::string const str) { std::vector tokens = split(str, '|'); @@ -4287,3 +4356,434 @@ void TravelMgr::printObj(WorldObject* obj, std::string const type) } } } + +void TravelMgr::Init() +{ + if (sPlayerbotAIConfig.enabled) + { + PrepareZone2LevelBracket(); + PrepareDestinationCache(); + } + sTravelNodeMap.InitTaxiGraph(); + LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built."); +} + +Creature* TravelMgr::GetNearestFlightMaster(Player* bot) +{ + std::map& flightMasterCache = + (bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; + + Creature* nearestFlightMaster = nullptr; + float nearestDistance = std::numeric_limits::max(); + + for (auto const& [entry, pos] : flightMasterCache) + { + if (pos.GetMapId() != bot->GetMapId()) + continue; + + float distance = bot->GetExactDist2dSq(pos); + if (distance > nearestDistance) + continue; + + Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry); + if (flightMaster) + { + nearestDistance = distance; + nearestFlightMaster = flightMaster; + } + } + + return nearestFlightMaster; +} + +ObjectGuid TravelMgr::GetNearestFlightMasterGuid(Player* bot) +{ + Creature* nearestFlightMaster = GetNearestFlightMaster(bot); + if (!nearestFlightMaster) + return ObjectGuid::Empty; + + return nearestFlightMaster->GetGUID(); +} + +std::vector> TravelMgr::GetOptimalFlightDestinations(Player* bot) +{ + std::vector> validDestinations; + + Creature* nearestFlightMaster = GetNearestFlightMaster(bot); + if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f) + return validDestinations; + + uint32 fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(), + nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(), + bot->GetTeamId()); + if (!fromNode) + return validDestinations; + std::vector candidateLocations; + if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) + candidateLocations = GetCityLocations(bot); + + std::vector hubLocations = GetTravelHubs(bot); + candidateLocations.insert(candidateLocations.end(), hubLocations.begin(), hubLocations.end()); + + for (auto const& loc : candidateLocations) + { + uint32 candidateNode = sObjectMgr->GetNearestTaxiNode(loc.GetPositionX(), loc.GetPositionY(), + loc.GetPositionZ(), loc.GetMapId(), + bot->GetTeamId()); + if (!candidateNode) + continue; + + std::vector path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode); + if (!path.empty()) + validDestinations.push_back(path); + } + return validDestinations; +} + +const std::vector TravelMgr::GetTeleportLocations(Player* bot) +{ + uint32 level = bot->GetLevel(); + uint8 isAlliance = bot->GetTeamId() == TEAM_ALLIANCE; + if (sPlayerbotAIConfig.enableNewRpgStrategy) + return isAlliance ? allianceHubsPerLevelCache[level] : hordeHubsPerLevelCache[level]; + + return locsPerLevelCache[level]; +} + +const std::vector TravelMgr::GetTravelHubs(Player* bot) +{ + std::vector locs = bot->GetTeamId() == TEAM_ALLIANCE + ? allianceHubsPerLevelCache[bot->GetLevel()] + : hordeHubsPerLevelCache[bot->GetLevel()]; + return locs; +} + +std::vector TravelMgr::GetCityLocations(Player* bot) +{ + uint32 level = bot->GetLevel(); + + std::vector fallbackLocations; + for (auto& bLoc : bankerLocsPerLevelCache[level]) + fallbackLocations.push_back(bLoc.loc); + + if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers) + return fallbackLocations; + + TeamId botTeamId = bot->GetTeamId(); + std::unordered_set validBankerCities; + for (auto& loc : bankerLocsPerLevelCache[level]) + { + auto cityIt = bankerToCity.find(loc.entry); + if (cityIt == bankerToCity.end()) + continue; + + TeamId cityTeamId = cityIt->second.second; + + if (cityTeamId == botTeamId || + (cityTeamId == TEAM_NEUTRAL) + ) + validBankerCities.insert(cityIt->second.first); + } + // Fallback if no valid cities + if (validBankerCities.empty()) + return fallbackLocations; + + // Apply weights to valid cities + std::vector weightedCities; + for (CityId city : validBankerCities) + { + int weight = GetCityWeight(city); + if (weight <= 0) + continue; + + for (int i = 0; i < weight; ++i) + weightedCities.push_back(city); + } + + // Fallback if no valid cities + if (weightedCities.empty()) + return fallbackLocations; + + // Pick a weighted city randomly, then a random banker in that city + CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; + + auto const& bankers = cityToBankers.at(selectedCity); + uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)]; + auto locIt = bankerEntryToLocation.find(selectedBankerEntry); + if (locIt != bankerEntryToLocation.end()) + return { locIt->second }; + // Fallback if something went wrong + return fallbackLocations; +} + +void TravelMgr::PrepareZone2LevelBracket() +{ + // Classic WoW - Low - level zones + zone2LevelBracket[1] = {5, 12}; // Dun Morogh + zone2LevelBracket[12] = {5, 12}; // Elwynn Forest + zone2LevelBracket[14] = {5, 12}; // Durotar + zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades + zone2LevelBracket[141] = {5, 12}; // Teldrassil + zone2LevelBracket[215] = {5, 12}; // Mulgore + zone2LevelBracket[3430] = {5, 12}; // Eversong Woods + zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle + + // Classic WoW - Mid - level zones + zone2LevelBracket[17] = {10, 25}; // Barrens + zone2LevelBracket[38] = {10, 20}; // Loch Modan + zone2LevelBracket[40] = {10, 21}; // Westfall + zone2LevelBracket[130] = {10, 23}; // Silverpine Forest + zone2LevelBracket[148] = {10, 21}; // Darkshore + zone2LevelBracket[3433] = {10, 22}; // Ghostlands + zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle + + // Classic WoW - High - level zones + zone2LevelBracket[10] = {19, 33}; // Deadwind Pass + zone2LevelBracket[11] = {21, 30}; // Wetlands + zone2LevelBracket[44] = {16, 28}; // Redridge Mountains + zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills + zone2LevelBracket[331] = {18, 33}; // Ashenvale + zone2LevelBracket[400] = {24, 36}; // Thousand Needles + zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains + + // Classic WoW - Higher - level zones + zone2LevelBracket[3] = {36, 46}; // Badlands + zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows + zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh + zone2LevelBracket[16] = {45, 52}; // Azshara + zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale + zone2LevelBracket[45] = {30, 42}; // Arathi Highlands + zone2LevelBracket[47] = {42, 51}; // Hinterlands + zone2LevelBracket[51] = {45, 51}; // Searing Gorge + zone2LevelBracket[357] = {40, 52}; // Feralas + zone2LevelBracket[405] = {30, 41}; // Desolace + zone2LevelBracket[440] = {41, 52}; // Tanaris + + // Classic WoW - Top - level zones + zone2LevelBracket[4] = {52, 57}; // Blasted Lands + zone2LevelBracket[28] = {50, 60}; // Western Plaguelands + zone2LevelBracket[46] = {51, 60}; // Burning Steppes + zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands + zone2LevelBracket[361] = {47, 57}; // Felwood + zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater + zone2LevelBracket[618] = {54, 61}; // Winterspring + zone2LevelBracket[1377] = {54, 63}; // Silithus + + // The Burning Crusade - Zones + zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula + zone2LevelBracket[3518] = {64, 70}; // Nagrand + zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest + zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley + zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh + zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains + zone2LevelBracket[3523] = {67, 73}; // Netherstorm + zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas + + // Wrath of the Lich King - Zones + zone2LevelBracket[65] = {71, 77}; // Dragonblight + zone2LevelBracket[66] = {74, 80}; // Zul'Drak + zone2LevelBracket[67] = {77, 80}; // Storm Peaks + zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier + zone2LevelBracket[394] = {72, 78}; // Grizzly Hills + zone2LevelBracket[495] = {68, 74}; // Howling Fjord + zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest + zone2LevelBracket[3537] = {68, 75}; // Borean Tundra + zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin + zone2LevelBracket[4197] = {79, 80}; // Wintergrasp + + // Override with values from config + for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets) + zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second}; +} + +void TravelMgr::PrepareDestinationCache() +{ + uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); + uint32 flightMastersCount = 0; + uint32 innkeepersCount = 0; + uint32 bankerCount = 0; + + LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel); + // Temporary map to group creatures by entry and area + std::map, std::vector> tempLocsCache; + std::map>> tempCreatureCache; + for (auto const& [guid, creatureData] : sObjectMgr->GetAllCreatureData()) + { + CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureData.id1); + if (!creatureTemplate) + continue; + + uint16 mapId = creatureData.mapid; + if (std::find(sPlayerbotAIConfig.randomBotMaps.begin(), sPlayerbotAIConfig.randomBotMaps.end(), mapId) + == sPlayerbotAIConfig.randomBotMaps.end()) + continue; + + float x = creatureData.posX; + float y = creatureData.posY; + float z = creatureData.posZ; + float orient = creatureData.orientation; + uint32 templateEntry = creatureData.id1; + + Map* map = sMapMgr->FindMap(mapId, 0); + if (!map) + continue; + + AreaTableEntry const* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z)); + if (!area) + continue; + + uint32 areaId = area->zone ? area->zone : area->ID; + + // CREATURES + if (creatureTemplate->npcflag == 0 && + creatureTemplate->lootid != 0 && + creatureTemplate->maxlevel - creatureTemplate->minlevel < 3 && + creatureTemplate->Entry != 32820 && creatureTemplate->Entry != 24196 && + creatureTemplate->Entry != 30627 && creatureTemplate->Entry != 30617 && + creatureData.spawntimesecs < 1000 && + creatureTemplate->faction != 11 && creatureTemplate->faction != 71 && + creatureTemplate->faction != 79 && creatureTemplate->faction != 85 && + creatureTemplate->faction != 188 && creatureTemplate->faction != 1575 && + (creatureTemplate->unit_flags & 256) == 0 && + (creatureTemplate->unit_flags & 4096) == 0 && + creatureTemplate->rank == 0) + { + uint32 roundX = (x / 50.0f) * 10.0f; + uint32 roundY = (y / 50.0f) * 10.0f; + uint32 roundZ = (z / 50.0f) * 10.0f; + tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData); + tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z)); + } + // FLIGHT MASTERS + else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER || + creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) && + creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480) + { + FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction); + bool forHorde = !(factionEntry->hostileMask & 4); + bool forAlliance = !(factionEntry->hostileMask & 2); + + if (creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER) + { + WorldPosition pos(mapId, x, y, z, orient); + if (forHorde) + hordeFlightMasterCache[guid] = pos; + + if (forAlliance) + allianceFlightMasterCache[guid] = pos; + flightMastersCount++; + } + else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) + { + if (zone2LevelBracket.find(areaId) == zone2LevelBracket.end()) + continue; + + LevelBracket bracket = zone2LevelBracket[areaId]; + WorldPosition loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); + for (int i = bracket.low; i <= bracket.high; i++) + { + if (forHorde) + hordeHubsPerLevelCache[i].push_back(loc); + + if (forAlliance) + allianceHubsPerLevelCache[i].push_back(loc); + innkeepersCount++; + } + } + } + // === BANKERS === + else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_BANKER && + creatureTemplate->npcflag != 135298 && + creatureTemplate->minlevel != 55 && + creatureTemplate->minlevel != 65 && + creatureTemplate->faction != 35 && creatureTemplate->faction != 474 && + creatureTemplate->faction != 69 && creatureTemplate->faction != 57 && + creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 && + creatureTemplate->Entry != 29282) + { + BankerLocation bLoc; + bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI); + bLoc.entry = templateEntry; + uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; + for (int32 l = 1; l <= maxLevel; l++) + { + // Bots 1-60 go to base game bankers (all have minlevel 30 or 45) + if (l <=60 && level > 45) + continue; + + // Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70) + if ((l >=61 && l <=70) && (level < 60 || level > 70)) + continue; + + // Bots 71+ go to Dalaran bankers (all have minlevel 75) + if ((l >=71) && level != 75) + continue; + + bankerLocsPerLevelCache[(uint8)l].push_back(bLoc); + bankerEntryToLocation[bLoc.entry] = bLoc.loc; + } + bankerCount++; + } + } + + // Process temporary caches + for (auto const& [gridTuple, creatureDataList] : tempLocsCache) + { + if (creatureDataList.size() > 2) + { + CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1); + uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; + for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; + l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) + { + if (l < 1 || l > maxLevel) + continue; + + locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple))); + } + } + } + for (auto const& [entry, areaMap] : tempCreatureCache) + { + for (auto const& [area, locList] : areaMap) + { + if (locList.size() > 3) + continue; + + float totalX = 0, totalY = 0, totalZ = 0; + for (auto const& loc : locList) + { + totalX += loc.GetPositionX(); + totalY += loc.GetPositionY(); + totalZ += loc.GetPositionZ(); + } + float avgX = totalX / locList.size(); + float avgY = totalY / locList.size(); + float avgZ = totalZ / locList.size(); + creatureSpawnsByTemplate[entry].push_back(WorldLocation(locList[0].GetMapId(), avgX, avgY, avgZ, 0)); + } + } + // Add travel hubs based on player start locations + for (uint32 i = 1; i < MAX_RACES; i++) + { + for (uint32 j = 1; j < MAX_CLASSES; j++) + { + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j); + + if (!info) + continue; + + WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation); + + for (int32 l = 1; l <= 5; l++) + { + if ((1 << (i - 1)) & RACEMASK_ALLIANCE) + allianceHubsPerLevelCache[(uint8)l].push_back(pos); + else + hordeHubsPerLevelCache[(uint8)l].push_back(pos); + } + break; + } + } + LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount); +} diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index 1f5f848cd..f300ae636 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -7,6 +7,7 @@ #define _PLAYERBOT_TRAVELMGR_H #include +#include #include #include "AiObject.h" @@ -15,6 +16,7 @@ #include "GridDefines.h" #include "PlayerbotAIConfig.h" +class Creature; class GuidPosition; class ObjectGuid; class Quest; @@ -854,6 +856,16 @@ public: void Clear(); void LoadQuestTravelTable(); + // Navigation + void Init(); + Creature* GetNearestFlightMaster(Player* bot); + ObjectGuid GetNearestFlightMasterGuid(Player* bot); + std::vector> GetOptimalFlightDestinations(Player* bot); + const std::vector GetTeleportLocations(Player* bot); + const std::vector GetTravelHubs(Player* bot); + std::vector GetCityLocations(Player* bot); + const std::vector& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; } + template void weighted_shuffle(D first, D last, W first_weight, W last_weight, URBG&& g) { @@ -943,6 +955,37 @@ private: TravelMgr(TravelMgr&&) = delete; TravelMgr& operator=(TravelMgr&&) = delete; + + // Navigation initialization + void PrepareZone2LevelBracket(); + void PrepareDestinationCache(); + + // Internal types + struct LevelBracket + { + uint32 low; + uint32 high; + bool InsideBracket(uint32 val) const { return val >= low && val <= high; } + }; + + struct BankerLocation + { + WorldLocation loc; + uint32 entry; + }; + + // Navigation caches + std::map allianceFlightMasterCache; + std::map hordeFlightMasterCache; + std::map> allianceHubsPerLevelCache; + std::map> hordeHubsPerLevelCache; + std::map> bankerLocsPerLevelCache; + std::unordered_map bankerEntryToLocation; + std::map> locsPerLevelCache; + std::unordered_map> creatureSpawnsByTemplate; + std::map zone2LevelBracket; }; +#define sTravelMgr TravelMgr::instance() + #endif diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index 3e304677f..3b4996e97 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -7,6 +7,7 @@ #include #include +#include #include "BudgetValues.h" #include "PathGenerator.h" @@ -2447,3 +2448,127 @@ WorldPosition TravelNodeMap::getMapOffset(uint32 mapId) return WorldPosition(mapId, 0, 0, 0, 0); } + +// ============================================================ +// TravelNodeMap taxi graph (BFS-based flight path lookup) +// ============================================================ + +void TravelNodeMap::InitTaxiGraph() +{ + BuildTaxiGraph(); + ComputeAllPaths(); +} + +std::vector TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode) +{ + if (fromNode == toNode) + return {}; + + TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode); + TaxiNodesEntry const* endNode = sTaxiNodesStore.LookupEntry(toNode); + + if (!startNode || !endNode || startNode->map_id != endNode->map_id) + return {}; + + auto cacheItr = taxiPathCache.find(fromNode); + if (cacheItr == taxiPathCache.end()) + return {}; + + auto toNodeItr = cacheItr->second.find(toNode); + if (toNodeItr == cacheItr->second.end()) + return {}; + + return toNodeItr->second; +} + +void TravelNodeMap::BuildTaxiGraph() +{ + taxiGraph.clear(); + std::unordered_map> tempGraph; + for (uint32 i = 0; i < sTaxiPathStore.GetNumRows(); ++i) + { + TaxiPathEntry const* path = sTaxiPathStore.LookupEntry(i); + if (!path) + continue; + + if (path->to == 0 || path->to == uint32(-1)) + continue; + + tempGraph[path->from].insert(path->to); + tempGraph[path->to].insert(path->from); + } + for (auto const& [node, neighbors] : tempGraph) + taxiGraph[node] = std::vector(neighbors.begin(), neighbors.end()); +} + +void TravelNodeMap::ComputeAllPaths() +{ + std::set allNodes; + for (auto const& [source, neighbors] : taxiGraph) + allNodes.insert(source); + + for (uint32 source : allNodes) + { + auto parentMap = BFS(source); + + for (uint32 target : allNodes) + { + if (source == target) + continue; + + auto path = BuildPath(source, target, parentMap); + if (!path.empty()) + taxiPathCache[source][target] = path; + } + } +} + +std::unordered_map TravelNodeMap::BFS(uint32 fromNode) +{ + std::queue workQueue; + std::unordered_set visited; + std::unordered_map parentMap; + + workQueue.push(fromNode); + visited.insert(fromNode); + parentMap[fromNode] = 0; + + while (!workQueue.empty()) + { + uint32 current = workQueue.front(); + workQueue.pop(); + + for (uint32 next : taxiGraph.at(current)) + { + if (visited.count(next)) + continue; + + visited.insert(next); + parentMap[next] = current; + workQueue.push(next); + } + } + return parentMap; +} + +std::vector TravelNodeMap::BuildPath(uint32 fromNode, uint32 toNode, + const std::unordered_map& parentMap) +{ + if (!parentMap.count(toNode)) + return {}; // unreachable + + std::vector path; + uint32 current = toNode; + while (current != fromNode) + { + path.push_back(current); + auto it = parentMap.find(current); + if (it == parentMap.end() || it->second == 0) + break; + current = it->second; + } + + path.push_back(fromNode); + std::reverse(path.begin(), path.end()); + return path; +} diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index 4dc235721..9e05e2490 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -580,6 +580,10 @@ public: void calcMapOffset(); WorldPosition getMapOffset(uint32 mapId); + // Taxi graph (BFS-based path lookup between taxi nodes) + void InitTaxiGraph(); + std::vector FindTaxiPath(uint32 fromNode, uint32 toNode); + std::shared_timed_mutex m_nMapMtx; std::unordered_map> teleportNodes; @@ -593,6 +597,16 @@ private: TravelNodeMap(TravelNodeMap&&) = delete; TravelNodeMap& operator=(TravelNodeMap&&) = delete; + // Taxi graph internals + void BuildTaxiGraph(); + void ComputeAllPaths(); + std::unordered_map BFS(uint32 startNode); + std::vector BuildPath(uint32 fromNode, uint32 toNode, + const std::unordered_map& parentMap); + + std::unordered_map> taxiGraph; + std::map>> taxiPathCache; + std::vector m_nodes; std::vector> mapOffsets; diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index fb0ffad4d..6a8d60129 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -15,6 +15,7 @@ #include "RandomPlayerbotFactory.h" #include "RandomPlayerbotMgr.h" #include "Talentspec.h" +#include "TravelMgr.h" template void LoadList(std::string const value, T& list) @@ -691,6 +692,7 @@ bool PlayerbotAIConfig::Initialize() { PlayerbotDungeonRepository::instance().LoadDungeonSuggestions(); } + sTravelMgr.Init(); excludedHunterPetFamilies.clear(); LoadList>(sConfigMgr->GetOption("AiPlayerbot.ExcludedHunterPetFamilies", ""), excludedHunterPetFamilies);