Compare commits

...

151 Commits

Author SHA1 Message Date
bash
eec2923076 feat(Core/Debug): Combat-aware state label + retry counter visibility + give-up event emit 2026-06-01 00:25:16 +02:00
bash
61067a302e feat(Core/RPG): Per-spawn destination pattern for incomplete quests (drops POI roam) 2026-06-01 00:22:00 +02:00
bash
5ffd2ad89c refactor(Core/RPG): Retry counter + give-up state replaces MoveRandomNear nudge on MoveFarTo failure 2026-06-01 00:08:01 +02:00
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
bash
7772dc4c0d fix(Core/Travel): Revert AC-side 'improvements' over reference — hearthstone deathCount underflow, A* iteration cap, endPath tolerance 2026-05-31 13:45:33 +02:00
bash
1f9fa42082 fix(Core/Travel): Strip AC-side meaningfulProgress branch in probe-first; match reference acceptance exactly 2026-05-31 13:43:40 +02:00
bash
899f2cba94 fix(Core/Travel): Loosen probe-progress threshold + relax endPath validation to INTERACTION_DISTANCE 2026-05-31 00:54:29 +02:00
bash
77db342969 fix(Core/Travel): Use GetGroupLeader()==bot instead of nonexistent IsGroupLeader 2026-05-31 00:43:49 +02:00
bash
694ba0c64c fix(Core/Travel): Restore probe-first short-circuit in GetFullPath — AC-side workaround for cave-interior destinations the graph misses 2026-05-31 00:42:19 +02:00
bash
14ac3a39b0 debug(Core/Movement): Telemetry whisper showing path tail coords vs bot vs dest 2026-05-31 00:41:20 +02:00
bash
4fab2e4fe6 fix(Core/Travel): GetNodeRoute parity — group-min gold accounting + hearthstone cost formula 2026-05-31 00:38:09 +02:00
bash
e2bcf9683b fix(Core/Travel): Include Transport.h in TravelNode.cpp for GetEntry() call 2026-05-31 00:32:07 +02:00
bash
14c7de977a refactor(Core/Movement): Drop no-progress guard now that probe + zone + validation fixes prevent the oscillation it was masking 2026-05-31 00:28:26 +02:00
bash
0682817b42 fix(Core/Movement): HandleSpecialMovement parity — portal spell-effect check, mount-flying refuse, area-trigger orientation, transport board throttle, teleport failure clears lastPath 2026-05-31 00:26:30 +02:00
bash
69dd655b96 fix(Core/Travel): Restructure GetFullPath to mirror reference: drop probe short-circuit, add per-candidate validation + bad-node tracking + transport early-return + hearthstone fallback 2026-05-31 00:21:51 +02:00
bash
0cbee1621d fix(Core/Travel): Map-wide node scan in GetFullPath candidate pick (was zone-restricted) 2026-05-31 00:18:53 +02:00
bash
8efe3a4321 fix(Core/Travel): Tighten GetFullPath probe gate so graph routing wins when probe misses 2026-05-31 00:10:39 +02:00
bash
1c9fd126ba fix(Core/Movement): No-progress guard in MoveFarTo to break stuck oscillation near unreachable targets 2026-05-31 00:08:39 +02:00
bash
1c12d8ff3e fix(Core/Travel): Hoist AiObjectContext* context in GetNodeRoute so PortalNode injection blocks see it 2026-05-31 00:03:45 +02:00
bash
ea69b56829 fix(Core/Travel): Declare AiObjectContext* context in ClipPath for AI_VALUE macro 2026-05-31 00:02:30 +02:00
bash
35d00b499e fix(Core/Travel): Re-add PathNodeType::NODE_TELEPORT now that BuildPath emits + handler consumes 2026-05-30 23:56:32 +02:00
bash
8844a775f4 fix(Core/Movement): ResolveMovePath takes WorldPosition by value (distance() not const-safe) 2026-05-30 23:55:23 +02:00
bash
77feb8ea56 fix(Core/Travel): Re-add TravelNodePathType::teleportSpell now that PortalNode emits it 2026-05-30 23:52:43 +02:00
bash
2110529b6b fix(Core/Movement): Align Follow IsSafe/bounding-radius + ChaseTo emote clear + IsSitState->IsStandState normalization + MoveNear bounding radius 2026-05-30 23:47:52 +02:00
bash
bdc11b07b3 feat(Core/Travel): Inject hearthstone + mage teleport spells into A* via PortalNode 2026-05-30 23:39:31 +02:00
bash
f8f3de001b feat(Core/Travel): Emit NODE_TELEPORT for teleport-spell edges; add HandleSpecialMovement consumer 2026-05-30 23:29:51 +02:00
bash
a24e1b033c feat(Core/Travel): Port TravelPath::ClipPath; call from MoveFarTo, drop inline clip 2026-05-30 23:28:02 +02:00
bash
8a3a91070b fix(Core/Movement): Force graph routing for vertical moves on map 609 (Ebon Hold) 2026-05-30 23:20:40 +02:00
bash
3bbe51c232 refactor(Core/Movement): Drop unused lastMoveTo* fields + std::future scratch 2026-05-30 23:17:27 +02:00
bash
990e2f2016 refactor(Core/Movement): Drop redundant prefix-trim + setPath in DispatchPathPoints 2026-05-30 23:13:32 +02:00
bash
2b50205e2a fix(Core/Movement): Skip walk dispatch when bot is on transport without special segment 2026-05-30 23:09:09 +02:00
bash
82e7958d2c fix(Core/Movement): Align WaitForTransport + HandleSpecialMovement disembark 2026-05-30 23:06:13 +02:00
bash
cf0bdf13fc refactor(Core/Movement): Rename stale TravelPlan:* labels + drop unused lastdelayTime 2026-05-30 23:01:50 +02:00
bash
3952ebff6e refactor(Core/Travel): Drop TravelPlan struct; GetFullPath returns TravelPath 2026-05-30 22:56:48 +02:00
bash
7ab57c184e refactor(Core/Movement): Drop dead ExecuteTravelPlan + LaunchWalkSpline + MoveToSpline + GetTravelPlan + RefineWalkPoints 2026-05-30 22:53:23 +02:00
bash
db87416f04 refactor(Core/Movement): Drop dead StartTravelPlan + UpdateTravelPlan + debug node lookup 2026-05-30 22:49:47 +02:00
bash
8b87ab091f refactor(Core/Movement): Rewrite MoveFarTo to use ResolveMovePath + HandleSpecialMovement 2026-05-30 22:47:55 +02:00
bash
05cf5a7702 feat(Core/Movement): Add HandleSpecialMovement + WaitForTransport 2026-05-30 22:43:04 +02:00
bash
35a30cdbef feat(Core/Movement): Add MovementAction::ResolveMovePath unified resolver 2026-05-30 22:38:47 +02:00
bash
c55f554bb4 feat(Core/Movement): Add LastMovement::lastTransportEntry for transport-resume gate 2026-05-30 22:05:06 +02:00
bash
d26ac742bb feat(Core/Travel): Port UpcommingSpecialMovement + getNextPoint helpers 2026-05-30 21:59:08 +02:00
bash
c1285bb0ae feat(Core/Travel): Add WorldPosition::projectOnSegment for path-progress checks 2026-05-30 21:52:35 +02:00
bash
77c5c6d8cd feat(Core/Travel): Port TravelPath::cutTo for upcoming special-movement handling 2026-05-30 21:41:34 +02:00
bash
0c9131692c refactor(Core/Movement): Align MoveFarTo preamble + drop spline-plan throttle 2026-05-30 21:28:48 +02:00
bash
1601d6a514 refactor(Core/Movement): Drop IsWaitingForLastMove throttle 2026-05-30 21:07:36 +02:00
bash
dd05767dcc fix(Core/Movement): Bypass stale lastMove gate when bot stopped + loosen probe short-circuit 2026-05-30 20:56:05 +02:00
bash
7278a3bfcb refactor(Conf): Hardcode master-walk-pace distance to 5y, drop config 2026-05-30 20:29:01 +02:00
bash
ecbf3fdec2 fix(Conf): Add missing AiPlayerbot.WalkDistance to playerbots.conf.dist 2026-05-30 20:26:05 +02:00
bash
01ea88624a fix(Core/Travel): Batch NODE_PREPATH into the walk-spline dispatch so per-tick re-resolve actually moves the bot 2026-05-30 20:22:27 +02:00
bash
02844dffd4 fix(Core/RPG): Drop per-tick travelplan whisper to silence spam 2026-05-30 20:18:59 +02:00
bash
2597880d38 fix(Core/Travel): Pass GAMEOBJECT_TYPE_SPELLCASTER to GetGameObjectIfCanInteractWith 2026-05-30 20:07:51 +02:00
bash
f4d308b684 refactor(Core/Travel): Remove dead spline-progress tracking and unused NODE_TELEPORT path 2026-05-30 19:56:41 +02:00
bash
a0e21d9f38 feat(Core/Travel): Re-enable area-trigger, static-portal, and teleport-spell nodes 2026-05-30 19:34:04 +02:00
bash
ed9e7227fb feat(Core/Travel): K-nearest node search, cropPathTo reuse, cross-map pathToEnd 2026-05-30 19:20:25 +02:00
bash
72d9ecabb9 fix(Core/Travel): mmap-path startPath and endPath in GetFullPath 2026-05-30 19:05:00 +02:00
bash
d9a8ac3a2a feat(Core/Travel): Exclude area-trigger, static-portal, teleport-spell path types from PR 2026-05-30 18:57:42 +02:00
bash
8cb54416bf fix(Core/RPG): Per-tick re-resolve travel plan instead of advancing cached plan 2026-05-30 18:38:22 +02:00
bash
558e9ee1e1 feat(Core/Travel): Handle NODE_TELEPORT (hearthstone) and NODE_AREA_TRIGGER 2026-05-30 18:27:52 +02:00
bash
563a415532 fix(Core/Movement): ChaseTo tries mmap path before MoveChase 2026-05-30 18:19:54 +02:00
bash
126294cc38 fix(Core/RPG): Use GetNearPoint and followAngle in MoveWorldObjectTo, bump travel-node threshold to sightDistance 2026-05-30 18:11:06 +02:00
bash
3b106260ac fix(Core/Travel): Soft-bias STEEP at regen PathGenerator sites 2026-05-30 18:07:02 +02:00
bash
b3a8d9f4be Revert "fix(Core/RPG): Drop chained probe and waypoint dispatch in MoveFarTo"
This reverts commit 3384fa4fcfdc8e394653f4604f7de97cf7da9571.
2026-05-30 18:05:39 +02:00
bash
3384fa4fcf fix(Core/RPG): Drop chained probe and waypoint dispatch in MoveFarTo 2026-05-30 15:44:43 +02:00
bash
0d3d38b007 fix(Core/RPG): Align MoveFarTo, MoveWorldObjectTo, MoveRandomNear with cmangos 2026-05-30 15:35:57 +02:00
bash
4e8e3e2afe fix(Core/RPG): Scope do-quest yield-to-grind to current objective only 2026-05-30 15:04:10 +02:00
bash
8c027e3a70 fix(Core/RPG): Drop over-strict MoveFarTo and MoveWorldObjectTo guards 2026-05-30 14:54:36 +02:00
bash
896ad3bf75 fix(Core/RPG): Require LOS from candidate to GO in MoveWorldObjectTo 2026-05-30 14:37:38 +02:00
bash
bdefd38830 fix(Core/Loot): Drop hostiles-in-sight gate on loot-available trigger 2026-05-30 14:28:03 +02:00
bash
5f61fe9ddf refactor(Core/Movement): Drop redundant bot filter setters at PathGenerator sites 2026-05-30 13:53:18 +02:00
bash
82ebaa9594 refactor(Core/Movement): Rename SetAreaCost calls to SetNavTerrainCost 2026-05-30 13:53:18 +02:00
bash
51cea4d76c fix(Core/Movement): Apply bot filter setters at all PathGenerator construction sites 2026-05-30 13:53:18 +02:00
bash
aae47b06c7 fix(Core/Travel): Apply NAV_WATER cost bias on regen PathGenerator 2026-05-30 13:53:18 +02:00
bash
d72d3ded6c fix(Core/Travel): Exclude NAV_GROUND_STEEP on regen PathGenerator 2026-05-30 13:53:17 +02:00
bash
974faf0cb0 fix(Core/Travel): Hoist portal/transport cheat above 2-point reject 2026-05-30 13:53:17 +02:00
bash
e052ec3b17 fix(Core/Travel): Match cmangos buildPath stitching, drop 75y guard 2026-05-30 13:53:17 +02:00
bash
4a991c194d fix(Core/Travel): Preserve walk paths from taxi-path overwrite 2026-05-30 13:53:17 +02:00
bash
ed31f8f8a7 chore(Core/Travel): Warn admins to shutdown after generatenode 2026-05-30 13:53:17 +02:00
bash
479794b66b fix(Core/Travel): Skip 5y dedup when loading nodes from DB 2026-05-30 13:53:17 +02:00
bash
337fbca8c0 chore(DB/Travel): Temporarily disable Aldrassil ramp anchors 2026-05-30 13:53:17 +02:00
bash
fe12f1a708 fix(Core/Travel): Drop 2-point check, keep last-segment teleport guard 2026-05-30 13:53:17 +02:00
bash
8916cf83c0 fix(Core/Travel): Reject paths with >75y final-segment teleport jumps 2026-05-30 13:53:17 +02:00
bash
77caf85fd1 fix(Core/Travel): Reject 2-point BuildShortcut paths between non-adjacent nodes 2026-05-30 13:53:17 +02:00
bash
5e5d41f878 chore(Core/Travel): Bump 2-point shortcut threshold to 75y 2026-05-30 13:53:17 +02:00
bash
63c5d674d6 fix(Core/Travel): Reject 2-point BuildShortcut teleports in chained probe 2026-05-30 13:53:17 +02:00
bash
43ee732003 Revert non-progress chained-probe detection (broke valid paths) 2026-05-30 13:53:17 +02:00
bash
f42f37399f fix(Core/Travel): Loosen chained-probe non-progress threshold 2026-05-30 13:53:17 +02:00
bash
c7929482c4 fix(Core/Travel): Bail chained probe on non-progress oscillation 2026-05-30 13:53:17 +02:00
bash
e1489f213e fix(Core/Travel): Chunk all saveNodeStore phases (deletes, nodes, links) 2026-05-30 13:53:17 +02:00
bash
007189fd5c fix(Core/Travel): Chunk saveNodeStore path inserts to avoid mega-tx 2026-05-30 13:53:17 +02:00
bash
eabefb1d33 feat(DB/Travel): Add Aldrassil ramp travelnode anchors 2026-05-30 13:53:17 +02:00
bash
2208c80caa chore(Core/Debug): Compact debug-move whisper format 2026-05-30 13:53:17 +02:00
bash
a472fc2d68 feat(Core/Travel): Sparse-segment clip in LaunchWalkSpline 2026-05-30 13:53:17 +02:00
bash
bac63e2a8c feat(Core/RPG): Prefix-trim and sparse-segment clip on path dispatch 2026-05-30 13:53:17 +02:00
bash
690288b5cc feat(Core/RPG): Port cmangos 8-angle LOS+navmesh-snap to MoveWorldObjectTo 2026-05-30 13:53:17 +02:00
bash
57134918cb chore(Core/RPG): Loosen Z-mismatch threshold from 5y to 10y 2026-05-30 13:53:17 +02:00
bash
324b50f1be fix(Core/RPG): Reject mmap paths whose endpoint Z misses dest 2026-05-30 13:53:17 +02:00
bash
7d8d8c6b31 fix(Core/RPG): Reject mmap paths that LOS-fail any segment 2026-05-30 13:53:17 +02:00
bash
34b0432aaa feat(Core/RPG): Switch POI when current cluster is empty 2026-05-30 13:53:17 +02:00
bash
edc999c8ac fix(Core/RPG): Stop next to quest objects instead of on top of them 2026-05-30 13:53:17 +02:00
bash
85e2a940a1 chore: Drop bot movement console logs 2026-05-30 13:53:17 +02:00
bash
6754a95890 chore: Tighten comments in travel and movement code 2026-05-30 13:53:17 +02:00
bash
d0fac16c85 chore(Core/Travel): Drop cmangos reference in RefineWalkPoints comment 2026-05-30 13:53:17 +02:00
bash
a64c721f35 fix(Core/RPG): LOS check on MoveRandomNear samples to avoid tree tunneling 2026-05-30 13:53:17 +02:00
bash
27503a9c37 Revert "fix(Core/Travel): LOS check before trusting raw cmangos waypoints" 2026-05-30 13:53:17 +02:00
bash
1a7e6db0c9 fix(Core/Travel): LOS gate on empty-probe single-waypoint fallback 2026-05-30 13:53:16 +02:00
bash
6ae973bb8e fix(Core/Travel): LOS check before trusting raw cmangos waypoints 2026-05-30 13:53:16 +02:00
bash
101da6ecd3 chore(Core/Travel): Revert travelnode threshold to 50y 2026-05-30 13:53:16 +02:00
bash
605e7586c5 chore(Core/Travel): Bump travelnode threshold to 75y 2026-05-30 13:53:16 +02:00
bash
129cb252cf fix(Core/Travel): Trust travelnode waypoints when AC mmap rejects segments 2026-05-30 13:53:16 +02:00
bash
088537277c feat(Core/Travel): Hardcode 50y travelnode threshold 2026-05-30 13:53:16 +02:00
bash
6944da8d69 core filter isnt working yet 2026-05-30 13:53:16 +02:00
bash
980c1b8cd8 refactor(Core/Travel): Drop redundant NAV_GROUND_STEEP excludes (core handles via IsBot) 2026-05-30 13:53:16 +02:00
bash
ad14420400 fix(Core/Travel): Exclude NAV_GROUND_STEEP at all bot PathGenerator sites 2026-05-30 13:53:16 +02:00
bash
1d0aeec7b9 feat(Core/Travel): Align MoveFarTo and probe pipeline with cmangos 2026-05-30 13:53:16 +02:00
bash
7741626631 feat(Core/Travel): Cap bots at 50° via NAV_GROUND_STEEP exclude 2026-05-30 13:53:16 +02:00
bash
806013a4c9 feat(Core/Debug): Trace movement entry points and visualize travel nodes 2026-05-30 13:53:16 +02:00
bash
3269d1a4b3 feat(Core/RPG): MoveFarTo flow, quest-pursuit at POI, MoveRandomNear retries 2026-05-30 13:53:16 +02:00
bash
1ae72b0888 feat(Core/Travel): Travel-node graph routing for long-distance pathing 2026-05-30 13:53:16 +02:00
bash
0a9bf70305 feat(Core/Loot): Quest GO loot, bag-make-room, item-pursuit 2026-05-30 13:53:16 +02:00
bash
e18fdd02cd chore(Tools): Add mmap/vmap client-data extraction script 2026-05-30 13:53:16 +02:00
bash
a23158ef52 feat(DB/Travel): Import cmangos travel-node graph 2026-05-30 13:53:16 +02:00
46 changed files with 152510 additions and 3198 deletions

88
.claude/settings.json Normal file
View File

@ -0,0 +1,88 @@
{
"permissions": {
"allow": [
"Bash(awk '-F[\\(\\),]' ' *)",
"Bash(xargs ls -lah)",
"Bash(py --version)",
"Bash(py -3 --version)",
"Bash(py scripts/import_cmangos_travel_nodes.py)",
"Read(//home/dev/azerothcore_installer/_server/azerothcore/modules/mod-playerbots/data/sql/playerbots/updates/**)",
"Bash(py scripts/fixup_id_collision.py)",
"Bash(py scripts/insert_ignore.py)",
"Bash(grep -nA 6 \"passFilter\" C:/Users/Admin/git/main/azerothcore-wotlk/deps/recastnavigation/Detour/Include/DetourNavMeshQuery.h)",
"Bash(grep -nA 5 \"dtQueryFilter::passFilter\" C:/Users/Admin/git/main/azerothcore-wotlk/deps/recastnavigation/Detour/Source/DetourNavMeshQuery.cpp)",
"Bash(grep -nB1 -A2 \"modAlmostUnwalkableTriangles\" C:/Users/Admin/git/main/azerothcore-wotlk/src/tools/mmaps_generator/MapBuilder.cpp)",
"Bash(xargs grep -l \"MoveFarTo\\\\|ResolveMovePath\\\\|DispatchMovement\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" status)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" add src/server/game/Movement/MovementGenerators/PathGenerator.h)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"feat\\(Core/Movement\\): Expose dtQueryFilter::setAreaCost via PathGenerator\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" push)",
"Bash(wait)",
"Bash(git -C \"c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots\" show 3710c35a)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" show 9cccc5d26)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" add src/server/game/Movement/MovementGenerators/PathGenerator.cpp)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"feat\\(Core/Movement\\): Bias NAV_WATER 10x in default Player path filter\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"Revert \\\\\"feat\\(Core/Movement\\): Bias NAV_WATER 10x in default Player path filter\\\\\"\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"Revert \\\\\"feat\\(Core/Movement\\): Expose dtQueryFilter::setAreaCost via PathGenerator\\\\\"\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" log --oneline cf9c6e9f3353d386f398cbe7a821abfd8fe9a4b3..HEAD)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" diff cf9c6e9f3353d386f398cbe7a821abfd8fe9a4b3..HEAD --stat)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" reset --hard cf9c6e9f3353d386f398cbe7a821abfd8fe9a4b3)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" push --force-with-lease)",
"Bash(git cherry-pick *)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" status -s)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" diff --stat)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" add src/server/game/Movement/MovementGenerators/PathGenerator.cpp src/server/game/Movement/MovementGenerators/PathGenerator.h)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"feat\\(Core/Movement\\): Cap Player path filter at 50° + water bias under MOD_PLAYERBOTS\")",
"Bash(grep -n \"CollectIncludeDirectories\\\\|sourcePath\\\\|GLOB.*\\\\\\\\.cpp\\\\\\\\|target_include\" C:/Users/Admin/git/main/azerothcore-wotlk/modules/CMakeLists.txt)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"fix\\(Core/Movement\\): Gate playerbot path filter on WorldSession::IsBot\\(\\)\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" fetch origin fix/mmaps-config-overrides-and-aliases)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" log --oneline HEAD..origin/fix/mmaps-config-overrides-and-aliases)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" rebase origin/fix/mmaps-config-overrides-and-aliases)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" log --oneline -3)",
"Bash(grep -v \"//\")",
"Bash(grep -v \"^//\")",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk push --force-with-lease)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots push --force-with-lease)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk diff --stat)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk add src/server/game/Movement/MovementGenerators/PathGenerator.h src/server/game/Movement/MovementGenerators/PathGenerator.cpp)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk commit -m \"feat\\(Core/Movement\\): Apply bot filter rules in PathGenerator::CreateFilter\")",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"refactor\\(Core/Movement\\): Drop redundant bot filter setters at PathGenerator sites\")",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk log --all --oneline -- src/server/game/Movement/MovementGenerators/PathGenerator.cpp)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk show 82a544b03 -- src/server/game/Movement/MovementGenerators/PathGenerator.cpp)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk log --all --oneline -- src/tools/mmaps_generator/MapBuilder.cpp)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk log --all --oneline -S \"modAlmostUnwalkableTriangles\")",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots log --oneline --since=\"2026-05-25\" -- src/Ai/Base/Actions/LootAction.cpp src/Mgr/Item/LootObjectStack.cpp src/Mgr/Item/LootObjectStack.h src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots log --oneline -20)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots log --oneline 82a92f62 d0ba99f3 --not 82a92f62~30 -- src/Ai/Base/Actions/LootAction.cpp src/Mgr/Item/)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots log --oneline d0ba99f3..82a92f62 -- src/Ai/ src/Mgr/Item/)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots show c7b4b9aa --stat)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots log --oneline 82a92f62..origin/test-staging)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk rev-parse origin/test-staging)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"fix\\(Core/RPG\\): Drop over-strict MoveFarTo and MoveWorldObjectTo guards\")",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots show 3269d1a4 --stat)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"fix\\(Core/RPG\\): Align MoveFarTo, MoveWorldObjectTo, MoveRandomNear with cmangos\")",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk commit -m \"feat\\(Core/Movement\\): Double MAX_PATH_LENGTH to 148 under MOD_PLAYERBOTS\")",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"fix\\(Core/RPG\\): Drop chained probe and waypoint dispatch in MoveFarTo\")",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots revert 3384fa4f --no-edit)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots add src/Mgr/Travel/TravelMgr.cpp)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"fix\\(Core/Travel\\): Soft-bias STEEP at regen PathGenerator sites\")",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk add src/server/game/Movement/MovementGenerators/PathGenerator.cpp)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk commit -m \"fix\\(Core/Movement\\): Bot filter uses cost bias for STEEP, not hard exclude\")",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"feat\\(Core/Travel\\): K-nearest node search, cropPathTo reuse, cross-map pathToEnd\")",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots add src/Mgr/Travel/TravelNode.cpp src/Mgr/Travel/TravelNode.h src/Ai/Base/Actions/MovementActions.cpp)",
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"feat\\(Core/Travel\\): Re-enable area-trigger, static-portal, and teleport-spell nodes\")",
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots add src/Ai/Base/Actions/FollowActions.cpp src/Ai/Base/Actions/MovementActions.cpp src/Ai/Base/Actions/MovementActions.h src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp)",
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"refactor\\(Core/Movement\\): Drop IsWaitingForLastMove throttle\")",
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots push)",
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots diff --stat)",
"Bash(grep -v \"EmitDebugMove\\\\|//\")",
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots add src/Mgr/Travel/TravelNode.h src/Mgr/Travel/TravelNode.cpp src/Ai/Base/Actions/MovementActions.cpp src/Ai/World/Rpg/NewRpgInfo.h src/Ai/World/Rpg/NewRpgInfo.cpp)",
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"refactor\\(Core/Travel\\): Drop TravelPlan struct; GetFullPath returns TravelPath\")"
],
"additionalDirectories": [
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\common\\Collision\\Maps",
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\tools\\mmaps_generator",
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\server\\game\\Movement\\MovementGenerators"
]
}
}

