feat(Core/Travel): Enable travel node system for RPG pathfinding (#2312)

This commit is contained in:
bash 2026-05-02 00:50:18 +02:00
parent 714bb6bca3
commit c87297ff0a
22 changed files with 1665 additions and 950 deletions

View File

@ -1036,6 +1036,12 @@ AiPlayerbot.RestrictedHealerDPSMaps = "33,34,36,43,47,48,70,90,109,129,209,229,2
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.EnableNewRpgStrategy = 1 AiPlayerbot.EnableNewRpgStrategy = 1
# Use pre-computed travel node paths for long-distance movement (>300 yards).
# When enabled, bots use the travel node graph (A*, flight paths, transports)
# instead of repeated mmap hops. Experimental.
# Default: 0 (disabled)
AiPlayerbot.EnableTravelNodes = 0
# Control probability weights for RPG status of bots. Takes effect only when the status meets its premise. # Control probability weights for RPG status of bots. Takes effect only when the status meets its premise.
# Sum of weights need not be 100. Set to 0 to disable the status. # Sum of weights need not be 100. Set to 0 to disable the status.
# #

View File

@ -6,10 +6,10 @@
#include "CheckValuesAction.h" #include "CheckValuesAction.h"
#include "Event.h" #include "Event.h"
#include "ObjectGuid.h"
#include "ServerFacade.h" #include "ServerFacade.h"
#include "PlayerbotAI.h" #include "PlayerbotAI.h"
#include "TravelNode.h"
#include "AiObjectContext.h" #include "AiObjectContext.h"
CheckValuesAction::CheckValuesAction(PlayerbotAI* botAI) : Action(botAI, "check values") {} CheckValuesAction::CheckValuesAction(PlayerbotAI* botAI) : Action(botAI, "check values") {}
@ -21,11 +21,6 @@ bool CheckValuesAction::Execute(Event /*event*/)
botAI->Ping(bot->GetPositionX(), bot->GetPositionY()); botAI->Ping(bot->GetPositionX(), bot->GetPositionY());
} }
if (botAI->HasStrategy("map", BOT_STATE_NON_COMBAT) || botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT))
{
TravelNodeMap::instance().manageNodes(bot, botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT));
}
GuidVector possible_targets = *context->GetValue<GuidVector>("possible targets"); GuidVector possible_targets = *context->GetValue<GuidVector>("possible targets");
GuidVector all_targets = *context->GetValue<GuidVector>("all targets"); GuidVector all_targets = *context->GetValue<GuidVector>("all targets");
GuidVector npcs = *context->GetValue<GuidVector>("nearest npcs"); GuidVector npcs = *context->GetValue<GuidVector>("nearest npcs");

View File

