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)
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.
# 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 Position;
#define ANGLE_45_DEG (static_cast<float>(M_PI) / 4.f)
#define ANGLE_90_DEG M_PI_2
#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 normal_only = false, bool exact_waypoint = 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,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool MoveNear(WorldObject* target, float distance = sPlayerbotAIConfig.contactDistance,
@ -65,10 +111,22 @@ protected:
float GetFollowAngle();
bool Follow(Unit* target, float distance = sPlayerbotAIConfig.followDistance);
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 ReachCombatTo(Unit* target, float distance = 0.0f);
float MoveDelay(float distance, bool backwards = false);
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);
bool IsMovingAllowed(WorldObject* target);
bool IsDuplicateMove(float x, float y, float z);

View File

@ -12,7 +12,6 @@ LastMovement::LastMovement() { clear(); }
LastMovement::LastMovement(LastMovement& other)
: taxiNodes(other.taxiNodes),
taxiMaster(other.taxiMaster),
lastFollow(other.lastFollow),
lastAreaTrigger(other.lastAreaTrigger),
lastFlee(other.lastFlee)
{
@ -27,7 +26,6 @@ void LastMovement::clear()
{
lastMoveShort = WorldPosition();
lastPath.clear();
lastFollow = nullptr;
lastAreaTrigger = 0;
lastFlee = 0;
nextTeleport = 0;
@ -36,17 +34,18 @@ void LastMovement::clear()
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);
setShort(WorldPosition());
setPath(TravelPath());
lastFollow = follow;
}
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);
msTime = getMSTime();
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)
{
lastMoveShort = point;
lastFollow = nullptr;
}
void LastMovement::setPath(TravelPath path) { lastPath = path; }

View File

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

View File

@ -9,12 +9,8 @@
#include "Creature.h"
#include "G3D/Vector2.h"
#include "GameObject.h"
#include "GossipDef.h"
#include "GridTerrainData.h"
#include "IVMapMgr.h"
#include "Item.h"
#include "ItemTemplate.h"
#include "LootMgr.h"
#include "Map.h"
#include "ModelIgnoreFlags.h"
#include "MotionMaster.h"
@ -34,7 +30,6 @@
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "Position.h"
#include "QuestDef.h"
#include "Random.h"
#include "RandomPlayerbotMgr.h"
#include "SharedDefines.h"
@ -53,218 +48,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
EmitDebugMove("MoveFar", "empty-dest", 0.0f, 0.0f, 0.0f);
return false;
}
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;
return MoveTo2(dest);
}
bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)

View File

@ -82,15 +82,6 @@ protected:
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
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

View File