View File

@ -48,6 +48,16 @@ Then build the server following the platform-specific instructions in our **[Ins
> **Testing branch:** A `test-staging` branch is available with the latest features and fixes before they are merged into `master`. To use it, clone with `--branch=test-staging` instead. Note that this branch may contain unstable or breaking changes — use it at your own risk and only if you are comfortable troubleshooting issues.
### Required server configuration
In `worldserver.conf` (AzerothCore core config), set:
```ini
PreloadAllNonInstancedMapGrids = 1
```
This is required for `mod-playerbots`.
### Detailed Guides
| Guide | Description |

View File

@ -342,10 +342,6 @@ AiPlayerbot.MaxWaitForMove = 5000
# 2 - MoveSplinePath disabled everywhere
AiPlayerbot.DisableMoveSplinePath = 0
# Max search time for movement (higher for better movement on slopes)
# Default: 3
AiPlayerbot.MaxMovementSearchTime = 3
# Action expiration time
AiPlayerbot.ExpireActionTime = 5000
@ -1063,6 +1059,22 @@ AiPlayerbot.RestrictedHealerDPSMaps = "33,34,36,43,47,48,70,90,109,129,209,229,2
# Default: 1 (enabled)
AiPlayerbot.EnableNewRpgStrategy = 1
# Use pre-computed travel node paths for long-distance movement (>300 yards).
# When enabled, bots use the travel node graph (A*, flight paths, transports)
# instead of repeated mmap hops. Experimental.
# Default: 0 (disabled)
AiPlayerbot.EnableTravelNodes = 0
# 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.
#

View File

@ -0,0 +1,632 @@
-- Imported from cmangos-playerbots ai_playerbot_travelnode (wotlk)
-- 626 new nodes (cmangos has them, we didn't)
-- Matched on (name, mapId) + closest position; remaining are unique to cmangos.
INSERT INTO `playerbots_travelnode` (`id`, `name`, `map_id`, `x`, `y`, `z`, `linked`) VALUES
(3781, ' portal', 0, 3476.3600, -4493.3600, 137.4900, 1),
(3782, ' portal', 530, 6107.2600, -6990.6400, 133.3170, 1),
(3783, ' spirithealer', 609, 1886.7800, -5784.5900, 102.8610, 1),
(3784, ' spirithealer', 609, 2116.1900, -5286.9400, 81.2151, 1),
(3785, ' spirithealer', 609, 2364.4200, -5771.3200, 151.3670, 1),
(3786, 'Absalan the Pious', 623, 36.0312, 40.4600, 25.0322, 1),
(3787, 'Acherus: The Ebon Hold flightMaster', 0, 2348.6300, -5669.2900, 382.3240, 1),
(3788, 'Acherus: The Ebon Hold spirithealer', 0, 2356.6500, -5663.0600, 382.2570, 1),
(3789, 'Alliance Log Ride 01 Begin', 571, 4274.5300, -3055.5500, 319.4630, 1),
(3790, 'Alliance PVP Barracks', 449, -9.1189, -4.2670, 5.5710, 1),
(3791, 'Alterac Mountains The Headland', 0, -80.9055, -331.6660, 136.4710, 1),
(3792, 'Alterac Mountains', 0, 498.3190, -1076.6400, 195.8960, 1),
(3793, 'Arathi Highlands The Sanctum', 0, -1532.4300, -1882.5600, 69.5533, 1),
(3794, 'Arathi Highlands The Tower of Arathor', 0, -1774.4800, -1518.3900, 75.2667, 1),
(3795, 'Archmage Pentarus', 603, -718.4560, -57.1132, 429.9240, 1),
(3796, 'Area 52 innkeeper', 530, 3062.1500, 3701.8200, 142.5620, 1),
(3797, 'Area52 Transporter', 530, 3092.5800, 3644.7200, 143.1360, 1),
(3798, 'Ashenvale Kargathia Keep', 1, 2437.0600, -3543.6300, 98.3115, 1),
(3799, 'Auberdine innkeeper', 1, 6406.5100, 515.3670, 8.7257, 1),
(3800, 'Auchindoun: Auchenai Crypts', 558, 63.4074, -175.2640, 15.4378, 1),
(3801, 'Auchindoun: Mana-Tombs', 557, -220.0700, -177.6620, -0.9810, 1),
(3802, 'Auchindoun: Sethekk Halls', 556, -102.4290, 177.5860, 0.0932, 1),
(3803, 'Auchindoun: Shadow Labyrinth', 555, -272.2950, -140.0660, 8.1563, 1),
(3804, 'Balzaphon', 329, 3733.2700, -3480.1100, 131.0400, 1),
(3805, 'Blackfathom Deeps', 48, -570.3640, 0.8977, -47.1378, 1),
(3806, 'Blackrock Depths', 230, 870.1800, -239.3270, -71.6776, 1),
(3807, 'Blackrock Spire', 229, -16.0931, -392.4040, 48.5157, 1),
(3808, 'Blades Edge Mountains Circle of Blood', 530, 2879.1300, 5979.4500, 6.2402, 1),
(3809, 'Blades Edge Mountains Vimgols Circle', 530, 3279.9000, 4640.3600, 216.5280, 1),
(3810, 'Blood elf start', 530, 10349.6000, -6357.2900, 33.4026, 1),
(3811, 'Bloodmyst Isle The Hidden Reef', 530, -1148.6600, -11127.4000, -76.0074, 1),
(3812, 'Borean Tundra Coldarra', 571, 3917.0600, 6817.9200, 150.5070, 1),
(3813, 'Borean Tundra Naxxanar', 571, 3750.9300, 3583.8600, 353.1330, 1),
(3814, 'Burning Steppes Blackrock Mountain', 0, -7996.6300, -1013.4100, 131.8670, 1),
(3815, 'Cannon Charging (Port)', 530, 1920.1300, 5581.9000, 269.7220, 1),
(3816, 'Cannon Prep', 0, -9569.6000, -13.7809, 63.9459, 1),
(3817, 'Cannon Prep', 1, -1327.6600, 85.9815, 130.2070, 1),
(3818, 'Cannon Prep', 530, -1742.2500, 5457.4000, -11.9282, 1),
(3819, 'Chief Thunder-Skins', 230, 847.8390, -181.1150, -49.6707, 1),
(3820, 'Coilfang: Serpentshrine Cavern', 548, 135.1620, -488.7410, 0.8400, 1),
(3821, 'Coilfang: The Slave Pens', 547, -71.6723, -322.2440, -1.5438, 1),
(3822, 'Coilfang: The Steamvault', 545, -91.9829, -255.4900, -12.5306, 1),
(3823, 'Coilfang: The Underbog', 546, 108.0610, -198.7420, 50.1184, 1),
(3824, 'Coilskar Witch', 585, 139.1010, -104.6440, -20.9245, 1),
(3825, 'Crystalsong Forest The Azure Front', 571, 5434.6500, 729.6780, 186.6960, 1),
(3826, 'Crystalsong Forest The Great Tree', 571, 5895.1000, 1022.6900, 185.5390, 1),
(3827, 'Crystalsong Forest The Twilight Rivulet', 571, 5508.9200, 432.9360, 161.7470, 1),
(3828, 'Dalaran Portal to Caverns of Time', 1, -8164.8000, -4768.5000, 34.3000, 1),
(3829, 'Dalaran Portal to Caverns of Time', 571, 5781.4800, 841.2580, 680.3790, 1),
(3830, 'Dalaran Portal to Darnassus', 1, 9656.5400, 2518.2600, 1331.6600, 1),
(3831, 'Dalaran Portal to Darnassus', 571, 5706.1600, 730.1020, 641.7450, 1),
(3832, 'Dalaran Portal to Exodar', 571, 5699.5800, 735.4690, 641.7690, 1),
(3833, 'Dalaran Portal to Ironforge', 0, -4613.7100, -915.2870, 501.0620, 1),
(3834, 'Dalaran Portal to Ironforge', 571, 5712.6800, 724.8450, 641.7360, 1),
(3835, 'Dalaran Portal to Orgrimmar', 1, 1470.0200, -4222.5900, 59.2213, 1),
(3836, 'Dalaran Portal to Orgrimmar', 571, 5925.8500, 593.2500, 640.5630, 1),
(3837, 'Dalaran Portal to Shattrath', 530, -1824.3200, 5417.2300, -12.4277, 1),
(3838, 'Dalaran Portal to Shattrath', 530, -1902.4900, 5442.8600, -12.4280, 1),
(3839, 'Dalaran Portal to Shattrath', 571, 5697.4900, 744.9120, 641.8190, 1),
(3840, 'Dalaran Portal to Shattrath', 571, 5941.6600, 584.8870, 640.5740, 1),
(3841, 'Dalaran Portal to Silvermoon', 530, 9998.4600, -7106.5500, 47.7054, 1),
(3842, 'Dalaran Portal to Silvermoon', 571, 5946.9800, 568.4790, 640.5730, 1),
(3843, 'Dalaran Portal to Stormwind', 571, 5719.1900, 719.6810, 641.7280, 1),
(3844, 'Dalaran Portal to Thunder Bluff', 1, -967.3750, 284.8200, 110.7730, 1),
(3845, 'Dalaran Portal to Thunder Bluff', 571, 5945.8100, 577.3570, 640.5740, 1),
(3846, 'Dalaran Portal to Undercity', 571, 5934.6600, 590.6880, 640.5750, 1),
(3847, 'Dalaran Violet Citadel Balcony', 571, 5865.0800, 840.2100, 846.3330, 1),
(3848, 'Darnassus The Temple Gardens', 1, 9814.7800, 2574.2100, 1314.4700, 1),
(3849, 'Darnassus Warriors Terrace', 1, 9976.2100, 2262.3700, 1334.5000, 1),
(3850, 'Darnassus innkeeper', 1, 9951.8800, 2282.8200, 1345.1600, 1),
(3851, 'Deadwind Pass Groshgok Compound', 0, -11121.6000, -2416.2600, 108.6530, 1),
(3852, 'Deadwind Pass Karazhan', 0, -11123.3000, -2006.7700, 47.2725, 1),
(3853, 'Deadwind Pass', 0, -10377.2000, -1785.4200, 94.3243, 1),
(3854, 'Deeprun Tram', 369, 13.6699, 80.6694, -4.2973, 1),
(3855, 'Desolace Bolgans Hole', 1, -2375.9500, 2469.7600, 74.4626, 1),
(3856, 'Desolace The Veiled Sea', 1, -1869.3900, 3404.3400, -50.7694, 1),
(3857, 'Dire Maul', 429, 135.5790, 192.0480, -3.3910, 1),
(3858, 'Doodad_CF_elevatorPlatform01entry', 548, 50.0000, -0.0071, -70.9017, 1),
(3859, 'Doodad_CF_elevatorPlatform_small01entry', 548, -59.1350, -98.7966, -51.7018, 1),
(3860, 'Doodad_FactoryElevator01entry', 554, 0.5438, -1.3935, -1.7012, 1),
(3861, 'Doodad_HF_Elevator_Gate01entry', 571, 254.1330, -5892.0000, 258.4950, 1),
(3862, 'Doodad_HF_Elevator_Gate02', 571, 263.1630, -5919.2500, 167.2690, 1),
(3863, 'Doodad_HF_Elevator_Gate02', 571, 263.1630, -5919.2500, 173.3200, 1),
(3864, 'Doodad_HF_Elevator_Gate02entry', 571, 275.2000, -5918.1300, 166.4950, 1),
(3865, 'Doodad_HF_Elevator_Gate03', 571, 135.1250, -5765.3500, 291.7530, 1),
(3866, 'Doodad_HF_Elevator_Gate03', 571, 135.1250, -5765.3500, 286.0490, 1),
(3867, 'Doodad_HF_Elevator_Gate03entry', 571, 133.3330, -5756.2700, 285.5940, 1),
(3868, 'Doodad_HF_Elevator_Lift01entry', 571, 259.9700, -5893.8300, 255.8280, 1),
(3869, 'Doodad_HF_Elevator_Lift01entry', 571, 261.1220, -5910.0100, 77.0138, 1),
(3870, 'Doodad_HF_Elevator_Lift01entry', 571, 261.9400, -5918.9400, 164.0950, 1),
(3871, 'Doodad_HF_Elevator_Lift_01entry', 571, 158.9330, -5760.0000, 38.3942, 1),
(3872, 'Doodad_ID_elevator01entry', 571, 3324.2300, -5134.7400, 300.5890, 1),
(3873, 'Doodad_ID_elevator02entry', 571, 3302.5400, -5103.7300, 300.5890, 1),
(3874, 'Doodad_ID_elevator03entry', 571, 3286.7600, -5135.8800, 300.5890, 1),
(3875, 'Doodad_LogRun_PumpElevator05', 571, 4264.2300, -3276.9500, 336.1010, 1),
(3876, 'Doodad_LogRun_PumpElevator05', 571, 4264.2300, -3276.9500, 330.6150, 1),
(3877, 'Doodad_LogRun_PumpElevator05entry', 571, 4264.2300, -3276.9500, 329.3720, 1),
(3878, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 571, 3556.2500, 6928.7200, 251.3120, 1),
(3879, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 571, 3979.2300, 7272.5600, 256.0160, 1),
(3880, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 571, 4110.2800, 6755.6900, 249.1790, 1),
(3881, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 578, 1213.8600, 1339.0600, 248.2530, 1),
(3882, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 578, 1343.4300, 825.8630, 242.8180, 1),
(3883, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 578, 789.8890, 992.0980, 251.2600, 1),
(3884, 'Doodad_Vrykul_Gondola01entry', 571, 698.9930, -3824.2500, 269.5360, 1),
(3885, 'Doodad_Vrykul_Gondola01entryentry', 571, 700.2670, -3823.5000, 269.3790, 1),
(3886, 'Doodad_Vrykul_Gondola_01entry', 575, -557.0330, 1543.4600, -288.9820, 1),
(3887, 'Doodad_icecrown_elevator02entry', 631, 4366.6900, 2365.6200, 358.4790, 1),
(3888, 'Doodad_mushroombase_elevator01entry', 530, 285.6000, 5927.2000, 26.6102, 1),
(3889, 'Doodad_org_arena_axe_pillar01entry', 618, 768.0000, -298.6670, 28.4867, 1),
(3890, 'Doodad_org_arena_lightning_pillar01entry', 618, 768.0000, -277.3330, 28.4867, 1),
(3891, 'Draenei start', 530, -3961.6400, -13931.2000, 100.6150, 1),
(3892, 'Dragonblight Frostmourne Cavern', 571, 4736.3900, -558.6370, 166.0830, 1),
(3893, 'Dragonblight Scarlet Tower', 571, 4692.0500, -356.1290, 178.7370, 1),
(3894, 'Dragonblight The Pit of Narjun', 571, 3740.0300, 2125.2500, 43.0709, 1),
(3895, 'Dragonblight Wintergarde Crypt', 571, 3584.5200, -781.0240, 155.6860, 1),
(3896, 'Dragonblight Wintergarde Mausoleum', 571, 3650.8400, -1123.3200, 89.2307, 1),
(3897, 'Drakuramas Teleport 02', 571, 6175.5900, -2000.6700, 241.7690, 1),
(3898, 'Dun Morogh Chill Breeze Valley', 0, -5550.9700, -76.1191, 426.9310, 1),
(3899, 'Dun Morogh Thunderbrew Distillery', 0, -5595.6700, -513.5950, 409.4150, 1),
(3900, 'Durotar Burning Blade Coven', 1, -88.3030, -4285.0100, 62.0652, 1),
(3901, 'Durotar Dustwind Cave', 1, 877.9880, -4745.6200, 30.4963, 1),
(3902, 'Durotar Razor Hill Barracks', 1, 319.0970, -4812.8800, 10.6054, 1),
(3903, 'Durotar The Den', 1, -588.7030, -4144.9400, 41.1033, 1),
(3904, 'Duskwood Forlorn Rowe', 0, -10320.8000, 368.2760, 60.5037, 1),
(3905, 'Duskwood Rolands Doom', 0, -11106.5000, -1160.6700, 42.2494, 1),
(3906, 'Dustwallow Marsh Emberstrifes Den', 1, -5103.3600, -3949.5300, 41.4934, 1),
(3907, 'Dustwallow Marsh Foothold Citadel', 1, -3721.7400, -4538.6000, 25.9170, 1),
(3908, 'Dustwallow Marsh North Point Tower', 1, -2885.3400, -3419.3200, 47.2420, 1),
(3909, 'Dustwallow Marsh The Great Sea', 1, -4026.5700, -4975.8200, 8.2153, 1),
(3910, 'Dustwallow to Stormwind Teleport', 0, -9008.7900, 851.3200, 105.8900, 1),
(3911, 'Eastern Plaguelands MazraAlor', 0, 3439.2500, -4980.9600, 195.8110, 1),
(3912, 'Eastern Plaguelands Terrorweb Tunnel', 0, 2903.1100, -2614.6700, 89.8071, 1),
(3913, 'Eastern Plaguelands The Noxious Glade', 0, 2714.6600, -5421.4700, 161.4070, 1),
(3914, 'Elevatorentry', 0, -5065.6200, 437.5100, 424.1080, 1),
(3915, 'Elevatorentry', 1, 2261.3300, -5565.5600, 34.2689, 1),
(3916, 'Elevatorentry', 530, 1914.5100, 5514.0200, 280.6880, 1),
(3917, 'Elevatorentry', 571, 2872.9500, 6234.9700, 104.9120, 1),
(3918, 'Elevatorentry', 571, 2894.6900, 6244.8900, 209.1970, 1),
(3919, 'Elevatorentry', 571, 4185.2900, 5283.6200, 39.6833, 1),
(3920, 'Elwynn Forest Thunder Falls', 0, -9291.1000, 677.6830, 131.7780, 1),
(3921, 'Entangle', 531, -8003.0000, 1222.9000, -82.1000, 1),
(3922, 'Entangle', 531, -8022.3000, 1149.0000, -89.1000, 1),
(3923, 'Entangle', 531, -8043.6000, 1254.1000, -84.3000, 1),
(3924, 'Eredar Soul-Eater', 552, 285.5190, 146.1550, 22.3118, 1),
(3925, 'Escape Voltarus', 571, 5875.4300, -1981.3700, 234.6710, 1),
(3926, 'Escape to the Isle of QuelDanas', 530, 12887.6000, -6869.2100, 10.1141, 1),
(3927, 'Escape to the Isle of QuelDanas', 585, 148.4010, 203.4430, -11.9579, 1),
(3928, 'Evergrove innkeeper', 530, 3022.9000, 5435.5900, 146.7010, 1),
(3929, 'Everlook Transporter', 1, 6755.2200, -4658.0400, 724.7950, 1),
(3930, 'Everlook innkeeper', 1, 6695.1500, -4673.0400, 721.6500, 1),
(3931, 'Eversong Woods Commons Hall', 530, 9822.0000, -6694.3700, 2.5945, 1),
(3932, 'Eversong Woods Falthrien Academy', 530, 10158.0000, -6026.5000, 63.7448, 1),
(3933, 'Eversong Woods Huntress of the Sun', 530, 9699.4300, -6701.3600, -0.2076, 1),
(3934, 'Exit Portal', 571, 3877.1400, 6980.8300, 152.0400, 1),
(3935, 'Feathermoon Stronghold innkeeper', 1, -4381.5900, 3289.4500, 13.6266, 1),
(3936, 'Felwood Bloodvenom River', 1, 5288.7400, -544.2570, 328.6870, 1),
(3937, 'Felwood Irontree Cavern', 1, 6353.3200, -1696.9200, 440.0420, 1),
(3938, 'Felwood Shrine of the Deceiver', 1, 4779.2600, -572.0400, 275.8440, 1),
(3939, 'Gadgetzan Transporter', 1, -7109.1200, -3825.2100, 10.1529, 1),
(3940, 'Gadgetzan innkeeper', 1, -7158.9600, -3841.6100, 8.8481, 1),
(3941, 'Gallows End Tavern innkeeper', 0, 2269.5100, 244.9440, 34.3402, 1),
(3942, 'Gates of AhnQiraj', 1, -8233.1200, 1535.7500, -0.3744, 1),
(3943, 'Ghostlands Deatholme', 530, 6465.7700, -6433.6600, 50.4155, 1),
(3944, 'Ghostlands Thalassian Pass', 530, 6554.0200, -6811.2000, 110.6000, 1),
(3945, 'Ghostly Baker', 532, -11057.7000, -1919.8300, 77.3515, 1),
(3946, 'Gnomeregan', 90, -617.4620, 370.6960, -247.1880, 1),
(3947, 'Gravity Lapse - Center Teleport', 585, 148.5000, 181.0000, -16.7000, 1),
(3948, 'Greater Shadowbat', 532, -10935.0000, -1999.5000, 49.4748, 1),
(3949, 'Grizzly Hills - Quest - Arugal Teleport Back', 571, 3841.4000, -3426.6500, 293.1040, 1),
(3950, 'Grizzly Hills Boulder Hills', 571, 5082.6400, -4724.1900, 287.5000, 1),
(3951, 'Grizzly Hills Duskhowl Den', 571, 3992.0100, -4523.4300, 195.6070, 1),
(3952, 'Grizzly Hills Ursocs Den', 571, 4692.7100, -3863.7500, 327.3770, 1),
(3953, 'Grizzly Hills Voldrune Dwelling', 571, 3005.7300, -2610.3100, 98.5530, 1),
(3954, 'Gruuls Lair', 565, 107.8710, 282.5120, 1.9718, 1),
(3955, 'Gunship Portal', 628, 747.0000, -1075.0000, 135.0000, 1),
(3956, 'Gunship Portal', 641, 16.4763, 0.0184, 20.4162, 1),
(3957, 'Gunship Portal', 642, 12.3199, 0.0963, 34.6508, 1),
(3958, 'Heart of the Deconstructor', 603, 886.2750, -12.0545, 409.6020, 1),
(3959, 'Hellfire Citadel: The Blood Furnace', 542, 331.0690, 28.0790, 9.7047, 1),
(3960, 'Hellfire Citadel: The Shattered Halls', 540, 203.6690, 157.2020, -42.3511, 1),
(3961, 'Hellfire Peninsula The Path of Anguish', 530, -525.2730, 2066.0700, 94.5205, 1),
(3962, 'Hex Lord Malacrass', 568, 117.3630, 923.5690, 33.9726, 1),
(3963, 'Hillsbrad Foothills Durnholde Keep', 0, -489.8100, -1480.9400, 88.1965, 1),
(3964, 'HiveZara Swarmer Teleport', 509, -9757.8700, 1416.7100, 76.7664, 1),
(3965, 'HiveZara Swarmer Teleport', 509, -9778.9100, 1419.9800, 61.0743, 1),
(3966, 'HiveZara Swarmer Teleport', 509, -9805.9500, 1422.8500, 77.5852, 1),
(3967, 'HiveZara Swarmer Teleport', 509, -9827.5800, 1506.2800, 82.3052, 1),
(3968, 'HiveZara Swarmer Teleport', 509, -9829.4200, 1456.3700, 90.7015, 1),
(3969, 'Horde Log Ride 01 Begin', 571, 4313.3700, -2958.1700, 318.4630, 1),
(3970, 'Howling Fjord New Agamand Inn', 571, 456.4610, -4519.2400, 244.9780, 1),
(3971, 'Howling Fjord Utgarde Keep', 571, 855.8150, -4831.8500, -115.7170, 1),
(3972, 'Howling Fjord Westguard Keep', 571, 1385.1700, -3244.6400, 162.8370, 1),
(3973, 'Howling Fjord Wildervar Mine', 571, 2604.4700, -5019.1400, 293.8260, 1),
(3974, 'Icecrown Jotunheim', 571, 7220.9200, 4113.1400, 630.8240, 1),
(3975, 'Icecrown Kulgalar Keep', 571, 6831.2300, 3817.0800, 621.1530, 1),
(3976, 'Icecrown Nazanak: The Forgotten Depths', 571, 5853.5100, 1807.1700, -344.9310, 1),
(3977, 'Icecrown The Fleshwerks', 571, 6610.5800, 3359.6700, 660.4740, 1),
(3978, 'Indisposed II', 571, 3454.1100, -2802.3700, 202.1400, 1),
(3979, 'Ironforge The Forlorn Cavern', 0, -4636.2200, -1110.0000, 501.4110, 1),
(3980, 'Ironforge flightMaster', 0, -4821.1300, -1152.4000, 502.2950, 1),
(3981, 'Ironforge innkeeper', 0, -4840.6700, -857.0940, 501.9970, 1),
(3982, 'Ironforge innkeeper', 0, -4908.1700, -970.9860, 505.2260, 1),
(3983, 'Ironforge', 0, -4832.2700, -1069.6400, 502.2680, 1),
(3984, 'Kalecgos', 585, 197.8630, -272.7440, -8.6516, 1),
(3985, 'Lady Faltheress', 129, 2583.1800, 695.8610, 56.8033, 1),
(3986, 'Lady Liadrin', 580, 1720.0200, 643.2330, 28.1335, 1),
(3987, 'Loch Modan Stoutlager Inn', 0, -5391.7300, -2974.8800, 325.9470, 1),
(3988, 'Lord Blackwood', 289, 200.2010, 150.8390, 109.8790, 1),
(3989, 'MaiKyl', 230, 842.7150, -181.5710, -49.6700, 1),
(3990, 'Majordomo Teleport', 409, 848.9330, -812.8750, -229.6010, 1),
(3991, 'Maraudon Portal Effect', 349, 386.2700, 33.4144, -130.9340, 1),
(3992, 'Maraudon', 349, 614.7090, -206.5360, -64.2963, 1),
(3993, 'Mesa Elevator', 209, 2600.8100, 1228.7200, -40.2788, 1),
(3994, 'Mesa Elevator', 209, 2600.8100, 1228.7200, 89.0211, 1),
(3995, 'Mesa Elevator', 209, 2617.5100, 1243.9200, -40.5284, 1),
(3996, 'Mesa Elevator', 209, 2617.5100, 1243.9200, 89.0253, 1),
(3997, 'Mesa Elevatorentry', 1, -1030.1400, -32.0337, 68.2329, 1),
(3998, 'Mesa Elevatorentry', 1, -1032.6600, -26.3241, 141.1470, 1),
(3999, 'Mesa Elevatorentry', 1, -1035.1900, -45.0725, 68.2586, 1),
(4000, 'Mesa Elevatorentry', 1, -1042.0500, -47.7792, 141.1470, 1),
(4001, 'Mesa Elevatorentry', 1, -1284.7700, 185.1110, 130.3230, 1),
(4002, 'Mesa Elevatorentry', 1, -1290.7100, 188.5430, 67.4105, 1),
(4003, 'Mesa Elevatorentry', 1, -1304.5600, 186.1990, 67.5106, 1),
(4004, 'Mesa Elevatorentry', 1, -1308.1800, 180.4040, 130.2800, 1),
(4005, 'Mesa Elevatorentry', 1, -4660.6000, -1828.2700, 85.6823, 1),
(4006, 'Mesa Elevatorentry', 1, -4666.2300, -1851.2600, 85.6813, 1),
(4007, 'Mesa Elevatorentry', 1, -4666.4800, -1832.0900, -45.3171, 1),
(4008, 'Mesa Elevatorentry', 1, -4670.1200, -1845.7300, -45.1181, 1),
(4009, 'Mesa Elevatorentry', 1, -5385.2000, -2485.3300, 89.4297, 1),
(4010, 'Mesa Elevatorentry', 1, -5385.2700, -2492.0100, -41.7147, 1),
(4011, 'Mesa Elevatorentry', 1, -5395.6800, -2501.7500, -41.5869, 1),
(4012, 'Mesa Elevatorentry', 1, -5402.5700, -2501.2400, 89.4297, 1),
(4013, 'Mesa Elevatorentry', 209, 2597.4300, 1232.1000, 89.4297, 1),
(4014, 'Mesa Elevatorentry', 209, 2604.3200, 1231.5800, -41.5870, 1),
(4015, 'Mesa Elevatorentry', 209, 2614.7300, 1241.3200, -41.7145, 1),
(4016, 'Mesa Elevatorentry', 209, 2614.8000, 1248.0000, 89.4297, 1),
(4017, 'Mesa Elevatorentry', 47, 1729.8800, 1354.2700, -45.1181, 1),
(4018, 'Mesa Elevatorentry', 47, 1733.5200, 1367.9100, -45.3171, 1),
(4019, 'Mesa Elevatorentry', 47, 1733.7700, 1348.7400, 85.6813, 1),
(4020, 'Mesa Elevatorentry', 47, 1739.4000, 1371.7300, 85.6823, 1),
(4021, 'Mole Machine Port to Grim Guzzler', 230, 901.0680, -143.9390, -49.7550, 1),
(4022, 'Molten Core', 409, 889.0990, -822.9660, -227.2440, 1),
(4023, 'Move Bind Sight', 230, 824.8090, -176.1660, -49.7551, 1),
(4024, 'Mudsprocket innkeeper', 1, -4629.4300, -3176.1400, 41.2339, 1),
(4025, 'Murta Grimgut', 209, 1891.0700, 1294.7800, 48.2347, 1),
(4026, 'Nagrand Abandoned Armory', 530, -2052.0900, 7432.7600, -24.9648, 1),
(4027, 'Naxxanar portal', 571, 3692.4300, 3577.9400, 473.3200, 1),
(4028, 'Naxxanar portal', 571, 3734.1400, 3571.1200, 341.6620, 1),
(4029, 'Naxxanar portal', 571, 3738.6400, 3567.6700, 294.5220, 1),
(4030, 'Naxxanar portal', 571, 3739.5000, 3567.0000, 337.5640, 1),
(4031, 'Naxxanar portal', 571, 3739.6600, 3567.2900, 286.7850, 1),
(4032, 'Naxxanar portal', 571, 3787.5700, 3558.1100, 469.3220, 1),
(4033, 'Naxxanar portal', 571, 3801.5000, 3586.0500, 49.5700, 1),
(4034, 'Naxxramas Teleport - Sapphiron Exit', 533, 3005.7300, -3414.7600, 297.0260, 1),
(4035, 'Netherstorm Manaforge Ara', 530, 3965.9500, 3911.9700, 178.4460, 1),
(4036, 'Netherstorm The Violet Tower', 530, 2243.2600, 2243.1400, 101.5590, 1),
(4037, 'Nexus Portal', 578, 1045.5700, 1104.2400, 361.0700, 1),
(4038, 'Nexus Portal', 578, 982.1730, 1055.4500, 359.9670, 1),
(4039, 'Nijels Point innkeeper', 1, 255.6150, 1253.7600, 192.2240, 1),
(4040, 'No Mans Land -> Out of Hyjal', 1, 5010.1700, -4554.9400, 852.1460, 1),
(4041, 'Onyxias Lair', 249, -90.7180, -106.2340, -38.1972, 1),
(4042, 'Orb of the Nexus', 578, 1048.2700, 991.3100, 361.0700, 1),
(4043, 'Orgrims Hammer', 622, 23.4324, 0.0201, 33.5795, 1),
(4044, 'Orgrims Hammerdock', 668, 5223.4700, 1537.8700, 645.6480, 1),
(4045, 'Orgrimmar flightMaster', 1, 1676.2500, -4313.4500, 61.9445, 1),
(4046, 'Out of Body Experience', 0, -465.6990, 1495.9800, 17.3595, 1),
(4047, 'Port to Mazthoril', 1, 5904.2000, -4045.9000, 596.4300, 1),
(4048, 'Portal Effect: Acherus', 609, 2400.0300, -5635.0000, 377.0170, 1),
(4049, 'Portal of Immolthar', 429, -37.5643, 813.4330, -7.4382, 1),
(4050, 'Portal to Blasted Lands', 0, -11708.4000, -3168.0000, -5.0700, 1),
(4051, 'Portal to Blasted Lands', 0, -4606.4400, -928.9970, 501.0700, 1),
(4052, 'Portal to Blasted Lands', 0, -9007.5800, 871.8700, 129.6920, 1),
(4053, 'Portal to Blasted Lands', 0, 1768.7700, 55.3698, -46.3194, 1),
(4054, 'Portal to Blasted Lands', 1, -944.0640, 274.8590, 111.7100, 1),
(4055, 'Portal to Blasted Lands', 1, 1472.0900, -4214.6300, 59.2208, 1),
(4056, 'Portal to Blasted Lands', 1, 9661.8300, 2509.6000, 1331.6300, 1),
(4057, 'Portal to Blasted Lands', 530, -4039.0700, -11555.1000, -138.3370, 1),
(4058, 'Portal to Blasted Lands', 530, 9984.0300, -7108.4300, 47.7049, 1),
(4059, 'Portal to Dalaran', 571, 5807.7500, 588.3470, 661.5050, 1),
(4060, 'Portal to Dalaran', 631, -72.7031, 2282.4500, 32.8673, 1),
(4061, 'Portal to Dalaran', 712, 4.3953, 13.6833, 20.8039, 1),
(4062, 'Portal to Dalaran', 713, 22.1770, 22.9527, 35.6576, 1),
(4063, 'Portal to Orgrimmar', 1, 1321.8100, -4383.1900, 26.2300, 1),
(4064, 'Portal to Orgrimmar', 1, 1921.3700, -4149.2400, 40.4075, 1),
(4065, 'Portal to Stormwind', 0, -8446.8300, 339.9310, 121.3290, 1),
(4066, 'Portal to Stormwind', 0, -9143.5800, 375.1030, 90.6907, 1),
(4067, 'Portal to The Purple Parlor', 571, 5822.3500, 837.0760, 680.6570, 1),
(4068, 'Portal to The Purple Parlor', 571, 5848.4800, 853.7060, 843.1820, 1),
(4069, 'Portal to The Violet Citadel', 571, 5413.0100, 2868.1800, 418.6750, 1),
(4070, 'Portal to The Violet Citadel', 571, 5819.2600, 829.7740, 680.2200, 1),
(4071, 'Portal to Undercity', 0, 1774.8000, 761.1270, 55.0477, 1),
(4072, 'Portal to Undercity', 0, 1962.6900, 235.9200, 39.7700, 1),
(4073, 'Portal: Moonglade Effect', 1, 7828.8400, -2245.6500, 463.7070, 1),
(4074, 'Portal: Return from Moonglade Effect', 571, 6215.5400, -8.4001, 410.1650, 1),
(4075, 'Portal: Valley of Echoes', 571, 6390.5300, 237.0120, 395.8130, 1),
(4076, 'Quest - Port Skettis Prisoner - Location 01', 530, -4106.6400, 3029.7600, 344.8770, 1),
(4077, 'Quest - Port Skettis Prisoner - Location 02', 530, -3720.3500, 3789.9100, 302.8880, 1),
(4078, 'Quest - Port Skettis Prisoner - Location 03', 530, -3669.5700, 3386.7400, 312.9550, 1),
(4079, 'Quest - Teleport: Caverns of Time', 1, -8380.5000, -4262.5600, -207.5340, 1),
(4080, 'Quetzlun', 571, 5716.2600, -4369.3400, 385.8850, 1),
(4081, 'Ragefire Chasm', 389, -223.4540, 87.2837, -24.9351, 1),
(4082, 'Raven', 209, 1886.6400, 1299.1100, 48.3146, 1),
(4083, 'Recall', 30, -1347.0000, -292.0000, 91.0000, 1),
(4084, 'Recall', 30, 648.0000, -34.0000, 47.0000, 1),
(4085, 'Redridge Mountains Renders Rock', 0, -8737.4300, -2213.5000, 149.9600, 1),
(4086, 'Redridge Mountains Stonewatch', 0, -9392.0000, -3026.6300, 136.7700, 1),
(4087, 'Return to Temper', 530, -3286.6300, -12908.6000, 11.7562, 1),
(4088, 'Revanchion', 429, -106.3030, 551.2760, -4.3970, 1),
(4089, 'Ritual Preparation', 575, 296.6890, -346.5040, 90.5482, 1),
(4090, 'Ritual of the Sword', 575, 296.6890, -346.5040, 108.5480, 1),
(4091, 'Rizzles Escape', 1, 3697.2000, -3967.1300, 28.3115, 1),
(4092, 'Sacrifice', 429, -25.9536, -448.2270, -36.0912, 1),
(4093, 'Sacrifice', 532, -11234.2000, -1698.4600, 179.2400, 1),
(4094, 'Sacrifice', 553, 4.0000, 608.0000, -13.8165, 1),
(4095, 'Samuro', 230, 847.6740, -175.8690, -49.6706, 1),
(4096, 'Scaffold Carsentry', 0, -6889.3100, -1211.7600, 240.4440, 1),
(4097, 'Scaffold Carsentry', 0, -6895.5000, -1338.1500, 240.2840, 1),
(4098, 'Scaffold Carsentry', 0, -6899.7000, -1207.6000, 240.4100, 1),
(4099, 'Scaffold Carsentry', 0, -6903.5800, -1201.6700, 193.8930, 1),
(4100, 'Scaffold Carsentry', 0, -6905.7300, -1211.8300, 214.0430, 1),
(4101, 'Scaffold Carsentry', 469, -6889.3100, -1211.7600, 240.4440, 1),
(4102, 'Scaffold Carsentry', 469, -6895.5000, -1338.1500, 240.2840, 1),
(4103, 'Scaffold Carsentry', 469, -6899.7000, -1207.6000, 240.4100, 1),
(4104, 'Scaffold Carsentry', 469, -6903.5800, -1201.6700, 193.8930, 1),
(4105, 'Scaffold Carsentry', 469, -6905.7300, -1211.8300, 214.0430, 1),
(4106, 'Scaffold Carsentry', 530, -3292.8900, 1901.2800, 142.1590, 1),
(4107, 'Scaffold Carsentry', 530, -3297.0700, 1898.6700, 101.3760, 1),
(4108, 'Scaffold Carsentry', 530, -3300.1400, 1902.2300, 168.4120, 1),
(4109, 'Scaffold Carsentry', 530, -3301.8300, 1895.4800, 122.1590, 1),
(4110, 'Scaffold Carsentry', 530, -3306.6700, 1902.6700, 168.5590, 1),
(4111, 'Scaffold Carsentry', 571, 8115.1400, -245.3940, 900.1890, 1),
(4112, 'Scaffold Carsentry', 571, 8118.4000, -380.0000, 1027.9200, 1),
(4113, 'Scaffold Carsentry', 571, 8118.7700, -386.8160, 981.7890, 1),
(4114, 'Scaffold Carsentry', 571, 8119.4000, -256.0000, 926.3570, 1),
(4115, 'Scaffold Carsentry', 571, 8119.6800, -379.0410, 1028.0500, 1),
(4116, 'Scaffold Carsentry', 571, 8122.6000, -247.5780, 926.3930, 1),
(4117, 'Scaffold Carsentry', 571, 8123.9300, -240.0710, 880.1890, 1),
(4118, 'Scaffold Carsentry', 571, 8127.2900, -380.4440, 1001.7900, 1),
(4119, 'Scroll of Recall', 0, -103.9880, -902.7950, 55.5340, 1),
(4120, 'Scroll of Recall', 0, -10446.9000, -3261.9100, 20.1790, 1),
(4121, 'Scroll of Recall', 0, -5506.3400, -704.3480, 392.6860, 1),
(4122, 'Scroll of Recall', 0, -9470.7600, 3.9090, 49.7940, 1),
(4123, 'Scroll of Recall', 0, 1804.8400, 196.3220, 70.3990, 1),
(4124, 'Scroll of Recall', 0, 286.3140, -2184.0900, 122.6120, 1),
(4125, 'Scroll of Recall', 1, -1060.2700, 23.1370, 141.4550, 1),
(4126, 'Scroll of Recall', 1, -506.2240, -2590.0800, 113.1500, 1),
(4127, 'Scroll of Recall', 1, -7135.7200, -3787.7700, 8.7990, 1),
(4128, 'Scroll of Recall', 1, 6395.7100, 433.2560, 33.2600, 1),
(4129, 'Scryers Tier innkeeper', 530, -2184.0400, 5399.6200, 51.9658, 1),
(4130, 'Searing Gorge Blackchar Cave', 0, -7249.7500, -814.5230, 296.6230, 1),
(4131, 'Searing Gorge Blackrock Mountain', 0, -7243.0600, -1122.9400, 274.6300, 1),
(4132, 'Searing Gorge The Slag Pit', 0, -6601.6300, -1250.7500, 188.0640, 1),
(4133, 'Sewer Teleport 01', 571, 5881.2000, 666.5000, 615.7940, 1),
(4134, 'Sewer Teleport 04', 571, 5815.3000, 714.6000, 619.0980, 1),
(4135, 'Shadow Port', 33, -103.4600, 2122.1000, 155.6550, 1),
(4136, 'Shadow Port', 33, -105.6540, 2154.9800, 156.4300, 1),
(4137, 'Shadow Port', 33, -84.9900, 2151.0100, 155.6200, 1),
(4138, 'Shadowblink', 469, -7469.9300, -1227.9300, 476.7770, 1),
(4139, 'Shadowblink', 469, -7486.3600, -1194.3200, 476.8000, 1),
(4140, 'Shadowblink', 469, -7500.7000, -1249.8900, 476.7980, 1),
(4141, 'Shadowblink', 469, -7506.5800, -1165.2600, 476.7960, 1),
(4142, 'Shadowblink', 469, -7524.3600, -1219.1200, 476.7940, 1),
(4143, 'Shadowblink', 469, -7538.6300, -1273.6400, 476.8000, 1),
(4144, 'Shadowblink', 469, -7542.4700, -1191.9200, 476.3550, 1),
(4145, 'Shadowblink', 469, -7561.5400, -1244.0100, 476.8000, 1),
(4146, 'Shadowblink', 469, -7581.1100, -1216.1900, 476.8000, 1),
(4147, 'Shadowmoon Darkcaster', 540, 72.4708, 184.4520, -13.2380, 1),
(4148, 'Shadowmoon Valley Oronoks Farm', 530, -2802.3200, 1286.4600, 77.3836, 1),
(4149, 'Shattrath Banish Teleport', 530, -1500.0300, 5217.1400, 32.4600, 1),
(4150, 'Shattrath City Aldor Rise', 530, -1751.2400, 5641.2300, 129.0710, 1),
(4151, 'Shattrath City Scryers Tier', 530, -2041.2900, 5561.9700, 54.4371, 1),
(4152, 'Shattrath City innkeeper', 530, -1903.7900, 5765.7000, 131.2960, 1),
(4153, 'Shattrath Portal to Darnassus', 530, -1790.9800, 5413.9800, -12.4282, 1),
(4154, 'Shattrath Portal to Exodar', 530, -1880.2800, 5357.5300, -12.4281, 1),
(4155, 'Shattrath Portal to Exodar', 530, -4031.2400, -11569.6000, -138.2990, 1),
(4156, 'Shattrath Portal to Ironforge', 530, -1795.7900, 5399.6300, -12.4281, 1),
(4157, 'Shattrath Portal to Orgrimmar', 530, -1934.4900, 5453.4800, -12.4279, 1),
(4158, 'Shattrath Portal to Silvermoon', 530, -1894.6900, 5362.3400, -12.4282, 1),
(4159, 'Shattrath Portal to Stormwind', 0, -8998.1400, 861.2540, 29.6206, 1),
(4160, 'Shattrath Portal to Stormwind', 530, -1792.7800, 5406.5400, -12.4279, 1),
(4161, 'Shattrath Portal to Thunder Bluff', 530, -1936.3200, 5445.9500, -12.4282, 1),
(4162, 'Shattrath Portal to Undercity', 0, 1773.4200, 61.7391, -46.3215, 1),
(4163, 'Shattrath Portal to Undercity', 530, -1931.4800, 5460.4900, -12.4281, 1),
(4164, 'Ship (The Bravery)dock', 0, -8644.6400, 1333.7100, 5.7993, 1),
(4165, 'Ship (The Bravery)dock', 1, 6419.1900, 818.6660, 6.1894, 1),
(4166, 'Ship (The Lady Mehley)dock', 0, -3898.2700, -597.7260, 5.5310, 1),
(4167, 'Ship (The Lady Mehley)dock', 1, -4006.9700, -4730.3300, 5.5366, 1),
(4168, 'Ship (The Maidens Fancy)dock', 0, -14279.5000, 571.2000, 6.0777, 1),
(4169, 'Ship (The Maidens Fancy)dock', 1, -997.3520, -3830.2500, 5.5803, 1),
(4170, 'Ship, Icebreaker (Northspear)dock', 571, 584.0140, -5098.6700, -6.4857, 1),
(4171, 'Ship, Night Elf (Elunes Blessing)dock', 1, 6546.9800, 927.7040, 6.1894, 1),
(4172, 'Ship, Night Elf (Elunes Blessing)dock', 530, -4264.1400, -11328.3000, 6.0035, 1),
(4173, 'Ship, Night Elf (Feathermoon Ferry)dock', 1, -4210.9100, 3280.7300, 5.7750, 1),
(4174, 'Ship, Night Elf (Moonspray)dock', 1, 6584.1600, 773.2750, 6.0986, 1),
(4175, 'Ship, Night Elf (Moonspray)dock', 1, 8545.0900, 1016.1700, 6.2912, 1),
(4176, 'Sholazar Basin Nesingwary Base Camp', 571, 5559.1100, 5765.4300, -77.9385, 1),
(4177, 'Sholazar Basin The Seabreach Flow', 571, 5527.8500, 5348.4900, -134.4620, 1),
(4178, 'Silithus Ortells Hideout', 1, -7578.7200, 196.9140, 11.5488, 1),
(4179, 'Silithus Twilights Run', 1, -6265.9300, 41.8866, 9.0625, 1),
(4180, 'Silverpine Forest Fenris Keep', 0, 993.8510, 690.0600, 74.8984, 1),
(4181, 'Sister Mercydock', 571, 254.1330, -3752.8000, 0.2291, 1),
(4182, 'Sister Mercydock', 571, 93.8904, -3681.4100, 0.2103, 1),
(4183, 'Skadi Teleport', 575, 476.7990, -511.1670, 104.7230, 1),
(4184, 'Skarthis the Summoner', 547, -76.9917, -157.0810, -2.1064, 1),
(4185, 'Socrethar Return Portal Effect', 530, 4778.4600, 3455.3600, 104.1300, 1),
(4186, 'Stonetalon Mountains Boulderslide Ravine', 1, -97.6734, 185.5910, 96.6757, 1),
(4187, 'Stonetalon Mountains Cragpool Lake', 1, 1605.9900, 96.7067, 98.5654, 1),
(4188, 'Stonetalon Mountains Windshear Mine', 1, 935.9090, -302.7410, 0.0220, 1),
(4189, 'Storm Peaks', 571, 8974.3700, -1280.5600, 1059.0100, 1),
(4190, 'Stormwind Stockade', 34, 131.1490, 3.3395, -25.5229, 1),
(4191, 'Stormwind to Dustwallow Teleport', 1, -3722.9100, -4413.9600, 26.1300, 1),
(4192, 'Stoutlager Inn innkeeper', 0, -5377.9100, -2973.9100, 323.2520, 1),
(4193, 'Stranglethorn Vale Kurzens Compound', 0, -11571.3000, -644.0530, 31.2554, 1),
(4194, 'Stranglethorn Vale The Vile Reef', 0, -12211.5000, 644.0560, -67.1350, 1),
(4195, 'Subwayentry', 369, -1.0668, -11.3475, -3.9157, 1),
(4196, 'Subwayentry', 369, -1.0668, 2470.3000, -3.9157, 1),
(4197, 'Subwayentry', 369, -39.7335, 9.7886, -3.9157, 1),
(4198, 'Subwayentry', 369, -39.7335, -10.3899, -3.9157, 1),
(4199, 'Subwayentry', 369, -39.7335, 30.3816, -3.9157, 1),
(4200, 'Subwayentry', 369, -50.9334, 2472.9300, -3.9157, 1),
(4201, 'Subwayentry', 369, -50.9334, 2512.1500, -3.9157, 1),
(4202, 'Subwayentry', 369, -51.0097, 2492.2300, -3.9157, 1),
(4203, 'Subwayentry', 369, 10.1333, 2510.4900, -3.9157, 1),
(4204, 'Subwayentry', 369, 10.1333, 2490.7400, -3.9157, 1),
(4205, 'Subwayentry', 369, 10.1333, 8.8000, -3.9157, 1),
(4206, 'Subwayentry', 369, 10.1896, 28.7706, -3.9157, 1),
(4207, 'Summon Menagerie', 578, 1116.1100, 1075.1700, 508.3490, 1),
(4208, 'Summon Menagerie', 578, 1163.7200, 1170.9900, 527.3220, 1),
(4209, 'Summon Menagerie', 578, 968.7080, 1042.4900, 527.3220, 1),
(4210, 'Sunken Temple', 109, -455.3780, 76.5158, -93.3827, 1),
(4211, 'Surface Portal', 571, 5795.8800, 2070.8700, -344.0460, 1),
(4212, 'Surface Portal', 571, 6203.8700, 2262.1700, 497.1970, 1),
(4213, 'Surge Needle Teleporter', 571, 3444.3800, 2361.8700, 38.7409, 1),
(4214, 'Swamp of Sorrows Stagalbog Cave', 0, -10919.3000, -3673.0500, 11.0245, 1),
(4215, 'Swamp of Sorrows Stonard', 0, -10437.8000, -3307.0400, 20.4499, 1),
(4216, 'Teldrassil Banethil Barrow Den', 1, 9794.8400, 1549.1500, 1262.8000, 1),
(4217, 'Teldrassil Fel Rock', 1, 10124.0000, 1115.9100, 1322.8200, 1),
(4218, 'Teldrassil Shadowthread Cave', 1, 10889.4000, 917.5860, 1326.6400, 1),
(4219, 'Teleport - Coldarra, Transitus Shield to Amber Ledge', 571, 3594.1800, 5997.5100, 136.2150, 1),
(4220, 'Teleport - Dalaran to Wintergrasp', 571, 5325.0600, 2843.3600, 409.2850, 1),
(4221, 'Teleport Darkshire', 0, -10566.0000, -1189.0000, 28.0000, 1),
(4222, 'Teleport Defenders', 607, 1226.4300, -71.9616, 70.0842, 1),
(4223, 'Teleport Duskwood', 0, -10368.0000, -422.0000, 64.1214, 1),
(4224, 'Teleport Elwynn', 0, -9104.0000, -70.0000, 83.0000, 1),
(4225, 'Teleport Goldshire', 0, -9464.0000, 62.0000, 56.0000, 1),
(4226, 'Teleport Left', 533, 2692.0000, -3399.2700, 267.6860, 1),
(4227, 'Teleport Moonbrook', 0, -11020.0000, 1436.0000, 43.6892, 1),
(4228, 'Teleport Players on Victory', 631, -548.9830, 2211.2400, 539.2900, 1),
(4229, 'Teleport Return', 533, 2685.0600, -3502.3700, 261.3150, 1),
(4230, 'Teleport Right', 533, 2692.0000, -3321.8600, 267.6860, 1),
(4231, 'Teleport Sanctum Moon - Down', 530, 7513.6300, -6388.9300, 23.8000, 1),
(4232, 'Teleport Sanctum Sun - Down', 530, 7199.4000, -7097.3600, 66.9700, 1),
(4233, 'Teleport To Zone In', 616, 728.0550, 1329.0300, 275.0000, 1),
(4234, 'Teleport Violet Citadel Spire Down', 571, 5790.0000, 734.0000, 640.0000, 1),
(4235, 'Teleport Westfall', 0, -10643.0000, 1052.0000, 33.8437, 1),
(4236, 'Teleport and Transform', 580, 1667.6400, 633.4660, 28.0500, 1),
(4237, 'Teleport back to Main Room', 603, 1970.6100, -25.5988, 324.5500, 1),
(4238, 'Teleport from Azshara Tower', 1, 3641.0000, -4702.0000, 121.0000, 1),
(4239, 'Teleport to Ashtongue NPCs', 564, 702.2200, 200.3000, 125.0100, 1),
(4240, 'Teleport to Center', 568, -34.3160, 1149.6400, 19.1550, 1),
(4241, 'Teleport to Chamber Illusion', 603, 2043.1200, -25.6981, 239.7210, 1),
(4242, 'Teleport to CoT Stratholme Phase 4', 595, 2071.5500, 1287.6800, 141.6870, 1),
(4243, 'Teleport to Council', 564, 603.4200, 305.9820, 271.9000, 1),
(4244, 'Teleport to Final Chamber Effect DND', 531, -8632.8400, 2055.8700, 108.8600, 1),
(4245, 'Teleport to Gnomeregan', 0, -5095.0000, 757.0000, 261.0000, 1),
(4246, 'Teleport to Hall of Command', 0, 2402.6200, -5633.2800, 377.0210, 1),
(4247, 'Teleport to Heart of Acherus', 609, 2419.9100, -5620.4800, 420.6440, 1),
(4248, 'Teleport to Icecrown Illusion', 603, 1949.1300, -80.6744, 239.9900, 1),
(4249, 'Teleport to Inside Violet Hold', 608, 1857.2400, 803.8770, 44.0085, 1),
(4250, 'Teleport to Lake Wintergrasp', 571, 4561.5800, 2835.3300, 389.7900, 1),
(4251, 'Teleport to Lake Wintergrasp', 571, 5025.7100, 3673.4100, 362.6870, 1),
(4252, 'Teleport to Lake Wintergrasp', 571, 5094.6700, 2170.3300, 365.6010, 1),
(4253, 'Teleport to Lake Wintergrasp', 571, 5386.0500, 2840.9700, 418.6750, 1),
(4254, 'Teleport to Molten Core DND', 409, 1080.0000, -483.0000, -108.0000, 1),
(4255, 'Teleport to Stormwind Illusion', 603, 1954.1400, 21.5220, 239.7180, 1),
(4256, 'Teleport to Sunwell Plateau', 580, 1861.4500, 495.1250, 82.9059, 1),
(4257, 'Teleport to Twin Emps Effect DND', 531, -8971.8100, 1321.4700, -104.2490, 1),
(4258, 'Teleport to Violet Stand', 571, 5724.6200, 1013.1700, 174.4800, 1),
(4259, 'Teleport to the Silvermoon', 530, 10021.1000, -7014.8700, 49.7100, 1),
(4260, 'Teleport to the Undercity', 0, 1805.9300, 335.6600, 70.3900, 1),
(4261, 'Teleport', 1, -3891.8000, -4609.9700, 9.5011, 1),
(4262, 'Teleport', 409, 736.5160, -1176.3500, -119.0060, 1),
(4263, 'Teleport', 531, -8306.6800, 2060.8400, 133.0620, 1),
(4264, 'Teleport', 531, -8330.6300, 2123.1400, 133.0620, 1),
(4265, 'Teleport', 533, 2633.4900, -3529.5600, 274.1110, 1),
(4266, 'Teleport', 533, 2905.6300, -3769.9600, 273.6200, 1),
(4267, 'Teleport', 571, 3574.2200, 6652.1300, 195.1850, 1),
(4268, 'Teleport', 571, 3646.7400, 5893.2000, 174.4830, 1),
(4269, 'Teleport', 571, 4590.9400, -5711.2400, 184.5070, 1),
(4270, 'Teleport', 576, 504.7420, 88.9122, -16.1245, 1),
(4271, 'Teleport', 578, 1103.4700, 1049.5700, 512.0000, 1),
(4272, 'Teleport', 600, -369.0000, -601.0000, 2.0000, 1),
(4273, 'Teleport: Argent Tournament', 571, 8480.5500, 1092.9000, 554.4850, 1),
(4274, 'Teleport: Black Temple', 530, -3560.5200, 583.3530, 10.9431, 1),
(4275, 'Teleport: Darnassus', 1, 9664.1400, 2526.3600, 1332.6900, 1),
(4276, 'Teleport: Moonglade', 1, 7992.6200, -2680.0400, 512.0990, 1),
(4277, 'Teleport: Shattrath', 530, -1842.0700, 5497.1700, -12.4306, 1),
(4278, 'Teleport: Stonard', 0, -10469.0000, -3331.5400, 25.4716, 1),
(4279, 'Teleport: Theramore', 1, -3748.1100, -4440.2100, 30.5688, 1),
(4280, 'Tempest Keep', 550, 411.4090, -39.8267, 20.1802, 1);
INSERT INTO `playerbots_travelnode` (`id`, `name`, `map_id`, `x`, `y`, `z`, `linked`) VALUES
(4281, 'Tempest Keep: The Arcatraz', 552, 278.6480, -12.6903, 22.4479, 1),
(4282, 'Tempest Keep: The Botanica', 553, 17.5470, 404.8610, -27.1645, 1),
(4283, 'Tempest Keep: The Mechanar', 554, 169.2420, -12.2941, -0.0010, 1),
(4284, 'Terokkar Forest Skettis', 530, -3944.9300, 3664.0900, 287.9900, 1),
(4285, 'Terokkar Forest Terokks Rest', 530, -3868.3500, 3521.8800, 278.5610, 1),
(4286, 'Test of Lore', 1, -2354.0300, -1902.0700, 95.7800, 1),
(4287, 'The Barrens Baeldun Keep', 1, -4071.7300, -2381.6400, 126.2140, 1),
(4288, 'The Barrens Dreadmist Den', 1, 318.8870, -2225.7300, 212.5040, 1),
(4289, 'The Chilled Quagmire spiritguide', 571, 5103.1300, 3462.1300, 368.5680, 1),
(4290, 'The Chilled Quagmire spirithealer', 571, 5099.0300, 3469.6700, 368.4850, 1),
(4291, 'The Nexus', 576, 545.9580, -125.9330, -24.9367, 1),
(4292, 'The Skybreaker', 623, 5.2403, 0.2579, 20.8691, 1),
(4293, 'The Storm Peaks Frostfloe Deep', 571, 8300.4300, -2564.8600, 1153.5900, 1),
(4294, 'The Storm Peaks Frostgrips Hollow', 571, 6956.1400, -2.5143, 808.5300, 1),
(4295, 'The Storm Peaks Gimoraks Den', 571, 7920.2400, -1625.2100, 910.8520, 1),
(4296, 'The Storm Peaks Hibernal Cavern', 571, 7241.0300, -2075.5900, 763.0780, 1),
(4297, 'The Storm Peaks Plain of Echoes', 571, 8109.8900, -2815.7900, 1135.3200, 1),
(4298, 'The Storm Peaks The Forlorn Mine', 571, 6929.4000, -1315.7700, 831.3460, 1),
(4299, 'The Storm Peaks The Frozen Mine', 571, 7768.5000, -16.3215, 864.4010, 1),
(4300, 'The Storm Peaks', 571, 7268.5100, -2125.3800, 778.1580, 1),
(4301, 'The Sunken Ring spiritguide', 571, 5104.7500, 2300.9500, 368.5680, 1),
(4302, 'The Zephyrdock', 1, -1026.8600, 358.4000, 133.3400, 1),
(4303, 'The Zephyrdock', 1, 1139.7300, -4142.9300, 52.0080, 1),
(4304, 'Thousand Needles Darkcloud Pinnacle', 1, -4904.6700, -1970.1300, 86.8811, 1),
(4305, 'Thousand Needles Roguefeather Den', 1, -5533.0400, -1602.8800, 29.1719, 1),
(4306, 'Thousand Needles Splithoof Hold', 1, -4952.9400, -2337.3000, -56.5321, 1),
(4307, 'Thousand Needles The Weathered Nook', 1, -5217.6500, -2788.9900, -7.4459, 1),
(4308, 'Thrall', 0, 1953.8400, 233.8350, 41.8800, 1),
(4309, 'Thrall', 1, 1920.0100, -4123.9500, 43.6300, 1),
(4310, 'Thunderbrew Distillery innkeeper', 0, -5601.6000, -531.2030, 399.7370, 1),
(4311, 'Tirisfal Glades Agamand Family Crypt', 0, 3043.6500, 681.8670, 67.0126, 1),
(4312, 'Tirisfal Glades Gallows End Tavern', 0, 2262.2600, 244.2570, 33.7170, 1),
(4313, 'To Icecrown Airship - Player - Aura - Teleport to Dalaran Trigger', 571, 5831.5300, 497.0880, 657.4660, 1),
(4314, 'Toshleys Station Transporter', 530, 2054.0500, 5569.1600, 263.5710, 1),
(4315, 'Translocate', 0, 1805.0000, 327.0000, 70.5000, 1),
(4316, 'Translocate', 530, -2259.7400, 3215.0300, -4.0500, 1),
(4317, 'Translocate', 530, -2307.3500, 3123.9200, 13.6900, 1),
(4318, 'Translocate', 530, -589.0000, 4079.0000, 143.3000, 1),
(4319, 'Translocate', 530, 12780.9000, -6877.5000, 22.7861, 1),
(4320, 'Translocate', 530, 9334.5000, -7880.7600, 74.9094, 1),
(4321, 'Translocation', 530, -594.0000, 4079.0000, 94.0000, 1),
(4322, 'Trespasser!', 571, 5773.0000, 703.5000, 641.6000, 1),
(4323, 'Trespasser!', 571, 5846.5000, 605.5000, 650.9000, 1),
(4324, 'Trespasser!', 571, 8460.0000, 700.0000, 547.4000, 1),
(4325, 'Trespasser!', 571, 8573.0000, 703.9000, 547.3000, 1),
(4326, 'Turtle (Green Island)dock', 571, 2649.6000, 844.8000, 1.7773, 1),
(4327, 'Turtle (Walker of Waves)dock', 571, 2638.1300, 938.4000, 1.7773, 1),
(4328, 'Uldaman', 70, -42.3058, 263.8750, -48.9355, 1),
(4329, 'Ulduar spirithealer', 571, 9025.6600, -1178.5700, 1060.0800, 1),
(4330, 'Undervatorentry', 0, 1552.8000, 240.7730, 55.4904, 1),
(4331, 'Undervatorentry', 0, 1596.1500, 283.2000, 55.4904, 1),
(4332, 'Undervatorentry', 0, 1596.2000, 197.1040, 55.4904, 1),
(4333, 'Undervatorentryentry', 0, 1564.0000, 240.6560, 55.7571, 1),
(4334, 'Undervatorentryentry', 0, 1595.3800, 197.7060, 55.4904, 1),
(4335, 'Undervatorentryentry', 0, 1595.3800, 213.3330, 57.8905, 1),
(4336, 'Undervatorentryentry', 0, 1595.6500, 271.8890, 55.7571, 1),
(4337, 'Use Legion Teleporter', 530, -2833.0900, 1949.8900, 201.2560, 1),
(4338, 'Vampiric Shadowbat', 532, -10930.9000, -1995.7500, 49.4768, 1),
(4339, 'Vanish', 309, -11516.1000, -1605.3100, 41.3000, 1),
(4340, 'Vator2entry', 90, -800.3290, 314.9250, -272.4900, 1),
(4341, 'Vatorentry', 0, -5163.7700, 655.3050, 348.5880, 1),
(4342, 'Vatorentry', 0, -5164.2400, 650.3540, 247.9780, 1),
(4343, 'Vortex', 616, 755.0000, 1301.0000, 280.0000, 1),
(4344, 'Watery Grave', 548, 337.6900, -732.8700, -13.7400, 1),
(4345, 'Watery Grave', 548, 365.5300, -737.1200, -14.0000, 1),
(4346, 'Watery Grave', 548, 366.2700, -709.4000, -13.9200, 1),
(4347, 'Watery Grave', 548, 372.8500, -690.8400, -13.9100, 1),
(4348, 'Weegli Blastfuse', 209, 1881.0500, 1297.3600, 48.4190, 1),
(4349, 'Western Plaguelands Mardenholde Keep', 0, 2936.4100, -1395.9000, 166.0270, 1),
(4350, 'Westfall The Cooper Residence', 0, -11023.1000, 1547.4800, 44.4936, 1),
(4351, 'Winterspring Everlook', 1, 6768.3400, -4668.5200, 723.7480, 1),
(4352, 'Winterspring Moon Horror Den', 1, 7124.2700, -4637.7800, 639.6550, 1),
(4353, 'Wyrmrest Temple flightMaster', 571, 3647.2600, 244.0510, 52.3397, 1),
(4354, 'Wyrmrest Temple spirithealer', 571, 3546.8600, 272.9880, 45.5817, 1),
(4355, 'Zeppelin (The Iron Eagle)dock', 0, -12452.0000, 221.0670, 31.7681, 1),
(4356, 'Zeppelin (The Iron Eagle)dock', 1, 1359.5700, -4632.2000, 53.6569, 1),
(4357, 'Zeppelin (The Purple Princess)dock', 0, -12407.5000, 211.8380, 31.5014, 1),
(4358, 'Zeppelin (The Purple Princess)dock', 0, 2061.4200, 235.5580, 100.1520, 1),
(4359, 'Zeppelin (The Thundercaller)dock', 0, 2064.2400, 291.6030, 97.0904, 1),
(4360, 'Zeppelin (The Thundercaller)dock', 1, 1319.3200, -4658.0900, 53.8358, 1),
(4361, 'Zeppelindock', 571, 1412.9900, -3092.5800, 166.2270, 1),
(4362, 'ZulDrak Amphitheater of Anguish', 571, 5715.7900, -2945.0100, 296.5510, 1),
(4363, 'ZulDrak ZimTorga', 571, 5756.5800, -3567.6300, 387.0390, 1),
(4364, '[PH] Teleport to Auberdine', 1, 6581.0500, 767.5000, 5.7843, 1),
(4365, '[PH] Teleport to Booty Bay', 0, -14457.0000, 496.4500, 39.1392, 1),
(4366, '[PH] Teleport to Felwood', 1, 5483.9000, -749.8810, 334.6210, 1),
(4367, '[PH] Teleport to GromGol', 0, -12415.0000, 207.6180, 31.5017, 1),
(4368, '[PH] Teleport to Menethil Harbor', 0, -3752.8100, -851.5580, 10.1153, 1),
(4369, '[PH] Teleport to Orgrimmar', 1, 1552.5000, -4420.6600, 8.9480, 1),
(4370, '[PH] Teleport to Theramore', 1, -3615.4900, -4467.3400, 21.6032, 1),
(4371, 'c-Maraudon', 1, -1424.0400, 2945.0100, 134.5400, 1),
(4372, 'c-Tauren start', 1, -3034.7000, 144.0400, 70.8700, 1),
(4373, 'c1-Blackfathom Deeps', 1, 4158.0100, 877.6000, -20.6800, 1),
(4374, 'c1-Blackrock Mountain', 0, -7502.2000, -1152.9800, 269.5500, 1),
(4375, 'c1-Coilfang', 530, 571.1000, 6938.9700, -16.8100, 1),
(4376, 'c1-Deadmine exit', 0, -11367.5000, 1617.1000, 71.2200, 1),
(4377, 'c1-Dire Maul', 1, -3626.3900, 917.3700, 150.1300, 1),
(4378, 'c1-Ebon Hold', 609, 2390.0200, -5640.9100, 377.0900, 1),
(4379, 'c1-Sunken Temple', 0, -10416.5000, -3832.5300, -36.9200, 1),
(4380, 'c1-The Noxious Pass', 609, 2528.2200, -5580.4400, 162.0200, 1),
(4381, 'c1-Timbermaw Hold', 1, 7016.7500, -2153.8400, 595.0900, 1),
(4382, 'c1-Wailing Caverns', 1, -588.5300, -2037.6900, 57.6000, 1),
(4383, 'c2-Blackfathom Deeps', 1, 4156.6000, 909.8900, -20.9700, 1),
(4384, 'c2-Blackrock Mountain', 0, -7591.3100, -1114.4400, 249.9100, 1),
(4385, 'c2-Coilfang', 530, 571.1000, 6938.9700, -15.2000, 1),
(4386, 'c2-Deadmine exit', 0, -11367.1000, 1610.4800, 76.6300, 1),
(4387, 'c2-Dire Maul', 1, -3628.0800, 919.5500, 137.8400, 1),
(4388, 'c2-Ebon Hold', 609, 2383.6500, -5645.2000, 420.7700, 1),
(4389, 'c2-Sunken Temple', 0, -10408.7000, -3834.2900, -44.6900, 1),
(4390, 'c2-The Noxious Pass', 609, 2538.3500, -5573.0500, 162.4600, 1),
(4391, 'c3-Blackfathom Deeps', 1, 4157.4600, 916.4400, -17.4000, 1),
(4392, 'c3-Coilfang', 530, 651.0700, 6865.3700, -82.3400, 1),
(4393, 'c3-Deadmine exit', 0, -11381.5000, 1584.1100, 82.1000, 1),
(4394, 'c3-Ebon Hold', 609, 2325.0300, -5659.6000, 382.2400, 1),
(4395, 'c3-Scarlet Enclave', 609, 2409.0900, -5722.3700, 154.0000, 1),
(4396, 'c3-Sunken Temple', 0, -10325.2000, -3865.8600, -44.4500, 1),
(4397, 'c3-The Noxious Pass', 609, 2546.6100, -5563.8900, 162.8800, 1),
(4398, 'c4-Coilfang', 530, 607.1700, 6908.6800, -49.2000, 1),
(4399, 'c4-Deadmine exit', 0, -11379.6000, 1578.7900, 87.8400, 1),
(4400, 'c4-Ebon Hold', 609, 2348.5800, -5695.3500, 382.2400, 1),
(4401, 'c4-Scarlet Enclave', 609, 2402.8600, -5727.0300, 154.0000, 1),
(4402, 'c4-The Noxious Pass', 609, 2563.5200, -5547.7900, 163.2700, 1),
(4403, 'c5-Coilfang', 530, 574.6800, 6942.9300, -37.7200, 1),
(4404, 'c5-Deadmine exit', 0, -11340.2000, 1571.6100, 94.4400, 1),
(4405, 'c6-Coilfang', 530, 723.7400, 6865.7800, -74.1000, 1),
(4406, 'c7-Coilfang', 530, 731.5700, 6866.0100, -70.4700, 1);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
-- Manual travelnode coverage for the Aldrassil ramp in Shadowglen
-- (Teldrassil, map 1, zone 141). Adds 9 anchor nodes along the spiral
-- ramp (base -> intermediate ramp waypoints -> top platform near
-- Tenaron Stormgrip). All nodes are `linked = 0` so
-- `.playerbots travel generatenode` will iterate them and let mmap
-- compute the actual walk paths between consecutive nodes. Splitting
-- the climb into short segments (~30y each) gives mmap a much better
-- chance of resolving each piece than a single 300y end-to-end probe.
--
-- TEMPORARILY DISABLED: isolating generatenode behaviour on the OG
-- (cmangos-imported) graph. Re-enable by removing the comment prefix
-- once the regen flow is verified stable.
-- SET @n1 := (SELECT IFNULL(MAX(id), 0) + 1 FROM playerbots_travelnode);
-- SET @n2 := @n1 + 1;
-- SET @n3 := @n1 + 2;
-- SET @n4 := @n1 + 3;
-- SET @n5 := @n1 + 4;
-- SET @n6 := @n1 + 5;
-- SET @n7 := @n1 + 6;
-- SET @n8 := @n1 + 7;
-- SET @n9 := @n1 + 8;
-- INSERT INTO playerbots_travelnode (id, name, map_id, x, y, z, linked) VALUES
-- (@n1, 'Aldrassil Ramp 1 (base)', 1, 10413.756, 887.97363, 1319.3668, 0),
-- (@n2, 'Aldrassil Ramp 2', 1, 10440.520, 870.32320, 1328.9324, 0),
-- (@n3, 'Aldrassil Ramp 3', 1, 10497.001, 854.46014, 1345.1770, 0),
-- (@n4, 'Aldrassil Ramp 4', 1, 10517.199, 821.48640, 1354.7914, 0),
-- (@n5, 'Aldrassil Ramp 5', 1, 10477.926, 847.88855, 1372.1685, 0),
-- (@n6, 'Aldrassil Ramp 6', 1, 10455.358, 831.34240, 1380.9377, 0),
-- (@n7, 'Aldrassil Ramp 7', 1, 10460.220, 800.71716, 1388.3368, 0),
-- (@n8, 'Aldrassil Ramp 8', 1, 10507.434, 793.30420, 1397.2166, 0),
-- (@n9, 'Aldrassil Ramp 9 (top)', 1, 10495.496, 804.67700, 1397.2662, 0);
SELECT 1; -- no-op so the file is still valid SQL for the updater

351
extract_ac_client_data.sh Normal file
View File

@ -0,0 +1,351 @@
#!/usr/bin/env bash
# =============================================================================
# AzerothCore client-data extraction for mod-playerbots.
# Run from anywhere — output lands in $SERVER_DATA_DIR.
# =============================================================================
set -euo pipefail
# ─── PATHS ──────────────────────────────────────────────────────────────────
WOW_CLIENT_DATA="/home/dev/wow_client_data"
TOOLS_DIR="/home/dev/azerothcore_installer/_server/azerothcore/env/dist/bin"
SERVER_DATA_DIR="$TOOLS_DIR"
# ─── TOGGLES ────────────────────────────────────────────────────────────────
EXTRACT_DBC_AND_MAPS=true
EXTRACT_VMAPS=true
EXTRACT_MMAPS=true
MMAP_THREADS=0 # 0 = auto-detect (each thread uses 1-2 GB RAM)
MMAP_SINGLE_MAP="" # e.g. "489" for Warsong Gulch only
# Verbatim copy of azerothcore-wotlk/master:src/tools/mmaps_generator/mmaps-config.yaml
# (also the same config the mod-playerbots fork ships).
MMAPS_CONFIG_YAML=$(cat <<'YAML_EOF'
mmapsConfig:
skipLiquid: false
skipContinents: false
skipJunkMaps: true
skipBattlegrounds: false
# Path to the directory containing navigation data files.
# This directory should contain the "maps" and "vmaps" folders,
# and is also where the "mmaps" folder will be created or located.
dataDir: "./"
meshSettings:
# Here we have global config for recast navigation.
# It's possible to override these data on map or tile level (see mapsOverrides).
# Maximum slope angle (in degrees) NPCs can walk on.
# Surfaces steeper than this will be considered unwalkable.
walkableSlopeAngle: 50
# --- Cell Size Calculation ---
# Many parameters below are defined in "cell units".
# In RecastDemo, you often work with world units instead of cell units.
# The actual generator uses (src/tools/mmaps_generator/Config.cpp:28):
#
# cellSize = MMAP::GRID_SIZE / vertexPerMapEdge
#
# Where:
# MMAP::GRID_SIZE = 533.3333f (the size of one map tile in world units)
# vertexPerMapEdge = number of vertices along one edge of the full map grid
#
# Example (AC stock):
# vertexPerMapEdge = 2000 → cellSize ≈ 533.3333 / 2000 ≈ 0.2667 yd
#
# IMPORTANT: when changing vertexPerMapEdge, the per-cell parameters
# below (walkableHeight, walkableClimb, walkableRadius) must be re-scaled
# to preserve their world-unit semantics. Doubling vertexPerMapEdge
# halves cellSize, so cell counts must double to keep the same yd value.
#
# To convert a value from cell units to world units (e.g., walkableClimb),
# multiply by cellSize. For example, a walkableClimb of 6 at 2000 resolution:
# 6 × 0.2667 ≈ 1.60 yd
# Minimum ceiling height (in cell units) NPCs need to pass under an obstacle.
# Controls how much vertical clearance is required.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
# 6 cells × 0.2667 yd ≈ 1.60 yd — matches WoW player capsule height at
# 2000 resolution (AC stock). Preserves the 1.60 yd world-unit
# ceiling-clearance requirement.
walkableHeight: 6
# Maximum height difference (in cell units) NPCs can step up or down.
# Higher values allow walking over fences, ledges, or steps.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
#
# Vanilla WotLK uses 6, which allows creatures to "jump" over fences.
# Classic WotLK uses 4, which forces creatures to walk around fences.
# 6 cells × 0.2667 yd ≈ 1.60 yd — Vanilla-WotLK step semantics at
# 2000 resolution. Preserves the 1.60 yd world-unit step. The mmap
# is shared with every creature, NPC patrol, escort, and quest mob;
# tightening below stock breaks patrols that cross 1.5y ledges.
walkableClimb: 4
# Minimum distance (in cell units) around walkable surfaces.
# Helps prevent NPCs from clipping into walls and narrow gaps.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
# 2 cells × 0.2667 yd ≈ 0.53 yd — AC stock world-unit buffer at
# 2000 resolution. Tested wider (= 0.71y world units): erodes polys
# near mountains/cliffs so pathfinder routes through surviving
# (higher/worse) polys → bot climbs mountains.
walkableRadius: 2
# Number of vertices along one edge of the entire map's navmesh grid.
# Higher values increase mesh resolution but also CPU/memory usage.
# 2000 = AC stock baseline. cellSize ≈ 0.2667 yd.
vertexPerMapEdge: 2000
# Number of vertices along one edge of each tile chunk.
# Must divide vertexPerMapEdge evenly — the generator uses integer
# division: tilesPerMapEdge = vertexPerMap / vertexPerTile
# (src/tools/mmaps_generator/Config.cpp:144).
# A higher vertex count per tile means fewer total tiles,
# reducing runtime work to load, unload, and manage tiles.
# 80 = AC stock baseline. 2000 / 80 = 25 tiles per map edge, 625
# tiles per map (~21y per tile). Lots of small tiles, low per-tile
# RAM, more seams to stitch across.
vertexPerTileEdge: 80
# Tolerance for how much a polygon can deviate from the original geometry when simplified.
# Higher values produce simpler (faster) meshes but can reduce accuracy.
# 0.8 (vs the AC stock 1.8 and recast canonical 1.3) keeps polygon
# edges close to real terrain. Targets "merged step into ramp"
# simplification artifacts that produce corner-cuts and false NOPATH.
maxSimplificationError: 0.8
# You can override any global parameter for a specific map by specifying its map ID.
# Inside each map override, you can also override parameters per individual tile,
# identified by a string "tileX,tileY" (coordinates).
#
# Overrides cascade: global settings → map overrides → tile overrides.
# For example:
#
# mapsOverrides:
# "0": # Map ID 0 overrides
# walkableRadius: 5 # Override global climb height for entire map 0
#
# tilesOverrides:
# "50,70": # Tile at coordinates (50,70) on map 0
# walkableSlopeAngle: 70 # Override slope angle locally just here
# walkableClimb: 4 # Also override climb height for this tile only
#
# "51,71":
# walkableClimb: 3 # Override climb height for tile (51,71)
#
# "48,32":
# walkableClimb: 1 # Even smaller climb for tile (48,32)
#
# "1": # Map ID 1 overrides example
# walkableHeight: 8 # Increase clearance for whole map 1
#
# tilesOverrides:
# "100,100":
# maxSimplificationError: 2.5 # Looser mesh simplification for this tile only
#
# "101,101":
# walkableRadius: 1 # Smaller NPC radius here for tight corridors
#
# This approach allows very fine-grained control of navigation mesh parameters
# on a per-map and per-tile basis, optimizing pathfinding quality and performance.
#
# All parameters defined globally are eligible for override.
# Just specify the parameter name and new value in the override section.
mapsOverrides:
"562": # Blade's Edge Arena
walkableRadius: 0 # This allows walking on the ropes to the pillars
"48": # Blackfathom Deeps
cellSizeVertical: 0.5334 # ch*2 = 0.2667 * 2 ≈ 0.5334. Reduce the chance to have underground levels.
"529": # Arathi Basin
tilesOverrides:
"30,29": # Lumber Mill
# Make sure that Fear will not drop players rom cliff -
# https://github.com/azerothcore/azerothcore-wotlk/pull/22462#issuecomment-3067024680
walkableSlopeAngle: 45
"530": # Outland
tilesOverrides:
"32,30": # Dark portal
walkableSlopeAngle: 45 # https://github.com/chromiecraft/chromiecraft/issues/8404#issuecomment-3476012660
# debugOutput generates debug files in the `meshes` directory for use with RecastDemo.
# This is useful for inspecting and debugging mmap generation visually.
#
# My workflow:
# 1. Install RecastDemo. I'm building it from the source of this fork: https://github.com/jackpoz/recastnavigation
# 2. In-game, move your character to the area you want to debug.
# 3. Type `.mmap loc` in chat. This will output:
# - The current tile file name (e.g., `04832.mmtile`)
# - The Recast config values used to generate that tile
# 4. Enable `debugOutput` and regenerate mmaps (preferably just the tile from step 3).
# - To regenerate only one tile, delete it from the `mmaps` folder.
# 5. After generation, you will find debug files in the `meshes` folder, including an OBJ file (e.g., `map0004832.obj`)
# 6. Copy these debug files to the `Meshes` folder used by RecastDemo.
# - RecastDemo expects this folder to be in the same directory as its executable.
# 7. In RecastDemo:
# - Click "Input Mesh" and select the `.obj` file
# - Choose "Solo Mesh" in the Sample selector
# 8. (Optional) Reuse the Recast config values from step 3:
# - `cellSizeHorizontal` → "Cell Size"
# - `walkableSlopeAngle` → "Max Slope"
# - `walkableClimb` → "Max Climb"
# - and so on
# 9. Scroll to the bottom of RecastDemo UI and press "Build" to generate the navigation mesh
debugOutput: false
YAML_EOF
)
# =============================================================================
# ─── DO NOT EDIT BELOW ──────────────────────────────────────────────────────
# =============================================================================
[ -n "$SERVER_DATA_DIR" ] || { echo "SERVER_DATA_DIR is not set"; exit 1; }
[ -n "$WOW_CLIENT_DATA" ] || { echo "WOW_CLIENT_DATA is not set"; exit 1; }
mkdir -p "$SERVER_DATA_DIR"
cd "$SERVER_DATA_DIR"
# ─── SAFETY: source MPQs are READ-ONLY to this script ──────────────────────
# Resolve both paths to canonical form and refuse to run if the output dir
# is inside the source. Combined with safe_rm() below, this script cannot
# touch any file inside WOW_CLIENT_DATA.
SERVER_DATA_DIR_REAL="$(cd "$SERVER_DATA_DIR" && pwd -P)"
WOW_CLIENT_DATA_REAL="$(cd "$WOW_CLIENT_DATA" && pwd -P 2>/dev/null || echo "$WOW_CLIENT_DATA")"
case "$SERVER_DATA_DIR_REAL/" in
"$WOW_CLIENT_DATA_REAL"/|"$WOW_CLIENT_DATA_REAL"/*)
echo "ERROR: SERVER_DATA_DIR ($SERVER_DATA_DIR_REAL) is inside WOW_CLIENT_DATA — refusing." >&2
exit 1
;;
esac
# Refuses to remove anything outside SERVER_DATA_DIR. Resolves the parent
# to absolute path so a symlink inside cwd can't trick us into traversing
# into the source. Use this for every cleanup in this script.
safe_rm() {
local target="$1"
local parent_abs base
parent_abs="$(cd "$(dirname -- "$target")" 2>/dev/null && pwd -P)" || return 0
base="$(basename -- "$target")"
local abs="$parent_abs/$base"
case "$abs/" in
"$SERVER_DATA_DIR_REAL"/|"$SERVER_DATA_DIR_REAL"/*) ;;
*)
echo "REFUSING to rm path outside SERVER_DATA_DIR: $target$abs" >&2
exit 1 ;;
esac
rm -rf -- "$target"
}
[ "$MMAP_THREADS" -eq 0 ] && MMAP_THREADS=$(nproc 2>/dev/null || echo 4)
echo "Working dir : $(pwd)"
echo "Tools dir : $TOOLS_DIR"
echo "Threads : $MMAP_THREADS"
echo "Steps : maps=$EXTRACT_DBC_AND_MAPS vmaps=$EXTRACT_VMAPS mmaps=$EXTRACT_MMAPS"
echo
# ─── Symlink Data/ → MPQ source (only when extracting from client) ──────────
if [ "$EXTRACT_DBC_AND_MAPS" = true ] || [ "$EXTRACT_VMAPS" = true ]; then
has_mpqs() { find "$1" -maxdepth 1 -iname "*.mpq" -print -quit 2>/dev/null | grep -q .; }
if has_mpqs "$WOW_CLIENT_DATA"; then
MPQ_DIR="$WOW_CLIENT_DATA"
elif has_mpqs "$WOW_CLIENT_DATA/Data"; then
MPQ_DIR="$WOW_CLIENT_DATA/Data"
else
echo "ERROR: no .mpq files in $WOW_CLIENT_DATA" >&2
exit 1
fi
MPQ_DIR="$(cd "$MPQ_DIR" && pwd)"
# Symlink only — refuse to clobber an existing real directory.
if [ -e Data ] && [ ! -L Data ]; then
echo "ERROR: Data/ exists in $(pwd) but is not a symlink" >&2
exit 1
fi
ln -sfn "$MPQ_DIR" Data
echo "Data/ → $MPQ_DIR"
fi
# ─── STEP 1: DBCs + Maps ────────────────────────────────────────────────────
if [ "$EXTRACT_DBC_AND_MAPS" = true ]; then
echo
echo "[1/3] Extracting DBCs + Maps..."
# Clean slate — map_extractor refuses to run if these dirs already exist.
safe_rm dbc
safe_rm maps
safe_rm Cameras
# -e 7 = bitfield MAP(1)|DBC(2)|CAMERA(4) — extract everything.
# The old "-e 2" was DBC-only and skipped maps + cameras entirely.
"$TOOLS_DIR/map_extractor" -e 7 -f 0
if [ ! -d maps ] || [ -z "$(ls -A maps 2>/dev/null)" ]; then
echo "ERROR: map_extractor finished but maps/ is empty — check its output above" >&2
exit 1
fi
fi
# ─── STEP 2: VMaps ──────────────────────────────────────────────────────────
if [ "$EXTRACT_VMAPS" = true ]; then
echo
echo "[2/3] Extracting VMaps..."
# Clean slate — vmap4_extractor refuses to run if these dirs already exist.
safe_rm Buildings
safe_rm vmaps
"$TOOLS_DIR/vmap4_extractor" -l -d ./Data
mkdir -p vmaps
"$TOOLS_DIR/vmap4_assembler" Buildings vmaps
safe_rm Buildings
if [ ! -d vmaps ] || [ -z "$(ls -A vmaps 2>/dev/null)" ]; then
echo "ERROR: vmap4_assembler finished but vmaps/ is empty — check output above" >&2
exit 1
fi
fi
# ─── STEP 3: MMaps ──────────────────────────────────────────────────────────
if [ "$EXTRACT_MMAPS" = true ]; then
if [ ! -d maps ]; then
echo "ERROR: maps/ missing in $(pwd) — run with EXTRACT_DBC_AND_MAPS=true once" >&2
exit 1
fi
if [ ! -d vmaps ]; then
echo "ERROR: vmaps/ missing in $(pwd) — run with EXTRACT_VMAPS=true once" >&2
exit 1
fi
echo
echo "[3/3] Generating MMaps... (do not interrupt)"
printf '%s\n' "$MMAPS_CONFIG_YAML" > mmaps-config.yaml
# Wipe any existing tiles before regenerating. Mixed tiles from
# previous runs (different cellSize / verticesPerTileEdge / etc.)
# would otherwise be silently kept and mixed with new ones,
# producing a corrupt navmesh. Clean slate every mmap run.
safe_rm mmaps
mkdir -p mmaps
# Workaround: some mmaps_generator builds write a few tiles to /mmaps
# via an absolute path. Pre-create it so the writes don't fail, then
# fold the strays into our local mmaps/ at the end.
sudo rm -rf /mmaps
sudo mkdir -p /mmaps && sudo chmod 777 /mmaps
CMD=("$TOOLS_DIR/mmaps_generator" --config mmaps-config.yaml --threads "$MMAP_THREADS")
[ -n "$MMAP_SINGLE_MAP" ] && CMD+=("$MMAP_SINGLE_MAP")
START=$(date +%s)
"${CMD[@]}"
ELAPSED=$(( $(date +%s) - START ))
if compgen -G "/mmaps/*.mmtile" >/dev/null; then
cp /mmaps/*.mmtile mmaps/ && rm -f /mmaps/*.mmtile
fi
echo
echo "MMap done in $((ELAPSED / 60))m $((ELAPSED % 60))s"
echo "Tiles: $(ls mmaps/*.mmtile 2>/dev/null | wc -l)"
fi
echo
echo "Done. Restart worldserver to pick up changes."

View File

@ -173,7 +173,7 @@ std::vector<uint32> const vFlagsIC = {GO_HORDE_BANNER,
GO_HORDE_BANNER_GRAVEYARD_H,
GO_HORDE_BANNER_GRAVEYARD_H_CONT};
// BG Waypoints (vmangos)
// BG Waypoints
// Horde Flag Room to Horde Graveyard
BattleBotPath vPath_WSG_HordeFlagRoom_to_HordeGraveyard = {

View File

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

View File

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

View File

@ -29,6 +29,10 @@ void DestroyItemAction::DestroyItem(FindItemVisitor* visitor)
std::vector<Item*> items = visitor->GetResult();
for (Item* item : items)
{
// backstop: never drop an active quest item
if (bot->HasQuestForItem(item->GetTemplate()->ItemId))
continue;
std::ostringstream out;
out << chat->FormatItem(item->GetTemplate()) << " destroyed";
botAI->TellMaster(out);
@ -67,18 +71,11 @@ bool SmartDestroyItemAction::Execute(Event /*event*/)
return true;
}
// ITEM_USAGE_QUEST is excluded — those are still-needed quest items
std::vector<uint32> bestToDestroy = {ITEM_USAGE_NONE}; // First destroy anything useless.
if (!AI_VALUE(bool, "can sell") &&
AI_VALUE(
bool,
"should get money")) // We need money so quest items are less important since they can't directly be sold.
bestToDestroy.push_back(ITEM_USAGE_QUEST);
else // We don't need money so destroy the cheapest stuff.
{
bestToDestroy.push_back(ITEM_USAGE_VENDOR);
bestToDestroy.push_back(ITEM_USAGE_AH);
}
// If we still need room
bestToDestroy.push_back(

View File

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

View File

@ -124,7 +124,6 @@ bool GoAction::Execute(Event event)
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
{
PathGenerator path(bot);
path.CalculatePath(x, y, z, false);
Movement::Vector3 end = path.GetEndPosition();

View File

@ -5,6 +5,9 @@
#include "LootAction.h"
#include <limits>
#include "Bag.h"
#include "ChatHelper.h"
#include "Event.h"
#include "GuildMgr.h"
@ -76,7 +79,11 @@ bool OpenLootAction::Execute(Event /*event*/)
bool result = DoLoot(lootObject);
if (result)
{
AI_VALUE(LootObjectStack*, "available loot")->Remove(lootObject.guid);
// MarkCompleted (not Remove) — "add all loot" reads
// "nearest corpses" without a lootable filter, so a plain
// Remove lets the same corpse re-enter the stack on the next
// tick. The completed set blocks re-add for ~5 min.
AI_VALUE(LootObjectStack*, "available loot")->MarkCompleted(lootObject.guid);
context->GetValue<LootObject>("loot target")->Set(LootObject());
}
return result;
@ -139,8 +146,9 @@ bool OpenLootAction::DoLoot(LootObject& lootObject)
if (go && (go->GetGoState() != GO_STATE_READY))
return false;
// This prevents dungeon chests like Tribunal Chest (Halls of Stone) from being ninja'd by the bots
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND))
// Block event-gated chests (Tribunal Chest, Gunship Armory) but allow
// wild quest GOs (Moonpetal Lily etc.) when the bot is on the quest.
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND) && !lootObject.isNeededQuestItem)
return false;
// This prevents raid chests like Gunship Armory (ICC) from being ninja'd by the bots
@ -377,6 +385,12 @@ bool StoreLootAction::Execute(Event event)
// bot->GetSession()->HandleLootMoneyOpcode(packet);
}
// one make-room destroy per loot packet — CanStoreNewItem after a junk
// destroy can still report full while CMSG_AUTOSTORE_LOOT_ITEM is
// queued, so a multi-quest-item packet would otherwise destroy more
// junk than necessary
bool destroyedThisPacket = false;
for (uint8 i = 0; i < items; ++i)
{
uint32 itemid;
@ -402,7 +416,9 @@ bool StoreLootAction::Execute(Event event)
if (!proto)
continue;
if (!botAI->HasActivePlayerMaster() && AI_VALUE(uint8, "bag space") > 80)
// bags >80%: skip non-stackable junk (quest items exempt)
if (!botAI->HasActivePlayerMaster() && AI_VALUE(uint8, "bag space") > 80 &&
!bot->HasQuestForItem(itemid))
{
uint32 maxStack = proto->GetMaxStackSize();
if (maxStack == 1)
@ -438,6 +454,55 @@ bool StoreLootAction::Execute(Event event)
GuildTaskMgr::instance().CheckItemTask(itemid, itemcount, ref->GetSource(), bot);
}
// bags full + quest item: make room by dropping cheapest junk
if (!destroyedThisPacket && bot->HasQuestForItem(itemid))
{
ItemPosCountVec dest;
InventoryResult can =
bot->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemid, itemcount);
if (can == EQUIP_ERR_INVENTORY_FULL || can == EQUIP_ERR_BAG_FULL)
{
// picked by usage, not quality — high-level bots have no grays
Item* victim = nullptr;
uint32 minPrice = std::numeric_limits<uint32>::max();
auto consider = [&](uint8 bag, uint8 slot)
{
Item* it = bot->GetItemByPos(bag, slot);
if (!it)
return;
ItemTemplate const* tpl = it->GetTemplate();
if (!tpl)
return;
if (bot->HasQuestForItem(tpl->ItemId))
return;
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", tpl->ItemId);
if (usage != ITEM_USAGE_NONE && usage != ITEM_USAGE_VENDOR &&
usage != ITEM_USAGE_BAD_EQUIP && usage != ITEM_USAGE_BROKEN_EQUIP)
return;
if (tpl->SellPrice < minPrice)
{
minPrice = tpl->SellPrice;
victim = it;
}
};
for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot)
consider(INVENTORY_SLOT_BAG_0, slot);
for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag)
{
Bag* pBag = bot->GetBagByPos(bag);
if (!pBag)
continue;
for (uint32 slot = 0; slot < pBag->GetBagSize(); ++slot)
consider(bag, static_cast<uint8>(slot));
}
if (victim)
{
bot->DestroyItem(victim->GetBagSlot(), victim->GetSlot(), true);
destroyedThisPacket = true;
}
}
}
WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1);
*packet << itemindex;
bot->GetSession()->QueuePacket(packet);
@ -453,7 +518,7 @@ bool StoreLootAction::Execute(Event event)
BroadcastHelper::BroadcastLootingItem(botAI, bot, proto);
}
AI_VALUE(LootObjectStack*, "available loot")->Remove(guid);
AI_VALUE(LootObjectStack*, "available loot")->MarkCompleted(guid);
// release loot
WorldPacket* packet = new WorldPacket(CMSG_LOOT_RELEASE, 8);

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
#include "Action.h"
#include "LastMovementValue.h"
#include "PathGenerator.h"
#include "PlayerbotAIConfig.h"
class Player;
@ -18,16 +19,36 @@ 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)
// Default acceptable path types for GeneratePath
constexpr uint32 DEFAULT_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE;
constexpr uint32 RELAXED_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
struct PathResult
{
Movement::PointsArray points;
G3D::Vector3 actualEnd;
G3D::Vector3 end;
PathType pathType;
bool reachable;
};
class MovementAction : public Action
{
public:
MovementAction(PlayerbotAI* botAI, std::string const name);
protected:
// Emit a one-line trace describing the imminent movement. No-op
// unless the bot has the "debug move" non-combat strategy.
// Subclasses (e.g. NewRpgBaseAction) may override to append richer
// context such as RPG status and target name. Optional `extra`
// is appended verbatim (use it to attach hop labels like
// "node:Stormwind innkeeper" or fallback reasons).
virtual void EmitDebugMove(char const* method, char const* generator, float x, float y, float z, char const* extra = nullptr);
bool JumpTo(uint32 mapId, float x, float y, float z, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool MoveNear(uint32 mapId, float x, float y, float z, float distance = sPlayerbotAIConfig.contactDistance,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
@ -35,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,
@ -43,14 +111,25 @@ 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);
bool IsWaitingForLastMove(MovementPriority priority);
bool IsMovingAllowed();
bool Flee(Unit* target);
void ClearIdleState();
@ -66,6 +145,42 @@ protected:
bool FleePosition(Position pos, float radius, uint32 minInterval = 1000);
bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList);
PathResult GeneratePath(float x, float y, float z, uint32 acceptMask = DEFAULT_PATH_ACCEPT_MASK, bool forceDestination = false);
// Returns a unified TravelPath for the move. Mirror of the reference
// ResolveMovePath shape: 10% lastPath reuse short-circuit, choose
// graph (cross-map / >sightDistance) or live mmap probe, regression
// guard preferring cached path when no better, fall back to a
// single-point path on dest. Stateless — does not dispatch.
TravelPath ResolveMovePath(WorldPosition startPos,
WorldPosition endPos,
LastMovement& lastMove);
// Dispatches the head-of-path special segment (portal interact /
// area-trigger marker / transport boarding / flight master taxi).
// Caller is expected to first call TravelPath::UpcommingSpecialMovement
// which cuts the path so the head is the special segment. Returns
// true if a movement-consuming action was dispatched this tick.
// Returns false for AREA_TRIGGER-with-entry (caller still dispatches
// the walk into the trigger volume).
bool HandleSpecialMovement(TravelPath& path);
// Top-of-MoveFarTo gate that keeps a bot riding a transport across
// ticks. Returns true if the bot is still on the transport we last
// boarded (caller should skip the rest of MoveFarTo this tick).
// Clears lastTransportEntry and returns false if the bot has
// disembarked or is no longer on the expected transport.
bool WaitForTransport();
// Transport boarding helpers (shared by FollowAction and travel plan)
static Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref,
uint32 phaseMask, float x, float y, float z);
static bool FindBoardingPointOnTransport(Map* map, Transport* transport,
WorldObject* ref, float refX, float refY, float refZ,
float botX, float botY, float botZ,
float& outX, float& outY, float& outZ);
bool BoardTransport(Transport* transport);
protected:
struct CheckAngle
{
@ -74,10 +189,6 @@ protected:
};
private:
// float SearchBestGroundZForPath(float x, float y, float z, bool generatePath, float range = 20.0f, bool
// normal_only = false, float step = 8.0f);
const Movement::PointsArray SearchForBestPath(float x, float y, float z, float& modified_z, int maxSearchCount = 5,
bool normal_only = false, float step = 8.0f);
bool wasMovementRestricted = false;
void DoMovePoint(Unit* unit, float x, float y, float z, bool generatePath, bool backwards);
};

