Compare commits

...

31 Commits

Author SHA1 Message Date
bash
e92af1cc06 fix(Core/Movement): AC has no MAX_GAMEOBJECT_TYPE sentinel + no sAreaTriggerStore DBC store 2026-05-31 19:07:32 +02:00
bash
b97da5c741 fix(Core/Movement): MoveNear engine-aware near-point + FollowOnTransport port + drop dead botZoneId 2026-05-31 19:05:06 +02:00
bash
cbd5f8748c fix(Core/Movement): Align HandleSpecialMovement + ClipPath details with reference 2026-05-31 18:53:11 +02:00
bash
1d85510a9e refactor(Core/Movement): Close walking-path divergences from reference (A-E) 2026-05-31 18:38:29 +02:00
bash
c82cd18677 fix(Core/Movement): Take TravelPath/WorldPosition by value in DispatchMovement 2026-05-31 18:20:45 +02:00
bash
566f4975e6 docs(Core/Travel): Note why setAreaCost(12,13) is not ported (mmap dataset diverges) 2026-05-31 18:19:02 +02:00
bash
dae09388ad refactor(Core/Movement): DispatchPathPoints → DispatchMovement (TravelPath sig + transport sandwich) 2026-05-31 18:15:24 +02:00
bash
d00ad8d327 refactor(Core/Movement): WaitForReach formula parity + PointsArray overload 2026-05-31 18:05:35 +02:00
bash
32b687f00a fix(Core/Movement): Take WorldPosition by value in MoveTo2 (IsValid is non-const) 2026-05-31 18:01:15 +02:00
bash
7f7cfb33d8 fix(Core/Movement): WorldPosition::IsValid is PascalCase on AC 2026-05-31 18:00:18 +02:00
bash
bd7422e98b refactor(Core/Movement): Funnel all MoveTo through MoveTo2 path-aware pipeline 2026-05-31 17:55:30 +02:00
bash
97b3e345a8 fix(Core/Debug): Inline zone filter in showpath cmd — GetNodesInZone was removed 2026-05-31 17:12:45 +02:00
bash
0eff76f3ec fix(Core/Travel): Remove stray '}' left over from dead-code sweep 2026-05-31 17:09:47 +02:00
bash
2c822affd2 fix(Core/Movement): Drop FORCED_MOVEMENT_FLIGHT — AC enum has no FLIGHT variant 2026-05-31 17:07:31 +02:00
bash
7cb9dc622c fix(Core/Movement): Restore SpellAuraEffects.h (provides AuraEffect definition) 2026-05-31 16:49:38 +02:00
bash
fa070f5e07 fix(Core/Movement): Restore PositionValue.h include (provides PositionInfo type) 2026-05-31 16:48:09 +02:00
bash
783210c4d0 refactor: Dead-code sweep — TravelMgr.cpp 5x /* */ blocks (-823 lines) + NewRpgBaseAction 5 unused includes 2026-05-31 16:46:11 +02:00
bash
d8c4425409 refactor(Core/Travel): Drop dead zone-index machinery + isEqual + cropUselessLink(single) (-124 lines) 2026-05-31 16:41:11 +02:00
bash
ae0baa3fcc refactor: Dead-code sweep — Follow /* */ block + TravelNode 2x /* */ blocks + LastMovement.lastFollow + 6 unused includes 2026-05-31 16:13:45 +02:00
bash
cd65fda93b refactor(Core/Movement): Remove partial vehicle handling in MoveTo+ChaseTo (deferred to dedicated PR) 2026-05-31 16:00:39 +02:00
bash
3813909341 refactor(Core/Movement): Dead-code sweep — drop old MoveTo commented block + ANGLE_45_DEG + cropUselessNode + addZoneLinkNode + addRandomExtNode 2026-05-31 15:55:40 +02:00
bash
a2d0b4530b refactor(Core/Travel): Drop unused MAX_PATHFINDING_DISTANCE constant (orphaned by ExecuteTravelPlan removal) 2026-05-31 15:50:01 +02:00
bash
1cf604dc60 refactor(Core/Travel): Remove teleportSpell + NODE_TELEPORT + PortalNode + hearthstone/mage A* injection (staticPortal kept) 2026-05-31 15:48:57 +02:00
bash
c6e0cc9cef refactor(Core/Travel): Simplify transport config to TransportSkipRide bool; drop mode-0 deck-walk approximations 2026-05-31 15:38:45 +02:00
bash
fff692e3f3 feat(Core/Movement): Mode-0 transport board/disembark — snap-and-deck-walk on board, NearTeleport+walk on disembark 2026-05-31 15:29:36 +02:00
bash
f6c41f57e4 feat(Core/Movement): BoardTransport mode 1 — teleport directly to boarding edge when transportTeleportType >= 1 2026-05-31 15:21:39 +02:00
bash
6aea0c2ba7 feat(Core/Travel): Add transportTeleportType config + teleport-across-water branch in UpcommingSpecialMovement 2026-05-31 15:17:20 +02:00
bash
e165a1e79b fix(Core/Movement): MoveFarTo re-caches lastPath after UpcommingSpecialMovement (matches reference) 2026-05-31 13:55:31 +02:00
bash
42768fe360 fix(Core/Movement): WaitForTransport now actively disembarks (matches reference UseTransport flow) 2026-05-31 13:54:27 +02:00
bash
eb97533387 fix(Core/Movement): MoveFarTo clears lastMove on collapse + drops AC single-point branch; DispatchPathPoints mirrors reference dispatch order (Clear -> MovePoint(last) -> MoveSplinePath) 2026-05-31 13:52:44 +02:00
bash
ec52e5c310 fix(Core/Travel): GetFullPath now reuses failed probe waypoints as startPath via cropPathTo (matches reference) 2026-05-31 13:49:59 +02:00
14 changed files with 702 additions and 2320 deletions

View File