@ -76,7 +76,7 @@ bool DebugAction::Execute(Event event)
return false; return false;
std::vector<WorldPosition> beginPath, endPath; std::vector<WorldPosition> beginPath, endPath;
TravelNodeRoute route = TravelNodeMap::instance().getRoute(botPos, *points.front(), beginPath, bot); TravelNodeRoute route = TravelNodeMap::instance().FindRouteNearestNodes(botPos, *points.front(), beginPath, bot);
std::ostringstream out; std::ostringstream out;
out << "Traveling to " << dest->getTitle() << ": "; out << "Traveling to " << dest->getTitle() << ": ";
@ -196,18 +196,18 @@ bool DebugAction::Execute(Event event)
{ {
WorldPosition pos(bot); WorldPosition pos(bot);
std::string const name = "USER:" + text.substr(9); std::string suffix = text.size() > 9 ? text.substr(9) : pos.getAreaName();
std::string const name = "USER:" + suffix;
/* TravelNode* startNode = */ TravelNodeMap::instance().addNode(pos, name, false, false); // startNode not used, but addNode as side effect, fragment marked for removal. {
std::lock_guard<std::shared_timed_mutex> lock(TravelNodeMap::instance().m_nMapMtx);
TravelNodeMap::instance().addNode(pos, name, false, true);
for (auto& endNode : TravelNodeMap::instance().getNodes(pos, 2000)) for (auto& endNode : TravelNodeMap::instance().getNodes(pos, 2000))
{
endNode->setLinked(false); endNode->setLinked(false);
} }
botAI->TellMasterNoFacing("Node " + name + " created."); botAI->TellMasterNoFacing("Node " + name + " created. Use console command '.playerbots travel generatenode' to connect nodes.");
TravelNodeMap::instance().setHasToGen();
return true; return true;
} }
@ -223,14 +223,15 @@ bool DebugAction::Execute(Event event)
if (startNode->isImportant()) if (startNode->isImportant())
{ {
botAI->TellMasterNoFacing("Node can not be removed."); botAI->TellMasterNoFacing("Node can not be removed.");
return true;
} }
TravelNodeMap::instance().m_nMapMtx.lock(); {
std::lock_guard<std::shared_timed_mutex> lock(TravelNodeMap::instance().m_nMapMtx);
TravelNodeMap::instance().removeNode(startNode); TravelNodeMap::instance().removeNode(startNode);
botAI->TellMasterNoFacing("Node removed."); }
TravelNodeMap::instance().m_nMapMtx.unlock();
TravelNodeMap::instance().setHasToGen(); botAI->TellMasterNoFacing("Node removed. Use console command '.playerbots travel generatenode' to finalize nodes.");
return true; return true;
} }
@ -247,15 +248,17 @@ bool DebugAction::Execute(Event event)
node->removeLinkTo(path.first, true); node->removeLinkTo(path.first, true);
return true; return true;
} }
else if (text.find("gen node") != std::string::npos) else if (text.find("gen node") != std::string::npos ||
text.find("gen path") != std::string::npos)
{ {
// Pathfinder // Disabled: generateAll() touches Map / grid / mmap state that is only
TravelNodeMap::instance().generateNodes(); // safe to mutate on the world thread. Running it from a detached worker
return true; // (or from a bot tick on a MapUpdater thread) races with world updates
} // and freezes the server. Use the console command instead, which runs
else if (text.find("gen path") != std::string::npos) // synchronously on the world thread:
{ // .playerbots travel generatenode
TravelNodeMap::instance().generatePaths(); botAI->TellMasterNoFacing(
"Disabled in chat. Run '.playerbots travel generatenode' from the server console.");
return true; return true;
} }
else if (text.find("crop path") != std::string::npos) else if (text.find("crop path") != std::string::npos)
@ -275,7 +278,7 @@ bool DebugAction::Execute(Event event)
[] []
{ {
TravelNodeMap::instance().removeNodes(); TravelNodeMap::instance().removeNodes();
TravelNodeMap::instance().loadNodeStore(); TravelNodeMap::instance().LoadNodeStore();
}); });
t.detach(); t.detach();
@ -297,7 +300,7 @@ bool DebugAction::Execute(Event event)
// uint32 time = 60 * IN_MILLISECONDS; //not used, line marked for removal. // uint32 time = 60 * IN_MILLISECONDS; //not used, line marked for removal.
std::vector<WorldPosition> ppath = l.second->getPath(); std::vector<WorldPosition> ppath = l.second->GetPath();
for (auto p : ppath) for (auto p : ppath)
{ {

View File

@ -19,83 +19,8 @@
#include "Transport.h" #include "Transport.h"
#include "Map.h" #include "Map.h"
namespace // Transport helpers (GetTransportForPosTolerant, FindBoardingPointOnTransport,
{ // BoardTransport) are now on MovementAction — inherited by FollowAction.
Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z)
{
if (!map || !ref)
return nullptr;
std::array<float, 4> const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f };
for (float const pz : probes)
{
if (Transport* t = map->GetTransportForPos(phaseMask, x, y, pz, ref))
return t;
}
return nullptr;
}
// Attempts to find a point on the leader's transport that is closer to the bot,
// by probing along the segment from master -> bot and returning the last point
// that is still detected as being on the expected transport.
bool FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref,
float masterX, float masterY, float masterZ,
float botX, float botY, float botZ,
float& outX, float& outY, float& outZ)
{
if (!map || !expectedTransport || !ref)
return false;
uint32 const phaseMask = ref->GetPhaseMask();
// Ensure master is actually detected on that transport (tolerant).
if (GetTransportForPosTolerant(map, ref, phaseMask, masterX, masterY, masterZ) != expectedTransport)
return false;
// The raycast in GetTransportForPos starts at (z + 2). Probe with a safe Z.
float const probeZ = std::max(masterZ, botZ);
// Adaptive step count: small platforms need tighter sampling.
float const dx2 = botX - masterX;
float const dy2 = botY - masterY;
float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2);
int32 const steps = std::clamp(static_cast<int32>(dist2d / 0.75f), 10, 28);
float const dx = (botX - masterX) / static_cast<float>(steps);
float const dy = (botY - masterY) / static_cast<float>(steps);
// Master must actually be on the expected transport for this to work.
if (map->GetTransportForPos(ref->GetPhaseMask(), masterX, masterY, probeZ, ref) != expectedTransport)
return false;
float lastX = masterX;
float lastY = masterY;
bool found = false;
for (int32 i = 1; i <= steps; ++i)
{
float const px = masterX + dx * i;
float const py = masterY + dy * i;
Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ);
if (t != expectedTransport)
break;
lastX = px;
lastY = py;
found = true;
}
if (!found)
return false;
outX = lastX;
outY = lastY;
outZ = masterZ; // keep deck-level Z to encourage stepping onto the platform/boat
return true;
}
}
bool FollowAction::Execute(Event /*event*/) bool FollowAction::Execute(Event /*event*/)
{ {

View File

@ -11,6 +11,7 @@
#include <string> #include <string>
#include "Corpse.h" #include "Corpse.h"
#include "DBCStores.h"
#include "Event.h" #include "Event.h"
#include "FleeManager.h" #include "FleeManager.h"
#include "G3D/Vector3.h" #include "G3D/Vector3.h"
@ -19,7 +20,9 @@
#include "LootObjectStack.h" #include "LootObjectStack.h"
#include "Map.h" #include "Map.h"
#include "MotionMaster.h" #include "MotionMaster.h"
#include "MoveSpline.h"
#include "MoveSplineInitArgs.h" #include "MoveSplineInitArgs.h"
#include "TravelNode.h"
#include "MovementGenerator.h" #include "MovementGenerator.h"
#include "ObjectDefines.h" #include "ObjectDefines.h"
#include "ObjectGuid.h" #include "ObjectGuid.h"
@ -36,6 +39,7 @@
#include "SpellInfo.h" #include "SpellInfo.h"
#include "Stances.h" #include "Stances.h"
#include "Timer.h" #include "Timer.h"
#include "Transport.h"
#include "Unit.h" #include "Unit.h"
#include "Vehicle.h" #include "Vehicle.h"
#include "WaypointMovementGenerator.h" #include "WaypointMovementGenerator.h"
@ -128,10 +132,8 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged)
float z = target->GetPositionZ(); float z = target->GetPositionZ();
// Use standard PathGenerator to find a route. // Use standard PathGenerator to find a route.
PathGenerator path(bot); PathResult path = GeneratePath(x, y, z, DEFAULT_PATH_ACCEPT_MASK, false);
path.CalculatePath(x, y, z, false); if (!path.reachable)
PathType type = path.GetPathType();
if (type != PATHFIND_NORMAL && type != PATHFIND_INCOMPLETE)
return false; return false;
if (!ranged) if (!ranged)
@ -140,9 +142,9 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged)
float dist = FLT_MAX; float dist = FLT_MAX;
PositionInfo dest; PositionInfo dest;
if (!path.GetPath().empty()) if (!path.points.empty())
{ {
for (auto& point : path.GetPath()) for (auto& point : path.points)
{ {
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT)) if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
CreateWp(bot, point.x, point.y, point.z, 0.0, 2334); CreateWp(bot, point.x, point.y, point.z, 0.0, 2334);
@ -1719,6 +1721,19 @@ bool MovementAction::MoveInside(uint32 mapId, float x, float y, float z, float d
// return current_z; // return current_z;
// } // }
PathResult MovementAction::GeneratePath(float x, float y, float z, uint32 acceptMask, bool forceDestination)
{
PathResult result;
PathGenerator gen(bot);
gen.CalculatePath(x, y, z, forceDestination);
result.pathType = gen.GetPathType();
result.reachable = !(result.pathType & (~acceptMask));
result.points = gen.GetPath();
result.actualEnd = gen.GetActualEndPosition();
result.end = gen.GetEndPosition();
return result;
}
const Movement::PointsArray MovementAction::SearchForBestPath(float x, float y, float z, float& modified_z, const Movement::PointsArray MovementAction::SearchForBestPath(float x, float y, float z, float& modified_z,
int maxSearchCount, bool normal_only, float step) int maxSearchCount, bool normal_only, float step)
{ {
@ -2959,4 +2974,571 @@ bool MoveAwayFromPlayerWithDebuffAction::Execute(Event /*event*/)
return false; return false;
} }
bool MoveAwayFromPlayerWithDebuffAction::isPossible() { return bot->CanFreeMove(); } bool MoveAwayFromPlayerWithDebuffAction::isPossible()
{
return bot->CanFreeMove();
}
bool MovementAction::CheckSplineProgress(TravelPlan& state)
{
if (!state.splineActive)
return false;
// walkPoints may have been cleared by a map transfer or external reset
// while the spline was still flagged active; bail out safely.
if (state.walkPoints.empty())
{
state.splineActive = false;
return false;
}
if (bot->movespline->Finalized())
{
G3D::Vector3 const& endPt = state.walkPoints.back();
float distToEnd = bot->GetExactDist(endPt.x, endPt.y, endPt.z);
if (distToEnd < 10.0f)
{
state.splineActive = false;
state.walkPoints.clear();
return true; // Arrived
}
// Spline finalized short of target — interrupted (combat/knockback/etc).
// Caller will re-launch.
state.splineActive = false;
return false;
}
// Stuck detection
if (state.splineStartTime &&
GetMSTimeDiffToNow(state.splineStartTime) > state.expectedDuration * 2 + (30 * IN_MILLISECONDS))
{
G3D::Vector3 const& endPt = state.walkPoints.back();
botAI->TeleportTo(WorldLocation(bot->GetMapId(), endPt.x, endPt.y, endPt.z));
state.splineActive = false;
state.walkPoints.clear();
return true;
}
return false; // Still moving
}
bool MovementAction::LaunchWalkSpline(TravelPlan& state)
{
if (state.walkPoints.size() < 2)
{
state.walkPoints.clear();
return false;
}
// Trim past any stored points the bot has already moved past — useful
// when a spline is interrupted (combat, knockback, mid-spline reissue)
// and we re-launch from a position later in the route.
G3D::Vector3 botPos(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
float closestDist = FLT_MAX;
size_t closestIdx = 0;
for (size_t i = 0; i < state.walkPoints.size(); ++i)
{
float distance = (state.walkPoints[i] - botPos).squaredLength();
if (distance < closestDist)
{
closestDist = distance;
closestIdx = i;
}
}
if (closestIdx > 0)
state.walkPoints.erase(state.walkPoints.begin(), state.walkPoints.begin() + closestIdx);
if (state.walkPoints.size() < 2)
{
state.walkPoints.clear();
return true;
}
// Mount up
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
botAI->DoSpecificAction("check mount state", Event(), true);
float totalDist = 0;
for (size_t i = 1; i < state.walkPoints.size(); ++i)
totalDist += (state.walkPoints[i] - state.walkPoints[i - 1]).length();
float speed = bot->GetSpeed(MOVE_RUN);
state.expectedDuration = static_cast<uint32>((totalDist / speed) * IN_MILLISECONDS);
bot->GetMotionMaster()->MoveSplinePath(&state.walkPoints, FORCED_MOVEMENT_RUN);
state.splineStartTime = getMSTime();
state.splineActive = true;
return false; // Walking
}
bool MovementAction::MoveToSpline(TravelPlan& state, WorldPosition target)
{
if (!IsMovingAllowed())
return false;
// Generate path
state.walkPoints.clear();
PathResult path = GeneratePath(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
for (auto const& pt : path.points)
state.walkPoints.push_back(G3D::Vector3(pt.x, pt.y, pt.z));
if (state.walkPoints.size() < 2)
{
state.walkPoints.clear();
return false;
}
// Launch spline movement
LaunchWalkSpline(state);
return true;
}
bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination)
{
WorldPosition botPos(bot->GetMapId(), bot->GetPositionX(),
bot->GetPositionY(), bot->GetPositionZ());
LOG_DEBUG("playerbots",
"[TravelPlan] {} requesting plan: from ({:.0f},{:.0f},{:.0f}) map={} zone={} → "
"({:.0f},{:.0f},{:.0f}) map={} (straight={:.0f}yd)",
bot->GetName(), botPos.GetPositionX(), botPos.GetPositionY(), botPos.GetPositionZ(),
bot->GetMapId(), bot->GetZoneId(),
destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(),
destination.GetMapId(), botPos.fDist(destination));
return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination);
}
bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
{
if (!state.IsActive())
return false;
if (bot->IsInFlight())
return true;
// Handle active spline
if (state.splineActive)
{
if (!CheckSplineProgress(state))
{
if (state.splineActive)
return true; // Still moving
else
LaunchWalkSpline(state); // Interrupted, re-launch
}
return true;
}
if (state.stepIdx >= state.steps.size())
{
state.Reset();
return true;
}
const PathNodePoint& pt = state.steps[state.stepIdx];
switch (pt.type)
{
case PathNodeType::NODE_PREPATH:
{
if (state.stepIdx + 1 >= state.steps.size())
{
state.stepIdx++;
return true;
}
float const botX = bot->GetPositionX();
float const botY = bot->GetPositionY();
float const botZ = bot->GetPositionZ();
// Walk forward through the route while distance keeps shrinking.
// Once it starts growing we're past the closest waypoint — break.
size_t bestIdx = state.stepIdx + 1;
float bestDistSq = FLT_MAX;
for (size_t i = state.stepIdx + 1; i < state.steps.size(); ++i)
{
const PathNodePoint& cand = state.steps[i];
if (cand.type != PathNodeType::NODE_PATH &&
cand.type != PathNodeType::NODE_NODE)
break; // stop at portal/transport/etc — can't walk past
float const dx = cand.point.GetPositionX() - botX;
float const dy = cand.point.GetPositionY() - botY;
float const dz = cand.point.GetPositionZ() - botZ;
float const dSq = dx * dx + dy * dy + dz * dz;
if (dSq >= bestDistSq)
break; // moving away — closest waypoint already found
bestDistSq = dSq;
bestIdx = i;
}
constexpr float ARRIVAL_DIST = 5.0f;
WorldPosition const& target = state.steps[bestIdx].point;
float const distToTarget = bot->GetExactDist(
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
if (distToTarget < ARRIVAL_DIST)
{
state.stepIdx = bestIdx;
return true;
}
return MoveTo(target.GetMapId(),
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
false, false, false, true /*exact_waypoint*/);
}
case PathNodeType::NODE_PATH:
case PathNodeType::NODE_NODE:
{
// Batch consecutive walk points into one spline. Capped small 20 points per tick.
static constexpr uint32 MAX_SPLINE_POINTS = 20;
state.walkPoints.clear();
while (state.stepIdx < state.steps.size() && state.walkPoints.size() < MAX_SPLINE_POINTS)
{
const PathNodePoint& wp = state.steps[state.stepIdx];
if (wp.type != PathNodeType::NODE_PATH && wp.type != PathNodeType::NODE_NODE)
break;
state.walkPoints.push_back(G3D::Vector3(wp.point.GetPositionX(), wp.point.GetPositionY(), wp.point.GetPositionZ()));
state.stepIdx++;
}
if (state.walkPoints.empty())
return true;
// Already near end of batch?
G3D::Vector3 const& last = state.walkPoints.back();
float dist = bot->GetExactDist(last.x, last.y, last.z);
if (dist < 10.0f)
{
state.walkPoints.clear();
return true;
}
// Too far from first point — abort the plan and let the
// caller's stuck-recovery decide what to do. (cmangos
// doesn't teleport here either; an abandoned plan is
// recovered by the next MoveFarTo cycle.)
if (state.walkPoints.size() >= 2)
{
G3D::Vector3 const& first = state.walkPoints.front();
float distToFirst = bot->GetExactDist(first.x, first.y, first.z);
if (distToFirst > MAX_PATHFINDING_DISTANCE)
{
state.walkPoints.clear();
state.Reset();
return false;
}
}
// Single point — use PathGenerator directly
if (state.walkPoints.size() < 2)
{
WorldPosition target(bot->GetMapId(), last.x, last.y, last.z);
MoveToSpline(state, target);
state.walkPoints.clear();
return true;
}
LaunchWalkSpline(state);
return true;
}
case PathNodeType::NODE_PORTAL:
{
// Pair: source (pointIdx) + dest (pointIdx+1)
if (state.stepIdx + 1 >= state.steps.size())
{
state.Reset();
return false;
}
const PathNodePoint& src = state.steps[state.stepIdx];
const PathNodePoint& dst = state.steps[state.stepIdx + 1];
// Already on destination map?
if (bot->GetMapId() == dst.point.GetMapId())
{
state.stepIdx += 2;
return true;
}
// Walk to portal source
float dist = bot->GetExactDist(src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ());
if (dist > INTERACTION_DISTANCE)
return MoveTo(src.point.GetMapId(), src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ());
// At portal but didn't cross — natural collision missed.
// Abort the plan; stuck-recovery in MoveFarTo will decide
// whether to retry or teleport the bot. (cmangos doesn't
// teleport here either.)
state.Reset();
return false;
}
case PathNodeType::NODE_TRANSPORT:
{
if (state.stepIdx + 1 >= state.steps.size())
{
state.Reset();
return false;
}
const PathNodePoint& board = state.steps[state.stepIdx];
const PathNodePoint& arrive = state.steps[state.stepIdx + 1];
// Arrived at destination?
if (bot->GetMapId() == arrive.point.GetMapId() && !bot->GetTransport())
{
state.stepIdx += 2;
return true;
}
// On transport — wait
if (bot->GetTransport())
{
if (bot->GetMapId() == arrive.point.GetMapId())
{
bot->GetTransport()->RemovePassenger(bot);
bot->StopMovingOnCurrentPos();
state.stepIdx += 2;
}
return true;
}
// Walk to boarding point
float dist = bot->GetExactDist(board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
if (dist > 60.0f)
return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
// Try to board
if (board.entry)
{
Map* map = bot->GetMap();
if (map)
{
Transport* transport =
GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), board.point.GetPositionX(),
board.point.GetPositionY(), board.point.GetPositionZ());
if (transport && transport->GetEntry() == board.entry)
{
BoardTransport(transport);
return true;
}
}
}
// Wait at boarding point
if (dist > INTERACTION_DISTANCE)
return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
return true;
}
case PathNodeType::NODE_FLIGHTPATH:
{
if (state.stepIdx + 1 >= state.steps.size())
{
state.Reset();
return false;
}
const PathNodePoint& dep = state.steps[state.stepIdx];
const PathNodePoint& arr = state.steps[state.stepIdx + 1];
if (bot->IsInFlight())
return true;
// Resolve taxi path
if (state.route.empty())
{
uint32 fromTaxi = sObjectMgr->GetNearestTaxiNode(dep.point.GetPositionX(), dep.point.GetPositionY(),
dep.point.GetPositionZ(), dep.point.GetMapId(), bot->GetTeamId());
uint32 toTaxi = sObjectMgr->GetNearestTaxiNode(arr.point.GetPositionX(), arr.point.GetPositionY(),
arr.point.GetPositionZ(), arr.point.GetMapId(), bot->GetTeamId());
if (fromTaxi && toTaxi && fromTaxi != toTaxi)
state.route = sTravelNodeMap.FindTaxiPath(fromTaxi, toTaxi);
if (state.route.empty())
{
state.stepIdx += 2;
return true;
}
}
TravelMgr::FlightMasterInfo const* fmInfo = sTravelMgr.GetNearestFlightMasterInfo(bot);
if (!fmInfo)
{
state.route.clear();
state.stepIdx += 2;
return true;
}
if (bot->GetDistance(fmInfo->pos) > INTERACTION_DISTANCE)
return MoveTo(fmInfo->pos.GetMapId(), fmInfo->pos.GetPositionX(),
fmInfo->pos.GetPositionY(), fmInfo->pos.GetPositionZ());
ObjectGuid fmGuid = ObjectGuid::Create<HighGuid::Unit>(fmInfo->templateEntry, fmInfo->dbGuid);
Creature* flightMaster = ObjectAccessor::GetCreature(*bot, fmGuid);
if (!flightMaster || !flightMaster->IsAlive())
{
state.route.clear();
state.stepIdx += 2;
return true;
}
botAI->RemoveShapeshift();
if (bot->IsMounted())
bot->Dismount();
if (bot->ActivateTaxiPathTo(state.route, flightMaster, 0))
LOG_DEBUG("playerbots","[TravelPlan] Bot {} taking flight ({} nodes)", bot->GetName(), state.route.size());
state.route.clear();
state.stepIdx += 2;
return true;
}
case PathNodeType::NODE_TELEPORT:
{
// Teleport-spell node (e.g. mage portals). Not implemented
// — abort the plan instead of silently teleporting the
// bot. The plan executor regards this node as terminal.
state.Reset();
return false;
}
case PathNodeType::NODE_FLYING_MOUNT:
{
// Flying-mount node not implemented — abort. cmangos
// produces these nodes during graph generation but their
// execution is server-specific; we treat them as
// unreachable rather than papering over with a teleport.
state.Reset();
return false;
}
default:
{
LOG_ERROR("playerbots",
"[TravelPlan] Bot {} encountered unknown PathNodeType ({}); resetting plan",
bot->GetName(), static_cast<uint32>(pt.type));
state.Reset();
return false;
}
}
return false;
}
Transport* MovementAction::GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z)
{
if (!map || !ref)
return nullptr;
std::array<float, 4> const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f };
for (float const pz : probes)
{
if (Transport* transport = map->GetTransportForPos(phaseMask, x, y, pz, ref))
return transport;
}
return nullptr;
}
bool MovementAction::FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref,
float refX, float refY, float refZ, float botX, float botY, float botZ, float& outX, float& outY, float& outZ)
{
if (!map || !expectedTransport || !ref)
return false;
uint32 const phaseMask = ref->GetPhaseMask();
if (GetTransportForPosTolerant(map, ref, phaseMask, refX, refY, refZ)
!= expectedTransport)
return false;
float const probeZ = std::max(refZ, botZ);
float const dx2 = botX - refX;
float const dy2 = botY - refY;
float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2);
int32 const steps = std::clamp(static_cast<int32>(dist2d / 0.75f), 10, 28);
float const dx = (botX - refX) / static_cast<float>(steps);
float const dy = (botY - refY) / static_cast<float>(steps);
if (map->GetTransportForPos(phaseMask, refX, refY, probeZ, ref) != expectedTransport)
return false;
float lastX = refX;
float lastY = refY;
bool found = false;
for (int32 i = 1; i <= steps; ++i)
{
float const px = refX + dx * i;
float const py = refY + dy * i;
Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ);
if (t != expectedTransport)
break;
lastX = px;
lastY = py;
found = true;
}
if (!found)
return false;
outX = lastX;
outY = lastY;
outZ = refZ;
return true;
}
bool MovementAction::BoardTransport(Transport* transport)
{
if (!transport || transport->IsStaticTransport())
return false;
Map* map = bot->GetMap();
if (!map)
return false;
// Already on this transport
if (bot->GetTransport() == transport)
return true;
// Check if bot is on the transport surface
float probeZ = std::max(bot->GetPositionZ(), transport->GetPositionZ());
Transport* surface = GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), bot->GetPositionX(),
bot->GetPositionY(), probeZ);
if (surface == transport)
{
transport->AddPassenger(bot, true);
bot->StopMovingOnCurrentPos();
return true;
}
// Not on surface — move toward the transport
float destX = transport->GetPositionX();
float destY = transport->GetPositionY();
float destZ = transport->GetPositionZ();
// Try to find nearest boarding edge
float edgeX, edgeY, edgeZ;
if (FindBoardingPointOnTransport(map, transport, transport, transport->GetPositionX(), transport->GetPositionY(),
transport->GetPositionZ(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), edgeX, edgeY, edgeZ))
{
destX = edgeX;
destY = edgeY;
destZ = edgeZ;
}
// MovePoint without pathfinding (transport is a moving object)
if (MotionMaster* mm = bot->GetMotionMaster())
{
if (bot->IsSitState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
mm->MovePoint(0, destX, destY, destZ, FORCED_MOVEMENT_NONE, 0.0f, 0.0f, false, false);
}
return false;
}