View File

@ -11,21 +11,20 @@
bool LootAvailableTrigger::IsActive()
{
bool distanceCheck = false;
if (botAI->HasStrategy("stay", BOT_STATE_NON_COMBAT))
{
distanceCheck =
ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"), CONTACT_DISTANCE);
}
else
{
distanceCheck = ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"),
INTERACTION_DISTANCE - 2.0f);
}
// Strategy is non-combat-only — the engine state separation is the
// safety net. Don't gate on hostiles-in-sight: that locked out
// looting in zones with continuous respawns (e.g. cave farms).
// If a new enemy aggros mid-loot the combat engine takes over, loot
// resumes on the next non-combat window.
if (!AI_VALUE(bool, "has available loot"))
return false;
// if loot target if empty, always pass distance check
return AI_VALUE(bool, "has available loot") &&
(distanceCheck || AI_VALUE(GuidVector, "all targets").empty());
// "stay" strategy is restrictive: only loot if corpse is at our feet.
if (botAI->HasStrategy("stay", BOT_STATE_NON_COMBAT))
return ServerFacade::instance().IsDistanceLessOrEqualThan(
AI_VALUE2(float, "distance", "loot target"), CONTACT_DISTANCE);
return true;
}
bool FarFromCurrentLootTrigger::IsActive()