@ -1065,6 +1065,16 @@ AiPlayerbot.EnableNewRpgStrategy = 1
# Default: 0 (disabled) # Default: 0 (disabled)
AiPlayerbot.EnableTravelNodes = 0 AiPlayerbot.EnableTravelNodes = 0
# Transport ride mode (travel node graph only):
# 0 = bot walks to dock, teleport-snaps onto transport, rides, teleport-snaps off
# 1 = bot teleports directly across the transport route, skipping the ride
# Default 0 is the visible behavior. Set to 1 for invisible/random-bot mass
# travel performance — the bot never actually boards anything.
# (AC has no transport-surface mmap, so an on-deck walking mode can't be
# faithfully implemented; the on-board phase always teleport-snaps.)
# Default: 0
AiPlayerbot.TransportSkipRide = 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.
# #

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,6 @@ class Unit;
class WorldObject; class WorldObject;
class Position; class Position;
#define ANGLE_45_DEG (static_cast<float>(M_PI) / 4.f)
#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)
@ -57,7 +56,54 @@ protected:
bool MoveTo(uint32 mapId, float x, float y, float z, bool idle = false, bool react = false, bool MoveTo(uint32 mapId, float x, float y, float z, bool idle = false, bool react = false,
bool normal_only = false, bool exact_waypoint = false, bool normal_only = false, bool exact_waypoint = false,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, bool lessDelay = false, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, bool lessDelay = false,
bool backwards = false); bool backwards = false, bool ignoreEnemyTargets = false);
// Path-aware funnel mirroring the reference movement implementation.
// Runs UpdateMovementState + IsMovingAllowed + WaitForTransport gates,
// applies the targetPosRecalcDistance short-stop, resolves a TravelPath
// via ResolveMovePath (which gates graph A* by sightDistance), trims
// with makeShortCut, handles special head segments
// (portal/area-trigger/transport/flight) via HandleSpecialMovement,
// clips at hostile creatures via ClipPath (unless ignoreEnemyTargets),
// and dispatches the resulting walk via DispatchMovement.
// MoveTo(mapId,...) delegates here unless an intentional bypass
// (exact_waypoint / disableMoveSplinePath / flying / swimming /
// backwards) routes the move straight to DoMovePoint.
// `react=true` opts the move out of the end-of-dispatch
// WaitForReach AI-loop block — combat callers should set this so the
// bot can keep re-evaluating mid-chase. Default false matches the
// reference's MoveTo2 default.
bool MoveTo2(WorldPosition endPos,
bool idle = false, bool react = false,
bool noPath = false, bool ignoreEnemyTargets = false,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL,
bool lessDelay = false);
// Centralized walk dispatch. Mirrors the reference's DispatchMovement
// shape: takes a TravelPath, builds the PointsArray internally,
// applies inactive-bot teleport carve-out, masterWalking mode,
// pre-dispatch state cleanup (clear emote, stand, interrupt cast),
// transport-passenger coordinate sandwich
// (CalculatePassengerPosition → UpdateAllowedPositionZ → Offset)
// around the per-point Z snap, mm.Clear → MovePoint(last) →
// MoveSplinePath. Caches the destination + duration on lastMove.
//
// Divergence from reference: reference ends with WaitForReach(size)
// which blocks the AI loop until the move completes. AC's combat
// callers (ReachCombatTo) currently funnel through MoveTo → MoveTo2
// → DispatchMovement; blocking the AI loop here would suspend combat
// re-evaluation for the full move duration. Until combat dispatch is
// restructured to bypass MoveTo2, the WaitForReach is deliberately
// omitted.
// `react=true` skips the end-of-dispatch WaitForReach so the AI
// loop isn't blocked while the spline plays — combat callers use
// this to keep re-evaluating mid-chase.
bool DispatchMovement(TravelPath path,
WorldPosition dest,
char const* label,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL,
bool lessDelay = false,
bool react = false);
bool MoveTo(WorldObject* target, float distance = 0.0f, bool MoveTo(WorldObject* target, float distance = 0.0f,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool MoveNear(WorldObject* target, float distance = sPlayerbotAIConfig.contactDistance, bool MoveNear(WorldObject* target, float distance = sPlayerbotAIConfig.contactDistance,
@ -65,10 +111,22 @@ protected:
float GetFollowAngle(); float GetFollowAngle();
bool Follow(Unit* target, float distance = sPlayerbotAIConfig.followDistance); bool Follow(Unit* target, float distance = sPlayerbotAIConfig.followDistance);
bool Follow(Unit* target, float distance, float angle); bool Follow(Unit* target, float distance, float angle);
// Handles the cross-transport follow case: when bot and target are
// on different transports (or one is off-transport) and within
// sight, this disembarks the bot from its current transport (if
// any), teleports it to the target's position, and boards the
// target's transport (if any). Returns true if the transport
// transition was performed this tick (caller should skip the
// engine-level follow for this tick).
bool FollowOnTransport(Unit* target);
bool ChaseTo(WorldObject* obj, float distance = 0.0f); bool ChaseTo(WorldObject* obj, float distance = 0.0f);
bool ReachCombatTo(Unit* target, float distance = 0.0f); bool ReachCombatTo(Unit* target, float distance = 0.0f);
float MoveDelay(float distance, bool backwards = false); float MoveDelay(float distance, bool backwards = false);
void WaitForReach(float distance); void WaitForReach(float distance);
// PointsArray overload: sums segment distances and calls the float
// version. Matches the reference's WaitForReach(PointsArray) used at
// the end of DispatchMovement.
void WaitForReach(Movement::PointsArray const& path);
void SetNextMovementDelay(float delayMillis); void SetNextMovementDelay(float delayMillis);
bool IsMovingAllowed(WorldObject* target); bool IsMovingAllowed(WorldObject* target);
bool IsDuplicateMove(float x, float y, float z); bool IsDuplicateMove(float x, float y, float z);

View File

@ -12,7 +12,6 @@ LastMovement::LastMovement() { clear(); }
LastMovement::LastMovement(LastMovement& other) LastMovement::LastMovement(LastMovement& other)
: taxiNodes(other.taxiNodes), : taxiNodes(other.taxiNodes),
taxiMaster(other.taxiMaster), taxiMaster(other.taxiMaster),
lastFollow(other.lastFollow),
lastAreaTrigger(other.lastAreaTrigger), lastAreaTrigger(other.lastAreaTrigger),
lastFlee(other.lastFlee) lastFlee(other.lastFlee)
{ {
@ -27,7 +26,6 @@ void LastMovement::clear()
{ {
lastMoveShort = WorldPosition(); lastMoveShort = WorldPosition();
lastPath.clear(); lastPath.clear();
lastFollow = nullptr;
lastAreaTrigger = 0; lastAreaTrigger = 0;
lastFlee = 0; lastFlee = 0;
nextTeleport = 0; nextTeleport = 0;
@ -36,17 +34,18 @@ void LastMovement::clear()
lastTransportEntry = 0; lastTransportEntry = 0;
} }
void LastMovement::Set(Unit* follow) void LastMovement::Set([[maybe_unused]] Unit* follow)
{ {
// Legacy signature — `follow` is ignored (lastFollow field removed).
// The function still serves callers that want a soft-reset:
// clears short + path, resets msTime/priority via the chain below.
Set(0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); Set(0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
setShort(WorldPosition()); setShort(WorldPosition());
setPath(TravelPath()); setPath(TravelPath());
lastFollow = follow;
} }
void LastMovement::Set(uint32 mapId, float x, float y, float z, float ori, float delayTime, MovementPriority pri) void LastMovement::Set(uint32 mapId, float x, float y, float z, float ori, float delayTime, MovementPriority pri)
{ {
lastFollow = nullptr;
lastMoveShort = WorldPosition(mapId, x, y, z, ori); lastMoveShort = WorldPosition(mapId, x, y, z, ori);
msTime = getMSTime(); msTime = getMSTime();
priority = pri; priority = pri;
@ -55,7 +54,6 @@ void LastMovement::Set(uint32 mapId, float x, float y, float z, float ori, float
void LastMovement::setShort(WorldPosition point) void LastMovement::setShort(WorldPosition point)
{ {
lastMoveShort = point; lastMoveShort = point;
lastFollow = nullptr;
} }
void LastMovement::setPath(TravelPath path) { lastPath = path; } void LastMovement::setPath(TravelPath path) { lastPath = path; }

View File

@ -33,7 +33,6 @@ public:
{ {
taxiNodes = other.taxiNodes; taxiNodes = other.taxiNodes;
taxiMaster = other.taxiMaster; taxiMaster = other.taxiMaster;
lastFollow = other.lastFollow;
lastAreaTrigger = other.lastAreaTrigger; lastAreaTrigger = other.lastAreaTrigger;
lastMoveShort = other.lastMoveShort; lastMoveShort = other.lastMoveShort;
lastPath = other.lastPath; lastPath = other.lastPath;
@ -53,7 +52,6 @@ public:
std::vector<uint32> taxiNodes; std::vector<uint32> taxiNodes;
ObjectGuid taxiMaster; ObjectGuid taxiMaster;
Unit* lastFollow;
uint32 lastAreaTrigger; uint32 lastAreaTrigger;
time_t lastFlee; time_t lastFlee;
WorldPosition lastMoveShort; WorldPosition lastMoveShort;

View File

@ -9,12 +9,8 @@
#include "Creature.h" #include "Creature.h"
#include "G3D/Vector2.h" #include "G3D/Vector2.h"
#include "GameObject.h" #include "GameObject.h"
#include "GossipDef.h"
#include "GridTerrainData.h"
#include "IVMapMgr.h"
#include "Item.h" #include "Item.h"
#include "ItemTemplate.h" #include "ItemTemplate.h"
#include "LootMgr.h"
#include "Map.h" #include "Map.h"
#include "ModelIgnoreFlags.h" #include "ModelIgnoreFlags.h"
#include "MotionMaster.h" #include "MotionMaster.h"
@ -34,7 +30,6 @@
#include "PlayerbotTextMgr.h" #include "PlayerbotTextMgr.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "Position.h" #include "Position.h"
#include "QuestDef.h"
#include "Random.h" #include "Random.h"
#include "RandomPlayerbotMgr.h" #include "RandomPlayerbotMgr.h"
#include "SharedDefines.h" #include "SharedDefines.h"
@ -53,218 +48,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
EmitDebugMove("MoveFar", "empty-dest", 0.0f, 0.0f, 0.0f); EmitDebugMove("MoveFar", "empty-dest", 0.0f, 0.0f, 0.0f);
return false; return false;
} }
return MoveTo2(dest);
UpdateMovementState();
if (!IsMovingAllowed())
{
EmitDebugMove("MoveFar", "cant-move",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
// Resume a transport ride if we're still on the same boat as last tick.
if (WaitForTransport())
return true;
WorldPosition botPos(bot);
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
// Short-stop: at destination — stop and clear the cached path.
{
float const totalDistance = botPos.distance(dest);
if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance)
{
if (!lastMove.lastPath.empty() &&
lastMove.lastPath.getBack().distance(dest) <= totalDistance)
lastMove.clear();
bot->StopMoving();
EmitDebugMove("MoveFar", "arrived",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
}
// Per-tick re-resolve: rebuild the TravelPath from the bot's current
// position every tick (10% reuse short-circuits via the cached
// lastPath). Recovers naturally from knockback, off-route drift,
// destination changes, and blocked waypoints.
TravelPath path = ResolveMovePath(botPos, dest, lastMove);
lastMove.setPath(path);
if (path.empty())
{
EmitDebugMove("MoveFar", "no-path",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
// Trim leading waypoints behind the bot, bridge with mmap probe if
// the new head requires it. May empty the path (collapsed) — let
// the next tick rebuild from a fresh start.
path.makeShortCut(botPos, sPlayerbotAIConfig.reactDistance, bot);
if (path.empty())
return true;
// Special head segment (portal / area-trigger / transport / flight)?
// UpcommingSpecialMovement cuts the path so the head is the special;
// HandleSpecialMovement dispatches the matching action.
bool const onTransport = bot->GetTransport() != nullptr;
if (path.UpcommingSpecialMovement(botPos,
sPlayerbotAIConfig.reactDistance,
onTransport))
{
if (HandleSpecialMovement(path))
return true;
// Special handler declined (e.g. AREA_TRIGGER with entry → caller
// dispatches the walk into the trigger volume). Fall through.
}
// Transport guard: bot is on a transport but no special movement
// applies this tick — don't dispatch a walk spline (would fight the
// transport's own movement).
if (onTransport)
return false;
// ClipPath — truncate at first hostile creature in range / non-walkable
// hop / drifted past reactDistance / > 125 sqDist jump.
path.ClipPath(botAI, bot, false);
if (path.empty())
return false;
// Telemetry: show the path's actual tail coords vs bot + dest so we
// can see whether the resolved path is heading toward the right
// place. dest-distance == 0 means tail IS the dest (good); large
// dest-distance means graph picked a far-off endNode.
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
{
WorldPosition tail = path.getBack();
float const tailToDest = tail.distance(dest);
float const botToTail = bot->GetExactDist(tail.GetPositionX(),
tail.GetPositionY(),
tail.GetPositionZ());
std::ostringstream tlog;
tlog << "[PATH] tail=(" << std::fixed << std::setprecision(1)
<< tail.GetPositionX() << "," << tail.GetPositionY() << "," << tail.GetPositionZ()
<< ") botToTail=" << botToTail << "y tailToDest=" << tailToDest << "y";
botAI->TellMasterNoFacing(tlog);
}
// Walk dispatch.
std::vector<WorldPosition> const& pts = path.getPointPath();
Movement::PointsArray points;
points.reserve(pts.size());
for (auto const& wp : pts)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
if (points.size() < 2)
{
// Single-point fallback path (cmangos pattern: ResolveMovePath
// emits a single dest point if nothing else worked). Hand it
// to the engine's MovePoint via MoveTo.
EmitDebugMove("MoveFar", "single-point",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return MoveTo(dest.GetMapId(), dest.GetPositionX(),
dest.GetPositionY(), dest.GetPositionZ(),
false, false, false, false);
}
if (!bot->IsMounted() && !bot->IsInCombat() &&
bot->IsOutdoors() && bot->IsAlive())
botAI->DoSpecificAction("check mount state", Event(), true);
return DispatchPathPoints(dest, points, "walk");
}
bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label)
{
if (points.size() < 2)
return false;
// MoveFarTo runs makeShortCut + setPath upstream now, so no need
// for the local prefix-trim or lastMove.setPath here.
for (auto& pt : points)
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
// ClipPath now runs at MoveFarTo level on the TravelPath before the
// points array is built. No per-dispatch clip here.
if (points.size() < 2)
return false;
G3D::Vector3 const& last = points.back();
float totalDist = 0.f;
for (size_t i = 1; i < points.size(); ++i)
totalDist += (points[i] - points[i - 1]).length();
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
// Skip cosmetic walking for random bots with no nearby player —
// teleport to the path tail and schedule a cooldown instead.
if (sRandomPlayerbotMgr.IsRandomBot(bot))
{
WorldPosition tail(dest.GetMapId(), last.x, last.y, last.z);
time_t now = time(nullptr);
if (totalDist > sPlayerbotAIConfig.reactDistance &&
lastMove.nextTeleport <= now &&
!botAI->HasPlayerNearby(&tail))
{
float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f);
lastMove.nextTeleport = now + (time_t)(totalDist / speed);
EmitDebugMove("MoveFar", "teleport",
tail.GetPositionX(), tail.GetPositionY(), tail.GetPositionZ());
WorldPosition botPos(bot);
return bot->TeleportTo(dest.GetMapId(),
tail.GetPositionX(), tail.GetPositionY(),
tail.GetPositionZ(),
botPos.getAngleTo(tail));
}
}
// Match master's walk pace when they're walking and within 5y.
ForcedMovement moveMode = FORCED_MOVEMENT_RUN;
if (Player* master = botAI->GetMaster())
{
if (bot->IsFriendlyTo(master) && master->IsWalking() &&
bot->GetExactDist2d(master) < 5.0f)
{
moveMode = FORCED_MOVEMENT_WALK;
}
}
// Clear emote/sit/cast so the spline can begin cleanly.
bot->ClearEmoteState();
if (!bot->IsStandState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
if (bot->IsNonMeleeSpellCast(true))
bot->InterruptNonMeleeSpells(true);
bot->GetMotionMaster()->Clear();
bot->GetMotionMaster()->MoveSplinePath(&points, moveMode);
EmitDebugMove("MoveFar", label, last.x, last.y, last.z);
// WaitForReach: leave ~10y headroom on long paths.
float waitDist = totalDist > sPlayerbotAIConfig.reactDistance
? std::max(totalDist - 10.0f, 0.0f) : totalDist;
UnitMoveType const speedType = (moveMode == FORCED_MOVEMENT_WALK) ? MOVE_WALK : MOVE_RUN;
float speed = std::max(bot->GetSpeed(speedType), 0.1f);
float duration = 1000.0f * (waitDist / speed) + sPlayerbotAIConfig.reactDelay;
duration = std::min(duration, (float)sPlayerbotAIConfig.maxWaitForMove);
if (duration < 0.0f)
duration = 0.0f;
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z,
bot->GetOrientation(), (uint32)duration,
MovementPriority::MOVEMENT_NORMAL);
return true;
} }
bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance) bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)

View File

@ -82,15 +82,6 @@ protected:
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus); bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status); bool CheckRpgStatusAvailable(NewRpgStatus status);
private:
// Centralized dispatch helper. Applies underwater fixup, ClipPath
// (truncate at first hostile in attack range with LOS, level+5 cap),
// inactive-bot teleport (with self-bot carve-out), masterWalking
// mode, pre-dispatch state cleanup, then dispatches via
// MoveSplinePath and schedules via WaitForReach formula.
bool DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label);
}; };
#endif #endif

View File

@ -274,6 +274,27 @@ bool WorldPosition::isUnderWater()
: false; : false;
}; };
bool WorldPosition::setAtWaterSurface()
{
if (!isInWater() && !isUnderWater())
return false;
Map* map = getMap();
if (!map)
return false;
// Returns the water level when liquid is present; falls back to
// ground level otherwise. Our isInWater/isUnderWater preconditions
// ensure liquid exists, so the +0.5y nudge lands the point on top
// of the surface (matches the reference's surface snap).
float const level = map->GetWaterOrGroundLevel(PHASEMASK_NORMAL,
GetPositionX(),
GetPositionY(),
GetPositionZ());
if (level <= INVALID_HEIGHT)
return false;
setZ(level + 0.5f);
return true;
}
bool WorldPosition::IsValid() bool WorldPosition::IsValid()
{ {
return !(GetMapId() == MAPID_INVALID && GetPositionX() == 0 && GetPositionY() == 0 && GetPositionZ() == 0); return !(GetMapId() == MAPID_INVALID && GetPositionX() == 0 && GetPositionY() == 0 && GetPositionZ() == 0);
@ -723,6 +744,13 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
// fire — apply the same bot cost biases here so generated paths // fire — apply the same bot cost biases here so generated paths
// match what bots prefer at runtime (STEEP/water are reachable // match what bots prefer at runtime (STEEP/water are reachable
// but not preferred). // but not preferred).
//
// Reference also applies setAreaCost(12, 5) + setAreaCost(13, 20)
// here. Not ported: reference and AC use different mmap generators
// and Detour area-id assignments diverge — raw IDs 12/13 are
// unlikely to match any polys on AC's navmesh and could no-op or
// bias something unintended. If we ever regenerate mmaps to match
// the reference dataset, revisit.
path.SetNavTerrainCost(NAV_GROUND_STEEP, 5.0f); path.SetNavTerrainCost(NAV_GROUND_STEEP, 5.0f);
path.SetNavTerrainCost(NAV_WATER, 10.0f); path.SetNavTerrainCost(NAV_WATER, 10.0f);
auto result = getPathStepFrom(startPos, path); auto result = getPathStepFrom(startPos, path);
@ -1943,93 +1971,6 @@ void TravelMgr::LoadQuestTravelTable()
units.push_back(t_unit); units.push_back(t_unit);
} }
/*
// 0 1 2 3 4 5 6 7 8
std::string const query = "SELECT 0,guid,id,map,position_x,position_y,position_z,orientation, (SELECT COUNT(*) FROM
creature k WHERE c.id1 = k.id1) FROM creature c UNION ALL SELECT
1,guid,id,map,position_x,position_y,position_z,orientation, (SELECT COUNT(*) FROM gameobject h WHERE h.id = g.id)
FROM gameobject g";
QueryResult result = WorldDatabase.Query(query.c_str());
if (result)
{
do
{
Field* fields = result->Fetch();
t_unit.type = fields[0].Get<uint32>();
t_unit.guid = fields[1].Get<uint32>();
t_unit.entry = fields[2].Get<uint32>();
t_unit.map = fields[3].Get<uint32>();
t_unit.x = fields[4].Get<float>();
t_unit.y = fields[5].Get<float>();
t_unit.z = fields[6].Get<float>();
t_unit.o = fields[7].Get<float>();
t_unit.c = uint32(fields[8].Get<uint64>());
units.push_back(t_unit);
} while (result->NextRow());
LOG_INFO("playerbots", ">> Loaded {} units locations.", units.size());
}
else
{
LOG_ERROR("playerbots", ">> Error loading units locations.");
}
query = "SELECT 0, 0, id, quest FROM creature_queststarter UNION ALL SELECT 0, 1, id, quest FROM creature_questender
UNION ALL SELECT 1, 0, id, quest FROM gameobject_queststarter UNION ALL SELECT 1, 1, id, quest FROM
gameobject_questender"; result = WorldDatabase.Query(query.c_str());
if (result)
{
do
{
Field* fields = result->Fetch();
t_rel.type = fields[0].Get<uint32>();
t_rel.role = fields[1].Get<uint32>();
t_rel.entry = fields[2].Get<uint32>();
t_rel.questId = fields[3].Get<uint32>();
relations.push_back(t_rel);
} while (result->NextRow());
LOG_INFO("playerbots", ">> Loaded {} relations.", relations.size());
}
else
{
LOG_ERROR("playerbots", ">> Error loading relations.");
}
query = "SELECT 0, ct.entry, item FROM creature_template ct JOIN creature_loot_template clt ON (ct.lootid =
clt.entry) UNION ALL SELECT 0, entry, item FROM npc_vendor UNION ALL SELECT 1, gt.entry, item FROM
gameobject_template gt JOIN gameobject_loot_template glt ON (gt.TYPE = 3 AND gt.DATA1 = glt.entry)"; result =
WorldDatabase.Query(query.c_str());
if (result)
{
do
{
Field* fields = result->Fetch();
t_loot.type = fields[0].Get<uint32>();
t_loot.entry = fields[1].Get<uint32>();
t_loot.item = fields[2].Get<uint32>();
loots.push_back(t_loot);
} while (result->NextRow());
LOG_INFO("playerbots", ">> Loaded {} loot lists.", loots.size());
}
else
{
LOG_ERROR("playerbots", ">> Error loading loot lists.");
}
*/
LOG_INFO("playerbots", "Loading quest data."); LOG_INFO("playerbots", "Loading quest data.");
@ -2115,164 +2056,6 @@ void TravelMgr::LoadQuestTravelTable()
} }
} }
/*
if (loadQuestData && false)
{
for (auto& questId : questIds)
{
Quest* quest = questMap.find(questId)->second;
QuestContainer* container = new QuestContainer;
QuestTravelDestination* loc = nullptr;
WorldPosition point;
bool hasError = false;
//Relations
for (auto& r : relations)
{
if (questId != r.questId)
continue;
int32 entry = r.type == 0 ? r.entry : r.entry * -1;
loc = new QuestRelationTravelDestination(r.questId, entry, r.role, sPlayerbotAIConfig.tooCloseDistance,
sPlayerbotAIConfig.sightDistance); loc->setExpireDelay(5 * 60 * 1000); loc->setMaxVisitors(15, 0);
for (auto& u : units)
{
if (r.type != u.type || r.entry != u.entry)
continue;
int32 guid = u.type == 0 ? u.guid : u.guid * -1;
point = WorldPosition(u.map, u.x, u.y, u.z, u.o);
loc->addPoint(&point);
}
if (loc->getPoints(0).empty())
{
logQuestError(1, quest, r.role, entry);
delete loc;
continue;
}
if (r.role == 0)
{
container->questGivers.push_back(loc);
}
else
container->questTakers.push_back(loc);
}
//Mobs
for (uint32 i = 0; i < 4; i++)
{
if (quest->RequiredNpcOrGoCount[i] == 0)
continue;
uint32 reqEntry = quest->RequiredNpcOrGo[i];
loc = new QuestObjectiveTravelDestination(questId, reqEntry, i, sPlayerbotAIConfig.tooCloseDistance,
sPlayerbotAIConfig.sightDistance); loc->setExpireDelay(1 * 60 * 1000); loc->setMaxVisitors(100, 1);
for (auto& u : units)
{
int32 entry = u.type == 0 ? u.entry : u.entry * -1;
if (entry != reqEntry)
continue;
int32 guid = u.type == 0 ? u.guid : u.guid * -1;
point = WorldPosition(u.map, u.x, u.y, u.z, u.o);
loc->addPoint(&point);
}
if (loc->getPoints(0).empty())
{
logQuestError(2, quest, i, reqEntry);
delete loc;
hasError = true;
continue;
}
container->questObjectives.push_back(loc);
}
//Loot
for (uint32 i = 0; i < 4; i++)
{
if (quest->RequiredItemCount[i] == 0)
continue;
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(quest->RequiredItemId[i]);
if (!proto)
{
logQuestError(3, quest, i, 0, quest->RequiredItemId[i]);
hasError = true;
continue;
}
uint32 foundLoot = 0;
for (auto& l : loots)
{
if (l.item != quest->RequiredItemId[i])
continue;
int32 entry = l.type == 0 ? l.entry : l.entry * -1;
loc = new QuestObjectiveTravelDestination(questId, entry, i, sPlayerbotAIConfig.tooCloseDistance,
sPlayerbotAIConfig.sightDistance, l.item); loc->setExpireDelay(1 * 60 * 1000); loc->setMaxVisitors(100, 1);
for (auto& u : units)
{
if (l.type != u.type || l.entry != u.entry)
continue;
int32 guid = u.type == 0 ? u.guid : u.guid * -1;
point = WorldPosition(u.map, u.x, u.y, u.z, u.o);
loc->addPoint(&point);
}
if (loc->getPoints(0).empty())
{
logQuestError(4, quest, i, entry, quest->RequiredItemId[i]);
delete loc;
continue;
}
container->questObjectives.push_back(loc);
foundLoot++;
}
if (foundLoot == 0)
{
hasError = true;
logQuestError(5, quest, i, 0, quest->RequiredItemId[i]);
}
}
if (container->questTakers.empty())
logQuestError(7, quest);
if (!container->questGivers.empty() || !container->questTakers.empty() || hasError)
{
quests.insert(std::make_pair(questId, container));
for (auto loc : container->questGivers)
questGivers.push_back(loc);
}
}
LOG_INFO("playerbots", ">> Loaded {} quest details.", questIds.size());
}
*/
WorldPosition point; WorldPosition point;
@ -2407,531 +2190,7 @@ void TravelMgr::LoadQuestTravelTable()
// Node loading/generation is handled by TravelNodeMap::Init() called from TravelMgr::Init(). // Node loading/generation is handled by TravelNodeMap::Init() called from TravelMgr::Init().
/*
bool fullNavPointReload = false;
bool storeNavPointReload = true;
if (!fullNavPointReload && true)
TravelNodeStore::loadNodes();
//TravelNodeMap::instance().loadNodeStore();
for (auto node : TravelNodeMap::instance().getNodes())
{
node->setLinked(true);
}
bool reloadNavigationPoints = false || fullNavPointReload || storeNavPointReload;
if (reloadNavigationPoints)
{
LOG_INFO("playerbots", "Loading navigation points");
//Npc nodes
WorldPosition pos;
for (auto& u : units)
{
if (u.type != 0)
continue;
CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(u.entry);
if (!cInfo)
continue;
std::vector<uint32> allowedNpcFlags;
allowedNpcFlags.push_back(UNIT_NPC_FLAG_INNKEEPER);
allowedNpcFlags.push_back(UNIT_NPC_FLAG_FLIGHTMASTER);
//allowedNpcFlags.push_back(UNIT_NPC_FLAG_QUESTGIVER);
for (std::vector<uint32>::iterator i = allowedNpcFlags.begin(); i != allowedNpcFlags.end(); ++i)
{
if ((cInfo->npcflag & *i) != 0)
{
pos = WorldPosition(u.map, u.x, u.y, u.z, u.o);
std::string const nodeName = pos.getAreaName(false);
if ((cInfo->npcflag & UNIT_NPC_FLAG_INNKEEPER) != 0)
nodeName += " innkeeper";
else
nodeName += " flightMaster";
TravelNodeMap::instance().addNode(&pos, nodeName, true, true);
break;
}
}
}
//Build flight paths
for (uint32 i = 0; i < sTaxiPathStore.GetNumRows(); ++i)
{
TaxiPathEntry const* taxiPath = sTaxiPathStore.LookupEntry(i);
if (!taxiPath)
continue;
TaxiNodesEntry const* startTaxiNode = sTaxiNodesStore.LookupEntry(taxiPath->from);
if (!startTaxiNode)
continue;
TaxiNodesEntry const* endTaxiNode = sTaxiNodesStore.LookupEntry(taxiPath->to);
if (!endTaxiNode)
continue;
TaxiPathNodeList const& nodes = sTaxiPathNodesByPath[taxiPath->ID];
if (nodes.empty())
continue;
WorldPosition startPos(startTaxiNode->map_id, startTaxiNode->x, startTaxiNode->y, startTaxiNode->z);
WorldPosition endPos(endTaxiNode->map_id, endTaxiNode->x, endTaxiNode->y, endTaxiNode->z);
TravelNode* startNode = TravelNodeMap::instance().getNode(&startPos, nullptr, 15.0f);
TravelNode* endNode = TravelNodeMap::instance().getNode(&endPos, nullptr, 15.0f);
if (!startNode || !endNode)
continue;
std::vector<WorldPosition> ppath;
for (auto& n : nodes)
ppath.push_back(WorldPosition(n->mapid, n->x, n->y, n->z, 0.0));
float totalTime = startPos.getPathLength(ppath) / (450 * 8.0f);
TravelNodePath travelPath(0.1f, totalTime, (uint8) TravelNodePathType::flightPath, i, true);
travelPath.setPath(ppath);
startNode->setPathTo(endNode, travelPath);
}
//Unique bosses
for (auto& u : units)
{
if (u.type != 0)
continue;
CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(u.entry);
if (!cInfo)
continue;
pos = WorldPosition(u.map, u.x, u.y, u.z, u.o);
if (cInfo->rank == 3 || (cInfo->rank == 1 && !pos.isOverworld() && u.c == 1))
{
std::string const nodeName = cInfo->Name;
TravelNodeMap::instance().addNode(&pos, nodeName, true, true);
}
}
std::map<uint8, std::string> startNames;
startNames[RACE_HUMAN] = "Human";
startNames[RACE_ORC] = "Orc and Troll";
startNames[RACE_DWARF] = "Dwarf and Gnome";
startNames[RACE_NIGHTELF] = "Night Elf";
startNames[RACE_UNDEAD_PLAYER] = "Undead";
startNames[RACE_TAUREN] = "Tauren";
startNames[RACE_GNOME] = "Dwarf and Gnome";
startNames[RACE_TROLL] = "Orc and Troll";
startNames[RACE_DRAENEI] = "Draenei";
startNames[RACE_BLOODELF] = "Blood Elf";
for (uint32 i = 0; i < MAX_RACES; i++)
{
for (uint32 j = 0; j < MAX_CLASSES; j++)
{
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j);
if (!info)
continue;
pos = WorldPosition(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation);
std::string const nodeName = startNames[i] + " start";
TravelNodeMap::instance().addNode(&pos, nodeName, true, true);
}
}
//Transports
GameObjectTemplateContainer const* goTemplates = sObjectMgr->GetGameObjectTemplates();
for (auto const& iter : *goTemplates)
{
GameObjectTemplate const* data = &iter.second;
if (data && (data->type == GAMEOBJECT_TYPE_TRANSPORT || data->type == GAMEOBJECT_TYPE_MO_TRANSPORT))
{
TransportAnimation const* animation = sTransportMgr->GetTransportAnimInfo(iter.first);
uint32 pathId = data->moTransport.taxiPathId;
float moveSpeed = data->moTransport.moveSpeed;
if (pathId >= sTaxiPathNodesByPath.size())
continue;
TaxiPathNodeList const& path = sTaxiPathNodesByPath[pathId];
std::vector<WorldPosition> ppath;
TravelNode* prevNode = nullptr;
//Elevators/Trams
if (path.empty())
{
if (animation)
{
TransportPathContainer aPath = animation->Path;
float timeStart;
for (auto& u : units)
{
if (u.type != 1)
continue;
if (u.entry != iter.first)
continue;
prevNode = nullptr;
WorldPosition lPos = WorldPosition(u.map, 0, 0, 0, 0);
for (auto& p : aPath)
{
float dx = cos(u.o) * p.second->X - sin(u.o) * p.second->Y;
float dy = sin(u.o) * p.second->X + cos(u.o) * p.second->Y;
WorldPosition pos = WorldPosition(u.map, u.x + dx, u.y + dy, u.z + p.second->Z, u.o);
if (prevNode)
{
ppath.push_back(pos);
}
if (pos.distance(&lPos) == 0)
{
TravelNode* node = TravelNodeMap::instance().addNode(&pos, data->name, true, true, true,
iter.first);
if (!prevNode)
{
ppath.push_back(pos);
timeStart = p.second->TimeSeg;
}
else
{
float totalTime = (p.second->TimeSeg - timeStart) / 1000.0f;
TravelNodePath travelPath(0.1f, totalTime, (uint8)
TravelNodePathType::transport, entry, true); node->setPathTo(prevNode, travelPath); ppath.clear();
ppath.push_back(pos);
timeStart = p.second->TimeSeg;
}
prevNode = node;
}
lPos = pos;
}
if (prevNode)
{
for (auto& p : aPath)
{
float dx = cos(u.o) * p.second->X - sin(u.o) * p.second->Y;
float dy = sin(u.o) * p.second->X + cos(u.o) * p.second->Y;
WorldPosition pos = WorldPosition(u.map, u.x + dx, u.y + dy, u.z + p.second->Z,
u.o);
ppath.push_back(pos);
if (pos.distance(&lPos) == 0)
{
TravelNode* node = TravelNodeMap::instance().addNode(&pos, data->name, true, true, true,
iter.first); if (node != prevNode)
{
float totalTime = (p.second->TimeSeg - timeStart) / 1000.0f;
TravelNodePath travelPath(0.1f, totalTime, (uint8)
TravelNodePathType::transport, entry, true); travelPath.setPath(ppath); node->setPathTo(prevNode, travelPath);
ppath.clear();
ppath.push_back(pos);
timeStart = p.second->TimeSeg;
}
}
lPos = pos;
}
}
ppath.clear();
}
}
}
else //Boats/Zepelins
{
//Loop over the path and connect stop locations.
for (auto& p : path)
{
WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0);
//if (data->displayId == 3015)
// pos.setZ(pos.getZ() + 6.0f);
//else if (data->displayId == 3031)
// pos.setZ(pos.getZ() - 17.0f);
if (prevNode)
{
ppath.push_back(pos);
}
if (p->delay > 0)
{
TravelNode* node = TravelNodeMap::instance().addNode(&pos, data->name, true, true, true, iter.first);
if (!prevNode)
{
ppath.push_back(pos);
}
else
{
TravelNodePath travelPath(0.1f, 0.0, (uint8) TravelNodePathType::transport, entry,
true); travelPath.setPathAndCost(ppath, moveSpeed); node->setPathTo(prevNode, travelPath); ppath.clear();
ppath.push_back(pos);
}
prevNode = node;
}
}
if (prevNode)
{
//Continue from start until first stop and connect to end.
for (auto& p : path)
{
WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0);
//if (data->displayId == 3015)
// pos.setZ(pos.getZ() + 6.0f);
//else if (data->displayId == 3031)
// pos.setZ(pos.getZ() - 17.0f);
ppath.push_back(pos);
if (p->delay > 0)
{
TravelNode* node = TravelNodeMap::instance().getNode(&pos, nullptr, 5.0f);
if (node != prevNode)
{
TravelNodePath travelPath(0.1f, 0.0, (uint8) TravelNodePathType::transport, entry,
true); travelPath.setPathAndCost(ppath, moveSpeed); node->setPathTo(prevNode, travelPath);
}
}
}
}
ppath.clear();
}
}
}
//Zone means
for (auto& loc : exploreLocs)
{
std::vector<WorldPosition*> points;
for (auto p : loc.second->getPoints(true))
if (!p->isUnderWater())
points.push_back(p);
if (points.empty())
points = loc.second->getPoints(true);
WorldPosition pos = WorldPosition(points, WP_MEAN_CENTROID);
TravelNode* node = TravelNodeMap::instance().addNode(&pos, pos.getAreaName(), true, true, false);
}
LOG_INFO("playerbots", ">> Loaded {} navigation points.", TravelNodeMap::instance().getNodes().size());
}
TravelNodeMap::instance().calcMapOffset();
loadMapTransfers();
*/
/*
bool preloadNodePaths = false || fullNavPointReload || storeNavPointReload; //Calculate paths using
PathGenerator. bool preloadReLinkFullyLinked = false || fullNavPointReload || storeNavPointReload; //Retry
nodes that are fully linked. bool preloadUnlinkedPaths = false || fullNavPointReload; //Try to connect points
currently unlinked. bool preloadWorldPaths = true; //Try to load paths in overworld. bool
preloadInstancePaths = true; //Try to load paths in instances. bool preloadSubPrint = false; //Print output
every 2%.
if (preloadNodePaths)
{
std::unordered_map<uint32, Map*> instances;
//PathGenerator
std::vector<WorldPosition> ppath;
uint32 cur = 0, max = TravelNodeMap::instance().getNodes().size();
for (auto& startNode : TravelNodeMap::instance().getNodes())
{
if (!preloadReLinkFullyLinked && startNode->isLinked())
continue;
for (auto& endNode : TravelNodeMap::instance().getNodes())
{
if (startNode == endNode)
continue;
if (startNode->getPosition()->isOverworld() && !preloadWorldPaths)
continue;
if (!startNode->getPosition()->isOverworld() && !preloadInstancePaths)
continue;
if (startNode->hasCompletePathTo(endNode))
continue;
if (!preloadUnlinkedPaths && !startNode->hasLinkTo(endNode))
continue;
if (startNode->getMapId() != endNode->getMapId())
continue;
//if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode))
// continue;
startNode->BuildPath(endNode, nullptr, false);
//if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete())
//startNode->removeLinkTo(endNode);
}
startNode->setLinked(true);
cur++;
if (preloadSubPrint && (cur * 50) / max > ((cur - 1) * 50) / max)
{
TravelNodeMap::instance().printMap();
TravelNodeMap::instance().printNodeStore();
}
}
if (!preloadSubPrint)
{
TravelNodeMap::instance().printNodeStore();
TravelNodeMap::instance().printMap();
}
LOG_INFO("playerbots", ">> Loaded paths for {} nodes.", TravelNodeMap::instance().getNodes().size());
}
bool removeLowLinkNodes = false || fullNavPointReload || storeNavPointReload;
if (removeLowLinkNodes)
{
std::vector<TravelNode*> goodNodes;
std::vector<TravelNode*> remNodes;
for (auto& node : TravelNodeMap::instance().getNodes())
{
if (!node->getPosition()->isOverworld())
continue;
if (std::find(goodNodes.begin(), goodNodes.end(), node) != goodNodes.end())
continue;
if (std::find(remNodes.begin(), remNodes.end(), node) != remNodes.end())
continue;
std::vector<TravelNode*> nodes = node->getNodeMap(true);
if (nodes.size() < 5)
remNodes.insert(remNodes.end(), nodes.begin(), nodes.end());
else
goodNodes.insert(goodNodes.end(), nodes.begin(), nodes.end());
}
for (auto& node : remNodes)
TravelNodeMap::instance().removeNode(node);
LOG_INFO("playerbots", ">> Checked {} nodes.", TravelNodeMap::instance().getNodes().size());
}
bool cleanUpNodeLinks = false || fullNavPointReload || storeNavPointReload;
bool cleanUpSubPrint = false; //Print output every 2%.
if (cleanUpNodeLinks)
{
//Routes
uint32 cur = 0;
uint32 max = TravelNodeMap::instance().getNodes().size();
//Clean up node links
for (auto& startNode : TravelNodeMap::instance().getNodes())
{
startNode->cropUselessLinks();
cur++;
if (cleanUpSubPrint && (cur * 10) / max > ((cur - 1) * 10) / max)
{
TravelNodeMap::instance().printMap();
TravelNodeMap::instance().printNodeStore();
}
}
LOG_INFO("playerbots", ">> Cleaned paths for {} nodes.", TravelNodeMap::instance().getNodes().size());
}
bool reCalculateCost = false || fullNavPointReload || storeNavPointReload;
bool forceReCalculate = false;
if (reCalculateCost)
{
for (auto& startNode : TravelNodeMap::instance().getNodes())
{
for (auto& path : *startNode->getLinks())
{
TravelNodePath* nodePath = path.second;
if (path.second->getPathType() != TravelNodePathType::walk)
continue;
if (nodePath->getCalculated() && !forceReCalculate)
continue;
nodePath->calculateCost();
}
}
LOG_INFO("playerbots", ">> Calculated pathcost for {} nodes.", TravelNodeMap::instance().getNodes().size());
}
bool mirrorMissingPaths = true || fullNavPointReload || storeNavPointReload;
if (mirrorMissingPaths)
{
for (auto& startNode : TravelNodeMap::instance().getNodes())
{
for (auto& path : *startNode->getLinks())
{
TravelNode* endNode = path.first;
if (endNode->hasLinkTo(startNode))
continue;
if (path.second->getPathType() != TravelNodePathType::walk)
continue;
TravelNodePath nodePath = *path.second;
std::vector<WorldPosition> pPath = nodePath.GetPath();
std::reverse(pPath.begin(), pPath.end());
nodePath.setPath(pPath);
endNode->setPathTo(startNode, nodePath, true);
}
}
LOG_INFO("playerbots", ">> Reversed missing paths for {} nodes.", TravelNodeMap::instance().getNodes().size());
}
*/
TravelNodeMap::instance().printMap(); TravelNodeMap::instance().printMap();
TravelNodeMap::instance().printNodeStore(); TravelNodeMap::instance().printNodeStore();
@ -3686,65 +2945,6 @@ void TravelMgr::LoadQuestTravelTable()
} }
} }
/*
sPlayerbotAIConfig.openLog(7, "w");
//Zone area map REMOVE!
uint32 k = 0;
for (auto& node : TravelNodeMap::instance().getNodes())
{
WorldPosition* pos = node->getPosition();
//map area
for (uint32 x = 0; x < 2000; x++)
{
for (uint32 y = 0; y < 2000; y++)
{
if (!pos->getMap())
continue;
float nx = pos->GetPositionX() + (x * 5) - 5000.0f;
float ny = pos->GetPositionY() + (y * 5) - 5000.0f;
float nz = pos->GetPositionZ() + 100.0f;
//pos->getMap()->GetHitPosition(nx, ny, nz + 200.0f, nx, ny, nz, -0.5f);
if (!pos->getMap()->GetHeightInRange(nx, ny, nz, 5000.0f)) // GetHeight can fail
continue;
WorldPosition npos = WorldPosition(pos->GetMapId(), nx, ny, nz, 0.0);
uint32 area = path.getArea(npos.GetMapId(), npos.GetPositionX(), npos.GetPositionY(),
npos.GetPositionZ());
std::ostringstream out;
out << std::fixed << area << "," << npos.getDisplayX() << "," << npos.getDisplayY();
sPlayerbotAIConfig.log(7, out.str().c_str());
}
}
k++;
if (k > 0)
break;
}
//Explore map output (REMOVE!)
sPlayerbotAIConfig.openLog(5, "w");
for (auto i : exploreLocs)
{
for (auto j : i.second->getPoints())
{
std::ostringstream out;
std::string const name = i.second->getTitle();
name.erase(remove(name.begin(), name.end(), '\"'), name.end());
out << std::fixed << std::setprecision(2) << name.c_str() << "," << i.first << "," << j->getDisplayX() <<
"," << j->getDisplayY() << "," << j->GetPositionX() << "," << j->GetPositionY() << "," << j->GetPositionZ();
sPlayerbotAIConfig.log(5,
out.str().c_str());
}
}
*/
} }
uint32 TravelMgr::getDialogStatus(Player* pPlayer, int32 questgiver, Quest const* pQuest) uint32 TravelMgr::getDialogStatus(Player* pPlayer, int32 questgiver, Quest const* pQuest)