View File

@ -10,6 +10,7 @@
#include "Action.h" #include "Action.h"
#include "LastMovementValue.h" #include "LastMovementValue.h"
#include "PathGenerator.h"
#include "PlayerbotAIConfig.h" #include "PlayerbotAIConfig.h"
class Player; class Player;
@ -22,6 +23,19 @@ class Position;
#define ANGLE_90_DEG M_PI_2 #define ANGLE_90_DEG M_PI_2
#define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f) #define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f)
// Default acceptable path types for GeneratePath
constexpr uint32 DEFAULT_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE;
constexpr uint32 RELAXED_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
struct PathResult
{
Movement::PointsArray points;
G3D::Vector3 actualEnd;
G3D::Vector3 end;
PathType pathType;
bool reachable;
};
class MovementAction : public Action class MovementAction : public Action
{ {
public: public:
@ -66,6 +80,25 @@ protected:
bool FleePosition(Position pos, float radius, uint32 minInterval = 1000); bool FleePosition(Position pos, float radius, uint32 minInterval = 1000);
bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList); bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList);
PathResult GeneratePath(float x, float y, float z, uint32 acceptMask = DEFAULT_PATH_ACCEPT_MASK, bool forceDestination = true);
bool GetTravelPlan(TravelPlan& plan, WorldPosition destination);
bool ExecuteTravelPlan(TravelPlan& state);
// Transport boarding helpers (shared by FollowAction and travel plan)
static Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref,
uint32 phaseMask, float x, float y, float z);
static bool FindBoardingPointOnTransport(Map* map, Transport* transport,
WorldObject* ref, float refX, float refY, float refZ,
float botX, float botY, float botZ,
float& outX, float& outY, float& outZ);
bool BoardTransport(Transport* transport);
private:
bool LaunchWalkSpline(TravelPlan& state);
bool CheckSplineProgress(TravelPlan& state);
bool MoveToSpline(TravelPlan& state, WorldPosition target);
protected: protected:
struct CheckAngle struct CheckAngle
{ {

View File

@ -120,17 +120,8 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/)
} }
break; break;
} }
case RPG_TRAVEL_FLIGHT: // RPG_TRAVEL_FLIGHT arrival is handled inside NewRpgTravelFlightAction
{ // so the flight action owns both take-off and landing transitions.
auto& data = std::get<NewRpgInfo::TravelFlight>(info.data);
if (data.inFlight && !bot->IsInFlight())
{
// flight arrival
info.ChangeToIdle();
return true;
}
break;
}
case RPG_REST: case RPG_REST:
{ {
// REST -> IDLE // REST -> IDLE
@ -464,6 +455,22 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
return false; return false;
auto& data = *dataPtr; auto& data = *dataPtr;
// Arrival: we had boarded a flight (data.inFlight) and we're no longer in
// it → we just landed. Special-case Rut'theran: walk to the portal GO so
// it teleports the bot into Darnassus, flipping the zone to AREA_DARNASSUS
// so this branch falls through to ChangeToIdle on the next tick.
if (data.inFlight && !bot->IsInFlight())
{
if (bot->GetZoneId() == AREA_TELDRASSIL)
{
static WorldPosition const rutTheranPortalEntrance(1, 8799.41f, 969.787f, 26.2409f, 0.0f);
return MoveFarTo(rutTheranPortalEntrance);
}
info.ChangeToIdle();
return true;
}
if (bot->IsInFlight()) if (bot->IsInFlight())
{ {
data.inFlight = true; data.inFlight = true;
@ -479,16 +486,8 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
info.ChangeToIdle(); info.ChangeToIdle();
return true; return true;
} }
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
return MoveFarTo(flightMaster);
std::vector<uint32> nodes = data.path; if (!TakeFlight(data.path, flightMaster))
botAI->RemoveShapeshift();
if (bot->IsMounted())
bot->Dismount();
if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0))
{ {
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(), LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]); flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);

View File