View File

@ -12,64 +12,48 @@ LastMovement::LastMovement() { clear(); }
LastMovement::LastMovement(LastMovement& other)
: taxiNodes(other.taxiNodes),
taxiMaster(other.taxiMaster),
lastFollow(other.lastFollow),
lastAreaTrigger(other.lastAreaTrigger),
lastMoveToX(other.lastMoveToX),
lastMoveToY(other.lastMoveToY),
lastMoveToZ(other.lastMoveToZ),
lastMoveToOri(other.lastMoveToOri),
lastFlee(other.lastFlee)
{
lastMoveShort = other.lastMoveShort;
nextTeleport = other.nextTeleport;
lastPath = other.lastPath;
priority = other.priority;
lastTransportEntry = other.lastTransportEntry;
}
void LastMovement::clear()
{
lastMoveShort = WorldPosition();
lastPath.clear();
lastMoveToMapId = 0;
lastMoveToX = 0;
lastMoveToY = 0;
lastMoveToZ = 0;
lastMoveToOri = 0;
lastFollow = nullptr;
lastAreaTrigger = 0;
lastFlee = 0;
nextTeleport = 0;
msTime = 0;
lastdelayTime = 0;
priority = MovementPriority::MOVEMENT_NORMAL;
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)
{
lastMoveToMapId = mapId;
lastMoveToX = x;
lastMoveToY = y;
lastMoveToZ = z;
lastMoveToOri = ori;
lastFollow = nullptr;
lastMoveShort = WorldPosition(mapId, x, y, z, ori);
msTime = getMSTime();
lastdelayTime = delayTime;
priority = pri;
}
void LastMovement::setShort(WorldPosition point)
{
lastMoveShort = point;
lastFollow = nullptr;
}
void LastMovement::setPath(TravelPath path) { lastPath = path; }

