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.
This commit is contained in:
Keleborn 2026-03-20 12:39:53 -07:00 committed by GitHub
parent 5c63aacd60
commit 957eca0263
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 721 additions and 651 deletions

View File

@ -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<uint32> nodes = {data.fromNode, data.toNode};
std::vector<uint32> 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;

View File

@ -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<WorldLocation>& locs = sRandomPlayerbotMgr.locsPerLevelCache[bot->GetLevel()];
const std::vector<WorldLocation>& 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<WorldLocation>& locs = IsAlliance(bot->getRace())
? sRandomPlayerbotMgr.allianceStarterPerLevelCache[bot->GetLevel()]
: sRandomPlayerbotMgr.hordeStarterPerLevelCache[bot->GetLevel()];
const std::vector<WorldLocation> 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<uint32>& 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<std::vector<uint32>> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot);
if (availablePaths.empty())
return false;
std::vector<uint32> 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<NewRpgStatus> candidateSta
case RPG_TRAVEL_FLIGHT:
{
ObjectGuid flightMaster;
uint32 fromNode, toNode;
if (SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode))
std::vector<uint32> 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<uint32> path;
return SelectRandomFlightTaxiNode(flightMaster, path);
}
default:
return false;

View File

@ -54,7 +54,7 @@ protected:
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& 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<uint32>& path);
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status);

View File

@ -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<uint32> 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

View File

@ -50,8 +50,7 @@ struct NewRpgInfo
struct TravelFlight
{
ObjectGuid fromFlightMaster{};
uint32 fromNode{0};
uint32 toNode{0};
std::vector<uint32> 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<uint32> path);
void ChangeToRest();
void ChangeToIdle();
bool CanChangeTo(NewRpgStatus status);

View File

@ -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;

View File

@ -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)
{

View File

@ -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<uint16, std::pair<CityId, FactionId>> 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<CityId, std::vector<uint16>> 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<uint32, WorldLocation> bankerEntryToLocation;
void PrintStatsThread() { sRandomPlayerbotMgr.PrintStats(); }
void activatePrintStatsThread()
@ -1718,7 +1673,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
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<WorldLocation>&
// 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<uint16>();
float x = fields[1].Get<float>();
float y = fields[2].Get<float>();
float z = fields[3].Get<float>();
uint32 min_level = fields[4].Get<uint32>();
uint32 max_level = fields[5].Get<uint32>();
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<uint16>();
float x = fields[1].Get<float>();
float y = fields[2].Get<float>();
float z = fields[3].Get<float>();
float orient = fields[4].Get<float>();
uint32 faction = fields[5].Get<uint32>();
uint32 tEntry = fields[6].Get<uint32>();
uint32 tNpcflag = fields[7].Get<uint32>();
uint32 guid = fields[8].Get<uint32>();
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<uint16>();
float x = fields[1].Get<float>();
float y = fields[2].Get<float>();
float z = fields[3].Get<float>();
float orient = fields[4].Get<float>();
uint32 level = fields[5].Get<uint32>();
uint32 entry = fields[6].Get<uint32>();
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,104 +1768,18 @@ void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot)
if (bot->InBattleground())
return;
uint32 level = bot->GetLevel();
uint8 race = bot->getRace();
std::vector<WorldLocation>* 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<WorldLocation> locs = sTravelMgr.GetCityLocations(bot);
if (!locs.empty())
{
std::vector<WorldLocation> fallbackLocs;
for (auto& bLoc : bankerLocsPerLevelCache[level])
fallbackLocs.push_back(bLoc.loc);
if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers)
{
RandomTeleport(bot, fallbackLocs, true);
RandomTeleport(bot, locs, true);
return;
}
// Collect valid cities based on bot faction.
std::unordered_set<CityId> validBankerCities;
for (auto& loc : bankerLocsPerLevelCache[level])
locs = sTravelMgr.GetTeleportLocations(bot);
if (!locs.empty())
{
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);
RandomTeleport(bot, locs, false);
return;
}
// Apply weights to valid cities
std::vector<CityId> 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<WorldLocation> teleportTarget = { locIt->second };
RandomTeleport(bot, teleportTarget, true);
return;
}
// Fallback if something went wrong
RandomTeleport(bot, *locs);
}
else
{
RandomTeleport(bot, *locs);
}
}
void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot)
@ -2246,17 +1787,11 @@ void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot)
if (bot->InBattleground())
return;
uint32 level = bot->GetLevel();
uint8 race = bot->getRace();
std::vector<WorldLocation>* locs = nullptr;
if (sPlayerbotAIConfig.enableNewRpgStrategy)
locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level];
else
locs = &locsPerLevelCache[level];
std::vector<WorldLocation> 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)

View File

@ -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<uint8, std::unordered_set<ObjectGuid>> addclassCache;
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> allianceStarterPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> hordeStarterPerLevelCache;
struct LevelBracket {
uint32 low;
uint32 high;
bool InsideBracket(uint32 val) { return val >= low && val <= high; }
};
std::map<uint32, LevelBracket> zone2LevelBracket;
struct BankerLocation {
WorldLocation loc;
uint32 entry;
};
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;
// Account type management
void AssignAccountTypes();

View File

@ -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<uint32, WorldPosition>& flightMasterCache =
(bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
Creature* nearestFlightMaster = nullptr;
float nearestDistance = std::numeric_limits<float>::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;
}

