mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
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:
parent
5c63aacd60
commit
957eca0263
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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,103 +1768,17 @@ 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);
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect valid cities based on bot faction.
|
||||
std::unordered_set<CityId> 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<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);
|
||||
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<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)
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user