View File

@ -33,12 +33,12 @@ public:
{
taxiNodes = other.taxiNodes;
taxiMaster = other.taxiMaster;
lastFollow = other.lastFollow;
lastAreaTrigger = other.lastAreaTrigger;
lastMoveShort = other.lastMoveShort;
lastPath = other.lastPath;
nextTeleport = other.nextTeleport;
priority = other.priority;
lastTransportEntry = other.lastTransportEntry;
return *this;
};
@ -52,21 +52,17 @@ public:
std::vector<uint32> taxiNodes;
ObjectGuid taxiMaster;
Unit* lastFollow;
uint32 lastAreaTrigger;
time_t lastFlee;
uint32 lastMoveToMapId;
float lastMoveToX;
float lastMoveToY;
float lastMoveToZ;
float lastMoveToOri;
float lastdelayTime;
WorldPosition lastMoveShort;
uint32 msTime;
MovementPriority priority;
TravelPath lastPath;
time_t nextTeleport;
std::future<TravelPath> future;
// Entry of the transport the bot is currently aboard mid-journey,
// used by WaitForTransport to resume a transport segment if the
// bot is still on it next tick (e.g. boat in motion). 0 = none.
uint32 lastTransportEntry{0};
};
class LastMovementValue : public ManualSetValue<LastMovement&>