@ -97,25 +97,35 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
botAI->rpgInfo.stuckAttempts = 0; botAI->rpgInfo.stuckAttempts = 0;
const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId()); const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId());
std::string zone_name = PlayerbotAI::GetLocalizedAreaName(entry); std::string zone_name = PlayerbotAI::GetLocalizedAreaName(entry);
LOG_DEBUG( LOG_DEBUG("playerbots","[New RPG] Teleport {} from ({},{},{},{}) to ({},{},{},{}) as it stuck when moving far - Zone: {} ({})",
"playerbots",
"[New RPG] Teleport {} from ({},{},{},{}) to ({},{},{},{}) as it stuck when moving far - Zone: {} ({})",
bot->GetName(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId(), bot->GetName(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId(),
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.GetMapId(), bot->GetZoneId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.GetMapId(), bot->GetZoneId(), zone_name);
zone_name); botAI->TeleportTo(dest);
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP); return true;
return bot->TeleportTo(dest);
} }
float dis = bot->GetExactDist(dest); float dis = bot->GetExactDist(dest);
// Long distance + travel nodes enabled: use the pre-computed node graph
// (A*, flight paths, transports) instead of repeated mmap hops.
if (dis > MAX_PATHFINDING_DISTANCE && sPlayerbotAIConfig.enableTravelNodes)
{
if (!botAI->rpgInfo.HasActiveTravelPlan())
StartTravelPlan(dest);
return UpdateTravelPlan();
}
// Crossed below the travel-node threshold — clear any leftover plan
if (botAI->rpgInfo.HasActiveTravelPlan())
botAI->rpgInfo.ClearTravel();
// Short range: close enough for a single mmap call
if (dis < pathFinderDis) if (dis < pathFinderDis)
{ {
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false, return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false,
false, true); false, true);
} }
const uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
// Primary strategy: ask mmap for a route to the TRUE destination. // Primary strategy: ask mmap for a route to the TRUE destination.
// If mmap can reach it directly (PATHFIND_NORMAL) or partially // If mmap can reach it directly (PATHFIND_NORMAL) or partially
// (PATHFIND_INCOMPLETE — destinations beyond the smooth-path cap // (PATHFIND_INCOMPLETE — destinations beyond the smooth-path cap
@ -127,23 +137,18 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
// subsequent ticks early-out via IsWaitingForLastMove and no // subsequent ticks early-out via IsWaitingForLastMove and no
// further PathGenerator calls fire until the bot arrives. // further PathGenerator calls fire until the bot arrives.
{ {
PathGenerator path(bot); PathResult path = GeneratePath(dest.GetPositionX(), dest.GetPositionY(),
path.CalculatePath(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); dest.GetPositionZ(), RELAXED_PATH_ACCEPT_MASK);
PathType type = path.GetPathType(); if (path.reachable)
bool canReach = !(type & (~typeOk));
if (canReach)
{ {
const G3D::Vector3& endPos = path.GetActualEndPosition();
// Only commit if the mmap endpoint actually makes progress // Only commit if the mmap endpoint actually makes progress
// toward the destination. For pathological INCOMPLETE // toward the destination. For pathological INCOMPLETE
// results (e.g. disconnected polys that still report // results (e.g. disconnected polys that still report
// INCOMPLETE) the endpoint can land right under the bot; // INCOMPLETE) the endpoint can land right under the bot;
// fall through to cone sampling in that case. // fall through to cone sampling in that case.
float endDistToDest = dest.GetExactDist(endPos.x, endPos.y, endPos.z); float endDistToDest = dest.GetExactDist(path.actualEnd.x, path.actualEnd.y, path.actualEnd.z);
if (endDistToDest + 5.0f < disToDest) if (endDistToDest + 5.0f < disToDest)
{ return MoveTo(bot->GetMapId(), path.actualEnd.x, path.actualEnd.y, path.actualEnd.z, false, false, false, true);
return MoveTo(bot->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, true);
}
} }
} }
@ -167,18 +172,14 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
float dx = x + cos(angle) * sampleDis; float dx = x + cos(angle) * sampleDis;
float dy = y + sin(angle) * sampleDis; float dy = y + sin(angle) * sampleDis;
float dz = z + 0.5f; float dz = z + 0.5f;
PathGenerator path(bot); PathResult path = GeneratePath(dx, dy, dz, RELAXED_PATH_ACCEPT_MASK);
path.CalculatePath(dx, dy, dz);
PathType type = path.GetPathType();
bool canReach = !(type & (~typeOk));
if (canReach && fabs(delta) <= minDelta) if (path.reachable && fabs(delta) <= minDelta)
{ {
found = true; found = true;
const G3D::Vector3& endPos = path.GetActualEndPosition(); rx = path.actualEnd.x;
rx = endPos.x; ry = path.actualEnd.y;
ry = endPos.y; rz = path.actualEnd.z;
rz = endPos.z;
minDelta = fabs(delta); minDelta = fabs(delta);
} }
} }
@ -189,12 +190,31 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
return false; return false;
} }
void NewRpgBaseAction::StartTravelPlan(WorldPosition dest)
{
TravelPlan& plan = botAI->rpgInfo.travelPlan;
GetTravelPlan(plan, dest);
LOG_DEBUG("playerbots","[New RPG] Bot {} starting travel plan to ({:.0f},{:.0f},{:.0f}) map={}, {} points",
bot->GetName(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.GetMapId(), plan.steps.size());
}
bool NewRpgBaseAction::UpdateTravelPlan()
{
TravelPlan& plan = botAI->rpgInfo.travelPlan;
bool result = ExecuteTravelPlan(plan);
if (!plan.IsActive())
botAI->rpgInfo.ClearTravel();
return result;
}
bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance) bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)
{ {
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL)) if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
{
return false; return false;
}
WorldObject* object = botAI->GetWorldObject(guid); WorldObject* object = botAI->GetWorldObject(guid);
if (!object) if (!object)
@ -246,13 +266,9 @@ bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority,
float dy = y + distance * sin(angle); float dy = y + distance * sin(angle);
float dz = z; float dz = z;
PathGenerator path(bot); PathResult path = GeneratePath(dx, dy, dz, RELAXED_PATH_ACCEPT_MASK);
path.CalculatePath(dx, dy, dz);
PathType type = path.GetPathType();
uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
bool canReach = !(type & (~typeOk));
if (!canReach) if (!path.reachable)
continue; continue;
if (!map->CanReachPositionAndGetValidCoords(bot, dx, dy, dz)) if (!map->CanReachPositionAndGetValidCoords(bot, dx, dy, dz))
@ -277,6 +293,27 @@ bool NewRpgBaseAction::ForceToWait(uint32 duration, MovementPriority priority)
return true; return true;
} }
bool NewRpgBaseAction::TakeFlight(std::vector<uint32> const& taxiNodes, Creature* flightMaster)
{
if (taxiNodes.size() < 2 || !flightMaster || !flightMaster->IsAlive())
return false;
botAI->RemoveShapeshift();
if (bot->IsMounted())
bot->Dismount();
if (!bot->ActivateTaxiPathTo(taxiNodes, flightMaster, 0))
{
LOG_DEBUG("playerbots", "[New RPG] Bot {} flight ({} nodes, {} to {}) failed",
bot->GetName(), taxiNodes.size(), taxiNodes.front(), taxiNodes.back());
return false;
}
LOG_DEBUG("playerbots", "[New RPG] Bot {} taking flight ({} nodes, {} to {})",
bot->GetName(), taxiNodes.size(), taxiNodes.front(), taxiNodes.back());
return true;
}
/// @TODO: Fix redundant code /// @TODO: Fix redundant code
/// Quest related method refer to TalkToQuestGiverAction.h /// Quest related method refer to TalkToQuestGiverAction.h
bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid) bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid)
@ -994,6 +1031,10 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
uint32 idx = urand(0, lo_prepared_locs.size() - 1); uint32 idx = urand(0, lo_prepared_locs.size() - 1);
dest = lo_prepared_locs[idx]; dest = lo_prepared_locs[idx];
} }
if (!dest.IsValid())
return dest;
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random grind pos Map:{} X:{} Y:{} Z:{} ({}+{} available in {})", LOG_DEBUG("playerbots", "[New RPG] Bot {} select random grind pos Map:{} X:{} Y:{} Z:{} ({}+{} available in {})",
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
hi_prepared_locs.size(), lo_prepared_locs.size() - hi_prepared_locs.size(), locs.size()); hi_prepared_locs.size(), lo_prepared_locs.size() - hi_prepared_locs.size(), locs.size());
@ -1037,6 +1078,10 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
uint32 idx = urand(0, prepared_locs.size() - 1); uint32 idx = urand(0, prepared_locs.size() - 1);
dest = prepared_locs[idx]; dest = prepared_locs[idx];
} }
if (!dest.IsValid())
return dest;
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random inn keeper pos Map:{} X:{} Y:{} Z:{} ({} available in {})", LOG_DEBUG("playerbots", "[New RPG] Bot {} select random inn keeper pos Map:{} X:{} Y:{} Z:{} ({} available in {})",
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
prepared_locs.size(), locs.size()); prepared_locs.size(), locs.size());
@ -1076,7 +1121,6 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector<NewRpgStatus> candidateSta
probSum += sPlayerbotAIConfig.RpgStatusProbWeight[status]; probSum += sPlayerbotAIConfig.RpgStatusProbWeight[status];
} }
} }
// Safety check. Default to "rest" if all RPG weights = 0
if (availableStatus.empty() || probSum == 0) if (availableStatus.empty() || probSum == 0)
{ {
botAI->rpgInfo.ChangeToRest(); botAI->rpgInfo.ChangeToRest();

View File

@ -33,6 +33,7 @@ protected:
bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE); bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE);
bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr); bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr);
bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool TakeFlight(std::vector<uint32> const& taxiNodes, Creature* flightMaster);
/* QUEST RELATED CHECK */ /* QUEST RELATED CHECK */
ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f); ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f);
@ -69,6 +70,10 @@ protected:
// the teleport fires, but long enough that a genuine long // the teleport fires, but long enough that a genuine long
// walk that is slowly making progress never triggers it. // walk that is slowly making progress never triggers it.
const uint32 stuckTime = 90 * 1000; const uint32 stuckTime = 90 * 1000;
private:
void StartTravelPlan(WorldPosition dest);
bool UpdateTravelPlan();
}; };
#endif #endif

View File