View File

@ -138,6 +138,9 @@ public:
bool isOverworld(); bool isOverworld();
bool isInWater(); bool isInWater();
bool isUnderWater(); bool isUnderWater();
// Snap Z to the water surface (level + 0.5y). Returns false if the
// point isn't in/under water or the water level can't be sampled.
bool setAtWaterSurface();
bool IsValid(); bool IsValid();
WorldPosition relPoint(WorldPosition* center); WorldPosition relPoint(WorldPosition* center);

View File

@ -425,11 +425,6 @@ bool TravelNode::isUselessLink(TravelNode* farNode)
return false; return false;
} }
void TravelNode::cropUselessLink(TravelNode* farNode)
{
if (isUselessLink(farNode))
removeLinkTo(farNode);
}
bool TravelNode::cropUselessLinks() bool TravelNode::cropUselessLinks()
{ {
@ -475,135 +470,8 @@ bool TravelNode::cropUselessLinks()
return hasRemoved; return hasRemoved;
/*
//std::vector<std::pair<TravelNode*, TravelNode*>> toRemove;
for (auto& firstLink : getLinks())
{
TravelNode* firstNode = firstLink.first;
float firstLength = firstLink.second.getDistance();
for (auto& secondLink : getLinks())
{
TravelNode* secondNode = secondLink.first;
float secondLength = secondLink.second.getDistance();
if (firstNode == secondNode)
continue;
if (std::find(toRemove.begin(), toRemove.end(), [firstNode, secondNode](std::pair<TravelNode*, TravelNode*>
pair) {return pair.first == firstNode || pair.first == secondNode;}) != toRemove.end()) continue;
if (firstNode->hasLinkTo(secondNode))
{
//Is it quicker to go past first node to reach second node instead of going directly?
if (firstLength + firstNode->linkLengthTo(secondNode) < secondLength * 1.1)
{
if (secondNode->hasLinkTo(this) && !firstNode->hasLinkTo(this))
continue;
toRemove.push_back(make_pair(this, secondNode));
}
}
else
{
TravelNodeRoute route = TravelNodeMap::instance().GetNodeRoute(firstNode, secondNode, nullptr);
if (route.isEmpty())
continue;
if (route.hasNode(this))
continue;
//Is it quicker to go past first (and multiple) nodes to reach the second node instead of going
directly? if (firstLength + route.getLength() < secondLength * 1.1)
{
if (secondNode->hasLinkTo(this) && !firstNode->hasLinkTo(this))
continue;
toRemove.push_back(make_pair(this, secondNode));
}
}
}
//Reverse cleanup. This is needed when we add a node in an existing map.
if (firstNode->hasLinkTo(this))
{
firstLength = firstNode->getPathTo(this)->getDistance();
for (auto& secondLink : firstNode->getLinks())
{
TravelNode* secondNode = secondLink.first;
float secondLength = secondLink.second.getDistance();
if (this == secondNode)
continue;
if (std::find(toRemove.begin(), toRemove.end(), [firstNode, secondNode](std::pair<TravelNode*,
TravelNode*> pair) {return pair.first == firstNode || pair.first == secondNode; }) != toRemove.end()) continue;
if (firstNode->hasLinkTo(secondNode))
{
//Is it quicker to go past first node to reach second node instead of going directly?
if (firstLength + firstNode->linkLengthTo(secondNode) < secondLength * 1.1)
{
if (secondNode->hasLinkTo(this) && !firstNode->hasLinkTo(this))
continue;
toRemove.push_back(make_pair(this, secondNode));
}
}
else
{
TravelNodeRoute route = TravelNodeMap::instance().GetNodeRoute(firstNode, secondNode, nullptr);
if (route.isEmpty())
continue;
if (route.hasNode(this))
continue;
//Is it quicker to go past first (and multiple) nodes to reach the second node instead of going
directly? if (firstLength + route.getLength() < secondLength * 1.1)
{
if (secondNode->hasLinkTo(this) && !firstNode->hasLinkTo(this))
continue;
toRemove.push_back(make_pair(this, secondNode));
}
}
}
}
}
for (auto& nodePair : toRemove)
nodePair.first->unlinkNode(nodePair.second, false);
*/
} }
bool TravelNode::isEqual(TravelNode* compareNode)
{
if (!hasLinkTo(compareNode))
return false;
if (!compareNode->hasLinkTo(this))
return false;
for (auto& node : TravelNodeMap::instance().getNodes())
{
if (node == this || node == compareNode)
continue;
if (node->hasLinkTo(this) != node->hasLinkTo(compareNode))
return false;
if (hasLinkTo(node) != compareNode->hasLinkTo(node))
return false;
}
return true;
}
void TravelNode::print([[maybe_unused]] bool printFailed) void TravelNode::print([[maybe_unused]] bool printFailed)
{ {
@ -889,6 +757,11 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
{ {
if (startP->entry) if (startP->entry)
{ {
// Reference also checks an AreaTriggerEntry DBC store
// (sAreaTriggerStore). AC doesn't expose a separate DBC
// store for area triggers — sObjectMgr->GetAreaTrigger is
// the loaded view of the same data, so it's the only
// existence check we need on this side.
AreaTrigger const* at = sObjectMgr->GetAreaTrigger(startP->entry); AreaTrigger const* at = sObjectMgr->GetAreaTrigger(startP->entry);
if (!at) if (!at)
return false; return false;
@ -912,13 +785,6 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
return true; return true;
} }
// Teleport spell (hearthstone et al.): fire on the next-step marker.
if (nextP->type == PathNodeType::NODE_TELEPORT)
{
cutTo(*nextP, false);
return true;
}
// Flight path: interact with flight master when in range. // Flight path: interact with flight master when in range.
if (startP->type == PathNodeType::NODE_FLIGHTPATH && if (startP->type == PathNodeType::NODE_FLIGHTPATH &&
startPos.distance(startP->point) < INTERACTION_DISTANCE) startPos.distance(startP->point) < INTERACTION_DISTANCE)
@ -927,12 +793,12 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
return true; return true;
} }
// Transport boarding/disembark. We don't expose a teleport-vs-walk // Board-and-ride mode (transportSkipRide == false). Cut to dock if
// toggle yet, so always take the walk-on-board path: cut to dock if
// off-transport, traverse to disembark if on-transport. // off-transport, traverse to disembark if on-transport.
if (startP->type == PathNodeType::NODE_TRANSPORT) if (!sPlayerbotAIConfig.transportSkipRide &&
startP->type == PathNodeType::NODE_TRANSPORT)
{ {
uint32 entry = nextP->entry; uint32 const entry = nextP->entry;
if (!onTransport) if (!onTransport)
{ {
@ -954,6 +820,24 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
} }
} }
// Skip-ride mode (transportSkipRide == true): bot is approaching a
// transport node — walk forward to find the first non-transport node
// (the disembark side), cut to prevP (last transport node) so
// HandleSpecialMovement teleports the bot across directly.
if (sPlayerbotAIConfig.transportSkipRide &&
nextP->type == PathNodeType::NODE_TRANSPORT)
{
for (auto p = std::next(startP); p != fullPath.end(); ++p)
{
if (p->type != PathNodeType::NODE_TRANSPORT)
{
cutTo(*prevP, false);
return true;
}
prevP = p;
}
}
return false; return false;
} }
@ -997,7 +881,13 @@ void TravelPath::ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets)
float const range = cre->GetAttackDistance(mover); float const range = cre->GetAttackDistance(mover);
if (WorldPosition(unit).sqDistance(p->point) > range * range) if (WorldPosition(unit).sqDistance(p->point) > range * range)
continue; continue;
if (!unit->IsHostileTo(mover) || !unit->IsWithinLOSInMap(mover)) // Reference uses CanAttackOnSight (faction + sanctuary +
// feign + phased + passive flags). AC equivalent is
// CanCreatureAttack with skipDistCheck=true (range already
// confirmed above). IsHostileTo alone over-clipped at
// neutral mobs that wouldn't actually aggro.
if (!cre->CanCreatureAttack(mover, true) ||
!unit->IsWithinLOSInMap(mover))
continue; continue;
endP = p; endP = p;
@ -1029,6 +919,22 @@ void TravelPath::ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets)
fullPath.erase(std::next(endP), fullPath.end()); fullPath.erase(std::next(endP), fullPath.end());
} }
void TravelPath::surfaceSnapWaypoints(WorldPosition endPos)
{
if (fullPath.empty())
return;
// Same map + dest is on land. If dest is itself underwater the bot
// wants to dive; leave waypoints alone.
if (fullPath.front().point.GetMapId() != endPos.GetMapId() ||
endPos.isUnderWater())
return;
for (auto& p : fullPath)
{
if (p.point.isUnderWater())
p.point.setAtWaterSurface();
}
}
bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist, Unit* bot) bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist, Unit* bot)
{ {
if (GetPath().empty()) if (GetPath().empty())
@ -1221,14 +1127,6 @@ TravelPath TravelNodeRoute::BuildPath(std::vector<WorldPosition> pathToStart, st
// Full taxi waypoint route; same reasoning as transport. // Full taxi waypoint route; same reasoning as transport.
travelPath.addPath(nodePath->GetPath(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject()); travelPath.addPath(nodePath->GetPath(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject());
} }
else if (nodePath->getPathType() == TravelNodePathType::teleportSpell)
{
// Hearthstone or spell-cast teleport edge: emit a paired
// NODE_TELEPORT (entry = exit) so HandleSpecialMovement can
// dispatch the cast when the head reaches the entry point.
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject());
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject());
}
else else
{ {
std::vector<WorldPosition> path = nodePath->GetPath(); std::vector<WorldPosition> path = nodePath->GetPath();
@ -1417,8 +1315,6 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
std::vector<TravelNodeStub*> open, closed; std::vector<TravelNodeStub*> open, closed;
std::vector<TravelNode*> portNodes; // synthetic teleport/portal edges
if (bot) if (bot)
{ {
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
@ -1455,97 +1351,20 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
PAI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::travel)); PAI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::travel));
} }
} }
// Hearthstone (item 6948 / spell 8690): inject a synthetic
// teleport edge from start to the node nearest the bot's
// home bind, so A* can pick hearthing over walking.
if (bot->IsAlive() && bot->HasItemCount(6948, 1))
{
WorldPosition homePos = AI_VALUE(WorldPosition, "home bind");
std::vector<WorldPosition> dummy;
TravelNode* homeNode = sTravelNodeMap.getNode(homePos, dummy, nullptr, 50.0f);
if (homeNode && homeNode != start)
{
PortalNode* portNode = new PortalNode(start);
portNode->SetPortal(start, homeNode, 8690);
TravelNodeStub* hsStub = &m_stubs.insert(std::make_pair(
static_cast<TravelNode*>(portNode), TravelNodeStub(portNode))).first->second;
// Cost: max(2, (10 - deathCount) * MINUTE) — matches
// reference exactly, including the uint32 underflow at
// deathCount > 10 (which makes hearthstone prohibitive
// for very-dead bots — apparently intentional).
hsStub->costFromStart = std::max<uint32>(2,
(10 - AI_VALUE(uint32, "death count")) * MINUTE);
hsStub->heuristic = hsStub->dataNode->fDist(goal) / botSpeed;
hsStub->totalCost = hsStub->costFromStart + hsStub->heuristic;
open.push_back(hsStub);
hsStub->open = true;
portNodes.push_back(portNode);
}
}
// Mage teleport spells: 3561 Stormwind, 3562 Ironforge, 3563 Undercity,
// 3565 Darnassus, 3566 Thunder Bluff, 3567 Orgrimmar, 18960 Moonglade.
// Inject one synthetic teleport edge per known + ready spell.
static const uint32 teleSpells[] = {3561, 3562, 3563, 3565, 3566, 3567, 18960};
for (uint32 spellId : teleSpells)
{
if (!bot->IsAlive() || bot->IsInCombat())
break;
if (!bot->HasSpell(spellId))
continue;
if (bot->HasSpellCooldown(spellId))
continue;
SpellTargetPosition const* stp =
sSpellMgr->GetSpellTargetPosition(spellId, EFFECT_0);
if (!stp)
continue;
WorldPosition telePos(stp->target_mapId, stp->target_X,
stp->target_Y, stp->target_Z, 0.0f);
std::vector<WorldPosition> dummy;
TravelNode* destNode = sTravelNodeMap.getNode(telePos, dummy, nullptr, 10.0f);
if (!destNode || destNode == start)
continue;
PortalNode* portNode = new PortalNode(start);
portNode->SetPortal(start, destNode, spellId);
TravelNodeStub* tsStub = &m_stubs.insert(std::make_pair(
static_cast<TravelNode*>(portNode), TravelNodeStub(portNode))).first->second;
tsStub->costFromStart = MINUTE; // cheaper than ~1-min walk
tsStub->heuristic = tsStub->dataNode->fDist(goal) / botSpeed;
tsStub->totalCost = tsStub->costFromStart + tsStub->heuristic;
open.push_back(tsStub);
tsStub->open = true;
portNodes.push_back(portNode);
}
} }
else else
startStub->currentGold = bot->GetMoney(); startStub->currentGold = bot->GetMoney();
} }
if (open.empty() && !start->hasRouteTo(goal)) if (!start->hasRouteTo(goal))
{
for (auto* p : portNodes)
delete p;
return TravelNodeRoute(); return TravelNodeRoute();
}
// Min-heap: smallest f at front // Min-heap: smallest f at front
auto heapComp = [](TravelNodeStub* i, TravelNodeStub* j) { return i->totalCost > j->totalCost; }; auto heapComp = [](TravelNodeStub* i, TravelNodeStub* j) { return i->totalCost > j->totalCost; };
open.push_back(startStub); open.push_back(startStub);
startStub->open = true; startStub->open = true;
// Heapify all of open in one pass — covers both startStub and any std::push_heap(open.begin(), open.end(), heapComp);
// PortalNode stubs injected above.
std::make_heap(open.begin(), open.end(), heapComp);
while (!open.empty()) while (!open.empty())
{ {
@ -1571,12 +1390,7 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
} }
reverse(path.begin(), path.end()); reverse(path.begin(), path.end());
return TravelNodeRoute(path);
// Successful route: hand off ownership of any synthetic
// PortalNodes injected at the head. Caller (GetFullPath)
// is expected to call cleanTempNodes() when done with the
// route — see the call site for the lifecycle.
return TravelNodeRoute(path, portNodes);
} }
for (auto const& link : *currentNode->dataNode->getLinks()) // for each successor n' of n for (auto const& link : *currentNode->dataNode->getLinks()) // for each successor n' of n
@ -1615,9 +1429,6 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
} }
} }
// A* exhausted open without reaching goal. Clean up synthetic nodes.
for (auto* p : portNodes)
delete p;
return TravelNodeRoute(); return TravelNodeRoute();
} }
@ -1697,7 +1508,7 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor
return TravelNodeRoute(); return TravelNodeRoute();
} }
TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uint32 botZoneId, TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos,
WorldPosition destination, Unit* bot) WorldPosition destination, Unit* bot)
{ {
TravelPath path; TravelPath path;
@ -1705,14 +1516,14 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uin
// Probe-first short-circuit (matches reference exactly): if a 40-step // Probe-first short-circuit (matches reference exactly): if a 40-step
// mmap probe from bot to destination reaches within spellDistance of // mmap probe from bot to destination reaches within spellDistance of
// dest, use the probe directly and skip graph routing. Otherwise // dest, use the probe directly and skip graph routing. Otherwise
// fall through to the graph A* below — the failed probe waypoints // the probe waypoints are kept as `beginPath` and fed into per-
// would ideally feed into getRoute as startPath (reference does // candidate startPath cropping below.
// this; we don't yet — TODO). std::vector<WorldPosition> beginPath;
if (botPos.GetMapId() == destination.GetMapId()) if (botPos.GetMapId() == destination.GetMapId())
{ {
std::vector<WorldPosition> probe = destination.getPathFromPath({botPos}, bot, 40); beginPath = destination.getPathFromPath({botPos}, bot, 40);
if (destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance)) if (destination.isPathTo(beginPath, sPlayerbotAIConfig.spellDistance))
return TravelPath(probe); return TravelPath(beginPath);
} }
std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx); std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx);
@ -1810,15 +1621,21 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uin
if (transportEntry) if (transportEntry)
{ {
path = route.BuildPath({botPos}, endProbe, bot); path = route.BuildPath({botPos}, endProbe, bot);
route.cleanTempNodes();
return path; return path;
} }
// Validate bot -> startNode is pathable within maxStartDistance. // Validate bot -> startNode is pathable within maxStartDistance.
// Reference reuses the (failed) probe waypoints first via
// cropPathTo, falling back to a fresh getPathTo only if the
// probe can't be cropped to reach startNode. This saves
// re-running mmap when the probe already covers part of
// the journey to startNode.
float const maxStartDistance = s->isTransport() ? 20.0f : 1.0f; float const maxStartDistance = s->isTransport() ? 20.0f : 1.0f;
std::vector<WorldPosition> pathToStart; std::vector<WorldPosition> pathToStart = beginPath;
bool startPathOk = false; bool startPathOk = !pathToStart.empty() &&
if (bot && botPos.GetMapId() == startNodePos.GetMapId()) startNodePos.cropPathTo(pathToStart, maxStartDistance);
if (!startPathOk && bot && botPos.GetMapId() == startNodePos.GetMapId())
{ {
pathToStart = botPos.getPathTo(startNodePos, bot); pathToStart = botPos.getPathTo(startNodePos, bot);
startPathOk = startNodePos.isPathTo(pathToStart, maxStartDistance); startPathOk = startNodePos.isPathTo(pathToStart, maxStartDistance);
@ -1827,139 +1644,21 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uin
if (!startPathOk) if (!startPathOk)
{ {
badStartNodes.push_back(s); badStartNodes.push_back(s);
route.cleanTempNodes();
continue; continue;
} }
// Both ends validated — build and return. // Both ends validated — build and return. Save the
// successful pathToStart back as beginPath so subsequent
// ResolveMovePath cycles can reuse it.
beginPath = pathToStart;
path = route.BuildPath(pathToStart, endProbe, bot); path = route.BuildPath(pathToStart, endProbe, bot);
route.cleanTempNodes();
return path; return path;
} }
} }
// No graph route found. Last-resort hearthstone fallback (reference
// also does this): if bot has hearthstone item and is alive, treat
// the bot's current position as a one-off node and try routing from
// it to each endCandidate via the hearthstone PortalNode edge.
if (Player* player = dynamic_cast<Player*>(bot))
{
if (player->IsAlive() && player->HasItemCount(6948, 1))
{
TravelNode* botNode = new TravelNode(botPos, "Bot Pos", false);
botNode->setPoint(botPos);
for (TravelNode* e : endCandidates)
{
if (!e || std::find(badEndNodes.begin(), badEndNodes.end(), e) != badEndNodes.end())
continue;
TravelNodeRoute route = GetNodeRoute(botNode, e, player);
if (route.isEmpty())
continue;
// Build the end-side path again for this candidate.
WorldPosition endNodePos = *e->getPosition();
std::vector<WorldPosition> endProbe;
if (endNodePos.GetMapId() == destination.GetMapId())
{
Unit* pathBot = (bot && bot->GetMapId() == destination.GetMapId()) ? bot : nullptr;
endProbe = endNodePos.getPathTo(destination, pathBot);
}
else
endProbe = {endNodePos, destination};
route.addTempNodes({botNode}); // transfer ownership of botNode
path = route.BuildPath({botPos}, endProbe, bot);
route.cleanTempNodes();
return path;
}
delete botNode;
}
}
return path; // empty return path; // empty
} }
bool TravelNodeMap::cropUselessNode(TravelNode* startNode)
{
if (!startNode->isLinked() || startNode->isImportant())
return false;
std::vector<TravelNode*> ignore = {startNode};
for (auto& node : getNodes(*startNode->getPosition(), 5000.f))
{
if (startNode == node)
continue;
if (node->getNodeMap(true).size() > node->getNodeMap(true, ignore).size())
return false;
}
removeNode(startNode);
return true;
}
TravelNode* TravelNodeMap::addZoneLinkNode(TravelNode* startNode)
{
for (auto& path : *startNode->getPaths())
{
//TravelNode* endNode = path.first; //not used, line marked for removal.
std::string zoneName = startNode->getPosition()->getAreaName(true, true);
for (auto& pos : path.second.GetPath())
{
std::string const newZoneName = pos.getAreaName(true, true);
if (zoneName != newZoneName)
{
if (!getNode(pos, nullptr, 100.0f))
{
std::string const nodeName = zoneName + " to " + newZoneName;
return TravelNodeMap::instance().addNode(pos, nodeName, false, true);
}
zoneName = newZoneName;
}
}
}
return nullptr;
}
TravelNode* TravelNodeMap::addRandomExtNode(TravelNode* startNode)
{
std::unordered_map<TravelNode*, TravelNodePath> paths = *startNode->getPaths();
if (paths.empty())
return nullptr;
for (uint32 i = 0; i < 20; i++)
{
auto random_it = std::next(std::begin(paths), urand(0, paths.size() - 1));
TravelNode* endNode = random_it->first;
std::vector<WorldPosition> path = random_it->second.GetPath();
if (path.empty())
continue;
// Prefer to skip complete links
if (endNode->hasLinkTo(startNode) && startNode->hasLinkTo(endNode) && !urand(0, 20))
continue;
// Prefer to skip no links
if (!startNode->hasLinkTo(endNode) && !urand(0, 20))
continue;
WorldPosition point = path[urand(0, path.size() - 1)];
if (!getNode(point, nullptr, 100.0f))
return TravelNodeMap::instance().addNode(point, startNode->getName(), false, true);
}
return nullptr;
}
void TravelNodeMap::generateNpcNodes() void TravelNodeMap::generateNpcNodes()
{ {
@ -2421,7 +2120,6 @@ void TravelNodeMap::generateAll()
hasToSave = true; hasToSave = true;
saveNodeStore(); saveNodeStore();
BuildZoneIndex();
PrecomputeReachability(); PrecomputeReachability();
} }
@ -2446,7 +2144,6 @@ void TravelNodeMap::Init()
saveNodeStore(); saveNodeStore();
} }
BuildZoneIndex();
PrecomputeReachability(); PrecomputeReachability();
} }
@ -2515,16 +2212,6 @@ void TravelNodeMap::printNodeStore()
out << "," << (node->isTransport() ? "true" : "false") << "," << node->getTransportId(); out << "," << (node->isTransport() ? "true" : "false") << "," << node->getTransportId();
out << "});"; out << "});";
/*
out << std::fixed << std::setprecision(2) << " nodes[" << i << "] =
TravelNodeMap::instance().addNode(&WorldPosition(" << node->GetMapId() << "," << node->getX() << "f," << node->getY()
<< "f," << node->getZ() << "f,"<< node->getO() <<"f), \""
<< name << "\", " << (node->isImportant() ? "true" : "false") << ", true";
if (node->isTransport())
out << "," << (node->isTransport() ? "true" : "false") << "," << node->getTransportId();
out << ");";
*/
sPlayerbotAIConfig.log(nodeStore, out.str().c_str()); sPlayerbotAIConfig.log(nodeStore, out.str().c_str());
saveNodes.insert(std::make_pair(node, i)); saveNodes.insert(std::make_pair(node, i));
@ -3056,87 +2743,6 @@ std::vector<uint32> TravelNodeMap::BuildPath(uint32 fromNode, uint32 toNode,
return path; return path;
} }
void TravelNodeMap::BuildZoneIndex()
{
m_zoneIndex.clear();
m_mapIndex.clear();
for (auto* node : nodes)
{
if (!node)
continue;
WorldPosition* pos = node->getPosition();
uint32 mapId = pos->GetMapId();
m_mapIndex[mapId].push_back(node);
uint32 zoneId = sMapMgr->GetZoneId(PHASEMASK_NORMAL, *pos);
if (zoneId)
m_zoneIndex[zoneId].push_back(node);
}
}
TravelNode* TravelNodeMap::GetNearestNodeInZone(WorldPosition pos, uint32 zoneId)
{
auto it = m_zoneIndex.find(zoneId);
if (it == m_zoneIndex.end() || it->second.empty())
return GetNearestNodeOnMap(pos); // Fallback to map-wide
TravelNode* bestNode = nullptr;
float bestDist = FLT_MAX;
for (auto* node : it->second)
{
if (!node || node->GetMapId() != pos.GetMapId())
continue;
float dist = node->fDist(pos);
if (dist < bestDist)
{
bestDist = dist;
bestNode = node;
}
}
if (!bestNode)
return GetNearestNodeOnMap(pos);
return bestNode;
}
std::vector<TravelNode*> const& TravelNodeMap::GetNodesInZone(uint32 zoneId) const
{
static std::vector<TravelNode*> const empty;
auto it = m_zoneIndex.find(zoneId);
if (it == m_zoneIndex.end())
return empty;
return it->second;
}
TravelNode* TravelNodeMap::GetNearestNodeOnMap(WorldPosition pos)
{
auto it = m_mapIndex.find(pos.GetMapId());
if (it == m_mapIndex.end() || it->second.empty())
return nullptr;
TravelNode* bestNode = nullptr;
float bestDist = FLT_MAX;
for (auto* node : it->second)
{
if (!node)
continue;
float d = node->fDist(pos);
if (d < bestDist)
{
bestDist = d;
bestNode = node;
}
}
return bestNode;
}
void TravelNodeMap::PrecomputeReachability() void TravelNodeMap::PrecomputeReachability()
{ {
// Find connected components via BFS // Find connected components via BFS

View File

@ -86,8 +86,6 @@
// (saveNodeStore) and by the debug dump command. // (saveNodeStore) and by the debug dump command.
// //
constexpr float MAX_PATHFINDING_DISTANCE = 296.0f;
enum class TravelNodePathType : uint8 enum class TravelNodePathType : uint8
{ {
none = 0, none = 0,
@ -95,10 +93,7 @@ enum class TravelNodePathType : uint8
areaTrigger = 2, areaTrigger = 2,
transport = 3, transport = 3,
flightPath = 4, flightPath = 4,
// Teleport-spell edges (hearthstone, mage portals). Generated at A* // teleportSpell = 5 // maybe someday
// search start via PortalNode injection; consumed by
// HandleSpecialMovement's NODE_TELEPORT case.
teleportSpell = 5,
staticPortal = 6 staticPortal = 6
}; };
@ -358,11 +353,8 @@ public:
} }
void removeLinkTo(TravelNode* node, bool removePaths = false); void removeLinkTo(TravelNode* node, bool removePaths = false);
bool isEqual(TravelNode* compareNode);
// Removes links to other nodes that can also be reached by passing another node. // Removes links to other nodes that can also be reached by passing another node.
bool isUselessLink(TravelNode* farNode); bool isUselessLink(TravelNode* farNode);
void cropUselessLink(TravelNode* farNode);
bool cropUselessLinks(); bool cropUselessLinks();
// Returns all nodes that can be reached from this node. // Returns all nodes that can be reached from this node.
@ -410,26 +402,6 @@ protected:
// uint32 transportId = 0; // uint32 transportId = 0;
}; };
// Synthetic A* node injected at search start to represent a teleport-spell
// (hearthstone, mage portal, etc.) as an alternative travel edge. Owned
// by GetNodeRoute caller; deleted after the route is built.
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 class PathNodeType : uint8 enum class PathNodeType : uint8
{ {
@ -439,10 +411,7 @@ enum class PathNodeType : uint8
NODE_AREA_TRIGGER = 3, NODE_AREA_TRIGGER = 3,
NODE_TRANSPORT = 4, NODE_TRANSPORT = 4,
NODE_FLIGHTPATH = 5, NODE_FLIGHTPATH = 5,
// Teleport-spell endpoint (hearthstone, mage portal). Emitted by // value 6 reserved (was NODE_TELEPORT — removed with teleportSpell)
// TravelNodeRoute::BuildPath when traversing a teleportSpell-type
// edge; consumed by HandleSpecialMovement.
NODE_TELEPORT = 6,
NODE_STATIC_PORTAL = 7 NODE_STATIC_PORTAL = 7
}; };
@ -517,6 +486,14 @@ public:
bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr); bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr);
// For each waypoint that's in/under water, snap its Z to the water
// surface. No-op when destination is itself underwater (caller wants
// the bot to dive) or path's front map differs from dest map.
// Mirrors the reference's underwater→surface snap so bots swim
// along the top of shallow water on land-bound paths instead of
// diving and air-walking the seafloor.
void surfaceSnapWaypoints(WorldPosition endPos);
// Trim the path up to (and optionally including) the given point. // Trim the path up to (and optionally including) the given point.
// Returns true if the point was found. Used by upcoming special- // Returns true if the point was found. Used by upcoming special-
// movement detection to advance the path past a portal/transport/ // movement detection to advance the path past a portal/transport/
@ -575,14 +552,6 @@ public:
{ {
nodes = nodes1; nodes = nodes1;
} }
TravelNodeRoute(std::vector<TravelNode*> nodes1,
std::vector<TravelNode*> const& tempNodes_)
{
nodes = nodes1;
if (!tempNodes_.empty())
addTempNodes(tempNodes_);
}
bool isEmpty() { return nodes.empty(); } bool isEmpty() { return nodes.empty(); }
bool hasNode(TravelNode* node) bool hasNode(TravelNode* node)
@ -593,19 +562,6 @@ public:
std::vector<TravelNode*> getNodes() { return nodes; } std::vector<TravelNode*> getNodes() { return nodes; }
// Take ownership of synthetic A* nodes (PortalNode etc.). Must call
// cleanTempNodes() to delete them when the route is no longer needed.
void addTempNodes(std::vector<TravelNode*> const& tempNodes_)
{
tempNodes.insert(tempNodes.end(), tempNodes_.begin(), tempNodes_.end());
}
void cleanTempNodes()
{
for (auto* n : tempNodes)
delete n;
tempNodes.clear();
}
TravelPath BuildPath( TravelPath BuildPath(
std::vector<WorldPosition> pathToStart = {}, std::vector<WorldPosition> pathToStart = {},
std::vector<WorldPosition> pathToEnd = {}, std::vector<WorldPosition> pathToEnd = {},
@ -619,7 +575,6 @@ private:
return std::find(nodes.begin(), nodes.end(), node); return std::find(nodes.begin(), nodes.end(), node);
} }
std::vector<TravelNode*> nodes; std::vector<TravelNode*> nodes;
std::vector<TravelNode*> tempNodes; // owned synthetic nodes (PortalNode etc.)
}; };
// A node container to aid A* calculations with nodes. // A node container to aid A* calculations with nodes.
@ -746,9 +701,6 @@ public:
void saveNodeStore(); void saveNodeStore();
void LoadNodeStore(); void LoadNodeStore();
bool cropUselessNode(TravelNode* startNode);
TravelNode* addZoneLinkNode(TravelNode* startNode);
TravelNode* addRandomExtNode(TravelNode* startNode);
void calcMapOffset(); void calcMapOffset();
WorldPosition getMapOffset(uint32 mapId); WorldPosition getMapOffset(uint32 mapId);
@ -757,20 +709,12 @@ 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(); void PrecomputeReachability();
TravelNode* GetNearestNodeInZone(WorldPosition pos, uint32 zoneId);
TravelNode* GetNearestNodeOnMap(WorldPosition pos);
// All nodes registered to a zone (post-BuildZoneIndex). Returns an
// empty static vector for unknown zones.
std::vector<TravelNode*> const& GetNodesInZone(uint32 zoneId) const;
// Resolve a full TravelPath from botPos to destination. Returns an // Resolve a full TravelPath from botPos to destination. Returns an
// empty TravelPath if no graph route + mmap stitch is reachable; // empty TravelPath if no graph route + mmap stitch is reachable;
// the caller is then expected to fall back to a single-point path. // the caller is then expected to fall back to a single-point path.
TravelPath GetFullPath(WorldPosition botPos, uint32 botZoneId, TravelPath GetFullPath(WorldPosition botPos,
WorldPosition destination, Unit* bot = nullptr); WorldPosition destination, Unit* bot = nullptr);
// Resolve A* route between two world positions (returns node vector) // Resolve A* route between two world positions (returns node vector)
@ -807,9 +751,6 @@ private:
std::vector<TravelNode*> 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;
bool hasToSave = false; bool hasToSave = false;