View File

@ -47,7 +47,13 @@ public:
{
if (allowedGOFlags.empty())
{
// questgivers for accept/turn-in; rest for quest progression
// (chests, runes, altars, moonwells, lily piles, …)
allowedGOFlags.push_back(GAMEOBJECT_TYPE_QUESTGIVER);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_CHEST);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_GOOBER);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_SPELL_FOCUS);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_GENERIC);
}
}

View File

@ -1462,15 +1462,6 @@ bool HodirBitingColdJumpAction::Execute(Event /*event*/)
// float speed = 7.96f;
// UpdateMovementState();
// if (!IsMovingAllowed(mapId, x, y, z))
//{
// return false;
// }
// MovementPriority priority;
// if (IsWaitingForLastMove(priority))
//{
// return false;
// }
// MotionMaster& mm = *bot->GetMotionMaster();
// mm.Clear();

View File

@ -19,6 +19,7 @@
#include "PathGenerator.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "QuestDef.h"
#include "Random.h"
#include "SharedDefines.h"
@ -120,17 +121,8 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/)
}
break;
}
case RPG_TRAVEL_FLIGHT:
{
auto& data = std::get<NewRpgInfo::TravelFlight>(info.data);
if (data.inFlight && !bot->IsInFlight())
{
// flight arrival
info.ChangeToIdle();
return true;
}
break;
}
// RPG_TRAVEL_FLIGHT arrival is handled inside NewRpgTravelFlightAction
// so the flight action owns both take-off and landing transitions.
case RPG_REST:
{
// REST -> IDLE
@ -163,11 +155,22 @@ bool NewRpgGoGrindAction::Execute(Event /*event*/)
if (auto* data = std::get_if<NewRpgInfo::GoGrind>(&botAI->rpgInfo.data))
{
if (MoveFarTo(data->pos))
{
botAI->rpgInfo.moveRetryCount = 0;
return true;
// Small nudge so the next tick's MoveFarTo starts from a
// slightly different position. Kept small so it doesn't look
// like the bot is abandoning its destination.
return MoveRandomNear(10.0f);
}
// Reference pattern (TravelTarget retry counter): count
// consecutive MoveFarTo failures, give up after N tries by
// transitioning out of the stuck state instead of nudging in
// place. Idle lets the status picker rotate to a new state.
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
{
EmitDebugMove("MoveFar", "give-up",
data->pos.GetPositionX(), data->pos.GetPositionY(), data->pos.GetPositionZ(),
"idle");
botAI->rpgInfo.ChangeToIdle();
}
return true; // consume tick, no nudge
}
return false;
@ -181,8 +184,18 @@ bool NewRpgGoCampAction::Execute(Event /*event*/)
if (auto* data = std::get_if<NewRpgInfo::GoCamp>(&botAI->rpgInfo.data))
{
if (MoveFarTo(data->pos))
{
botAI->rpgInfo.moveRetryCount = 0;
return true;
}
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
{
EmitDebugMove("MoveFar", "give-up",
data->pos.GetPositionX(), data->pos.GetPositionY(), data->pos.GetPositionZ(),
"idle");
botAI->rpgInfo.ChangeToIdle();
}
return true;
return MoveRandomNear(10.0f);
}
return false;
@ -238,11 +251,23 @@ bool NewRpgWanderNpcAction::Execute(Event /*event*/)
else
{
if (MoveWorldObjectTo(data.npcOrGo))
{
botAI->rpgInfo.moveRetryCount = 0;
return true;
}
// Retry counter (reference pattern): give up after N failures
// by clearing the picked NPC so next tick picks a different
// one. No nudge — stand still until retry.
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
{
EmitDebugMove("MoveFar", "give-up",
bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(),
"drop-npc");
data.npcOrGo = ObjectGuid();
data.lastReach = 0;
botAI->rpgInfo.moveRetryCount = 0;
}
return true;
// NPC pathing failed (random offset in a wall, mmap hiccup, etc).
// Take a small random step so the next tick retries from a
// different spot instead of staring at the NPC from afar.
return MoveRandomNear(15.0f);
}
return true;
@ -275,14 +300,23 @@ bool NewRpgDoQuestAction::Execute(Event /*event*/)
bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
{
uint32 questId = data.questId;
if (data.pos != WorldPosition())
uint32 const questId = data.questId;
// === Spawn-index pipeline ===
// Reference (cmangos) per-spawn pattern: walk to specific known
// spawns of the current objective one by one, advance through the
// candidate list on per-spawn timeout, refresh the list when the
// objective makes progress (so the list reflects what's still
// needed). No POI cluster roam, no random nudging.
// 1. Detect objective completion. If the current objective is done,
// drop the cached spawn list so we re-fetch for the next
// incomplete objective on this tick.
if (!data.candidateSpawns.empty())
{
/// @TODO: extract to a new function
int32 currentObjective = data.objectiveIdx;
// check if the objective has completed
int32 const currentObjective = data.objectiveIdx;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId);
QuestStatusData const& q_status = bot->getQuestStatusMap().at(questId);
bool completed = true;
if (currentObjective < QUEST_OBJECTIVES_COUNT)
{
@ -295,67 +329,109 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
completed = false;
}
// the current objective is completed, clear and find a new objective later
if (completed)
{
data.candidateSpawns.clear();
data.currentSpawnIdx = 0;
data.lastReachPOI = 0;
data.pos = WorldPosition();
data.objectiveIdx = 0;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
}
}
if (data.pos == WorldPosition())
// 2. Fetch spawn candidates if we don't have any. Abandon the
// quest if no spawns are indexed on the bot's current map (the
// quest is for another zone or our index is missing them).
if (data.candidateSpawns.empty())
{
std::vector<POIInfo> poiInfo;
if (!GetQuestPOIPosAndObjectiveIdx(questId, poiInfo))
std::vector<WorldPosition> spawns;
int32 objectiveIdx = 0;
if (!FetchQuestSpawnsForObjective(questId, spawns, objectiveIdx))
{
// can't find a poi pos to go, stop doing quest for now
botAI->lowPriorityQuest.insert(questId);
botAI->rpgStatistic.questAbandoned++;
LOG_DEBUG("playerbots", "[New RPG] {} abandoned quest {} — no spawns indexed",
bot->GetName(), questId);
botAI->rpgInfo.ChangeToIdle();
return true;
}
uint32 rndIdx = urand(0, poiInfo.size() - 1);
G3D::Vector2 nearestPoi = poiInfo[rndIdx].pos;
int32 objectiveIdx = poiInfo[rndIdx].objectiveIdx;
data.candidateSpawns = std::move(spawns);
data.currentSpawnIdx = 0;
data.lastReachPOI = 0;
data.objectiveIdx = objectiveIdx;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
}
float dx = nearestPoi.x, dy = nearestPoi.y;
// 3. If we've exhausted the candidate list, abandon (the spawn
// list was sorted by distance and we tried each).
if (data.currentSpawnIdx >= data.candidateSpawns.size())
{
botAI->lowPriorityQuest.insert(questId);
botAI->rpgStatistic.questAbandoned++;
LOG_DEBUG("playerbots", "[New RPG] {} abandoned quest {} — exhausted all {} candidate spawns",
bot->GetName(), questId, static_cast<uint32>(data.candidateSpawns.size()));
botAI->rpgInfo.ChangeToIdle();
return true;
}
// z = MAX_HEIGHT as we do not know accurate z
float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy));
WorldPosition const& target = data.candidateSpawns[data.currentSpawnIdx];
// double check for GetQuestPOIPosAndObjectiveIdx
if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE)
// 4. Walk to the current target spawn. Yield to attack-anything
// only if a quest mob for this specific objective is adjacent
// (so we don't walk past the target we just spawned next to).
if (bot->GetDistance(target) > 10.0f && !data.lastReachPOI)
{
if (HasNearbyQuestMobForObjective(15.0f, data.questId, data.objectiveIdx))
return false;
WorldPosition pos(bot->GetMapId(), dx, dy, dz);
data.lastReachPOI = 0;
data.pos = pos;
data.objectiveIdx = objectiveIdx;
}
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
if (MoveFarTo(target))
{
if (MoveFarTo(data.pos))
botAI->rpgInfo.moveRetryCount = 0;
return true;
}
// Retry counter: on N consecutive MoveFarTo failures, advance
// to the next candidate spawn rather than sit on an unreachable
// one. If that exhausts the list the abandon branch above
// catches it next tick.
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
{
std::ostringstream nx;
nx << "next-spawn(" << (data.currentSpawnIdx + 1) << "/"
<< data.candidateSpawns.size() << ")";
EmitDebugMove("MoveFar", "give-up",
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
nx.str().c_str());
++data.currentSpawnIdx;
data.lastReachPOI = 0;
botAI->rpgInfo.moveRetryCount = 0;
}
return true;
// Long-range sampler couldn't land a candidate — nudge the
// bot a short distance so the next tick retries from a
// different position instead of sitting idle.
return MoveRandomNear(10.0f);
}
// Now we are near the quest objective
// kill mobs and looting quest should be done automatically by grind strategy
// 5. At the spawn. Stamp arrival on first reach so the per-spawn
// timeout below has a baseline.
if (!data.lastReachPOI)
{
data.lastReachPOI = getMSTime();
return true;
}
// stayed at this POI for more than 5 minutes
if (GetMSTimeDiffToNow(data.lastReachPOI) >= poiStayTime)
// 6. Per-spawn timeout. The reference's TravelTarget expires after
// a configurable window; we use 30s — long enough to finish a
// melee pull, short enough to advance off an empty/dead spawn.
// On any progression since the list was fetched, refresh so we
// re-sort by distance and pick the next nearest live spawn.
constexpr uint32 perSpawnTimeoutMs = 30 * 1000;
if (GetMSTimeDiffToNow(data.lastReachPOI) >= perSpawnTimeoutMs)
{
bool hasProgression = false;
int32 currentObjective = data.objectiveIdx;
// check if the objective has progression
int32 const currentObjective = data.objectiveIdx;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId);
QuestStatusData const& q_status = bot->getQuestStatusMap().at(questId);
if (currentObjective < QUEST_OBJECTIVES_COUNT)
{
if (q_status.CreatureOrGOCount[currentObjective] != 0 && quest->RequiredNpcOrGoCount[currentObjective])
@ -367,29 +443,38 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
hasProgression = true;
}
if (!hasProgression)
if (hasProgression)
{
// we has reach the poi for more than 5 mins but no progession
// may not be able to complete this quest, marked as abandoned
/// @TODO: It may be better to make lowPriorityQuest a global set shared by all bots (or saved in db)
botAI->lowPriorityQuest.insert(questId);
botAI->rpgStatistic.questAbandoned++;
LOG_DEBUG("playerbots", "[New RPG] {} marked as abandoned quest {}", bot->GetName(), questId);
botAI->rpgInfo.ChangeToIdle();
// Refresh: re-fetch candidates so the list reflects what's
// still needed and is sorted from the bot's new position.
data.candidateSpawns.clear();
data.currentSpawnIdx = 0;
data.lastReachPOI = 0;
return true;
}
// clear and select another poi later
// No progression at this spawn — advance to the next candidate.
++data.currentSpawnIdx;
data.lastReachPOI = 0;
data.pos = WorldPosition();
data.objectiveIdx = 0;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
return true;
}
// At the POI: keep the bot actively placed but avoid large
// random 20yd hops that look like pacing back and forth. A small
// ~8yd wander reads as the bot looking around while grind/loot
// strategies do their work.
return MoveRandomNear(8.0f);
// 7. At spawn, within timeout: drive toward specific objectives.
// Combat strategy engages adjacent quest mobs; loot/use
// actions handle quest GOs and quest items.
if (TryUseQuestItem(data.pursuedUseGO, data.pursuedUseTarget))
return true;
if (TryLootQuestGO(data.pursuedLootGO))
return true;
if (TryUseQuestGO(data.pursuedUseGO))
return true;
// Yield this tick to combat/grind. No POI roam, no MoveRandomNear:
// bot stays at the spawn until either combat engages or the
// per-spawn timeout expires.
return false;
}
bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
@ -423,6 +508,15 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0;
data.pos = pos;
data.objectiveIdx = -1;
// Drop the spline + lastPath that DoIncompleteQuest committed
// to the now-completed objective. Without this, MoveFarTo on
// the next tick hits the bot->isMoving() / lastPath-reuse
// early-exits at the top of MoveFarTo and rides the stale
// path instead of replanning toward the turn-in POI. (This
// is what `.playerbot bot self` masks by recreating the AI.)
bot->GetMotionMaster()->Clear();
AI_VALUE(LastMovement&, "last movement").clear();
}
if (data.pos == WorldPosition())
@ -431,8 +525,21 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
{
if (MoveFarTo(data.pos))
{
botAI->rpgInfo.moveRetryCount = 0;
return true;
}
// Retry counter (reference pattern): mark quest as abandoned
// if turn-in POI is unreachable repeatedly so the bot doesn't
// sit on a broken handler.
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
{
EmitDebugMove("MoveFar", "give-up",
data.pos.GetPositionX(), data.pos.GetPositionY(), data.pos.GetPositionZ(),
"idle(turn-in)");
botAI->rpgInfo.ChangeToIdle();
}
return true;
return MoveRandomNear(10.0f);
}
// Now we are near the qoi of reward
@ -453,7 +560,9 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
botAI->rpgInfo.ChangeToIdle();
return true;
}
return false;
// waiting for SearchQuestGiverAndAcceptOrReward to pick up the NPC;
// wander instead of false so we don't fall through to grind
return MoveRandomNear(15.0f);
}
bool NewRpgTravelFlightAction::Execute(Event /*event*/)
@ -464,6 +573,22 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
return false;
auto& data = *dataPtr;
// Arrival: we had boarded a flight (data.inFlight) and we're no longer in
// it → we just landed. Special-case Rut'theran: walk to the portal GO so
// it teleports the bot into Darnassus, flipping the zone to AREA_DARNASSUS
// so this branch falls through to ChangeToIdle on the next tick.
if (data.inFlight && !bot->IsInFlight())
{
if (bot->GetZoneId() == AREA_TELDRASSIL)
{
static WorldPosition const rutTheranPortalEntrance(1, 8799.41f, 969.787f, 26.2409f, 0.0f);
return MoveFarTo(rutTheranPortalEntrance);
}
info.ChangeToIdle();
return true;
}
if (bot->IsInFlight())
{
data.inFlight = true;
@ -479,19 +604,9 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
info.ChangeToIdle();
return true;
}
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
return MoveFarTo(flightMaster);
std::vector<uint32> nodes = data.path;
botAI->RemoveShapeshift();
if (bot->IsMounted())
bot->Dismount();
if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0))
if (!TakeFlight(data.path, flightMaster))
{
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);
info.ChangeToIdle();
return true;
}

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ protected:
bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE);
bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr);
bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool TakeFlight(std::vector<uint32> const& taxiNodes, Creature* flightMaster);
/* QUEST RELATED CHECK */
ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f);
@ -50,25 +51,47 @@ protected:
bool TurnInQuest(Quest const* quest, ObjectGuid guid);
bool OrganizeQuestLog();
/* QUEST PROGRESSION HELPERS (at POI) */
// Walk to a GO that drops a needed quest item. The loot strategy
// opens and loots it once in range.
bool TryLootQuestGO(ObjectGuid& pursuedGO, float searchRange = 60.0f);
// Walk to / use a GO that is itself the objective (rune, lever,
// altar, coffin — RequiredNpcOrGo with a negative entry).
bool TryUseQuestGO(ObjectGuid& pursuedGO, float searchRange = 60.0f);
// Fire a quest item's OnUse spell at the right target: a spell-focus
// GO (moonwell), a required creature, or the bot itself.
bool TryUseQuestItem(ObjectGuid& pursuedGO, ObjectGuid& pursuedTarget, float searchRange = 60.0f);
// True when a quest-relevant mob is within range — used during
// travel so we yield to attack-anything instead of running past.
bool HasNearbyQuestMob(float range = 20.0f);
// Narrower variant: only yields for mobs needed by the SPECIFIC
// quest+objective the bot is currently working on. Without this,
// do-quest yields for any quest in the log, derailing turn-ins
// and cross-zone travel through other quests' mob clusters.
bool HasNearbyQuestMobForObjective(float range, uint32 questId, int32 objectiveIdx);
protected:
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
// Reference per-spawn destination pattern: pick the first
// incomplete objective on `questId`, look up its spawns
// (creature OR gameobject — RequiredNpcOrGo encodes both) on
// the bot's current map, sort by distance from the bot, and
// return them in `outSpawns` with the resolved `outObjectiveIdx`.
// Returns false if no incomplete objective has spawns on the
// current map.
bool FetchQuestSpawnsForObjective(uint32 questId,
std::vector<WorldPosition>& outSpawns,
int32& outObjectiveIdx);
static WorldPosition SelectRandomGrindPos(Player* bot);
static WorldPosition SelectRandomCampPos(Player* bot);
bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path);
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status);
protected:
/* FOR MOVE FAR */
const float pathFinderDis = 70.0f;
// Time without real progress toward dest before MoveFarTo
// falls back to teleport recovery. Kept short enough that a
// bot truly oscillating around an unreachable destination
// (mmap returning non-progressing partial paths, or NOPATH +
// cone fallback wandering) doesn't spin for 5 minutes before
// the teleport fires, but long enough that a genuine long
// walk that is slowly making progress never triggers it.
const uint32 stuckTime = 90 * 1000;
};
#endif

View File

@ -9,7 +9,7 @@ bool NewRpgOutdoorPvpAction::Execute(Event event)
botAI->rpgInfo.ChangeToIdle();
return false;
}
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL) || !bot->IsOutdoorPvPActive())
if (!bot->IsOutdoorPvPActive())
return false;
uint32 zoneId = bot->GetZoneId();
@ -113,9 +113,6 @@ OPvPCapturePoint* NewRpgOutdoorPvpAction::SelectNewObjective(OutdoorPvP::OPvPCap
bool NewRpgOutdoorPvpAction::PatrolCapturePoint(GameObject* objectiveGO, float radius)
{
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
return false;
// Randomly pause at the current spot before picking a new patrol point
if (urand(0, 2) == 0)
return ForceToWait(urand(3000, 6000));

View File

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

View File

@ -1,6 +1,8 @@
#ifndef _PLAYERBOT_NEWRPGINFO_H
#define _PLAYERBOT_NEWRPGINFO_H
#include <deque>
#include "Define.h"
#include "ObjectGuid.h"
#include "ObjectMgr.h"
@ -8,6 +10,7 @@
#include "Strategy.h"
#include "Timer.h"
#include "TravelMgr.h"
#include "TravelNode.h"
using NewRpgStatusTransitionProb = std::vector<std::vector<int>>;
@ -43,8 +46,23 @@ struct NewRpgInfo
const Quest* quest{nullptr};
uint32 questId{0};
int32 objectiveIdx{0};
// Turn-in POI (DoCompletedQuest). Kept POI-based since this is
// the quest-giver location, not a mob spawn.
WorldPosition pos{};
// Reference (cmangos) per-spawn destination pattern for
// incomplete objectives: candidate spawn positions sorted by
// distance, walked one-by-one (current =
// candidateSpawns[currentSpawnIdx]) instead of POI-wander.
// Refreshed when the objective changes or the list is
// exhausted.
std::vector<WorldPosition> candidateSpawns;
uint32 currentSpawnIdx{0};
uint32 lastReachPOI{0};
// committed target per objective type. stops zig-zagging in
// dense spawn clusters when "nearest" would flip each tick.
ObjectGuid pursuedLootGO{}; // GOs we loot (lilies, eggs)
ObjectGuid pursuedUseGO{}; // GOs we click or focus on
ObjectGuid pursuedUseTarget{}; // creature we apply an item to
};
// RPG_TRAVEL_FLIGHT
struct TravelFlight
@ -70,12 +88,13 @@ struct NewRpgInfo
uint32 startT{0}; // start timestamp of the current status
// MOVE_FAR
float nearestMoveFarDis{FLT_MAX};
uint32 stuckTs{0};
uint32 stuckAttempts{0};
WorldPosition moveFarPos;
// END MOVE_FAR
// Counts consecutive MoveFarTo failures for the current state.
// Reset on every status change (via Reset) and on every successful
// MoveFarTo. When it crosses MAX_MOVE_RETRIES the failing action
// gives up and transitions out of the current state instead of
// sitting on a stuck objective forever.
uint8 moveRetryCount{0};
static constexpr uint8 MAX_MOVE_RETRIES = 10;
using RpgData = std::variant<
Idle,
@ -103,7 +122,6 @@ struct NewRpgInfo
void ChangeToIdle();
bool CanChangeTo(NewRpgStatus status);
void Reset();
void SetMoveFarTo(WorldPosition pos);
std::string ToString();
};

View File