@ -274,6 +274,27 @@ bool WorldPosition::isUnderWater()
: 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()
{
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
// match what bots prefer at runtime (STEEP/water are reachable
// 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_WATER, 10.0f);
auto result = getPathStepFrom(startPos, path);
@ -1943,93 +1971,6 @@ void TravelMgr::LoadQuestTravelTable()
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.");
@ -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;
@ -2407,531 +2190,7 @@ void TravelMgr::LoadQuestTravelTable()
// 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().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)

View File

@ -138,6 +138,9 @@ public:
bool isOverworld();
bool isInWater();
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();
WorldPosition relPoint(WorldPosition* center);

View File

@ -425,11 +425,6 @@ bool TravelNode::isUselessLink(TravelNode* farNode)
return false;
}
void TravelNode::cropUselessLink(TravelNode* farNode)
{
if (isUselessLink(farNode))
removeLinkTo(farNode);
}
bool TravelNode::cropUselessLinks()
{
@ -475,135 +470,8 @@ bool TravelNode::cropUselessLinks()
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)
{
@ -889,6 +757,11 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
{
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);
if (!at)
return false;
@ -912,13 +785,6 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
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.
if (startP->type == PathNodeType::NODE_FLIGHTPATH &&
startPos.distance(startP->point) < INTERACTION_DISTANCE)
@ -927,12 +793,12 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
return true;
}
// Transport boarding/disembark. We don't expose a teleport-vs-walk
// toggle yet, so always take the walk-on-board path: cut to dock if
// Board-and-ride mode (transportSkipRide == false). Cut to dock if
// 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)
{
@ -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;
}
@ -997,7 +881,13 @@ void TravelPath::ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets)
float const range = cre->GetAttackDistance(mover);
if (WorldPosition(unit).sqDistance(p->point) > range * range)
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;
endP = p;
@ -1029,6 +919,22 @@ void TravelPath::ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets)
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)
{
if (GetPath().empty())
@ -1221,14 +1127,6 @@ TravelPath TravelNodeRoute::BuildPath(std::vector<WorldPosition> pathToStart, st
// Full taxi waypoint route; same reasoning as transport.
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
{
std::vector<WorldPosition> path = nodePath->GetPath();
@ -1417,8 +1315,6 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
std::vector<TravelNodeStub*> open, closed;
std::vector<TravelNode*> portNodes; // synthetic teleport/portal edges
if (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));
}
}
// 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
startStub->currentGold = bot->GetMoney();
}
if (open.empty() && !start->hasRouteTo(goal))
{
for (auto* p : portNodes)
delete p;
if (!start->hasRouteTo(goal))
return TravelNodeRoute();
}
// Min-heap: smallest f at front
auto heapComp = [](TravelNodeStub* i, TravelNodeStub* j) { return i->totalCost > j->totalCost; };
open.push_back(startStub);
startStub->open = true;
// Heapify all of open in one pass — covers both startStub and any
// PortalNode stubs injected above.
std::make_heap(open.begin(), open.end(), heapComp);
std::push_heap(open.begin(), open.end(), heapComp);
while (!open.empty())
{
@ -1571,12 +1390,7 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
}
reverse(path.begin(), path.end());
// 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);
return TravelNodeRoute(path);
}
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();
}
@ -1697,7 +1508,7 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor
return TravelNodeRoute();
}
TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uint32 botZoneId,
TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos,
WorldPosition destination, Unit* bot)
{
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
// mmap probe from bot to destination reaches within spellDistance of
// dest, use the probe directly and skip graph routing. Otherwise
// fall through to the graph A* below — the failed probe waypoints
// would ideally feed into getRoute as startPath (reference does
// this; we don't yet — TODO).
// the probe waypoints are kept as `beginPath` and fed into per-
// candidate startPath cropping below.
std::vector<WorldPosition> beginPath;
if (botPos.GetMapId() == destination.GetMapId())
{
std::vector<WorldPosition> probe = destination.getPathFromPath({botPos}, bot, 40);
if (destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance))
return TravelPath(probe);
beginPath = destination.getPathFromPath({botPos}, bot, 40);
if (destination.isPathTo(beginPath, sPlayerbotAIConfig.spellDistance))
return TravelPath(beginPath);
}
std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx);
@ -1810,15 +1621,21 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uin
if (transportEntry)
{
path = route.BuildPath({botPos}, endProbe, bot);
route.cleanTempNodes();
return path;
}
// 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;
std::vector<WorldPosition> pathToStart;
bool startPathOk = false;
if (bot && botPos.GetMapId() == startNodePos.GetMapId())
std::vector<WorldPosition> pathToStart = beginPath;
bool startPathOk = !pathToStart.empty() &&
startNodePos.cropPathTo(pathToStart, maxStartDistance);
if (!startPathOk && bot && botPos.GetMapId() == startNodePos.GetMapId())
{
pathToStart = botPos.getPathTo(startNodePos, bot);
startPathOk = startNodePos.isPathTo(pathToStart, maxStartDistance);
@ -1827,139 +1644,21 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uin
if (!startPathOk)
{
badStartNodes.push_back(s);
route.cleanTempNodes();
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);
route.cleanTempNodes();
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
}
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()
{
@ -2421,7 +2120,6 @@ void TravelNodeMap::generateAll()
hasToSave = true;
saveNodeStore();
BuildZoneIndex();
PrecomputeReachability();
}
@ -2446,7 +2144,6 @@ void TravelNodeMap::Init()
saveNodeStore();
}
BuildZoneIndex();
PrecomputeReachability();
}
@ -2515,16 +2212,6 @@ void TravelNodeMap::printNodeStore()
out << "," << (node->isTransport() ? "true" : "false") << "," << node->getTransportId();
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());
saveNodes.insert(std::make_pair(node, i));
@ -3056,87 +2743,6 @@ std::vector<uint32> TravelNodeMap::BuildPath(uint32 fromNode, uint32 toNode,
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()
{
// Find connected components via BFS

View File

@ -86,8 +86,6 @@
// (saveNodeStore) and by the debug dump command.
//
constexpr float MAX_PATHFINDING_DISTANCE = 296.0f;
enum class TravelNodePathType : uint8
{
none = 0,
@ -95,10 +93,7 @@ enum class TravelNodePathType : uint8
areaTrigger = 2,
transport = 3,
flightPath = 4,
// Teleport-spell edges (hearthstone, mage portals). Generated at A*
// search start via PortalNode injection; consumed by
// HandleSpecialMovement's NODE_TELEPORT case.
teleportSpell = 5,
// teleportSpell = 5 // maybe someday
staticPortal = 6
};
@ -358,11 +353,8 @@ public:
}
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.
bool isUselessLink(TravelNode* farNode);
void cropUselessLink(TravelNode* farNode);
bool cropUselessLinks();
// Returns all nodes that can be reached from this node.
@ -410,26 +402,6 @@ protected:
// 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
enum class PathNodeType : uint8
{
@ -439,10 +411,7 @@ enum class PathNodeType : uint8
NODE_AREA_TRIGGER = 3,
NODE_TRANSPORT = 4,
NODE_FLIGHTPATH = 5,
// Teleport-spell endpoint (hearthstone, mage portal). Emitted by
// TravelNodeRoute::BuildPath when traversing a teleportSpell-type
// edge; consumed by HandleSpecialMovement.
NODE_TELEPORT = 6,
// value 6 reserved (was NODE_TELEPORT — removed with teleportSpell)
NODE_STATIC_PORTAL = 7
};
@ -517,6 +486,14 @@ public:
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.
// Returns true if the point was found. Used by upcoming special-
// movement detection to advance the path past a portal/transport/
@ -575,14 +552,6 @@ public:
{
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 hasNode(TravelNode* node)
@ -593,19 +562,6 @@ public:
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(
std::vector<WorldPosition> pathToStart = {},
std::vector<WorldPosition> pathToEnd = {},
@ -619,7 +575,6 @@ private:
return std::find(nodes.begin(), nodes.end(), node);
}
std::vector<TravelNode*> nodes;
std::vector<TravelNode*> tempNodes; // owned synthetic nodes (PortalNode etc.)
};
// A node container to aid A* calculations with nodes.
@ -746,9 +701,6 @@ public:
void saveNodeStore();
void LoadNodeStore();
bool cropUselessNode(TravelNode* startNode);
TravelNode* addZoneLinkNode(TravelNode* startNode);
TravelNode* addRandomExtNode(TravelNode* startNode);
void calcMapOffset();
WorldPosition getMapOffset(uint32 mapId);
@ -757,20 +709,12 @@ public:
void InitTaxiGraph();
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
void BuildZoneIndex();
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
// empty TravelPath if no graph route + mmap stitch is reachable;
// 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);
// Resolve A* route between two world positions (returns node vector)
@ -807,9 +751,6 @@ private:
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;
bool hasToSave = false;

View File

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

View File

@ -93,6 +93,12 @@ public:
bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat;
uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, expireActionTime,
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;
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance,

View File

@ -16,6 +16,7 @@
#include "BattleGroundTactics.h"
#include "Chat.h"
#include "GuildTaskMgr.h"
#include "MapMgr.h"
#include "PerfMonitor.h"
#include "PlayerbotMgr.h"
#include "RandomPlayerbotMgr.h"
@ -186,7 +187,24 @@ public:
}
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())
{
handler->PSendSysMessage("No travel nodes registered in zone {} (is the travel node system loaded?)", zoneId);