@ -6,31 +6,31 @@
void NewRpgInfo::ChangeToGoGrind(WorldPosition pos) void NewRpgInfo::ChangeToGoGrind(WorldPosition pos)
{ {
startT = getMSTime(); Reset();
data = GoGrind{pos}; data = GoGrind{pos};
} }
void NewRpgInfo::ChangeToGoCamp(WorldPosition pos) void NewRpgInfo::ChangeToGoCamp(WorldPosition pos)
{ {
startT = getMSTime(); Reset();
data = GoCamp{pos}; data = GoCamp{pos};
} }
void NewRpgInfo::ChangeToWanderNpc() void NewRpgInfo::ChangeToWanderNpc()
{ {
startT = getMSTime(); Reset();
data = WanderNpc{}; data = WanderNpc{};
} }
void NewRpgInfo::ChangeToWanderRandom() void NewRpgInfo::ChangeToWanderRandom()
{ {
startT = getMSTime(); Reset();
data = WanderRandom{}; data = WanderRandom{};
} }
void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest) void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
{ {
startT = getMSTime(); Reset();
DoQuest do_quest; DoQuest do_quest;
do_quest.questId = questId; do_quest.questId = questId;
do_quest.quest = quest; do_quest.quest = quest;
@ -39,7 +39,7 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path) void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path)
{ {
startT = getMSTime(); Reset();
TravelFlight flight; TravelFlight flight;
flight.flightMasterEntry = flightMasterEntry; flight.flightMasterEntry = flightMasterEntry;
flight.flightMasterPos = flightMasterPos; flight.flightMasterPos = flightMasterPos;
@ -58,13 +58,13 @@ void NewRpgInfo::ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId)
void NewRpgInfo::ChangeToRest() void NewRpgInfo::ChangeToRest()
{ {
startT = getMSTime(); Reset();
data = Rest{}; data = Rest{};
} }
void NewRpgInfo::ChangeToIdle() void NewRpgInfo::ChangeToIdle()
{ {
startT = getMSTime(); Reset();
data = Idle{}; data = Idle{};
} }
@ -77,6 +77,7 @@ void NewRpgInfo::Reset()
{ {
data = Idle{}; data = Idle{};
startT = getMSTime(); startT = getMSTime();
ClearTravel();
} }
void NewRpgInfo::SetMoveFarTo(WorldPosition pos) void NewRpgInfo::SetMoveFarTo(WorldPosition pos)

View File

@ -8,6 +8,7 @@
#include "Strategy.h" #include "Strategy.h"
#include "Timer.h" #include "Timer.h"
#include "TravelMgr.h" #include "TravelMgr.h"
#include "TravelNode.h"
using NewRpgStatusTransitionProb = std::vector<std::vector<int>>; using NewRpgStatusTransitionProb = std::vector<std::vector<int>>;
@ -75,7 +76,10 @@ struct NewRpgInfo
uint32 stuckTs{0}; uint32 stuckTs{0};
uint32 stuckAttempts{0}; uint32 stuckAttempts{0};
WorldPosition moveFarPos; WorldPosition moveFarPos;
// END MOVE_FAR // Travel Node System
TravelPlan travelPlan;
bool HasActiveTravelPlan() const { return travelPlan.IsActive(); }
void ClearTravel() { travelPlan.Reset(); }
using RpgData = std::variant< using RpgData = std::variant<
Idle, Idle,

View File

@ -767,6 +767,21 @@ void PlayerbotAI::HandleCommand(uint32 type, const std::string& text, Player& fr
} }
} }
void PlayerbotAI::TeleportTo(WorldLocation loc, bool resetAI)
{
if (!bot || bot->IsBeingTeleported() || !bot->IsInWorld())
return;
bot->GetMotionMaster()->Clear();
if (resetAI)
Reset(true);
else
InterruptSpell();
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
bot->TeleportTo(loc.GetMapId(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ(), 0);
bot->SendMovementFlagUpdate();
}
void PlayerbotAI::HandleTeleportAck() void PlayerbotAI::HandleTeleportAck()
{ {
if (!bot || !bot->GetSession()) if (!bot || !bot->GetSession())

View File

@ -396,6 +396,7 @@ public:
void HandleMasterIncomingPacket(WorldPacket const& packet); void HandleMasterIncomingPacket(WorldPacket const& packet);
void HandleMasterOutgoingPacket(WorldPacket const& packet); void HandleMasterOutgoingPacket(WorldPacket const& packet);
void HandleTeleportAck(); void HandleTeleportAck();
void TeleportTo(WorldLocation loc, bool resetAI = false);
void ChangeEngine(BotState type); void ChangeEngine(BotState type);
void ChangeEngineOnCombat(); void ChangeEngineOnCombat();
void ChangeEngineOnNonCombat(); void ChangeEngineOnNonCombat();

View File

@ -1697,14 +1697,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
break; break;
} }
bot->GetMotionMaster()->Clear(); botAI->TeleportTo(WorldLocation(loc.GetMapId(), x, y, z, 0), true);
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (botAI)
botAI->Reset(true);
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
bot->TeleportTo(loc.GetMapId(), x, y, z, 0);
bot->SendMovementFlagUpdate();
if (pmo) if (pmo)
pmo->finish(); pmo->finish();

View File

@ -681,93 +681,6 @@ std::vector<WorldPosition> WorldPosition::frommGridCoord(mGridCoord GridCoord)
return retVec; return retVec;
} }
// TODO: Cleanup — make this actually work.
void WorldPosition::loadMapAndVMap(uint32 mapId, uint8 x, uint8 y)
{
std::string const fileName = "load_map_grid.csv";
/*
if (isOverworld() && false || false)
{
if (!MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(mapId, x, y))
if (sPlayerbotAIConfig.hasLog(fileName))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr();
out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1")
<< ",";
printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true);
sPlayerbotAIConfig.log(fileName, out.str().c_str());
}
}
else
{
// This needs to be disabled or maps will not load.
// Needs more testing to check for impact on movement.
if (false)
if (!TravelMgr::instance().isBadVmap(mapId, x, y))
{
// load VMAPs for current map/grid...
const MapEntry* i_mapEntry = sMapStore.LookupEntry(mapId);
//const char* mapName = i_mapEntry ? i_mapEntry->name[sWorld->GetDefaultDbcLocale()] : "UNNAMEDMAP\x0"; //not used, (usage are commented out below), line marked for removal.
int vmapLoadResult = VMAP::VMapFactory::createOrGetVMapMgr()->loadMap(
(sWorld->GetDataPath() + "vmaps").c_str(), mapId, x, y);
switch (vmapLoadResult)
{
case VMAP::VMAP_LOAD_RESULT_OK:
// LOG_ERROR("playerbots", "VMAP loaded name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})",
// mapName, mapId, x, y, x, y);
break;
case VMAP::VMAP_LOAD_RESULT_ERROR:
// LOG_ERROR("playerbots", "Could not load VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{},
// y:{})", mapName, mapId, x, y, x, y);
TravelMgr::instance().addBadVmap(mapId, x, y);
break;
case VMAP::VMAP_LOAD_RESULT_IGNORED:
TravelMgr::instance().addBadVmap(mapId, x, y);
// LOG_INFO("playerbots", "Ignored VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})",
// mapName, mapId, x, y, x, y);
break;
}
if (sPlayerbotAIConfig.hasLog(fileName))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr();
out << "+00,\"vmap\", " << x << "," << y << ", " << (TravelMgr::instance().isBadVmap(mapId, x, y) ? "0" : "1")
<< ",";
printWKT(frommGridCoord(mGridCoord(x, y)), out, 1, true);
sPlayerbotAIConfig.log(fileName, out.str().c_str());
}
}
*/
if (!TravelMgr::instance().isBadMmap(mapId, x, y))
{
// load navmesh
Map* map = getMap();
if (map && map->GetMapCollisionData().LoadMMapTile(x, y) == MMAP::MMAP_LOAD_RESULT_ERROR)
TravelMgr::instance().addBadMmap(mapId, x, y);
if (sPlayerbotAIConfig.hasLog(fileName))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr();
out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1")
<< ",";
printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true);
sPlayerbotAIConfig.log(fileName, out.str().c_str());
}
}
}
void WorldPosition::loadMapAndVMaps(WorldPosition secondPos)
{
for (auto& grid : getmGridCoords(secondPos))
{
loadMapAndVMap(GetMapId(), grid.first, grid.second);
}
}
std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vector3> path) std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vector3> path)
{ {
std::vector<WorldPosition> retVec; std::vector<WorldPosition> retVec;
@ -780,34 +693,42 @@ std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vecto
// A single pathfinding attempt from one position to another. Returns pathfinding status and path. // A single pathfinding attempt from one position to another. Returns pathfinding status and path.
std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, Unit* bot) std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, Unit* bot)
{ {
if (!bot) Unit* pathUnit = bot;
Creature* tempCreature = nullptr;
if (!pathUnit)
{
// Create a temporary creature for PathGenerator (same entry as DebugAction "show node")
Map* map = sMapMgr->FindBaseMap(startPos.GetMapId());
if (!map)
return {}; return {};
// Load mmaps and vmaps between the two points. tempCreature = new Creature();
loadMapAndVMaps(startPos); if (!tempCreature->Create(map->GenerateLowGuid<HighGuid::Unit>(), map,
PHASEMASK_NORMAL, 1 /*entry*/, 0,
startPos.GetPositionX(), startPos.GetPositionY(),
startPos.GetPositionZ(), 0))
{
delete tempCreature;
return {};
}
pathUnit = tempCreature;
PathGenerator path(bot); // Ensure grids are created at both endpoints so mmap tiles are available.
path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ()); // EnsureGridCreated loads terrain + vmaps + mmaps but NOT objects,
// which is all PathGenerator needs.
map->EnsureGridCreated(Acore::ComputeGridCoord(startPos.GetPositionX(), startPos.GetPositionY()));
map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY()));
}
PathGenerator path(pathUnit);
path.CalculatePath(GetPositionX(), GetPositionY(), GetPositionZ());
Movement::PointsArray points = path.GetPath(); Movement::PointsArray points = path.GetPath();
PathType type = path.GetPathType(); PathType type = path.GetPathType();
if (sPlayerbotAIConfig.hasLog("pathfind_attempt_point.csv")) if (tempCreature)
{ delete tempCreature;
std::ostringstream out;
out << std::fixed << std::setprecision(1);
printWKT({startPos, *this}, out);
sPlayerbotAIConfig.log("pathfind_attempt_point.csv", out.str().c_str());
}
if (sPlayerbotAIConfig.hasLog("pathfind_attempt.csv") && (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr() << "+00,";
out << std::fixed << std::setprecision(1) << type << ",";
printWKT(fromPointsArray(points), out, 1);
sPlayerbotAIConfig.log("pathfind_attempt.csv", out.str().c_str());
}
if (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL) if (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL)
return fromPointsArray(points); return fromPointsArray(points);
@ -1073,6 +994,14 @@ GuidPosition::GuidPosition(GameObjectData const& goData)
loadedFromDB = true; loadedFromDB = true;
} }
TravelDestination::~TravelDestination()
{
for (WorldPosition* point : points)
delete point;
points.clear();
}
std::vector<WorldPosition*> TravelDestination::getPoints(bool ignoreFull) std::vector<WorldPosition*> TravelDestination::getPoints(bool ignoreFull)
{ {
if (ignoreFull) if (ignoreFull)
@ -2379,9 +2308,7 @@ void TravelMgr::LoadQuestTravelTable()
sPlayerbotAIConfig.openLog("unload_grid.csv", "w"); sPlayerbotAIConfig.openLog("unload_grid.csv", "w");
sPlayerbotAIConfig.openLog("unload_obj.csv", "w"); sPlayerbotAIConfig.openLog("unload_obj.csv", "w");
TravelNodeMap::instance().loadNodeStore(); // Node loading/generation is handled by TravelNodeMap::Init() called from TravelMgr::Init().
TravelNodeMap::instance().generateAll();
/* /*
bool fullNavPointReload = false; bool fullNavPointReload = false;
@ -2772,7 +2699,7 @@ void TravelMgr::LoadQuestTravelTable()
//if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode)) //if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode))
// continue; // continue;
startNode->buildPath(endNode, nullptr, false); startNode->BuildPath(endNode, nullptr, false);
//if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete()) //if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete())
//startNode->removeLinkTo(endNode); //startNode->removeLinkTo(endNode);
@ -2896,7 +2823,7 @@ void TravelMgr::LoadQuestTravelTable()
TravelNodePath nodePath = *path.second; TravelNodePath nodePath = *path.second;
std::vector<WorldPosition> pPath = nodePath.getPath(); std::vector<WorldPosition> pPath = nodePath.GetPath();
std::reverse(pPath.begin(), pPath.end()); std::reverse(pPath.begin(), pPath.end());
nodePath.setPath(pPath); nodePath.setPath(pPath);
@ -4359,8 +4286,7 @@ void TravelMgr::Init()
PrepareZone2LevelBracket(); PrepareZone2LevelBracket();
PrepareDestinationCache(); PrepareDestinationCache();
} }
sTravelNodeMap.InitTaxiGraph(); sTravelNodeMap.Init();
LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built.");
} }
TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
@ -4555,6 +4481,34 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
return fallbackLocations; return fallbackLocations;
} }
bool TravelMgr::SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer)
{
uint16 botMapId = bot->GetMapId();
auto const& cache = (bot->GetTeamId() == TEAM_HORDE) ? hordeAuctioneerCache : allianceAuctioneerCache;
auto mapIt = cache.find(botMapId);
if (mapIt == cache.end() || mapIt->second.empty())
return false;
// Collect all areas on this map that have auctioneers
std::vector<uint32> areaIds;
areaIds.reserve(mapIt->second.size());
for (auto const& [areaId, npcs] : mapIt->second)
{
if (!npcs.empty())
areaIds.push_back(areaId);
}
if (areaIds.empty())
return false;
// Pick a random area, then a random auctioneer in that area
uint32 selectedArea = areaIds[urand(0, areaIds.size() - 1)];
auto const& auctioneers = mapIt->second.at(selectedArea);
outAuctioneer = auctioneers[urand(0, auctioneers.size() - 1)];
return true;
}
void TravelMgr::PrepareZone2LevelBracket() void TravelMgr::PrepareZone2LevelBracket()
{ {
// Classic WoW - starter zones // Classic WoW - starter zones
@ -4641,6 +4595,7 @@ void TravelMgr::PrepareDestinationCache()
uint32 flightMastersCount = 0; uint32 flightMastersCount = 0;
uint32 innkeepersCount = 0; uint32 innkeepersCount = 0;
uint32 bankerCount = 0; uint32 bankerCount = 0;
uint32 auctioneerCount = 0;
LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel); LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel);
// Temporary map to group creatures by entry and area // Temporary map to group creatures by entry and area
@ -4698,7 +4653,7 @@ void TravelMgr::PrepareDestinationCache()
// Entry 3838 is Vesprystus in Rut'Theran. Need Travel Node system to resolve this one. // Entry 3838 is Vesprystus in Rut'Theran. Need Travel Node system to resolve this one.
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER || else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) && creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480) creatureTemplate->Entry != 29480)
{ {
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction); FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
bool forHorde = !(factionEntry->hostileMask & 4); bool forHorde = !(factionEntry->hostileMask & 4);
@ -4783,7 +4738,7 @@ void TravelMgr::PrepareDestinationCache()
creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 && creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 &&
creatureTemplate->Entry != 29282) creatureTemplate->Entry != 29282)
{ {
BankerLocation bLoc; NpcLocation bLoc;
bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI); bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI);
bLoc.entry = templateEntry; bLoc.entry = templateEntry;
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
@ -4806,6 +4761,31 @@ void TravelMgr::PrepareDestinationCache()
} }
bankerCount++; bankerCount++;
} }
// === AUCTIONEERS ===
else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_AUCTIONEER)
{
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
if (!factionEntry)
continue;
bool forHorde = !(factionEntry->hostileMask & 4);
bool forAlliance = !(factionEntry->hostileMask & 2);
if (!forHorde && !forAlliance)
continue;
NpcLocation aLoc;
aLoc.loc = WorldLocation(mapId, x + cos(orient) * 3.0f, y + sin(orient) * 3.0f, z + 0.5f, orient + M_PI);
aLoc.entry = templateEntry;
if (forHorde)
hordeAuctioneerCache[mapId][areaId].push_back(aLoc);
if (forAlliance)
allianceAuctioneerCache[mapId][areaId].push_back(aLoc);
auctioneerCount++;
}
} }
// Process temporary caches // Process temporary caches
@ -4815,6 +4795,22 @@ void TravelMgr::PrepareDestinationCache()
{ {
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1); CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1);
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
float totalX = 0.0f;
float totalY = 0.0f;
float totalZ = 0.0f;
for (CreatureData const& creatureData : creatureDataList)
{
totalX += creatureData.posX;
totalY += creatureData.posY;
totalZ += creatureData.posZ;
}
float avgX = totalX / creatureDataList.size();
float avgY = totalY / creatureDataList.size();
float avgZ = totalZ / creatureDataList.size();
uint32 mapId = std::get<0>(gridTuple);
for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel;
l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++)
{ {
@ -4870,5 +4866,5 @@ void TravelMgr::PrepareDestinationCache()
break; break;
} }
} }
LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount); LOG_INFO("playerbots", ">> {} flight masters, {} innkeepers, {} bankers, {} auctioneers collected.", flightMastersCount, innkeepersCount, bankerCount, auctioneerCount);
} }

View File

@ -7,6 +7,7 @@
#define _PLAYERBOT_TRAVELMGR_H #define _PLAYERBOT_TRAVELMGR_H
#include <boost/functional/hash.hpp> #include <boost/functional/hash.hpp>
#include <cmath>
#include <map> #include <map>
#include <random> #include <random>
@ -268,12 +269,6 @@ public:
std::vector<mGridCoord> getmGridCoords(WorldPosition secondPos); std::vector<mGridCoord> getmGridCoords(WorldPosition secondPos);
std::vector<WorldPosition> frommGridCoord(mGridCoord GridCoord); std::vector<WorldPosition> frommGridCoord(mGridCoord GridCoord);
void loadMapAndVMap(uint32 mapId, uint8 x, uint8 y);
void loadMapAndVMap() { loadMapAndVMap(GetMapId(), getmGridCoord().first, getmGridCoord().second); }
void loadMapAndVMaps(WorldPosition secondPos);
// Display functions // Display functions
WorldPosition getDisplayLocation(); WorldPosition getDisplayLocation();
float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; } float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; }
@ -297,10 +292,26 @@ public:
std::vector<WorldPosition> getPathTo(WorldPosition endPos, Unit* bot) { return endPos.getPathFrom(*this, bot); } std::vector<WorldPosition> getPathTo(WorldPosition endPos, Unit* bot) { return endPos.getPathFrom(*this, bot); }
bool isPathTo(std::vector<WorldPosition> path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance) // Cmangos-aligned (WorldPosition.h:317): the path "reaches" this
// position when its last point is on the same map, within
// maxDistance horizontally, and within maxZDistance vertically.
// 3D Euclidean distance would falsely accept paths that end the
// right horizontal distance from us but on a roof/floor below.
// maxDistance == 0 falls back to targetPosRecalcDistance (0.1y).
bool isPathTo(std::vector<WorldPosition> const& path, float const maxDistance = 0.0f,
float const maxZDistance = 2.0f) const
{ {
return !path.empty() && distance(path.back()) < maxDistance; if (path.empty())
}; return false;
WorldPosition const& back = path.back();
if (back.GetMapId() != GetMapId())
return false;
float const realMax = maxDistance > 0.0f ? maxDistance
: sPlayerbotAIConfig.targetPosRecalcDistance;
if (GetExactDist2dSq(&back) >= realMax * realMax)
return false;
return std::fabs(back.GetPositionZ() - GetPositionZ()) < maxZDistance;
}
bool cropPathTo(std::vector<WorldPosition>& path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance); bool cropPathTo(std::vector<WorldPosition>& path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance);
bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); } bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); }
@ -507,9 +518,15 @@ public:
radiusMin = radiusMin1; radiusMin = radiusMin1;
radiusMax = radiusMax1; radiusMax = radiusMax1;
} }
virtual ~TravelDestination() = default; virtual ~TravelDestination();
void addPoint(WorldPosition* pos) { points.push_back(pos); } void addPoint(WorldPosition* pos)
{
if (!pos)
return;
points.push_back(new WorldPosition(*pos));
}
void setExpireDelay(uint32 delay) { expireDelay = delay; } void setExpireDelay(uint32 delay) { expireDelay = delay; }
@ -673,7 +690,7 @@ public:
bool isActive(Player* bot) override; bool isActive(Player* bot) override;
virtual CreatureTemplate const* GetCreatureTemplate(); virtual CreatureTemplate const* GetCreatureTemplate();
std::string const getName() override { return "RpgTravelDestination"; } std::string const getName() override { return "RpgTravelDestination"; }
int32 getEntry() override { return 0; } int32 getEntry() override { return entry; }
std::string const getTitle() override; std::string const getTitle() override;
protected: protected:
@ -985,18 +1002,14 @@ private:
bool InsideBracket(uint32 val) const { return val >= low && val <= high; } bool InsideBracket(uint32 val) const { return val >= low && val <= high; }
}; };
struct BankerLocation
{
WorldLocation loc;
uint32 entry;
};
// Navigation caches // Navigation caches
std::map<uint32, FlightMasterInfo> allianceFlightMasterCache; std::map<uint32, FlightMasterInfo> allianceFlightMasterCache;
std::map<uint32, FlightMasterInfo> hordeFlightMasterCache; std::map<uint32, FlightMasterInfo> hordeFlightMasterCache;
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache; std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache; std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache; std::map<uint8, std::vector<NpcLocation>> bankerLocsPerLevelCache;
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> hordeAuctioneerCache;
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> allianceAuctioneerCache;
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation; std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache; std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate; std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;

File diff suppressed because it is too large Load Diff

View File

@ -8,11 +8,12 @@
#include <shared_mutex> #include <shared_mutex>
#include "G3D/Vector3.h"
#include "TravelMgr.h" #include "TravelMgr.h"
// THEORY // THEORY
// //
// Pathfinding in (c)mangos is based on detour recast an opensource nashmesh creation and pathfinding codebase. // Pathfinding in (c)mangos is based on detour recast, an opensource navmesh creation and pathfinding codebase.
// This system is used for mob and npc pathfinding and in this codebase also for bots. // This system is used for mob and npc pathfinding and in this codebase also for bots.
// Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y. // Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y.
// This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a // This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a
@ -24,33 +25,68 @@
// <S> ---> [N1] ---> [N2] ---> [N3] ---> <E> // <S> ---> [N1] ---> [N2] ---> [N3] ---> <E>
// //
// Bot at <S> wants to move to <E> // Bot at <S> wants to move to <E>
// [N1],[N2],[N3] are predefined nodes for wich we know we can move from [N1] to [N2] and from [N2] to [N3] but not // [N1],[N2],[N3] are predefined nodes for which we know we can move from [N1] to [N2] and from [N2] to [N3] but not
// from [N1] to [N3] If we can move fom [S] to [N1] and from [N3] to [E] we have a complete route to travel. // from [N1] to [N3]. If we can move from [S] to [N1] and from [N3] to [E] we have a complete route to travel.
// //
// Termonology: // Terminology:
// Node: a location on a map for which we know bots are likely to want to travel to or need to travel past to reach // Node: A location on a map for which we know bots are likely to want to travel to or need to travel past to reach
// other nodes. Link: the connection between two nodes. A link signifies that the bot can travel from one node to // other nodes. Stored in DB table `playerbots_travelnode`.
// another. A link is one-directional. Path: the waypointpath returned by the standard PathGenerator to move from one // Link: The connection between two nodes. A link signifies that the bot can travel from one node to another.
// node (or position) to another. A path can be imcomplete or empty which means there is no link. Route: the list of // A link is one-directional. Stored in `playerbots_travelnode_link`.
// nodes that give the shortest route from a node to a distant node. Routes are calculated using a standard A* search // Path: The waypoint path returned by the standard PathGenerator to move from one node (or position) to another.
// based on links. // A path can be incomplete or empty which means there is no link. Stored in `playerbots_travelnode_path`.
// Route: The list of nodes that give the shortest route from a node to a distant node. Routes are calculated using
// a standard A* search based on links.
// //
// On server start saved nodes and links are loaded. Paths and routes are calculated on the fly but saved for future // Edge types (TravelNodePathType):
// use. Nodes can be added and removed realtime however because bots access the nodes from different threads this // walk(1) — Walk via navmesh waypoints (stored in DB)
// requires a locking mechanism. // portal(2) — AreaTrigger teleport (auto-discovered at startup)
// transport(3) — Boat/zeppelin (auto-discovered from MO_TRANSPORT)
// flightPath(4) — Taxi flight between flight masters
// teleportSpell(5) — Spell-based teleport (e.g. mage portals)
// staticPortal(6) — Manually defined teleport link (DB only, not pruned by generation)
// flyingMount (7) — Use Bots Flying mount to travel (Not currently enabled)
//
// On server start saved nodes and links are loaded via TravelNodeMap::Init(). An index of nodes by zone is prepared
// (instead of scanning all ~4000 nodes), precomputes connected components for O(1) reachability checks, and builds
// a taxi BFS graph. Paths and routes are calculated on the fly and saved for future use. Nodes are only added at
// startup or via the console `.generate` command — runtime mutation was removed because taking a unique_lock
// caused 100-250ms contention spikes against bot threads.
// //
// Initially the current nodes have been made: // Initially the current nodes have been made:
// Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route // Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route
// calculation) WorldBosses and Unique bosses in instances (These are a logical places bots might want to go in // calculation) WorldBosses and Unique bosses in instances (These are logical places bots might want to go in
// instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with // instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with
// teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators // teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators
// (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is // (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is
// good for global coverage) // good for global coverage).
// //
// To increase coverage/linking extra nodes can be automatically be created. // To increase coverage/linking extra nodes must be manually created via the "playerbot travel generatenode"
// Current implentation places nodes on paths (including complete) at sub-zone transitions or randomly. // console command after importing the specified node. Current implementation places nodes on paths (including
// After calculating possible links the node is removed if it does not create local coverage. // complete) at sub-zone transitions or randomly. After calculating possible links the node is removed if it
// does not create local coverage (.fullgenerate only).
// //
// Travel Flow:
//
// GetFullPath finds nearest nodes (zone-indexed), runs A* to get a node route, then
// BuildPath assembles a flat TravelPath with typed waypoints (walk, portal, transport, flight).
// ExecuteTravelPlan iterates the path by stepIdx, dispatching on each point's PathNodeType.
// Cross-map travel is handled naturally by portal/transport edges in the A* graph.
//
// If setup cannot resolve (no node, no route, no flight), the bot teleports directly to the destination
// as a fallback.
//
// The use of hearthstones and mage teleporting was removed — it caused route mutations requiring locking that no longer made sense. Mage portals may be future item.
//
// Thread Safety:
//
// The node graph is immutable at runtime (no adds/removes after Init). A shared_timed_mutex (m_nMapMtx) still
// exists and shared_locks are taken in GetFullPath and GenerateWalkPath for safety, but since there are no
// runtime mutations these are effectively uncontested. The only exclusive locks are taken at startup
// (saveNodeStore) and by the debug dump command.
//
constexpr float MAX_PATHFINDING_DISTANCE = 296.0f;
enum class TravelNodePathType : uint8 enum class TravelNodePathType : uint8
{ {
@ -59,21 +95,20 @@ enum class TravelNodePathType : uint8
portal = 2, portal = 2,
transport = 3, transport = 3,
flightPath = 4, flightPath = 4,
teleportSpell = 5 teleportSpell = 5,
staticPortal = 6,
flyingMount = 7
}; };
// A connection between two nodes. // A connection between two nodes.
class TravelNodePath class TravelNodePath
{ {
public: public:
// Legacy Constructor for travelnodestore
// TravelNodePath(float distance1, float extraCost1, bool portal1 = false, uint32 portalId1 = 0, bool transport1 =
// false, bool calculated = false, uint8 maxLevelMob1 = 0, uint8 maxLevelAlliance1 = 0, uint8 maxLevelHorde1 = 0,
// float swimDistance1 = 0, bool flightPath1 = false);
// Constructor // Constructor
TravelNodePath(float distance = 0.1f, float extraCost = 0, uint8 pathType = (uint8)TravelNodePathType::walk, TravelNodePath(float distance = 0.1f, float extraCost = 0,
uint32 pathObject = 0, bool calculated = false, std::vector<uint8> maxLevelCreature = {0, 0, 0}, uint8 pathType = (uint8)TravelNodePathType::walk,
uint32 pathObject = 0, bool calculated = false,
std::vector<uint8> maxLevelCreature = {0, 0, 0},
float swimDistance = 0) float swimDistance = 0)
: extraCost(extraCost), : extraCost(extraCost),
calculated(calculated), calculated(calculated),
@ -85,7 +120,7 @@ public:
{ {
if (pathType != (uint8)TravelNodePathType::walk) if (pathType != (uint8)TravelNodePathType::walk)
complete = true; complete = true;
}; }
TravelNodePath(TravelNodePath* basePath) TravelNodePath(TravelNodePath* basePath)
{ {
@ -98,11 +133,11 @@ public:
swimDistance = basePath->swimDistance; swimDistance = basePath->swimDistance;
pathType = basePath->pathType; pathType = basePath->pathType;
pathObject = basePath->pathObject; pathObject = basePath->pathObject;
}; }
// Getters // Getters
bool getComplete() { return complete || pathType != TravelNodePathType::walk; } bool getComplete() { return complete || pathType != TravelNodePathType::walk; }
std::vector<WorldPosition> getPath() { return path; } std::vector<WorldPosition> GetPath() { return path; }
TravelNodePathType getPathType() { return pathType; } TravelNodePathType getPathType() { return pathType; }
uint32 getPathObject() { return pathObject; } uint32 getPathObject() { return pathObject; }
@ -130,9 +165,6 @@ public:
extraCost = distance / speed; extraCost = distance / speed;
} }
// void setPortal(bool portal1, uint32 portalId1 = 0) { portal = portal1; portalId = portalId1; }
// void setTransport(bool transport1) { transport = transport1; }
void setPathType(TravelNodePathType pathType1) { pathType = pathType1; } void setPathType(TravelNodePathType pathType1) { pathType = pathType1; }
void setPathObject(uint32 pathObject1) { pathObject = pathObject1; } void setPathObject(uint32 pathObject1) { pathObject = pathObject1; }
@ -186,9 +218,10 @@ class TravelNode
{ {
public: public:
// Constructors // Constructors
TravelNode(){}; TravelNode() {}
TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node", bool important1 = false) TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node",
bool important1 = false)
{ {
nodeName = nodeName1; nodeName = nodeName1;
point = point1; point = point1;
@ -207,11 +240,11 @@ public:
void setPoint(WorldPosition point1) { point = point1; } void setPoint(WorldPosition point1) { point = point1; }
// Getters // Getters
std::string const getName() { return nodeName; }; std::string const getName() { return nodeName; }
WorldPosition* getPosition() { return &point; }; WorldPosition* getPosition() { return &point; }
std::unordered_map<TravelNode*, TravelNodePath>* getPaths() { return &paths; } std::unordered_map<TravelNode*, TravelNodePath>* getPaths() { return &paths; }
std::unordered_map<TravelNode*, TravelNodePath*>* getLinks() { return &links; } std::unordered_map<TravelNode*, TravelNodePath*>* getLinks() { return &links; }
bool isImportant() { return important; }; bool isImportant() { return important; }
bool isLinked() { return linked; } bool isLinked() { return linked; }
bool isTransport() bool isTransport()
@ -235,7 +268,8 @@ public:
bool isPortal() bool isPortal()
{ {
for (auto const& link : *getLinks()) for (auto const& link : *getLinks())
if (link.second->getPathType() == TravelNodePathType::portal) if (link.second->getPathType() == TravelNodePathType::portal ||
link.second->getPathType() == TravelNodePathType::staticPortal)
return true; return true;
return false; return false;
@ -251,17 +285,25 @@ public:
} }
// WorldLocation shortcuts // WorldLocation shortcuts
uint32 getMapId() { return point.GetMapId(); } uint32 GetMapId() { return point.GetMapId(); }
float getX() { return point.GetPositionX(); } float getX() { return point.GetPositionX(); }
float getY() { return point.GetPositionY(); } float getY() { return point.GetPositionY(); }
float getZ() { return point.GetPositionZ(); } float getZ() { return point.GetPositionZ(); }
float getO() { return point.GetOrientation(); } float getO() { return point.GetOrientation(); }
float getDistance(WorldPosition pos) { return point.distance(pos); } float getDistance(WorldPosition pos) { return point.distance(pos); }
float getDistance(TravelNode* node) { return point.distance(node->getPosition()); } float getDistance(TravelNode* node)
float fDist(TravelNode* node) { return point.fDist(node->getPosition()); } {
return point.distance(node->getPosition());
}
float fDist(TravelNode* node)
{
return point.fDist(node->getPosition());
}
float fDist(WorldPosition pos) { return point.fDist(pos); } float fDist(WorldPosition pos) { return point.fDist(pos); }
TravelNodePath* setPathTo(TravelNode* node, TravelNodePath path = TravelNodePath(), bool isLink = true) TravelNodePath* setPathTo(TravelNode* node,
TravelNodePath path = TravelNodePath(),
bool isLink = true)
{ {
if (this != node) if (this != node)
{ {
@ -275,10 +317,20 @@ public:
return nullptr; return nullptr;
} }
bool hasPathTo(TravelNode* node) { return paths.find(node) != paths.end(); } bool hasPathTo(TravelNode* node)
TravelNodePath* getPathTo(TravelNode* node) { return &paths[node]; } {
bool hasCompletePathTo(TravelNode* node) { return hasPathTo(node) && getPathTo(node)->getComplete(); } return paths.find(node) != paths.end();
TravelNodePath* buildPath(TravelNode* endNode, Unit* bot, bool postProcess = false); }
TravelNodePath* getPathTo(TravelNode* node)
{
return &paths[node];
}
bool hasCompletePathTo(TravelNode* node)
{
return hasPathTo(node) && getPathTo(node)->getComplete();
}
TravelNodePath* BuildPath(TravelNode* endNode, Unit* bot,
bool postProcess = false);
void setLinkTo(TravelNode* node, float distance = 0.1f) void setLinkTo(TravelNode* node, float distance = 0.1f)
{ {
@ -291,9 +343,18 @@ public:
} }
} }
bool hasLinkTo(TravelNode* node) { return links.find(node) != links.end(); } bool hasLinkTo(TravelNode* node)
float linkCostTo(TravelNode* node) { return paths.find(node)->second.getDistance(); } {
float linkDistanceTo(TravelNode* node) { return paths.find(node)->second.getDistance(); } return links.find(node) != links.end();
}
float linkCostTo(TravelNode* node)
{
return paths.find(node)->second.getDistance();
}
float linkDistanceTo(TravelNode* node)
{
return paths.find(node)->second.getDistance();
}
void removeLinkTo(TravelNode* node, bool removePaths = false); void removeLinkTo(TravelNode* node, bool removePaths = false);
bool isEqual(TravelNode* compareNode); bool isEqual(TravelNode* compareNode);
@ -304,7 +365,8 @@ public:
bool cropUselessLinks(); bool cropUselessLinks();
// Returns all nodes that can be reached from this node. // Returns all nodes that can be reached from this node.
std::vector<TravelNode*> getNodeMap(bool importantOnly = false, std::vector<TravelNode*> ignoreNodes = {}); std::vector<TravelNode*> getNodeMap(bool importantOnly = false,
std::vector<TravelNode*> ignoreNodes = {});
// Checks if it is even possible to route to this node. // Checks if it is even possible to route to this node.
bool hasRouteTo(TravelNode* node) bool hasRouteTo(TravelNode* node)
@ -314,7 +376,10 @@ public:
routes[mNode] = true; routes[mNode] = true;
return routes.find(node) != routes.end(); return routes.find(node) != routes.end();
}; }
void clearRoutes() { routes.clear(); }
void setRouteTo(TravelNode* node) { routes[node] = true; }
void print(bool printFailed = true); void print(bool printFailed = true);
@ -344,24 +409,8 @@ protected:
// uint32 transportId = 0; // uint32 transportId = 0;
}; };
class PortalNode : public TravelNode
{
public:
PortalNode(TravelNode* baseNode) : TravelNode(baseNode){};
void SetPortal(TravelNode* baseNode, TravelNode* endNode, uint32 portalSpell)
{
nodeName = baseNode->getName();
point = *baseNode->getPosition();
paths.clear();
links.clear();
TravelNodePath path(0.1f, 0.1f, (uint8)TravelNodePathType::teleportSpell, portalSpell, true);
setPathTo(endNode, path);
};
};
// Route step type // Route step type
enum PathNodeType enum class PathNodeType : uint8
{ {
NODE_PREPATH = 0, NODE_PREPATH = 0,
NODE_PATH = 1, NODE_PATH = 1,
@ -369,13 +418,14 @@ enum PathNodeType
NODE_PORTAL = 3, NODE_PORTAL = 3,
NODE_TRANSPORT = 4, NODE_TRANSPORT = 4,
NODE_FLIGHTPATH = 5, NODE_FLIGHTPATH = 5,
NODE_TELEPORT = 6 NODE_TELEPORT = 6,
NODE_FLYING_MOUNT = 7
}; };
struct PathNodePoint struct PathNodePoint
{ {
WorldPosition point; WorldPosition point;
PathNodeType type = NODE_PATH; PathNodeType type = PathNodeType::NODE_PATH;
uint32 entry = 0; uint32 entry = 0;
}; };
@ -383,24 +433,31 @@ struct PathNodePoint
class TravelPath class TravelPath
{ {
public: public:
TravelPath(){}; TravelPath() {}
TravelPath(std::vector<PathNodePoint> fullPath1) { fullPath = fullPath1; } TravelPath(std::vector<PathNodePoint> fullPath1)
TravelPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0) {
fullPath = fullPath1;
}
TravelPath(std::vector<WorldPosition> path,
PathNodeType type = PathNodeType::NODE_PATH,
uint32 entry = 0)
{ {
addPath(path, type, entry); addPath(path, type, entry);
} }
void addPoint(PathNodePoint point) { fullPath.push_back(point); } void addPoint(PathNodePoint point) { fullPath.push_back(point); }
void addPoint(WorldPosition point, PathNodeType type = NODE_PATH, uint32 entry = 0) void addPoint(WorldPosition point,
PathNodeType type = PathNodeType::NODE_PATH,
uint32 entry = 0)
{ {
fullPath.push_back(PathNodePoint{point, type, entry}); fullPath.push_back(PathNodePoint{point, type, entry});
} }
void addPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0) void addPath(std::vector<WorldPosition> path,
PathNodeType type = PathNodeType::NODE_PATH,
uint32 entry = 0)
{ {
for (auto& p : path) for (auto& p : path)
{
fullPath.push_back(PathNodePoint{p, type, entry}); fullPath.push_back(PathNodePoint{p, type, entry});
};
} }
void addPath(std::vector<PathNodePoint> newPath) void addPath(std::vector<PathNodePoint> newPath)
{ {
@ -408,8 +465,11 @@ public:
} }
void clear() { fullPath.clear(); } void clear() { fullPath.clear(); }
bool empty() { return fullPath.empty(); } bool empty() const { return fullPath.empty(); }
std::vector<PathNodePoint> getPath() { return fullPath; } size_t size() const { return fullPath.size(); }
const PathNodePoint& operator[](size_t idx) const { return fullPath[idx]; }
std::vector<PathNodePoint> GetPath() { return fullPath; }
const std::vector<PathNodePoint>& GetPathRef() const { return fullPath; }
WorldPosition getFront() { return fullPath.front().point; } WorldPosition getFront() { return fullPath.front().point; }
WorldPosition getBack() { return fullPath.back().point; } WorldPosition getBack() { return fullPath.back().point; }
@ -419,13 +479,9 @@ public:
for (auto const& p : fullPath) for (auto const& p : fullPath)
retVec.push_back(p.point); retVec.push_back(p.point);
return retVec; return retVec;
}; }
bool makeShortCut(WorldPosition startPos, float maxDist); bool makeShortCut(WorldPosition startPos, float maxDist);
bool shouldMoveToNextPoint(WorldPosition startPos, std::vector<PathNodePoint>::iterator beg,
std::vector<PathNodePoint>::iterator ed, std::vector<PathNodePoint>::iterator p,
float& moveDist, float maxDist);
WorldPosition getNextPoint(WorldPosition startPos, float maxDist, TravelNodePathType& pathType, uint32& entry);
std::ostringstream const print(); std::ostringstream const print();
@ -438,16 +494,24 @@ class TravelNodeRoute
{ {
public: public:
TravelNodeRoute() {} TravelNodeRoute() {}
TravelNodeRoute(std::vector<TravelNode*> nodes1) { nodes = nodes1; /*currentNode = route.begin();*/ } TravelNodeRoute(std::vector<TravelNode*> nodes1)
{
nodes = nodes1;
}
bool isEmpty() { return nodes.empty(); } bool isEmpty() { return nodes.empty(); }
bool hasNode(TravelNode* node) { return findNode(node) != nodes.end(); } bool hasNode(TravelNode* node)
{
return findNode(node) != nodes.end();
}
float getTotalDistance(); float getTotalDistance();
std::vector<TravelNode*> getNodes() { return nodes; } std::vector<TravelNode*> getNodes() { return nodes; }
TravelPath buildPath(std::vector<WorldPosition> pathToStart = {}, std::vector<WorldPosition> pathToEnd = {}, TravelPath BuildPath(
std::vector<WorldPosition> pathToStart = {},
std::vector<WorldPosition> pathToEnd = {},
Unit* bot = nullptr); Unit* bot = nullptr);
std::ostringstream const print(); std::ostringstream const print();
@ -467,12 +531,47 @@ public:
TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; } TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; }
TravelNode* dataNode; TravelNode* dataNode;
float m_f = 0.0, m_g = 0.0, m_h = 0.0; float totalCost = 0.0;
bool open = false, close = false; float costFromStart = 0.0;
float heuristic = 0.0;
bool open = false;
bool closed = false;
TravelNodeStub* parent = nullptr; TravelNodeStub* parent = nullptr;
uint32 currentGold = 0; uint32 currentGold = 0;
}; };
struct TravelPlan
{
WorldPosition destination;
// Flat waypoint path built upfront by GetFullPath:
TravelPath steps;
uint32 stepIdx{0};
// Spline scratch (used by executor):
std::vector<G3D::Vector3> walkPoints;
bool splineActive{false};
uint32 splineStartTime{0};
uint32 expectedDuration{0};
// Taxi scratch:
std::vector<uint32> route;
bool IsActive() const { return !steps.empty(); }
void Reset()
{
destination = WorldPosition();
steps.clear();
stepIdx = 0;
walkPoints.clear();
splineActive = false;
splineStartTime = 0;
expectedDuration = 0;
route.clear();
}
};
// The container of all nodes. // The container of all nodes.
class TravelNodeMap class TravelNodeMap
{ {
@ -484,14 +583,18 @@ public:
return instance; return instance;
} }
TravelNode* addNode(WorldPosition pos, std::string const preferedName = "Travel Node", bool isImportant = false, TravelNode* addNode(WorldPosition pos,
bool checkDuplicate = true, bool transport = false, uint32 transportId = 0); std::string const preferedName = "Travel Node",
bool isImportant = false,
bool checkDuplicate = true,
bool transport = false,
uint32 transportId = 0);
void removeNode(TravelNode* node); void removeNode(TravelNode* node);
bool removeNodes() bool removeNodes()
{ {
if (m_nMapMtx.try_lock_for(std::chrono::seconds(10))) if (m_nMapMtx.try_lock_for(std::chrono::seconds(10)))
{ {
for (auto& node : m_nodes) for (auto& node : nodes)
removeNode(node); removeNode(node);
m_nMapMtx.unlock(); m_nMapMtx.unlock();
@ -499,28 +602,32 @@ public:
} }
return false; return false;
}; }
void fullLinkNode(TravelNode* startNode, Unit* bot); void fullLinkNode(TravelNode* startNode, Unit* bot);
// Get all nodes // Get all nodes
std::vector<TravelNode*> getNodes() { return m_nodes; } std::vector<TravelNode*> getNodes() { return nodes; }
std::vector<TravelNode*> getNodes(WorldPosition pos, float range = -1); std::vector<TravelNode*> getNodes(WorldPosition pos, float range = -1);
// Find nearest node. // Find nearest node.
TravelNode* getNode(TravelNode* sameNode) TravelNode* getNode(TravelNode* sameNode)
{ {
for (auto& node : m_nodes) for (auto& node : nodes)
{ {
if (node->getName() == sameNode->getName() && node->getPosition() == sameNode->getPosition()) if (node->getName() == sameNode->getName()
&& node->getPosition() == sameNode->getPosition())
return node; return node;
} }
return nullptr; return nullptr;
} }
TravelNode* getNode(WorldPosition pos, std::vector<WorldPosition>& ppath, Unit* bot = nullptr, float range = -1); TravelNode* getNode(WorldPosition pos,
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr, float range = -1) std::vector<WorldPosition>& ppath,
Unit* bot = nullptr, float range = -1);
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr,
float range = -1)
{ {
std::vector<WorldPosition> ppath; std::vector<WorldPosition> ppath;
return getNode(pos, ppath, bot, range); return getNode(pos, ppath, bot, range);
@ -536,19 +643,17 @@ public:
return rNodes[urand(0, rNodes.size() - 1)]; return rNodes[urand(0, rNodes.size() - 1)];
} }
// Finds the best nodePath between two nodes // Finds the best nodePath between two nodes (A* over the node graph)
TravelNodeRoute getRoute(TravelNode* start, TravelNode* goal, Player* bot = nullptr); TravelNodeRoute GetNodeRoute(TravelNode* start, TravelNode* goal,
Player* bot);
// Find the best node between two positions // Picks the nearest start/end nodes for two world positions and runs A*
TravelNodeRoute getRoute(WorldPosition startPos, WorldPosition endPos, std::vector<WorldPosition>& startPath, // over the node graph to return a full route between them.
TravelNodeRoute FindRouteNearestNodes(WorldPosition startPos,
WorldPosition endPos,
std::vector<WorldPosition>& startPath,
Player* bot = nullptr); Player* bot = nullptr);
// Find the full path between those locations
static TravelPath getFullPath(WorldPosition startPos, WorldPosition endPos, Player* bot = nullptr);
// Manage/update nodes
void manageNodes(Unit* bot, bool mapFull = false);
void setHasToGen() { hasToGen = true; } void setHasToGen() { hasToGen = true; }
void generateNpcNodes(); void generateNpcNodes();
@ -563,15 +668,17 @@ public:
void removeUselessPaths(); void removeUselessPaths();
void calculatePathCosts(); void calculatePathCosts();
void generateTaxiPaths(); void generateTaxiPaths();
void generatePaths(); void generatePaths(bool fullGen = false);
void generateAll(); void generateAll();
void Init();
void printMap(); void printMap();
void printNodeStore(); void printNodeStore();
void saveNodeStore(); void saveNodeStore();
void loadNodeStore(); void LoadNodeStore();
bool cropUselessNode(TravelNode* startNode); bool cropUselessNode(TravelNode* startNode);
TravelNode* addZoneLinkNode(TravelNode* startNode); TravelNode* addZoneLinkNode(TravelNode* startNode);
@ -584,8 +691,24 @@ public:
void InitTaxiGraph(); void InitTaxiGraph();
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode); std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
void BuildZoneIndex();
void PrecomputeReachability();
TravelNode* GetNearestNodeInZone(WorldPosition pos, uint32 zoneId);
TravelNode* GetNearestNodeOnMap(WorldPosition pos);
bool GetFullPath(TravelPlan& plan, WorldPosition botPos,
uint32 botZoneId, WorldPosition destination);
// Resolve A* route between two world positions (returns node vector)
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,
WorldPosition endPos);
// Get stored walk points for one edge (from→to). Empty if no path.
std::vector<G3D::Vector3> GetEdgeWalkPoints(TravelNode* from,
TravelNode* to);
std::shared_timed_mutex m_nMapMtx; std::shared_timed_mutex m_nMapMtx;
std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes;
private: private:
TravelNodeMap() = default; TravelNodeMap() = default;
@ -601,13 +724,18 @@ private:
void BuildTaxiGraph(); void BuildTaxiGraph();
void ComputeAllPaths(); void ComputeAllPaths();
std::unordered_map<uint32, uint32> BFS(uint32 startNode); std::unordered_map<uint32, uint32> BFS(uint32 startNode);
std::vector<uint32> BuildPath(uint32 fromNode, uint32 toNode, std::vector<uint32> BuildPath(
uint32 fromNode, uint32 toNode,
const std::unordered_map<uint32, uint32>& parentMap); const std::unordered_map<uint32, uint32>& parentMap);
std::unordered_map<uint32, std::vector<uint32>> taxiGraph; std::unordered_map<uint32, std::vector<uint32>> m_taxiGraph;
std::map<uint32, std::map<uint32, std::vector<uint32>>> taxiPathCache; std::map<uint32, std::map<uint32, std::vector<uint32>>>
m_taxiPathCache;
std::vector<TravelNode*> m_nodes; std::vector<TravelNode*> nodes;
std::unordered_map<uint32, std::vector<TravelNode*>> m_zoneIndex;
std::unordered_map<uint32, std::vector<TravelNode*>> m_mapIndex;
std::vector<std::pair<uint32, WorldPosition>> mapOffsets; std::vector<std::pair<uint32, WorldPosition>> mapOffsets;