@ -5,11 +5,66 @@
#include "NewRpgStrategy.h"
#include "Action.h"
#include "NewRpgInfo.h"
#include "Player.h"
#include "PlayerbotAI.h"
static bool IsGatherObjectiveForDoQuest(NewRpgInfo::DoQuest const* data)
{
if (!data || !data->quest)
return false;
Quest const* q = data->quest;
int32 obj = data->objectiveIdx;
if (obj < QUEST_OBJECTIVES_COUNT)
{
int32 entry = q->RequiredNpcOrGo[obj];
if (entry < 0) // GO objective
return true;
if (entry == 0 && obj < QUEST_ITEM_OBJECTIVES_COUNT && q->RequiredItemId[obj])
return true;
}
else if (obj < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT)
{
return true;
}
// source-item quest: need to find the right target to use it on
if (q->GetSrcItemId())
return true;
return false;
}
float NewRpgDoQuestMultiplier::GetValue(Action* action)
{
if (!action || action->getName() != "attack anything")
return 1.0f;
NewRpgInfo& info = botAI->rpgInfo;
if (info.GetStatus() != RPG_DO_QUEST)
return 1.0f;
auto* data = std::get_if<NewRpgInfo::DoQuest>(&info.data);
if (!data)
return 1.0f;
// heading back to turn in, don't get sidetracked
if (data->questId && bot->GetQuestStatus(data->questId) == QUEST_STATUS_COMPLETE)
return 0.15f;
// at POI: gather stays low so mobs don't pull us off the cluster;
// kill runs full so attack-anything drives behavior
if (data->lastReachPOI)
return IsGatherObjectiveForDoQuest(data) ? 0.30f : 1.0f;
// traveling
return 0.20f;
}
NewRpgStrategy::NewRpgStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
std::vector<NextAction> NewRpgStrategy::getDefaultActions()
{
// the releavance should be greater than grind
// must outrank grind
return {
NextAction("new rpg status update", 11.0f)
};
@ -53,7 +108,8 @@ void NewRpgStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
new TriggerNode(
"do quest status",
{
NextAction("new rpg do quest", 3.0f)
// 4.5: above attack-anything (4.0), below loot (5.0+)
NextAction("new rpg do quest", 4.5f)
}
)
);
@ -75,6 +131,7 @@ void NewRpgStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
);
}
void NewRpgStrategy::InitMultipliers(std::vector<Multiplier*>&)
void NewRpgStrategy::InitMultipliers(std::vector<Multiplier*>& multipliers)
{
multipliers.push_back(new NewRpgDoQuestMultiplier(botAI));
}

View File

@ -12,6 +12,13 @@
class PlayerbotAI;
class NewRpgDoQuestMultiplier : public Multiplier
{
public:
NewRpgDoQuestMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "new rpg do quest") {}
float GetValue(Action* action) override;
};
class NewRpgStrategy : public Strategy
{
public:

View File

@ -767,6 +767,21 @@ void PlayerbotAI::HandleCommand(uint32 type, const std::string& text, Player& fr
}
}
void PlayerbotAI::TeleportTo(WorldLocation loc, bool resetAI)
{
if (!bot || bot->IsBeingTeleported() || !bot->IsInWorld())
return;
bot->GetMotionMaster()->Clear();
if (resetAI)
Reset(true);
else
InterruptSpell();
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
bot->TeleportTo(loc.GetMapId(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ(), 0);
bot->SendMovementFlagUpdate();
}
void PlayerbotAI::HandleTeleportAck()
{
if (!bot || !bot->GetSession())
@ -809,7 +824,7 @@ void PlayerbotAI::HandleTeleportAck()
bot->StopMoving();
}
// simulate far teleport latency (cmangos-style)
// simulate far teleport latency
SetNextCheckDelay(urand(2000, 5000));
return;
}

View File

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

View File

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

View File

@ -45,7 +45,8 @@ void LootTargetList::shrink(time_t fromTime)
}
}
LootObject::LootObject(Player* bot, ObjectGuid guid) : guid(), skillId(SKILL_NONE), reqSkillValue(0), reqItem(0)
LootObject::LootObject(Player* bot, ObjectGuid guid)
: guid(), skillId(SKILL_NONE), reqSkillValue(0), reqItem(0), isNeededQuestItem(false)
{
Refresh(bot, guid);
}
@ -55,6 +56,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
skillId = SKILL_NONE;
reqSkillValue = 0;
reqItem = 0;
isNeededQuestItem = false;
guid.Clear();
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
@ -101,6 +103,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
if (IsNeededForQuest(bot, itemId))
{
this->guid = lootGUID;
this->isNeededQuestItem = true;
return;
}
@ -135,10 +138,21 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
if (!proto)
continue;
// Moonpetal Lily, Hyacinth Mushroom etc. expose quest
// drops here (not in gameobject_questitem). Flag it so
// the INTERACT_COND gate lets the bot through.
if (IsNeededForQuest(bot, itemId))
{
this->guid = lootGUID;
this->isNeededQuestItem = true;
return;
}
if (proto->Class != ITEM_CLASS_QUEST)
{
onlyHasQuestItems = false;
break;
// keep scanning — a later item may be needed
continue;
}
// If this item references another loot table, process it
@ -157,11 +171,15 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
if (!refProto)
continue;
if (refProto->Class != ITEM_CLASS_QUEST)
if (IsNeededForQuest(bot, refItemId))
{
onlyHasQuestItems = false;
break;
this->guid = lootGUID;
this->isNeededQuestItem = true;
return;
}
if (refProto->Class != ITEM_CLASS_QUEST)
onlyHasQuestItems = false;
}
}
}
@ -270,6 +288,7 @@ LootObject::LootObject(LootObject const& other)
skillId = other.skillId;
reqSkillValue = other.reqSkillValue;
reqItem = other.reqItem;
isNeededQuestItem = other.isNeededQuestItem;
}
bool LootObject::IsLootPossible(Player* bot)
@ -299,10 +318,13 @@ bool LootObject::IsLootPossible(Player* bot)
return false;
}
// Prevent bot from running to chests that are unlootable (e.g. Gunship Armory before completing the event) or on
// respawn time
// Block event-gated chests (Gunship Armory pre-event) and unspawned
// GOs. INTERACT_COND alone is allowed when the GO holds a quest
// item we need — ConditionMgr already gates on quest state.
GameObject* go = botAI->GetGameObject(guid);
if (go && (go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND | GO_FLAG_NOT_SELECTABLE) || !go->isSpawned()))
if (go && (go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE) || !go->isSpawned()))
return false;
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND) && !isNeededQuestItem)
return false;
if (skillId == SKILL_NONE)
@ -340,6 +362,13 @@ bool LootObject::IsLootPossible(Player* bot)
bool LootObjectStack::Add(ObjectGuid guid)
{
// expire old completed entries so a despawn/respawn with a reused
// guid can still be looted later
completedLoot.shrink(time(nullptr) - 300);
if (completedLoot.find(guid) != completedLoot.end())
return false;
if (availableLoot.size() >= MAX_LOOT_OBJECT_COUNT)
{
availableLoot.shrink(time(nullptr) - 30);
@ -363,7 +392,17 @@ void LootObjectStack::Remove(ObjectGuid guid)
availableLoot.erase(i);
}
void LootObjectStack::Clear() { availableLoot.clear(); }
void LootObjectStack::MarkCompleted(ObjectGuid guid)
{
Remove(guid);
completedLoot.insert(guid);
}
void LootObjectStack::Clear()
{
availableLoot.clear();
completedLoot.clear();
}
bool LootObjectStack::CanLoot(float maxDistance)
{

View File

@ -26,7 +26,7 @@ public:
class LootObject
{
public:
LootObject() : skillId(0), reqSkillValue(0), reqItem(0) {}
LootObject() : skillId(0), reqSkillValue(0), reqItem(0), isNeededQuestItem(false) {}
LootObject(Player* bot, ObjectGuid guid);
LootObject(LootObject const& other);
LootObject& operator=(LootObject const& other) = default;
@ -40,6 +40,9 @@ public:
uint32 skillId;
uint32 reqSkillValue;
uint32 reqItem;
// GO holds a quest item we still need; lets us bypass the
// INTERACT_COND blanket reject in the loot path
bool isNeededQuestItem;
private:
static bool IsNeededForQuest(Player* bot, uint32 itemId);
@ -73,6 +76,7 @@ public:
bool Add(ObjectGuid guid);
void Remove(ObjectGuid guid);
void MarkCompleted(ObjectGuid guid);
void Clear();
bool CanLoot(float maxDistance);
LootObject GetLoot(float maxDistance = 0);
@ -82,6 +86,9 @@ private:
Player* bot;
LootTargetList availableLoot;
// Guids we already opened loot on; blocks "add all loot" from
// re-adding the same corpse before it despawns.
LootTargetList completedLoot;
};
#endif

View File

@ -0,0 +1,60 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "QuestSpawnIndex.h"
#include "CreatureData.h"
#include "GameObjectData.h"
#include "Log.h"
#include "ObjectMgr.h"
QuestSpawnIndex* QuestSpawnIndex::instance()
{
static QuestSpawnIndex inst;
return &inst;
}
void QuestSpawnIndex::Init()
{
if (_initialized)
return;
uint32 creatures = 0;
uint32 gos = 0;
for (auto const& kv : sObjectMgr->GetAllCreatureData())
{
CreatureData const& cd = kv.second;
if (!cd.id1)
continue;
Key const key{cd.mapid, static_cast<int32>(cd.id1)};
_index[key].emplace_back(cd.mapid, cd.posX, cd.posY, cd.posZ, cd.orientation);
++creatures;
}
for (auto const& kv : sObjectMgr->GetAllGOData())
{
GameObjectData const& gd = kv.second;
if (!gd.id)
continue;
// Negative entry encodes GO (matches Quest::RequiredNpcOrGo
// convention used by the do-quest action callers).
Key const key{gd.mapid, -static_cast<int32>(gd.id)};
_index[key].emplace_back(gd.mapid, gd.posX, gd.posY, gd.posZ, gd.orientation);
++gos;
}
_initialized = true;
LOG_INFO("playerbots",
">> QuestSpawnIndex: indexed {} creature spawns + {} GO spawns ({} unique keys).",
creatures, gos, static_cast<uint32>(_index.size()));
}
std::vector<WorldPosition> const& QuestSpawnIndex::GetSpawns(uint32 mapId, int32 entry) const
{
auto it = _index.find(Key{mapId, entry});
return (it != _index.end()) ? it->second : _empty;
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_QUESTSPAWNINDEX_H
#define _PLAYERBOT_QUESTSPAWNINDEX_H
#include <unordered_map>
#include <vector>
#include "Define.h"
#include "TravelMgr.h"
// Maps `(mapId, RequiredNpcOrGo-style entry)` → list of spawn
// positions for that template on that map. The entry convention
// matches Quest::RequiredNpcOrGo: positive value = creature template
// id, negative value = gameobject template id (use the absolute
// value to look up in gameobject_template).
//
// Built once at module startup by scanning sObjectMgr's
// CreatureDataStore + GameObjectDataStore. Read-only thereafter.
//
// Used by the RPG do-quest action to walk directly to specific known
// spawns of a quest objective instead of wandering inside a POI
// cluster. Mirrors the reference's TravelMgr per-spawn destination
// indexing.
class QuestSpawnIndex
{
public:
static QuestSpawnIndex* instance();
// Build the index from sObjectMgr's spawn data. Safe to call
// multiple times — second+ calls are no-ops. Call once after
// sObjectMgr->LoadCreatures / LoadGameObjects have populated
// their stores.
void Init();
// Returns spawns of `entry` on `mapId`. Empty list if none
// indexed. Stable reference for the lifetime of the index.
std::vector<WorldPosition> const& GetSpawns(uint32 mapId, int32 entry) const;
[[nodiscard]] bool IsInitialized() const { return _initialized; }
private:
QuestSpawnIndex() = default;
bool _initialized{false};
struct Key
{
uint32 mapId;
int32 entry;
bool operator==(Key const& o) const { return mapId == o.mapId && entry == o.entry; }
};
struct KeyHash
{
std::size_t operator()(Key const& k) const noexcept
{
return (std::size_t(k.mapId) << 32) ^ std::size_t(uint32(k.entry));
}
};
std::unordered_map<Key, std::vector<WorldPosition>, KeyHash> _index;
std::vector<WorldPosition> _empty;
};
#define sQuestSpawnIndex QuestSpawnIndex::instance()
#endif

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@
#define _PLAYERBOT_TRAVELMGR_H
#include <boost/functional/hash.hpp>
#include <cmath>
#include <map>
#include <random>
@ -18,6 +19,7 @@
class Creature;
class GuidPosition;
class PathGenerator;
class ObjectGuid;
class Quest;
class Player;
@ -136,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);
@ -227,6 +232,28 @@ public:
float getAngleBetween(WorldPosition dir1, WorldPosition dir2) { return abs(getAngleTo(dir1) - getAngleTo(dir2)); }
// Project this point onto the segment [p1, p2]. Returns t such that
// p1 + t*(p2-p1) is the projection. t=0 means at p1, t=1 means at p2,
// 0<t<1 means strictly between. Used to decide whether the bot has
// already passed a path waypoint and should skip to the next one.
float projectOnSegment(WorldPosition const& p1, WorldPosition const& p2) const
{
if (p1.GetMapId() != p2.GetMapId() || p1.GetMapId() != GetMapId())
return 0.0f;
float dx = p2.GetPositionX() - p1.GetPositionX();
float dy = p2.GetPositionY() - p1.GetPositionY();
float dz = p2.GetPositionZ() - p1.GetPositionZ();
float lenSq = dx * dx + dy * dy + dz * dz;
if (lenSq == 0.0f)
return 0.0f;
return ((GetPositionX() - p1.GetPositionX()) * dx +
(GetPositionY() - p1.GetPositionY()) * dy +
(GetPositionZ() - p1.GetPositionZ()) * dz) / lenSq;
}
WorldPosition lastInRange(std::vector<WorldPosition> list, float minDist = -1.f, float maxDist = -1.f);
WorldPosition firstOutRange(std::vector<WorldPosition> list, float minDist = -1.f, float maxDist = -1.f);
@ -268,12 +295,6 @@ public:
std::vector<mGridCoord> getmGridCoords(WorldPosition secondPos);
std::vector<WorldPosition> frommGridCoord(mGridCoord GridCoord);
void loadMapAndVMap(uint32 mapId, uint8 x, uint8 y);
void loadMapAndVMap() { loadMapAndVMap(GetMapId(), getmGridCoord().first, getmGridCoord().second); }
void loadMapAndVMaps(WorldPosition secondPos);
// Display functions
WorldPosition getDisplayLocation();
float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; }
@ -288,6 +309,7 @@ public:
// Pathfinding
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, Unit* bot);
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, PathGenerator& pathfinder);
std::vector<WorldPosition> getPathFromPath(std::vector<WorldPosition> startPath, Unit* bot, uint8 maxAttempt = 40);
std::vector<WorldPosition> getPathFrom(WorldPosition startPos, Unit* bot)
@ -297,10 +319,26 @@ public:
std::vector<WorldPosition> getPathTo(WorldPosition endPos, Unit* bot) { return endPos.getPathFrom(*this, bot); }
bool isPathTo(std::vector<WorldPosition> path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance)
// The path "reaches" this position when its last point is on
// the same map, within maxDistance horizontally, and within
// maxZDistance vertically. 3D Euclidean distance would falsely
// accept paths that end the right horizontal distance from us
// but on a roof/floor below. maxDistance == 0 falls back to
// targetPosRecalcDistance (0.1y).
bool isPathTo(std::vector<WorldPosition> const& path, float const maxDistance = 0.0f,
float const maxZDistance = 2.0f) const
{
return !path.empty() && distance(path.back()) < maxDistance;
};
if (path.empty())
return false;
WorldPosition const& back = path.back();
if (back.GetMapId() != GetMapId())
return false;
float const realMax = maxDistance > 0.0f ? maxDistance
: sPlayerbotAIConfig.targetPosRecalcDistance;
if (GetExactDist2dSq(&back) >= realMax * realMax)
return false;
return std::fabs(back.GetPositionZ() - GetPositionZ()) < maxZDistance;
}
bool cropPathTo(std::vector<WorldPosition>& path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance);
bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); }
@ -507,9 +545,15 @@ public:
radiusMin = radiusMin1;
radiusMax = radiusMax1;
}
virtual ~TravelDestination() = default;
virtual ~TravelDestination();
void addPoint(WorldPosition* pos) { points.push_back(pos); }
void addPoint(WorldPosition* pos)
{
if (!pos)
return;
points.push_back(new WorldPosition(*pos));
}
void setExpireDelay(uint32 delay) { expireDelay = delay; }
@ -673,7 +717,7 @@ public:
bool isActive(Player* bot) override;
virtual CreatureTemplate const* GetCreatureTemplate();
std::string const getName() override { return "RpgTravelDestination"; }
int32 getEntry() override { return 0; }
int32 getEntry() override { return entry; }
std::string const getTitle() override;
protected:
@ -985,18 +1029,14 @@ private:
bool InsideBracket(uint32 val) const { return val >= low && val <= high; }
};
struct BankerLocation
{
WorldLocation loc;
uint32 entry;
};
// Navigation caches
std::map<uint32, FlightMasterInfo> allianceFlightMasterCache;
std::map<uint32, FlightMasterInfo> hordeFlightMasterCache;
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;
std::map<uint8, std::vector<NpcLocation>> bankerLocsPerLevelCache;
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> hordeAuctioneerCache;
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> allianceAuctioneerCache;
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;

File diff suppressed because it is too large Load Diff

View File