View File

@ -88,6 +88,7 @@ bool PlayerbotAIConfig::Initialize()
farDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FarDistance", 20.0f); farDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FarDistance", 20.0f);
sightDistance = sConfigMgr->GetOption<float>("AiPlayerbot.SightDistance", 100.0f); sightDistance = sConfigMgr->GetOption<float>("AiPlayerbot.SightDistance", 100.0f);
transportSkipRide = sConfigMgr->GetOption<bool>("AiPlayerbot.TransportSkipRide", false);
spellDistance = sConfigMgr->GetOption<float>("AiPlayerbot.SpellDistance", 28.5f); spellDistance = sConfigMgr->GetOption<float>("AiPlayerbot.SpellDistance", 28.5f);
shootDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ShootDistance", 5.0f); shootDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ShootDistance", 5.0f);
healDistance = sConfigMgr->GetOption<float>("AiPlayerbot.HealDistance", 38.5f); healDistance = sConfigMgr->GetOption<float>("AiPlayerbot.HealDistance", 38.5f);

View File

@ -93,6 +93,12 @@ public:
bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat; bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat;
uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, expireActionTime, uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, expireActionTime,
dispelAuraDuration, passiveDelay, repeatDelay, errorDelay, rpgDelay, sitDelay, returnDelay, lootDelay; dispelAuraDuration, passiveDelay, repeatDelay, errorDelay, rpgDelay, sitDelay, returnDelay, lootDelay;
// Transport handling:
// false (default) = teleport-board, ride the transport, teleport-disembark
// true = skip the ride entirely (teleport directly across)
// AC has no transport-surface mmap so an in-deck walking mode can't be
// faithfully implemented — the on-board phase always teleport-snaps.
bool transportSkipRide;
bool dynamicReactDelay; bool dynamicReactDelay;
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance, float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance, tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance,