View File

@ -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<uint32, WorldPosition> allianceFlightMasterCache;
std::map<uint32, WorldPosition> hordeFlightMasterCache;
};
#endif

View File

@ -8,6 +8,10 @@
#include <iomanip>
#include <numeric>
#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<uint16, std::pair<CityId, TeamId>> 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<CityId, std::vector<uint16>> 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<std::string> 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<uint32, WorldPosition>& flightMasterCache =
(bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
Creature* nearestFlightMaster = nullptr;
float nearestDistance = std::numeric_limits<float>::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<std::vector<uint32>> TravelMgr::GetOptimalFlightDestinations(Player* bot)
{
std::vector<std::vector<uint32>> 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<WorldLocation> candidateLocations;
if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
candidateLocations = GetCityLocations(bot);
std::vector<WorldLocation> 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<uint32> path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode);
if (!path.empty())
validDestinations.push_back(path);
}
return validDestinations;
}
const std::vector<WorldLocation> 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<WorldLocation> TravelMgr::GetTravelHubs(Player* bot)
{
std::vector<WorldLocation> locs = bot->GetTeamId() == TEAM_ALLIANCE
? allianceHubsPerLevelCache[bot->GetLevel()]
: hordeHubsPerLevelCache[bot->GetLevel()];
return locs;
}
std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
{
uint32 level = bot->GetLevel();
std::vector<WorldLocation> fallbackLocations;
for (auto& bLoc : bankerLocsPerLevelCache[level])
fallbackLocations.push_back(bLoc.loc);
if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers)
return fallbackLocations;
TeamId botTeamId = bot->GetTeamId();
std::unordered_set<CityId> 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<CityId> 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::tuple<uint16, int32, int32, int32>, std::vector<CreatureData>> tempLocsCache;
std::map<uint32, std::map<uint32, std::vector<WorldLocation>>> 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);
}

View File

@ -7,6 +7,7 @@
#define _PLAYERBOT_TRAVELMGR_H
#include <boost/functional/hash.hpp>
#include <map>
#include <random>
#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<std::vector<uint32>> GetOptimalFlightDestinations(Player* bot);
const std::vector<WorldLocation> GetTeleportLocations(Player* bot);
const std::vector<WorldLocation> GetTravelHubs(Player* bot);
std::vector<WorldLocation> GetCityLocations(Player* bot);
const std::vector<WorldLocation>& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; }
template <class D, class W, class URBG>
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<uint32, WorldPosition> allianceFlightMasterCache;
std::map<uint32, WorldPosition> hordeFlightMasterCache;
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;
std::map<uint32, LevelBracket> zone2LevelBracket;
};
#define sTravelMgr TravelMgr::instance()
#endif

View File

@ -7,6 +7,7 @@
#include <iomanip>
#include <regex>
#include <unordered_set>
#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<uint32> 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<uint32, std::unordered_set<uint32>> 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<uint32>(neighbors.begin(), neighbors.end());
}
void TravelNodeMap::ComputeAllPaths()
{
std::set<uint32> 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<uint32, uint32> TravelNodeMap::BFS(uint32 fromNode)
{
std::queue<uint32> workQueue;
std::unordered_set<uint32> visited;
std::unordered_map<uint32, uint32> 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<uint32> TravelNodeMap::BuildPath(uint32 fromNode, uint32 toNode,
const std::unordered_map<uint32, uint32>& parentMap)
{
if (!parentMap.count(toNode))
return {}; // unreachable
std::vector<uint32> 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;
}

View File

@ -580,6 +580,10 @@ public:
void calcMapOffset();
WorldPosition getMapOffset(uint32 mapId);
// Taxi graph (BFS-based path lookup between taxi nodes)
void InitTaxiGraph();
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
std::shared_timed_mutex m_nMapMtx;
std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes;
@ -593,6 +597,16 @@ private:
TravelNodeMap(TravelNodeMap&&) = delete;
TravelNodeMap& operator=(TravelNodeMap&&) = delete;
// Taxi graph internals
void BuildTaxiGraph();
void ComputeAllPaths();
std::unordered_map<uint32, uint32> BFS(uint32 startNode);
std::vector<uint32> BuildPath(uint32 fromNode, uint32 toNode,
const std::unordered_map<uint32, uint32>& parentMap);
std::unordered_map<uint32, std::vector<uint32>> taxiGraph;
std::map<uint32, std::map<uint32, std::vector<uint32>>> taxiPathCache;
std::vector<TravelNode*> m_nodes;
std::vector<std::pair<uint32, WorldPosition>> mapOffsets;

View File

@ -15,6 +15,7 @@
#include "RandomPlayerbotFactory.h"
#include "RandomPlayerbotMgr.h"
#include "Talentspec.h"
#include "TravelMgr.h"
template <class T>
void LoadList(std::string const value, T& list)
@ -691,6 +692,7 @@ bool PlayerbotAIConfig::Initialize()
{
PlayerbotDungeonRepository::instance().LoadDungeonSuggestions();
}
sTravelMgr.Init();
excludedHunterPetFamilies.clear();
LoadList<std::vector<uint32>>(sConfigMgr->GetOption<std::string>("AiPlayerbot.ExcludedHunterPetFamilies", ""), excludedHunterPetFamilies);