@ -8,12 +8,12 @@
#include <shared_mutex>
#include "G3D/Vector3.h"
#include "TravelMgr.h"
// THEORY
//
// Pathfinding in (c)mangos is based on detour recast an opensource nashmesh creation and pathfinding codebase.
// This system is used for mob and npc pathfinding and in this codebase also for bots.
// Pathfinding uses the detour recast navmesh engine for mob, npc, and bot movement.
// Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y.
// This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a
// straight path. Bots would get stuck moving from Northshire to Stormwind because there is no 296y path that doesn't
@ -24,56 +24,88 @@
// <S> ---> [N1] ---> [N2] ---> [N3] ---> <E>
//
// Bot at <S> wants to move to <E>
// [N1],[N2],[N3] are predefined nodes for wich we know we can move from [N1] to [N2] and from [N2] to [N3] but not
// from [N1] to [N3] If we can move fom [S] to [N1] and from [N3] to [E] we have a complete route to travel.
// [N1],[N2],[N3] are predefined nodes for which we know we can move from [N1] to [N2] and from [N2] to [N3] but not
// from [N1] to [N3]. If we can move from [S] to [N1] and from [N3] to [E] we have a complete route to travel.
//
// Termonology:
// Node: a location on a map for which we know bots are likely to want to travel to or need to travel past to reach
// other nodes. Link: the connection between two nodes. A link signifies that the bot can travel from one node to
// another. A link is one-directional. Path: the waypointpath returned by the standard PathGenerator to move from one
// node (or position) to another. A path can be imcomplete or empty which means there is no link. Route: the list of
// nodes that give the shortest route from a node to a distant node. Routes are calculated using a standard A* search
// based on links.
// Terminology:
// Node: A location on a map for which we know bots are likely to want to travel to or need to travel past to reach
// other nodes. Stored in DB table `playerbots_travelnode`.
// Link: The connection between two nodes. A link signifies that the bot can travel from one node to another.
// A link is one-directional. Stored in `playerbots_travelnode_link`.
// Path: The waypoint path returned by the standard PathGenerator to move from one node (or position) to another.
// A path can be incomplete or empty which means there is no link. Stored in `playerbots_travelnode_path`.
// Route: The list of nodes that give the shortest route from a node to a distant node. Routes are calculated using
// a standard A* search based on links.
//
// On server start saved nodes and links are loaded. Paths and routes are calculated on the fly but saved for future
// use. Nodes can be added and removed realtime however because bots access the nodes from different threads this
// requires a locking mechanism.
// Edge types (TravelNodePathType):
// walk(1) — Walk via navmesh waypoints (stored in DB)
// areaTrigger(2) — AreaTrigger teleport (auto-discovered at startup)
// transport(3) — Boat/zeppelin (auto-discovered from MO_TRANSPORT)
// flightPath(4) — Taxi flight between flight masters
// teleportSpell(5) — Spell-based teleport (e.g. mage portals)
// staticPortal(6) — Manually defined teleport link (DB only, not pruned by generation)
//
// On server start saved nodes and links are loaded via TravelNodeMap::Init(). An index of nodes by zone is prepared
// (instead of scanning all ~4000 nodes), precomputes connected components for O(1) reachability checks, and builds
// a taxi BFS graph. Paths and routes are calculated on the fly and saved for future use. Nodes are only added at
// startup or via the console `.generate` command — runtime mutation was removed because taking a unique_lock
// caused 100-250ms contention spikes against bot threads.
//
// Initially the current nodes have been made:
// Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route
// calculation) WorldBosses and Unique bosses in instances (These are a logical places bots might want to go in
// calculation) WorldBosses and Unique bosses in instances (These are logical places bots might want to go in
// instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with
// teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators
// (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is
// good for global coverage)
// good for global coverage).
//
// To increase coverage/linking extra nodes can be automatically be created.
// Current implentation places nodes on paths (including complete) at sub-zone transitions or randomly.
// After calculating possible links the node is removed if it does not create local coverage.
// To increase coverage/linking extra nodes must be manually created via the "playerbot travel generatenode"
// console command after importing the specified node. Current implementation places nodes on paths (including
// complete) at sub-zone transitions or randomly. After calculating possible links the node is removed if it
// does not create local coverage (.fullgenerate only).
//
// Travel Flow:
//
// GetFullPath finds nearest nodes (zone-indexed), runs A* to get a node route, then
// BuildPath assembles a flat TravelPath with typed waypoints (walk, portal, transport, flight).
// MoveFarTo re-resolves a fresh TravelPath each tick; UpcommingSpecialMovement cuts
// to the head segment when special; HandleSpecialMovement dispatches the matching
// action (portal interact, area-trigger marker, transport board, flight taxi).
// Cross-map travel is handled naturally by portal/transport edges in the A* graph.
//
// If setup cannot resolve (no node, no route, no flight), the bot teleports directly to the destination
// as a fallback.
//
// The use of hearthstones and mage teleporting was removed — it caused route mutations requiring locking that no longer made sense. Mage portals may be future item.
//
// Thread Safety:
//
// The node graph is immutable at runtime (no adds/removes after Init). A shared_timed_mutex (m_nMapMtx) still
// exists and shared_locks are taken in GetFullPath and GenerateWalkPath for safety, but since there are no
// runtime mutations these are effectively uncontested. The only exclusive locks are taken at startup
// (saveNodeStore) and by the debug dump command.
//
enum class TravelNodePathType : uint8
{
none = 0,
walk = 1,
portal = 2,
areaTrigger = 2,
transport = 3,
flightPath = 4,
teleportSpell = 5
// teleportSpell = 5 // maybe someday
staticPortal = 6
};
// A connection between two nodes.
class TravelNodePath
{
public:
// Legacy Constructor for travelnodestore
// TravelNodePath(float distance1, float extraCost1, bool portal1 = false, uint32 portalId1 = 0, bool transport1 =
// false, bool calculated = false, uint8 maxLevelMob1 = 0, uint8 maxLevelAlliance1 = 0, uint8 maxLevelHorde1 = 0,
// float swimDistance1 = 0, bool flightPath1 = false);
// Constructor
TravelNodePath(float distance = 0.1f, float extraCost = 0, uint8 pathType = (uint8)TravelNodePathType::walk,
uint32 pathObject = 0, bool calculated = false, std::vector<uint8> maxLevelCreature = {0, 0, 0},
TravelNodePath(float distance = 0.1f, float extraCost = 0,
uint8 pathType = (uint8)TravelNodePathType::walk,
uint32 pathObject = 0, bool calculated = false,
std::vector<uint8> maxLevelCreature = {0, 0, 0},
float swimDistance = 0)
: extraCost(extraCost),
calculated(calculated),
@ -85,7 +117,7 @@ public:
{
if (pathType != (uint8)TravelNodePathType::walk)
complete = true;
};
}
TravelNodePath(TravelNodePath* basePath)
{
@ -98,11 +130,11 @@ public:
swimDistance = basePath->swimDistance;
pathType = basePath->pathType;
pathObject = basePath->pathObject;
};
}
// Getters
bool getComplete() { return complete || pathType != TravelNodePathType::walk; }
std::vector<WorldPosition> getPath() { return path; }
std::vector<WorldPosition> GetPath() { return path; }
TravelNodePathType getPathType() { return pathType; }
uint32 getPathObject() { return pathObject; }
@ -130,9 +162,6 @@ public:
extraCost = distance / speed;
}
// void setPortal(bool portal1, uint32 portalId1 = 0) { portal = portal1; portalId = portalId1; }
// void setTransport(bool transport1) { transport = transport1; }
void setPathType(TravelNodePathType pathType1) { pathType = pathType1; }
void setPathObject(uint32 pathObject1) { pathObject = pathObject1; }
@ -186,9 +215,10 @@ class TravelNode
{
public:
// Constructors
TravelNode(){};
TravelNode() {}
TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node", bool important1 = false)
TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node",
bool important1 = false)
{
nodeName = nodeName1;
point = point1;
@ -207,11 +237,11 @@ public:
void setPoint(WorldPosition point1) { point = point1; }
// Getters
std::string const getName() { return nodeName; };
WorldPosition* getPosition() { return &point; };
std::string const getName() { return nodeName; }
WorldPosition* getPosition() { return &point; }
std::unordered_map<TravelNode*, TravelNodePath>* getPaths() { return &paths; }
std::unordered_map<TravelNode*, TravelNodePath*>* getLinks() { return &links; }
bool isImportant() { return important; };
bool isImportant() { return important; }
bool isLinked() { return linked; }
bool isTransport()
@ -235,9 +265,9 @@ public:
bool isPortal()
{
for (auto const& link : *getLinks())
if (link.second->getPathType() == TravelNodePathType::portal)
if (link.second->getPathType() == TravelNodePathType::areaTrigger ||
link.second->getPathType() == TravelNodePathType::staticPortal)
return true;
return false;
}
@ -251,17 +281,25 @@ public:
}
// WorldLocation shortcuts
uint32 getMapId() { return point.GetMapId(); }
uint32 GetMapId() { return point.GetMapId(); }
float getX() { return point.GetPositionX(); }
float getY() { return point.GetPositionY(); }
float getZ() { return point.GetPositionZ(); }
float getO() { return point.GetOrientation(); }
float getDistance(WorldPosition pos) { return point.distance(pos); }
float getDistance(TravelNode* node) { return point.distance(node->getPosition()); }
float fDist(TravelNode* node) { return point.fDist(node->getPosition()); }
float getDistance(TravelNode* node)
{
return point.distance(node->getPosition());
}
float fDist(TravelNode* node)
{
return point.fDist(node->getPosition());
}
float fDist(WorldPosition pos) { return point.fDist(pos); }
TravelNodePath* setPathTo(TravelNode* node, TravelNodePath path = TravelNodePath(), bool isLink = true)
TravelNodePath* setPathTo(TravelNode* node,
TravelNodePath path = TravelNodePath(),
bool isLink = true)
{
if (this != node)
{
@ -275,10 +313,20 @@ public:
return nullptr;
}
bool hasPathTo(TravelNode* node) { return paths.find(node) != paths.end(); }
TravelNodePath* getPathTo(TravelNode* node) { return &paths[node]; }
bool hasCompletePathTo(TravelNode* node) { return hasPathTo(node) && getPathTo(node)->getComplete(); }
TravelNodePath* buildPath(TravelNode* endNode, Unit* bot, bool postProcess = false);
bool hasPathTo(TravelNode* node)
{
return paths.find(node) != paths.end();
}
TravelNodePath* getPathTo(TravelNode* node)
{
return &paths[node];
}
bool hasCompletePathTo(TravelNode* node)
{
return hasPathTo(node) && getPathTo(node)->getComplete();
}
TravelNodePath* BuildPath(TravelNode* endNode, Unit* bot,
bool postProcess = false);
void setLinkTo(TravelNode* node, float distance = 0.1f)
{
@ -291,20 +339,27 @@ public:
}
}
bool hasLinkTo(TravelNode* node) { return links.find(node) != links.end(); }
float linkCostTo(TravelNode* node) { return paths.find(node)->second.getDistance(); }
float linkDistanceTo(TravelNode* node) { return paths.find(node)->second.getDistance(); }
bool hasLinkTo(TravelNode* node)
{
return links.find(node) != links.end();
}
float linkCostTo(TravelNode* node)
{
return paths.find(node)->second.getDistance();
}
float linkDistanceTo(TravelNode* node)
{
return paths.find(node)->second.getDistance();
}
void removeLinkTo(TravelNode* node, bool removePaths = false);
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.
std::vector<TravelNode*> getNodeMap(bool importantOnly = false, std::vector<TravelNode*> ignoreNodes = {});
std::vector<TravelNode*> getNodeMap(bool importantOnly = false,
std::vector<TravelNode*> ignoreNodes = {});
// Checks if it is even possible to route to this node.
bool hasRouteTo(TravelNode* node)
@ -314,7 +369,10 @@ public:
routes[mNode] = true;
return routes.find(node) != routes.end();
};
}
void clearRoutes() { routes.clear(); }
void setRouteTo(TravelNode* node) { routes[node] = true; }
void print(bool printFailed = true);
@ -344,63 +402,65 @@ protected:
// uint32 transportId = 0;
};
class PortalNode : public TravelNode
{
public:
PortalNode(TravelNode* baseNode) : TravelNode(baseNode){};
void SetPortal(TravelNode* baseNode, TravelNode* endNode, uint32 portalSpell)
{
nodeName = baseNode->getName();
point = *baseNode->getPosition();
paths.clear();
links.clear();
TravelNodePath path(0.1f, 0.1f, (uint8)TravelNodePathType::teleportSpell, portalSpell, true);
setPathTo(endNode, path);
};
};
// Route step type
enum PathNodeType
enum class PathNodeType : uint8
{
NODE_PREPATH = 0,
NODE_PATH = 1,
NODE_NODE = 2,
NODE_PORTAL = 3,
NODE_AREA_TRIGGER = 3,
NODE_TRANSPORT = 4,
NODE_FLIGHTPATH = 5,
NODE_TELEPORT = 6
// value 6 reserved (was NODE_TELEPORT — removed with teleportSpell)
NODE_STATIC_PORTAL = 7
};
struct PathNodePoint
{
WorldPosition point;
PathNodeType type = NODE_PATH;
PathNodeType type = PathNodeType::NODE_PATH;
uint32 entry = 0;
bool operator==(const PathNodePoint& p1) const
{
return point == p1.point && type == p1.type && entry == p1.entry;
}
// A "walkable" node is one we traverse on foot. Portals/transports/
// taxis/teleports are entry/exit hops, not points to anchor a
// shortcut on. Used by makeShortCut to skip them when picking the
// closest-point-on-path to the bot.
bool isWalkable() const { return (uint8)type <= (uint8)PathNodeType::NODE_NODE; }
};
// A complete list of points the bots has to walk to or teleport to.
class TravelPath
{
public:
TravelPath(){};
TravelPath(std::vector<PathNodePoint> fullPath1) { fullPath = fullPath1; }
TravelPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0)
TravelPath() {}
TravelPath(std::vector<PathNodePoint> fullPath1)
{
fullPath = fullPath1;
}
TravelPath(std::vector<WorldPosition> path,
PathNodeType type = PathNodeType::NODE_PATH,
uint32 entry = 0)
{
addPath(path, type, entry);
}
void addPoint(PathNodePoint point) { fullPath.push_back(point); }
void addPoint(WorldPosition point, PathNodeType type = NODE_PATH, uint32 entry = 0)
void addPoint(WorldPosition point,
PathNodeType type = PathNodeType::NODE_PATH,
uint32 entry = 0)
{
fullPath.push_back(PathNodePoint{point, type, entry});
}
void addPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0)
void addPath(std::vector<WorldPosition> path,
PathNodeType type = PathNodeType::NODE_PATH,
uint32 entry = 0)
{
for (auto& p : path)
{
fullPath.push_back(PathNodePoint{p, type, entry});
};
}
void addPath(std::vector<PathNodePoint> newPath)
{
@ -408,8 +468,11 @@ public:
}
void clear() { fullPath.clear(); }
bool empty() { return fullPath.empty(); }
std::vector<PathNodePoint> getPath() { return fullPath; }
bool empty() const { return fullPath.empty(); }
size_t size() const { return fullPath.size(); }
const PathNodePoint& operator[](size_t idx) const { return fullPath[idx]; }
std::vector<PathNodePoint> GetPath() { return fullPath; }
const std::vector<PathNodePoint>& GetPathRef() const { return fullPath; }
WorldPosition getFront() { return fullPath.front().point; }
WorldPosition getBack() { return fullPath.back().point; }
@ -419,17 +482,64 @@ public:
for (auto const& p : fullPath)
retVec.push_back(p.point);
return retVec;
};
}
bool makeShortCut(WorldPosition startPos, float maxDist);
bool shouldMoveToNextPoint(WorldPosition startPos, std::vector<PathNodePoint>::iterator beg,
std::vector<PathNodePoint>::iterator ed, std::vector<PathNodePoint>::iterator p,
float& moveDist, float maxDist);
WorldPosition getNextPoint(WorldPosition startPos, float maxDist, TravelNodePathType& pathType, uint32& entry);
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/
// area-trigger node once the bot reaches it.
bool cutTo(PathNodePoint point, bool including);
// Returns true if the next reachable segment is a special-handling
// node (portal / area-trigger / transport / flightpath / teleport)
// and the bot is close enough / positioned right to handle it now.
// Trims the path up to that segment as a side effect. Caller then
// dispatches the matching special-movement handler on the new head.
bool UpcommingSpecialMovement(WorldPosition startPos, float maxDist, bool onTransport);
// Truncate the path at the first waypoint that would put the bot in
// range of a hostile creature (within attack range, in LOS, level-cap
// sane), at a non-walkable hop, after drifting beyond reactDistance
// from the start, or across a > 125-sqDist jump. Set ignoreEnemyTargets
// to suppress the hostile-target check (used by combat repositioning).
void ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets = false);
// Reject paths the navmesh accepts but a player can't walk:
// 2-point shortcut over 5y, or > 10y vertical drop with slope steeper than 2:1.
static bool IsPathCheating(std::vector<WorldPosition> const& path,
float endpointDistance);
std::ostringstream const print();
private:
// Returns the next-best-point iterator within maxDist from startPos:
// skips waypoints behind the bot, advances while shouldMoveToNextPoint
// allows, projects onto current segment to decide if the bot has
// already passed it.
std::vector<PathNodePoint>::iterator getNextPoint(WorldPosition startPos,
float maxDist,
bool onTransport);
// Heuristic for getNextPoint: decides whether the iterator should
// step forward to nextP. Stops at special nodes (area triggers,
// portals, transports, flight paths), at map boundaries, and when
// accumulated distance exceeds maxDist.
bool shouldMoveToNextPoint(WorldPosition startPos,
std::vector<PathNodePoint>::iterator beg,
std::vector<PathNodePoint>::iterator ed,
std::vector<PathNodePoint>::iterator p,
float& moveDist, float maxDist);
std::vector<PathNodePoint> fullPath;
};
@ -438,16 +548,23 @@ class TravelNodeRoute
{
public:
TravelNodeRoute() {}
TravelNodeRoute(std::vector<TravelNode*> nodes1) { nodes = nodes1; /*currentNode = route.begin();*/ }
TravelNodeRoute(std::vector<TravelNode*> nodes1)
{
nodes = nodes1;
}
bool isEmpty() { return nodes.empty(); }
bool hasNode(TravelNode* node) { return findNode(node) != nodes.end(); }
bool hasNode(TravelNode* node)
{
return findNode(node) != nodes.end();
}
float getTotalDistance();
std::vector<TravelNode*> getNodes() { return nodes; }
TravelPath buildPath(std::vector<WorldPosition> pathToStart = {}, std::vector<WorldPosition> pathToEnd = {},
TravelPath BuildPath(
std::vector<WorldPosition> pathToStart = {},
std::vector<WorldPosition> pathToEnd = {},
Unit* bot = nullptr);
std::ostringstream const print();
@ -467,8 +584,11 @@ public:
TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; }
TravelNode* dataNode;
float m_f = 0.0, m_g = 0.0, m_h = 0.0;
bool open = false, close = false;
float totalCost = 0.0;
float costFromStart = 0.0;
float heuristic = 0.0;
bool open = false;
bool closed = false;
TravelNodeStub* parent = nullptr;
uint32 currentGold = 0;
};
@ -484,14 +604,18 @@ public:
return instance;
}
TravelNode* addNode(WorldPosition pos, std::string const preferedName = "Travel Node", bool isImportant = false,
bool checkDuplicate = true, bool transport = false, uint32 transportId = 0);
TravelNode* addNode(WorldPosition pos,
std::string const preferedName = "Travel Node",
bool isImportant = false,
bool checkDuplicate = true,
bool transport = false,
uint32 transportId = 0);
void removeNode(TravelNode* node);
bool removeNodes()
{
if (m_nMapMtx.try_lock_for(std::chrono::seconds(10)))
{
for (auto& node : m_nodes)
for (auto& node : nodes)
removeNode(node);
m_nMapMtx.unlock();
@ -499,28 +623,32 @@ public:
}
return false;
};
}
void fullLinkNode(TravelNode* startNode, Unit* bot);
// Get all nodes
std::vector<TravelNode*> getNodes() { return m_nodes; }
std::vector<TravelNode*> getNodes() { return nodes; }
std::vector<TravelNode*> getNodes(WorldPosition pos, float range = -1);
// Find nearest node.
TravelNode* getNode(TravelNode* sameNode)
{
for (auto& node : m_nodes)
for (auto& node : nodes)
{
if (node->getName() == sameNode->getName() && node->getPosition() == sameNode->getPosition())
if (node->getName() == sameNode->getName()
&& node->getPosition() == sameNode->getPosition())
return node;
}
return nullptr;
}
TravelNode* getNode(WorldPosition pos, std::vector<WorldPosition>& ppath, Unit* bot = nullptr, float range = -1);
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr, float range = -1)
TravelNode* getNode(WorldPosition pos,
std::vector<WorldPosition>& ppath,
Unit* bot = nullptr, float range = -1);
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr,
float range = -1)
{
std::vector<WorldPosition> ppath;
return getNode(pos, ppath, bot, range);
@ -536,19 +664,17 @@ public:
return rNodes[urand(0, rNodes.size() - 1)];
}
// Finds the best nodePath between two nodes
TravelNodeRoute getRoute(TravelNode* start, TravelNode* goal, Player* bot = nullptr);
// Finds the best nodePath between two nodes (A* over the node graph)
TravelNodeRoute GetNodeRoute(TravelNode* start, TravelNode* goal,
Player* bot);
// Find the best node between two positions
TravelNodeRoute getRoute(WorldPosition startPos, WorldPosition endPos, std::vector<WorldPosition>& startPath,
// Picks the nearest start/end nodes for two world positions and runs A*
// over the node graph to return a full route between them.
TravelNodeRoute FindRouteNearestNodes(WorldPosition startPos,
WorldPosition endPos,
std::vector<WorldPosition>& startPath,
Player* bot = nullptr);
// Find the full path between those locations
static TravelPath getFullPath(WorldPosition startPos, WorldPosition endPos, Player* bot = nullptr);
// Manage/update nodes
void manageNodes(Unit* bot, bool mapFull = false);
void setHasToGen() { hasToGen = true; }
void generateNpcNodes();
@ -563,19 +689,18 @@ public:
void removeUselessPaths();
void calculatePathCosts();
void generateTaxiPaths();
void generatePaths();
void generatePaths(bool fullGen = false);
void generateAll();
void Init();
void printMap();
void printNodeStore();
void saveNodeStore();
void loadNodeStore();
void LoadNodeStore();
bool cropUselessNode(TravelNode* startNode);
TravelNode* addZoneLinkNode(TravelNode* startNode);
TravelNode* addRandomExtNode(TravelNode* startNode);
void calcMapOffset();
WorldPosition getMapOffset(uint32 mapId);
@ -584,8 +709,23 @@ public:
void InitTaxiGraph();
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
void PrecomputeReachability();
// 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,
WorldPosition destination, Unit* bot = nullptr);
// Resolve A* route between two world positions (returns node vector)
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,
WorldPosition endPos);
// Get stored walk points for one edge (from→to). Empty if no path.
std::vector<G3D::Vector3> GetEdgeWalkPoints(TravelNode* from,
TravelNode* to);
std::shared_timed_mutex m_nMapMtx;
std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes;
private:
TravelNodeMap() = default;
@ -601,13 +741,15 @@ private:
void BuildTaxiGraph();
void ComputeAllPaths();
std::unordered_map<uint32, uint32> BFS(uint32 startNode);
std::vector<uint32> BuildPath(uint32 fromNode, uint32 toNode,
std::vector<uint32> BuildPath(
uint32 fromNode, uint32 toNode,
const std::unordered_map<uint32, uint32>& parentMap);
std::unordered_map<uint32, std::vector<uint32>> taxiGraph;
std::map<uint32, std::map<uint32, std::vector<uint32>>> taxiPathCache;
std::unordered_map<uint32, std::vector<uint32>> m_taxiGraph;
std::map<uint32, std::map<uint32, std::vector<uint32>>>
m_taxiPathCache;
std::vector<TravelNode*> m_nodes;
std::vector<TravelNode*> nodes;
std::vector<std::pair<uint32, WorldPosition>> mapOffsets;

View File

@ -72,7 +72,6 @@ bool PlayerbotAIConfig::Initialize()
globalCoolDown = sConfigMgr->GetOption<int32>("AiPlayerbot.GlobalCooldown", 500);
maxWaitForMove = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxWaitForMove", 5000);
disableMoveSplinePath = sConfigMgr->GetOption<int32>("AiPlayerbot.DisableMoveSplinePath", 0);
maxMovementSearchTime = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxMovementSearchTime", 3);
expireActionTime = sConfigMgr->GetOption<int32>("AiPlayerbot.ExpireActionTime", 5000);
dispelAuraDuration = sConfigMgr->GetOption<int32>("AiPlayerbot.DispelAuraDuration", 700);
reactDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReactDelay", 100);
@ -89,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);
@ -678,6 +678,7 @@ bool PlayerbotAIConfig::Initialize()
autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false);
autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true);
enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true);
enableTravelNodes = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableTravelNodes", false);
RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15);
RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20);

View File

@ -91,8 +91,14 @@ public:
bool EnableICCBuffs;
bool allowAccountBots, allowGuildBots, allowTrustedAccountBots;
bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat;
uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, maxMovementSearchTime, expireActionTime,
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,
@ -375,6 +381,7 @@ public:
bool autoLearnTrainerSpells;
bool autoDoQuests;
bool enableNewRpgStrategy;
bool enableTravelNodes;
std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight;
bool syncLevelWithPlayers;
bool autoLearnQuestSpells;

View File

@ -16,10 +16,12 @@
#include "BattleGroundTactics.h"
#include "Chat.h"
#include "GuildTaskMgr.h"
#include "MapMgr.h"
#include "PerfMonitor.h"
#include "PlayerbotMgr.h"
#include "RandomPlayerbotMgr.h"
#include "ScriptMgr.h"
#include "TravelNode.h"
using namespace Acore::ChatCommands;
@ -32,6 +34,7 @@ public:
{
static ChatCommandTable playerbotsDebugCommandTable = {
{"bg", HandleDebugBGCommand, SEC_GAMEMASTER, Console::Yes},
{"zone", HandleDebugZoneCommand, SEC_GAMEMASTER, Console::No},
};
static ChatCommandTable playerbotsAccountCommandTable = {
@ -41,11 +44,16 @@ public:
{"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No},
};
static ChatCommandTable playerbotsTravelCommandTable = {
{"generatenode", HandleGenerateTravelNodesCommand, SEC_GAMEMASTER, Console::Yes},
};
static ChatCommandTable playerbotsCommandTable = {
{"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No},
{"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes},
{"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes},
{"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes},
{"travel", playerbotsTravelCommandTable},
{"debug", playerbotsDebugCommandTable},
{"account", playerbotsAccountCommandTable},
};
@ -106,11 +114,194 @@ public:
return true;
}
static bool HandleGenerateTravelNodesCommand(ChatHandler* handler, char const* /*args*/)
{
handler->PSendSysMessage("Regenerating travel node paths...");
LOG_INFO("playerbots", "Manual travel node regeneration started via console command.");
sTravelNodeMap.generateAll();
handler->PSendSysMessage("Travel node regeneration complete. Paths saved to database.");
return true;
}
static bool HandleDebugBGCommand(ChatHandler* handler, char const* args)
{
return BGTactics::HandleConsoleCommand(handler, args);
}
// Visual constants for showpath markers. Two waypoint-family
// creatures give nodes vs path waypoints distinct visuals; both
// render at their creature_template default scale (no override).
// nodes (anchors) → 15897, prominent waypoint variant
// path waypoints → 15631, standard BG-showpath waypoint
//
// SHOWPATH_PATH_DISPLAY_ID = 0 uses the path-creature's default
// model. To experiment with a model override, set this to a known-
// good creature display ID for your DB (spell-visual IDs are not
// universally registered as creature displays — using one risks
// summoning invisible markers).
static constexpr uint32 SHOWPATH_NODE_CREATURE = 15897;
static constexpr uint32 SHOWPATH_PATH_CREATURE = 15631;
static constexpr uint32 SHOWPATH_PATH_DISPLAY_ID = 0; // 0 = default model
static constexpr uint32 SHOWPATH_DESPAWN_MS = 60000;
static bool HandleDebugZoneCommand(ChatHandler* handler, char const* args)
{
Player* player = handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr;
if (!player)
{
handler->PSendSysMessage("Command requires an in-game player.");
return false;
}
if (!args || !*args)
{
handler->PSendSysMessage("usage: .playerbots debug zone showpath=all|node|path");
return false;
}
char* cmd = strtok(const_cast<char*>(args), " ");
// showpath=all → nodes + cached path waypoints (full picture)
// showpath=node → only node anchors
// showpath=path → only cached path waypoints (no anchors)
bool showNodes = false;
bool showLinks = false;
if (cmd && strcmp(cmd, "showpath=all") == 0)
{
showNodes = true;
showLinks = true;
}
else if (cmd && strcmp(cmd, "showpath=node") == 0)
{
showNodes = true;
showLinks = false;
}
else if (cmd && strcmp(cmd, "showpath=path") == 0)
{
showNodes = false;
showLinks = true;
}
else
{
handler->PSendSysMessage("usage: .playerbots debug zone showpath=all|node|path");
return false;
}
uint32 zoneId = player->GetZoneId();
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);
return true;
}
// node markers — full-scale anchor at each travel-node position.
uint32 nodesPlaced = 0;
if (showNodes)
{
for (TravelNode* node : nodes)
{
if (!node)
continue;
WorldPosition* pos = node->getPosition();
if (!pos || pos->GetMapId() != player->GetMapId())
continue;
Creature* wp = player->SummonCreature(SHOWPATH_NODE_CREATURE,
pos->GetPositionX(), pos->GetPositionY(),
pos->GetPositionZ(), 0,
TEMPSUMMON_TIMED_DESPAWN, SHOWPATH_DESPAWN_MS);
if (wp)
{
wp->SetOwnerGUID(player->GetGUID());
++nodesPlaced;
}
}
}
if (!showLinks)
{
handler->PSendSysMessage("Showing {} travel nodes in zone {} (60s)", nodesPlaced, zoneId);
return true;
}
// path-waypoint markers — same creature, scaled down so they
// read as a breadcrumb trail between nodes rather than as more
// anchor points. Walk-type links from any in-zone node are
// drawn; the per-waypoint same-map filter keeps the trail from
// running into other continents. Sparse zones (e.g. Teldrassil)
// would draw nothing if we required dst-in-zone too, since their
// only links go to nodes in neighbouring zones.
constexpr uint32 MAX_PATH_MARKERS = 500;
uint32 pathPlaced = 0;
uint32 linksDrawn = 0;
bool capped = false;
for (TravelNode* node : nodes)
{
if (!node)
continue;
auto* links = node->getLinks();
if (!links)
continue;
for (auto const& kv : *links)
{
TravelNode* dst = kv.first;
TravelNodePath* path = kv.second;
if (!dst || !path)
continue;
if (path->getPathType() != TravelNodePathType::walk)
continue;
++linksDrawn;
for (WorldPosition const& wpPos : path->GetPath())
{
if (wpPos.GetMapId() != player->GetMapId())
continue;
if (pathPlaced >= MAX_PATH_MARKERS)
{
capped = true;
break;
}
Creature* mk = player->SummonCreature(SHOWPATH_PATH_CREATURE,
wpPos.GetPositionX(),
wpPos.GetPositionY(), wpPos.GetPositionZ(),
0, TEMPSUMMON_TIMED_DESPAWN,
SHOWPATH_DESPAWN_MS);
if (mk)
{
mk->SetOwnerGUID(player->GetGUID());
if (SHOWPATH_PATH_DISPLAY_ID)
mk->SetDisplayId(SHOWPATH_PATH_DISPLAY_ID);
++pathPlaced;
}
}
if (capped)
break;
}
if (capped)
break;
}
handler->PSendSysMessage("Showing {} nodes + {} path waypoints across {} walk links in zone {}{} (60s)",
nodesPlaced, pathPlaced, linksDrawn, zoneId,
capped ? " — capped at 500 path markers" : "");
return true;
}
static bool HandleSetSecurityKeyCommand(ChatHandler* handler, char const* args)
{
if (!args || !*args)

24
todo.md Normal file
View File

@ -0,0 +1,24 @@
Aligned:
Bot 50° cap + water 10× bias at engine filter level (covers every path query)
PathFinder reuse + Clear() per step
Underwater path-extension + dispatch fixup
GetFullPath mmap-probe-first
BG gating
ClipPath (LOS + level+5)
Inactive-bot teleport (with self-bot carve-out — intentional)
masterWalking
Pre-dispatch state cleanup
setPath before mutations
needsLongPath + cross-map gate
StopMoving short-stop
10% reuse + regression guard
WaitForReach formula
Stateless re-resolve
walkDistance config
Differ (blocked on infrastructure):
Hazard avoidance (GeneratePathAvoidingHazards) — needs hazard system
Out of scope:
Flying / transports / vehicles