View File

@ -674,6 +674,7 @@ bool PlayerbotAIConfig::Initialize()
autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false); autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false);
autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true); autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true);
enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true); enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true);
enableTravelNodes = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableTravelNodes", false);
RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15); RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15);
RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20); RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20);

View File

@ -370,6 +370,7 @@ public:
bool autoLearnTrainerSpells; bool autoLearnTrainerSpells;
bool autoDoQuests; bool autoDoQuests;
bool enableNewRpgStrategy; bool enableNewRpgStrategy;
bool enableTravelNodes;
std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight; std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight;
bool syncLevelWithPlayers; bool syncLevelWithPlayers;
bool autoLearnQuestSpells; bool autoLearnQuestSpells;

View File

@ -20,6 +20,7 @@
#include "PlayerbotMgr.h" #include "PlayerbotMgr.h"
#include "RandomPlayerbotMgr.h" #include "RandomPlayerbotMgr.h"
#include "ScriptMgr.h" #include "ScriptMgr.h"
#include "TravelNode.h"
using namespace Acore::ChatCommands; using namespace Acore::ChatCommands;
@ -41,11 +42,16 @@ public:
{"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No}, {"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No},
}; };
static ChatCommandTable playerbotsTravelCommandTable = {
{"generatenode", HandleGenerateTravelNodesCommand, SEC_GAMEMASTER, Console::Yes},
};
static ChatCommandTable playerbotsCommandTable = { static ChatCommandTable playerbotsCommandTable = {
{"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No}, {"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No},
{"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes}, {"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes},
{"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes}, {"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes},
{"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes}, {"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes},
{"travel", playerbotsTravelCommandTable},
{"debug", playerbotsDebugCommandTable}, {"debug", playerbotsDebugCommandTable},
{"account", playerbotsAccountCommandTable}, {"account", playerbotsAccountCommandTable},
}; };
@ -106,6 +112,15 @@ public:
return true; return true;
} }
static bool HandleGenerateTravelNodesCommand(ChatHandler* handler, char const* /*args*/)
{
handler->PSendSysMessage("Regenerating travel node paths...");
LOG_INFO("playerbots", "Manual travel node regeneration started via console command.");
sTravelNodeMap.generateAll();
handler->PSendSysMessage("Travel node regeneration complete. Paths saved to database.");
return true;
}
static bool HandleDebugBGCommand(ChatHandler* handler, char const* args) static bool HandleDebugBGCommand(ChatHandler* handler, char const* args)
{ {
return BGTactics::HandleConsoleCommand(handler, args); return BGTactics::HandleConsoleCommand(handler, args);