From 957eca0263232001c02476ea2b61576a7dceed9f Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:39:53 -0700 Subject: [PATCH] Feat. Enable multi node flying, and refactor into travel manager (#2156) # Pull Request Feature - Enable multi node flying for bots - Bots currently only do node to node flying. This PR makes it so they can connect multiple noted. -- This is enabled by sending a vector containing the node sequence instead of a single destination node -- To minimize the run-time cost of searching for available nodes and connection, a cache of all possible connections is prepared at start up using a BFS search algorithm. Refactor - Move all world destination logic (cities, banks, inns) to existing Travel manager - Eliminate flightmastercache and integrate to new manager - replace SQLs calls with in-memory data search by core - Add in new map that stores creature areas by template. Clean up - Move other rpg files to related folder. (Next steps) The selection for where bots fly to should be smarter than it is. Instead of trying to determine where a bot can go, it should first decide where it should go, and then identify the correct way to get there. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it ## Complexity & Impact Does this change add new decision branches? - - [x[ No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) The call itself is fairly infrequent, and although now there are a greater number of paths available for the bots, I dont think it would be significant. ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Gemini first suggested the use of a BFS algorithm. This was rewritten by me to actually work as intended. Verification by additional logging not present in final code. Claude code converted the SQL filtering to the atrocious if statements found in PrepareDestinationCache, but after verifying them it works. If there are better ways to do this Im open to it. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- src/Ai/World/Rpg/Action/NewRpgAction.cpp | 5 +- src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 82 +-- src/Ai/World/Rpg/Action/NewRpgBaseAction.h | 2 +- .../Rpg/Action}/RpgAction.cpp | 0 .../Actions => World/Rpg/Action}/RpgAction.h | 0 .../Rpg/Action}/RpgSubActions.cpp | 0 .../Rpg/Action}/RpgSubActions.h | 0 src/Ai/World/Rpg/NewRpgInfo.cpp | 9 +- src/Ai/World/Rpg/NewRpgInfo.h | 5 +- .../{Base/Actions => World/Rpg}/RpgValues.h | 0 .../Rpg}/Strategy/RpgStrategy.cpp | 0 .../Rpg}/Strategy/RpgStrategy.h | 0 src/Bot/PlayerbotAI.cpp | 2 +- src/Bot/PlayerbotAI.h | 2 +- src/Bot/RandomPlayerbotMgr.cpp | 489 +---------------- src/Bot/RandomPlayerbotMgr.h | 17 - src/Db/FlightMasterCache.cpp | 39 -- src/Db/FlightMasterCache.h | 36 -- src/Mgr/Travel/TravelMgr.cpp | 500 ++++++++++++++++++ src/Mgr/Travel/TravelMgr.h | 43 ++ src/Mgr/Travel/TravelNode.cpp | 125 +++++ src/Mgr/Travel/TravelNode.h | 14 + src/PlayerbotAIConfig.cpp | 2 + 23 files changed, 721 insertions(+), 651 deletions(-) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgAction.cpp (100%) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgAction.h (100%) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgSubActions.cpp (100%) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgSubActions.h (100%) rename src/Ai/{Base/Actions => World/Rpg}/RpgValues.h (100%) rename src/Ai/{Base => World/Rpg}/Strategy/RpgStrategy.cpp (100%) rename src/Ai/{Base => World/Rpg}/Strategy/RpgStrategy.h (100%) delete mode 100644 src/Db/FlightMasterCache.cpp delete mode 100644 src/Db/FlightMasterCache.h 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);