View File

@ -16,6 +16,7 @@
#include "BattleGroundTactics.h" #include "BattleGroundTactics.h"
#include "Chat.h" #include "Chat.h"
#include "GuildTaskMgr.h" #include "GuildTaskMgr.h"
#include "MapMgr.h"
#include "PerfMonitor.h" #include "PerfMonitor.h"
#include "PlayerbotMgr.h" #include "PlayerbotMgr.h"
#include "RandomPlayerbotMgr.h" #include "RandomPlayerbotMgr.h"
@ -186,7 +187,24 @@ public:
} }
uint32 zoneId = player->GetZoneId(); uint32 zoneId = player->GetZoneId();
std::vector<TravelNode*> const& nodes = sTravelNodeMap.GetNodesInZone(zoneId); uint32 const phaseMask = player->GetPhaseMask();
uint32 const mapId = player->GetMapId();
std::vector<TravelNode*> nodes;
for (TravelNode* n : sTravelNodeMap.getNodes())
{
if (!n)
continue;
WorldPosition* pos = n->getPosition();
if (!pos || pos->GetMapId() != mapId)
continue;
uint32 const nodeZone = sMapMgr->GetZoneId(phaseMask, mapId,
pos->GetPositionX(),
pos->GetPositionY(),
pos->GetPositionZ());
if (nodeZone != zoneId)
continue;
nodes.push_back(n);
}
if (nodes.empty()) if (nodes.empty())
{ {
handler->PSendSysMessage("No travel nodes registered in zone {} (is the travel node system loaded?)", zoneId); handler->PSendSysMessage("No travel nodes registered in zone {} (is the travel node system loaded?)", zoneId);