mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
Compare commits
151 Commits
5baba708e0
...
eec2923076
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eec2923076 | ||
|
|
61067a302e | ||
|
|
5ffd2ad89c | ||
|
|
e92af1cc06 | ||
|
|
b97da5c741 | ||
|
|
cbd5f8748c | ||
|
|
1d85510a9e | ||
|
|
c82cd18677 | ||
|
|
566f4975e6 | ||
|
|
dae09388ad | ||
|
|
d00ad8d327 | ||
|
|
32b687f00a | ||
|
|
7f7cfb33d8 | ||
|
|
bd7422e98b | ||
|
|
97b3e345a8 | ||
|
|
0eff76f3ec | ||
|
|
2c822affd2 | ||
|
|
7cb9dc622c | ||
|
|
fa070f5e07 | ||
|
|
783210c4d0 | ||
|
|
d8c4425409 | ||
|
|
ae0baa3fcc | ||
|
|
cd65fda93b | ||
|
|
3813909341 | ||
|
|
a2d0b4530b | ||
|
|
1cf604dc60 | ||
|
|
c6e0cc9cef | ||
|
|
fff692e3f3 | ||
|
|
f6c41f57e4 | ||
|
|
6aea0c2ba7 | ||
|
|
e165a1e79b | ||
|
|
42768fe360 | ||
|
|
eb97533387 | ||
|
|
ec52e5c310 | ||
|
|
7772dc4c0d | ||
|
|
1f9fa42082 | ||
|
|
899f2cba94 | ||
|
|
77db342969 | ||
|
|
694ba0c64c | ||
|
|
14ac3a39b0 | ||
|
|
4fab2e4fe6 | ||
|
|
e2bcf9683b | ||
|
|
14c7de977a | ||
|
|
0682817b42 | ||
|
|
69dd655b96 | ||
|
|
0cbee1621d | ||
|
|
8efe3a4321 | ||
|
|
1c9fd126ba | ||
|
|
1c12d8ff3e | ||
|
|
ea69b56829 | ||
|
|
35d00b499e | ||
|
|
8844a775f4 | ||
|
|
77feb8ea56 | ||
|
|
2110529b6b | ||
|
|
bdc11b07b3 | ||
|
|
f8f3de001b | ||
|
|
a24e1b033c | ||
|
|
8a3a91070b | ||
|
|
3bbe51c232 | ||
|
|
990e2f2016 | ||
|
|
2b50205e2a | ||
|
|
82e7958d2c | ||
|
|
cf0bdf13fc | ||
|
|
3952ebff6e | ||
|
|
7ab57c184e | ||
|
|
db87416f04 | ||
|
|
8b87ab091f | ||
|
|
05cf5a7702 | ||
|
|
35a30cdbef | ||
|
|
c55f554bb4 | ||
|
|
d26ac742bb | ||
|
|
c1285bb0ae | ||
|
|
77c5c6d8cd | ||
|
|
0c9131692c | ||
|
|
1601d6a514 | ||
|
|
dd05767dcc | ||
|
|
7278a3bfcb | ||
|
|
ecbf3fdec2 | ||
|
|
01ea88624a | ||
|
|
02844dffd4 | ||
|
|
2597880d38 | ||
|
|
f4d308b684 | ||
|
|
a0e21d9f38 | ||
|
|
ed9e7227fb | ||
|
|
72d9ecabb9 | ||
|
|
d9a8ac3a2a | ||
|
|
8cb54416bf | ||
|
|
558e9ee1e1 | ||
|
|
563a415532 | ||
|
|
126294cc38 | ||
|
|
3b106260ac | ||
|
|
b3a8d9f4be | ||
|
|
3384fa4fcf | ||
|
|
0d3d38b007 | ||
|
|
4e8e3e2afe | ||
|
|
8c027e3a70 | ||
|
|
896ad3bf75 | ||
|
|
bdefd38830 | ||
|
|
5f61fe9ddf | ||
|
|
82ebaa9594 | ||
|
|
51cea4d76c | ||
|
|
aae47b06c7 | ||
|
|
d72d3ded6c | ||
|
|
974faf0cb0 | ||
|
|
e052ec3b17 | ||
|
|
4a991c194d | ||
|
|
ed31f8f8a7 | ||
|
|
479794b66b | ||
|
|
337fbca8c0 | ||
|
|
fe12f1a708 | ||
|
|
8916cf83c0 | ||
|
|
77caf85fd1 | ||
|
|
5e5d41f878 | ||
|
|
63c5d674d6 | ||
|
|
43ee732003 | ||
|
|
f42f37399f | ||
|
|
c7929482c4 | ||
|
|
e1489f213e | ||
|
|
007189fd5c | ||
|
|
eabefb1d33 | ||
|
|
2208c80caa | ||
|
|
a472fc2d68 | ||
|
|
bac63e2a8c | ||
|
|
690288b5cc | ||
|
|
57134918cb | ||
|
|
324b50f1be | ||
|
|
7d8d8c6b31 | ||
|
|
34b0432aaa | ||
|
|
edc999c8ac | ||
|
|
85e2a940a1 | ||
|
|
6754a95890 | ||
|
|
d0fac16c85 | ||
|
|
a64c721f35 | ||
|
|
27503a9c37 | ||
|
|
1a7e6db0c9 | ||
|
|
6ae973bb8e | ||
|
|
101da6ecd3 | ||
|
|
605e7586c5 | ||
|
|
129cb252cf | ||
|
|
088537277c | ||
|
|
6944da8d69 | ||
|
|
980c1b8cd8 | ||
|
|
ad14420400 | ||
|
|
1d0aeec7b9 | ||
|
|
7741626631 | ||
|
|
806013a4c9 | ||
|
|
3269d1a4b3 | ||
|
|
1ae72b0888 | ||
|
|
0a9bf70305 | ||
|
|
e18fdd02cd | ||
|
|
a23158ef52 |
88
.claude/settings.json
Normal file
88
.claude/settings.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
10
README.md
10
README.md
@ -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.
|
> **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
|
### Detailed Guides
|
||||||
|
|
||||||
| Guide | Description |
|
| Guide | Description |
|
||||||
|
|||||||
@ -342,10 +342,6 @@ AiPlayerbot.MaxWaitForMove = 5000
|
|||||||
# 2 - MoveSplinePath disabled everywhere
|
# 2 - MoveSplinePath disabled everywhere
|
||||||
AiPlayerbot.DisableMoveSplinePath = 0
|
AiPlayerbot.DisableMoveSplinePath = 0
|
||||||
|
|
||||||
# Max search time for movement (higher for better movement on slopes)
|
|
||||||
# Default: 3
|
|
||||||
AiPlayerbot.MaxMovementSearchTime = 3
|
|
||||||
|
|
||||||
# Action expiration time
|
# Action expiration time
|
||||||
AiPlayerbot.ExpireActionTime = 5000
|
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)
|
# Default: 1 (enabled)
|
||||||
AiPlayerbot.EnableNewRpgStrategy = 1
|
AiPlayerbot.EnableNewRpgStrategy = 1
|
||||||
|
|
||||||
|
# Use pre-computed travel node paths for long-distance movement (>300 yards).
|
||||||
|
# When enabled, bots use the travel node graph (A*, flight paths, transports)
|
||||||
|
# instead of repeated mmap hops. Experimental.
|
||||||
|
# Default: 0 (disabled)
|
||||||
|
AiPlayerbot.EnableTravelNodes = 0
|
||||||
|
|
||||||
|
# Transport ride mode (travel node graph only):
|
||||||
|
# 0 = bot walks to dock, teleport-snaps onto transport, rides, teleport-snaps off
|
||||||
|
# 1 = bot teleports directly across the transport route, skipping the ride
|
||||||
|
# Default 0 is the visible behavior. Set to 1 for invisible/random-bot mass
|
||||||
|
# travel performance — the bot never actually boards anything.
|
||||||
|
# (AC has no transport-surface mmap, so an on-deck walking mode can't be
|
||||||
|
# faithfully implemented; the on-board phase always teleport-snaps.)
|
||||||
|
# Default: 0
|
||||||
|
AiPlayerbot.TransportSkipRide = 0
|
||||||
|
|
||||||
# Control probability weights for RPG status of bots. Takes effect only when the status meets its premise.
|
# Control probability weights for RPG status of bots. Takes effect only when the status meets its premise.
|
||||||
# Sum of weights need not be 100. Set to 0 to disable the status.
|
# Sum of weights need not be 100. Set to 0 to disable the status.
|
||||||
#
|
#
|
||||||
|
|||||||
@ -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
143899
data/sql/playerbots/updates/2026_05_09_03_travelnode_path_cmangos_import.sql
Normal file
143899
data/sql/playerbots/updates/2026_05_09_03_travelnode_path_cmangos_import.sql
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
351
extract_ac_client_data.sh
Normal 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."
|
||||||
@ -173,7 +173,7 @@ std::vector<uint32> const vFlagsIC = {GO_HORDE_BANNER,
|
|||||||
GO_HORDE_BANNER_GRAVEYARD_H,
|
GO_HORDE_BANNER_GRAVEYARD_H,
|
||||||
GO_HORDE_BANNER_GRAVEYARD_H_CONT};
|
GO_HORDE_BANNER_GRAVEYARD_H_CONT};
|
||||||
|
|
||||||
// BG Waypoints (vmangos)
|
// BG Waypoints
|
||||||
|
|
||||||
// Horde Flag Room to Horde Graveyard
|
// Horde Flag Room to Horde Graveyard
|
||||||
BattleBotPath vPath_WSG_HordeFlagRoom_to_HordeGraveyard = {
|
BattleBotPath vPath_WSG_HordeFlagRoom_to_HordeGraveyard = {
|
||||||
|
|||||||
@ -6,10 +6,10 @@
|
|||||||
#include "CheckValuesAction.h"
|
#include "CheckValuesAction.h"
|
||||||
|
|
||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
|
#include "ObjectGuid.h"
|
||||||
#include "ServerFacade.h"
|
#include "ServerFacade.h"
|
||||||
|
|
||||||
#include "PlayerbotAI.h"
|
#include "PlayerbotAI.h"
|
||||||
#include "TravelNode.h"
|
|
||||||
#include "AiObjectContext.h"
|
#include "AiObjectContext.h"
|
||||||
|
|
||||||
CheckValuesAction::CheckValuesAction(PlayerbotAI* botAI) : Action(botAI, "check values") {}
|
CheckValuesAction::CheckValuesAction(PlayerbotAI* botAI) : Action(botAI, "check values") {}
|
||||||
@ -21,11 +21,6 @@ bool CheckValuesAction::Execute(Event /*event*/)
|
|||||||
botAI->Ping(bot->GetPositionX(), bot->GetPositionY());
|
botAI->Ping(bot->GetPositionX(), bot->GetPositionY());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (botAI->HasStrategy("map", BOT_STATE_NON_COMBAT) || botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT))
|
|
||||||
{
|
|
||||||
TravelNodeMap::instance().manageNodes(bot, botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT));
|
|
||||||
}
|
|
||||||
|
|
||||||
GuidVector possible_targets = *context->GetValue<GuidVector>("possible targets");
|
GuidVector possible_targets = *context->GetValue<GuidVector>("possible targets");
|
||||||
GuidVector all_targets = *context->GetValue<GuidVector>("all targets");
|
GuidVector all_targets = *context->GetValue<GuidVector>("all targets");
|
||||||
GuidVector npcs = *context->GetValue<GuidVector>("nearest npcs");
|
GuidVector npcs = *context->GetValue<GuidVector>("nearest npcs");
|
||||||
|
|||||||
@ -76,7 +76,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
std::vector<WorldPosition> beginPath, endPath;
|
std::vector<WorldPosition> beginPath, endPath;
|
||||||
TravelNodeRoute route = TravelNodeMap::instance().getRoute(botPos, *points.front(), beginPath, bot);
|
TravelNodeRoute route = TravelNodeMap::instance().FindRouteNearestNodes(botPos, *points.front(), beginPath, bot);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Traveling to " << dest->getTitle() << ": ";
|
out << "Traveling to " << dest->getTitle() << ": ";
|
||||||
@ -196,18 +196,18 @@ bool DebugAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
WorldPosition pos(bot);
|
WorldPosition pos(bot);
|
||||||
|
|
||||||
std::string const name = "USER:" + text.substr(9);
|
std::string suffix = text.size() > 9 ? text.substr(9) : pos.getAreaName();
|
||||||
|
std::string const name = "USER:" + suffix;
|
||||||
|
|
||||||
/* TravelNode* startNode = */ TravelNodeMap::instance().addNode(pos, name, false, false); // startNode not used, but addNode as side effect, fragment marked for removal.
|
|
||||||
|
|
||||||
for (auto& endNode : TravelNodeMap::instance().getNodes(pos, 2000))
|
|
||||||
{
|
{
|
||||||
endNode->setLinked(false);
|
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.");
|
botAI->TellMasterNoFacing("Node " + name + " created. Use console command '.playerbots travel generatenode' to connect nodes.");
|
||||||
|
|
||||||
TravelNodeMap::instance().setHasToGen();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -223,14 +223,15 @@ bool DebugAction::Execute(Event event)
|
|||||||
if (startNode->isImportant())
|
if (startNode->isImportant())
|
||||||
{
|
{
|
||||||
botAI->TellMasterNoFacing("Node can not be removed.");
|
botAI->TellMasterNoFacing("Node can not be removed.");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelNodeMap::instance().m_nMapMtx.lock();
|
{
|
||||||
TravelNodeMap::instance().removeNode(startNode);
|
std::lock_guard<std::shared_timed_mutex> lock(TravelNodeMap::instance().m_nMapMtx);
|
||||||
botAI->TellMasterNoFacing("Node removed.");
|
TravelNodeMap::instance().removeNode(startNode);
|
||||||
TravelNodeMap::instance().m_nMapMtx.unlock();
|
}
|
||||||
|
|
||||||
TravelNodeMap::instance().setHasToGen();
|
botAI->TellMasterNoFacing("Node removed. Use console command '.playerbots travel generatenode' to finalize nodes.");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -247,15 +248,17 @@ bool DebugAction::Execute(Event event)
|
|||||||
node->removeLinkTo(path.first, true);
|
node->removeLinkTo(path.first, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (text.find("gen node") != std::string::npos)
|
else if (text.find("gen node") != std::string::npos ||
|
||||||
|
text.find("gen path") != std::string::npos)
|
||||||
{
|
{
|
||||||
// Pathfinder
|
// Disabled: generateAll() touches Map / grid / mmap state that is only
|
||||||
TravelNodeMap::instance().generateNodes();
|
// safe to mutate on the world thread. Running it from a detached worker
|
||||||
return true;
|
// (or from a bot tick on a MapUpdater thread) races with world updates
|
||||||
}
|
// and freezes the server. Use the console command instead, which runs
|
||||||
else if (text.find("gen path") != std::string::npos)
|
// synchronously on the world thread:
|
||||||
{
|
// .playerbots travel generatenode
|
||||||
TravelNodeMap::instance().generatePaths();
|
botAI->TellMasterNoFacing(
|
||||||
|
"Disabled in chat. Run '.playerbots travel generatenode' from the server console.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (text.find("crop path") != std::string::npos)
|
else if (text.find("crop path") != std::string::npos)
|
||||||
@ -275,7 +278,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
[]
|
[]
|
||||||
{
|
{
|
||||||
TravelNodeMap::instance().removeNodes();
|
TravelNodeMap::instance().removeNodes();
|
||||||
TravelNodeMap::instance().loadNodeStore();
|
TravelNodeMap::instance().LoadNodeStore();
|
||||||
});
|
});
|
||||||
|
|
||||||
t.detach();
|
t.detach();
|
||||||
@ -297,7 +300,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
|
|
||||||
// uint32 time = 60 * IN_MILLISECONDS; //not used, line marked for removal.
|
// uint32 time = 60 * IN_MILLISECONDS; //not used, line marked for removal.
|
||||||
|
|
||||||
std::vector<WorldPosition> ppath = l.second->getPath();
|
std::vector<WorldPosition> ppath = l.second->GetPath();
|
||||||
|
|
||||||
for (auto p : ppath)
|
for (auto p : ppath)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -29,6 +29,10 @@ void DestroyItemAction::DestroyItem(FindItemVisitor* visitor)
|
|||||||
std::vector<Item*> items = visitor->GetResult();
|
std::vector<Item*> items = visitor->GetResult();
|
||||||
for (Item* item : items)
|
for (Item* item : items)
|
||||||
{
|
{
|
||||||
|
// backstop: never drop an active quest item
|
||||||
|
if (bot->HasQuestForItem(item->GetTemplate()->ItemId))
|
||||||
|
continue;
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << chat->FormatItem(item->GetTemplate()) << " destroyed";
|
out << chat->FormatItem(item->GetTemplate()) << " destroyed";
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
@ -67,18 +71,11 @@ bool SmartDestroyItemAction::Execute(Event /*event*/)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ITEM_USAGE_QUEST is excluded — those are still-needed quest items
|
||||||
std::vector<uint32> bestToDestroy = {ITEM_USAGE_NONE}; // First destroy anything useless.
|
std::vector<uint32> bestToDestroy = {ITEM_USAGE_NONE}; // First destroy anything useless.
|
||||||
|
|
||||||
if (!AI_VALUE(bool, "can sell") &&
|
bestToDestroy.push_back(ITEM_USAGE_VENDOR);
|
||||||
AI_VALUE(
|
bestToDestroy.push_back(ITEM_USAGE_AH);
|
||||||
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
|
// If we still need room
|
||||||
bestToDestroy.push_back(
|
bestToDestroy.push_back(
|
||||||
|
|||||||
@ -19,83 +19,8 @@
|
|||||||
#include "Transport.h"
|
#include "Transport.h"
|
||||||
#include "Map.h"
|
#include "Map.h"
|
||||||
|
|
||||||
namespace
|
// Transport helpers (GetTransportForPosTolerant, FindBoardingPointOnTransport,
|
||||||
{
|
// BoardTransport) are now on MovementAction — inherited by FollowAction.
|
||||||
Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z)
|
|
||||||
{
|
|
||||||
if (!map || !ref)
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
std::array<float, 4> const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f };
|
|
||||||
for (float const pz : probes)
|
|
||||||
{
|
|
||||||
if (Transport* t = map->GetTransportForPos(phaseMask, x, y, pz, ref))
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempts to find a point on the leader's transport that is closer to the bot,
|
|
||||||
// by probing along the segment from master -> bot and returning the last point
|
|
||||||
// that is still detected as being on the expected transport.
|
|
||||||
bool FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref,
|
|
||||||
float masterX, float masterY, float masterZ,
|
|
||||||
float botX, float botY, float botZ,
|
|
||||||
float& outX, float& outY, float& outZ)
|
|
||||||
{
|
|
||||||
if (!map || !expectedTransport || !ref)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
uint32 const phaseMask = ref->GetPhaseMask();
|
|
||||||
|
|
||||||
// Ensure master is actually detected on that transport (tolerant).
|
|
||||||
if (GetTransportForPosTolerant(map, ref, phaseMask, masterX, masterY, masterZ) != expectedTransport)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// The raycast in GetTransportForPos starts at (z + 2). Probe with a safe Z.
|
|
||||||
float const probeZ = std::max(masterZ, botZ);
|
|
||||||
|
|
||||||
// Adaptive step count: small platforms need tighter sampling.
|
|
||||||
float const dx2 = botX - masterX;
|
|
||||||
float const dy2 = botY - masterY;
|
|
||||||
float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2);
|
|
||||||
int32 const steps = std::clamp(static_cast<int32>(dist2d / 0.75f), 10, 28);
|
|
||||||
|
|
||||||
float const dx = (botX - masterX) / static_cast<float>(steps);
|
|
||||||
float const dy = (botY - masterY) / static_cast<float>(steps);
|
|
||||||
|
|
||||||
// Master must actually be on the expected transport for this to work.
|
|
||||||
if (map->GetTransportForPos(ref->GetPhaseMask(), masterX, masterY, probeZ, ref) != expectedTransport)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
float lastX = masterX;
|
|
||||||
float lastY = masterY;
|
|
||||||
bool found = false;
|
|
||||||
|
|
||||||
for (int32 i = 1; i <= steps; ++i)
|
|
||||||
{
|
|
||||||
float const px = masterX + dx * i;
|
|
||||||
float const py = masterY + dy * i;
|
|
||||||
|
|
||||||
Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ);
|
|
||||||
if (t != expectedTransport)
|
|
||||||
break;
|
|
||||||
|
|
||||||
lastX = px;
|
|
||||||
lastY = py;
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
outX = lastX;
|
|
||||||
outY = lastY;
|
|
||||||
outZ = masterZ; // keep deck-level Z to encourage stepping onto the platform/boat
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FollowAction::Execute(Event /*event*/)
|
bool FollowAction::Execute(Event /*event*/)
|
||||||
{
|
{
|
||||||
@ -170,9 +95,8 @@ bool FollowAction::Execute(Event /*event*/)
|
|||||||
|
|
||||||
bool const movingAllowed = IsMovingAllowed();
|
bool const movingAllowed = IsMovingAllowed();
|
||||||
bool const dupMove = IsDuplicateMove(destX, destY, destZ);
|
bool const dupMove = IsDuplicateMove(destX, destY, destZ);
|
||||||
bool const waiting = IsWaitingForLastMove(priority);
|
|
||||||
|
|
||||||
if (movingAllowed && !dupMove && !waiting)
|
if (movingAllowed && !dupMove)
|
||||||
{
|
{
|
||||||
if (bot->IsSitState())
|
if (bot->IsSitState())
|
||||||
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
||||||
|
|||||||
@ -124,7 +124,6 @@ bool GoAction::Execute(Event event)
|
|||||||
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
|
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
|
||||||
{
|
{
|
||||||
PathGenerator path(bot);
|
PathGenerator path(bot);
|
||||||
|
|
||||||
path.CalculatePath(x, y, z, false);
|
path.CalculatePath(x, y, z, false);
|
||||||
|
|
||||||
Movement::Vector3 end = path.GetEndPosition();
|
Movement::Vector3 end = path.GetEndPosition();
|
||||||
|
|||||||
@ -5,6 +5,9 @@
|
|||||||
|
|
||||||
#include "LootAction.h"
|
#include "LootAction.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include "Bag.h"
|
||||||
#include "ChatHelper.h"
|
#include "ChatHelper.h"
|
||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
#include "GuildMgr.h"
|
#include "GuildMgr.h"
|
||||||
@ -76,7 +79,11 @@ bool OpenLootAction::Execute(Event /*event*/)
|
|||||||
bool result = DoLoot(lootObject);
|
bool result = DoLoot(lootObject);
|
||||||
if (result)
|
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());
|
context->GetValue<LootObject>("loot target")->Set(LootObject());
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@ -139,8 +146,9 @@ bool OpenLootAction::DoLoot(LootObject& lootObject)
|
|||||||
if (go && (go->GetGoState() != GO_STATE_READY))
|
if (go && (go->GetGoState() != GO_STATE_READY))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// This prevents dungeon chests like Tribunal Chest (Halls of Stone) from being ninja'd by the bots
|
// Block event-gated chests (Tribunal Chest, Gunship Armory) but allow
|
||||||
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND))
|
// 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;
|
return false;
|
||||||
|
|
||||||
// This prevents raid chests like Gunship Armory (ICC) from being ninja'd by the bots
|
// 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);
|
// 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)
|
for (uint8 i = 0; i < items; ++i)
|
||||||
{
|
{
|
||||||
uint32 itemid;
|
uint32 itemid;
|
||||||
@ -402,7 +416,9 @@ bool StoreLootAction::Execute(Event event)
|
|||||||
if (!proto)
|
if (!proto)
|
||||||
continue;
|
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();
|
uint32 maxStack = proto->GetMaxStackSize();
|
||||||
if (maxStack == 1)
|
if (maxStack == 1)
|
||||||
@ -438,6 +454,55 @@ bool StoreLootAction::Execute(Event event)
|
|||||||
GuildTaskMgr::instance().CheckItemTask(itemid, itemcount, ref->GetSource(), bot);
|
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);
|
WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1);
|
||||||
*packet << itemindex;
|
*packet << itemindex;
|
||||||
bot->GetSession()->QueuePacket(packet);
|
bot->GetSession()->QueuePacket(packet);
|
||||||
@ -453,7 +518,7 @@ bool StoreLootAction::Execute(Event event)
|
|||||||
BroadcastHelper::BroadcastLootingItem(botAI, bot, proto);
|
BroadcastHelper::BroadcastLootingItem(botAI, bot, proto);
|
||||||
}
|
}
|
||||||
|
|
||||||
AI_VALUE(LootObjectStack*, "available loot")->Remove(guid);
|
AI_VALUE(LootObjectStack*, "available loot")->MarkCompleted(guid);
|
||||||
|
|
||||||
// release loot
|
// release loot
|
||||||
WorldPacket* packet = new WorldPacket(CMSG_LOOT_RELEASE, 8);
|
WorldPacket* packet = new WorldPacket(CMSG_LOOT_RELEASE, 8);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "Action.h"
|
#include "Action.h"
|
||||||
#include "LastMovementValue.h"
|
#include "LastMovementValue.h"
|
||||||
|
#include "PathGenerator.h"
|
||||||
#include "PlayerbotAIConfig.h"
|
#include "PlayerbotAIConfig.h"
|
||||||
|
|
||||||
class Player;
|
class Player;
|
||||||
@ -18,16 +19,36 @@ class Unit;
|
|||||||
class WorldObject;
|
class WorldObject;
|
||||||
class Position;
|
class Position;
|
||||||
|
|
||||||
#define ANGLE_45_DEG (static_cast<float>(M_PI) / 4.f)
|
|
||||||
#define ANGLE_90_DEG M_PI_2
|
#define ANGLE_90_DEG M_PI_2
|
||||||
#define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f)
|
#define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f)
|
||||||
|
|
||||||
|
// Default acceptable path types for GeneratePath
|
||||||
|
constexpr uint32 DEFAULT_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE;
|
||||||
|
constexpr uint32 RELAXED_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
|
||||||
|
|
||||||
|
struct PathResult
|
||||||
|
{
|
||||||
|
Movement::PointsArray points;
|
||||||
|
G3D::Vector3 actualEnd;
|
||||||
|
G3D::Vector3 end;
|
||||||
|
PathType pathType;
|
||||||
|
bool reachable;
|
||||||
|
};
|
||||||
|
|
||||||
class MovementAction : public Action
|
class MovementAction : public Action
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
MovementAction(PlayerbotAI* botAI, std::string const name);
|
MovementAction(PlayerbotAI* botAI, std::string const name);
|
||||||
|
|
||||||
protected:
|
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 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,
|
bool MoveNear(uint32 mapId, float x, float y, float z, float distance = sPlayerbotAIConfig.contactDistance,
|
||||||
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
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 MoveTo(uint32 mapId, float x, float y, float z, bool idle = false, bool react = false,
|
||||||
bool normal_only = false, bool exact_waypoint = false,
|
bool normal_only = false, bool exact_waypoint = false,
|
||||||
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, bool lessDelay = false,
|
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, bool lessDelay = false,
|
||||||
bool backwards = false);
|
bool backwards = false, bool ignoreEnemyTargets = false);
|
||||||
|
|
||||||
|
// Path-aware funnel mirroring the reference movement implementation.
|
||||||
|
// Runs UpdateMovementState + IsMovingAllowed + WaitForTransport gates,
|
||||||
|
// applies the targetPosRecalcDistance short-stop, resolves a TravelPath
|
||||||
|
// via ResolveMovePath (which gates graph A* by sightDistance), trims
|
||||||
|
// with makeShortCut, handles special head segments
|
||||||
|
// (portal/area-trigger/transport/flight) via HandleSpecialMovement,
|
||||||
|
// clips at hostile creatures via ClipPath (unless ignoreEnemyTargets),
|
||||||
|
// and dispatches the resulting walk via DispatchMovement.
|
||||||
|
// MoveTo(mapId,...) delegates here unless an intentional bypass
|
||||||
|
// (exact_waypoint / disableMoveSplinePath / flying / swimming /
|
||||||
|
// backwards) routes the move straight to DoMovePoint.
|
||||||
|
// `react=true` opts the move out of the end-of-dispatch
|
||||||
|
// WaitForReach AI-loop block — combat callers should set this so the
|
||||||
|
// bot can keep re-evaluating mid-chase. Default false matches the
|
||||||
|
// reference's MoveTo2 default.
|
||||||
|
bool MoveTo2(WorldPosition endPos,
|
||||||
|
bool idle = false, bool react = false,
|
||||||
|
bool noPath = false, bool ignoreEnemyTargets = false,
|
||||||
|
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL,
|
||||||
|
bool lessDelay = false);
|
||||||
|
|
||||||
|
// Centralized walk dispatch. Mirrors the reference's DispatchMovement
|
||||||
|
// shape: takes a TravelPath, builds the PointsArray internally,
|
||||||
|
// applies inactive-bot teleport carve-out, masterWalking mode,
|
||||||
|
// pre-dispatch state cleanup (clear emote, stand, interrupt cast),
|
||||||
|
// transport-passenger coordinate sandwich
|
||||||
|
// (CalculatePassengerPosition → UpdateAllowedPositionZ → Offset)
|
||||||
|
// around the per-point Z snap, mm.Clear → MovePoint(last) →
|
||||||
|
// MoveSplinePath. Caches the destination + duration on lastMove.
|
||||||
|
//
|
||||||
|
// Divergence from reference: reference ends with WaitForReach(size)
|
||||||
|
// which blocks the AI loop until the move completes. AC's combat
|
||||||
|
// callers (ReachCombatTo) currently funnel through MoveTo → MoveTo2
|
||||||
|
// → DispatchMovement; blocking the AI loop here would suspend combat
|
||||||
|
// re-evaluation for the full move duration. Until combat dispatch is
|
||||||
|
// restructured to bypass MoveTo2, the WaitForReach is deliberately
|
||||||
|
// omitted.
|
||||||
|
// `react=true` skips the end-of-dispatch WaitForReach so the AI
|
||||||
|
// loop isn't blocked while the spline plays — combat callers use
|
||||||
|
// this to keep re-evaluating mid-chase.
|
||||||
|
bool DispatchMovement(TravelPath path,
|
||||||
|
WorldPosition dest,
|
||||||
|
char const* label,
|
||||||
|
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL,
|
||||||
|
bool lessDelay = false,
|
||||||
|
bool react = false);
|
||||||
bool MoveTo(WorldObject* target, float distance = 0.0f,
|
bool MoveTo(WorldObject* target, float distance = 0.0f,
|
||||||
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
||||||
bool MoveNear(WorldObject* target, float distance = sPlayerbotAIConfig.contactDistance,
|
bool MoveNear(WorldObject* target, float distance = sPlayerbotAIConfig.contactDistance,
|
||||||
@ -43,14 +111,25 @@ protected:
|
|||||||
float GetFollowAngle();
|
float GetFollowAngle();
|
||||||
bool Follow(Unit* target, float distance = sPlayerbotAIConfig.followDistance);
|
bool Follow(Unit* target, float distance = sPlayerbotAIConfig.followDistance);
|
||||||
bool Follow(Unit* target, float distance, float angle);
|
bool Follow(Unit* target, float distance, float angle);
|
||||||
|
// Handles the cross-transport follow case: when bot and target are
|
||||||
|
// on different transports (or one is off-transport) and within
|
||||||
|
// sight, this disembarks the bot from its current transport (if
|
||||||
|
// any), teleports it to the target's position, and boards the
|
||||||
|
// target's transport (if any). Returns true if the transport
|
||||||
|
// transition was performed this tick (caller should skip the
|
||||||
|
// engine-level follow for this tick).
|
||||||
|
bool FollowOnTransport(Unit* target);
|
||||||
bool ChaseTo(WorldObject* obj, float distance = 0.0f);
|
bool ChaseTo(WorldObject* obj, float distance = 0.0f);
|
||||||
bool ReachCombatTo(Unit* target, float distance = 0.0f);
|
bool ReachCombatTo(Unit* target, float distance = 0.0f);
|
||||||
float MoveDelay(float distance, bool backwards = false);
|
float MoveDelay(float distance, bool backwards = false);
|
||||||
void WaitForReach(float distance);
|
void WaitForReach(float distance);
|
||||||
|
// PointsArray overload: sums segment distances and calls the float
|
||||||
|
// version. Matches the reference's WaitForReach(PointsArray) used at
|
||||||
|
// the end of DispatchMovement.
|
||||||
|
void WaitForReach(Movement::PointsArray const& path);
|
||||||
void SetNextMovementDelay(float delayMillis);
|
void SetNextMovementDelay(float delayMillis);
|
||||||
bool IsMovingAllowed(WorldObject* target);
|
bool IsMovingAllowed(WorldObject* target);
|
||||||
bool IsDuplicateMove(float x, float y, float z);
|
bool IsDuplicateMove(float x, float y, float z);
|
||||||
bool IsWaitingForLastMove(MovementPriority priority);
|
|
||||||
bool IsMovingAllowed();
|
bool IsMovingAllowed();
|
||||||
bool Flee(Unit* target);
|
bool Flee(Unit* target);
|
||||||
void ClearIdleState();
|
void ClearIdleState();
|
||||||
@ -66,6 +145,42 @@ protected:
|
|||||||
bool FleePosition(Position pos, float radius, uint32 minInterval = 1000);
|
bool FleePosition(Position pos, float radius, uint32 minInterval = 1000);
|
||||||
bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList);
|
bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList);
|
||||||
|
|
||||||
|
PathResult GeneratePath(float x, float y, float z, uint32 acceptMask = DEFAULT_PATH_ACCEPT_MASK, bool forceDestination = 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:
|
protected:
|
||||||
struct CheckAngle
|
struct CheckAngle
|
||||||
{
|
{
|
||||||
@ -74,10 +189,6 @@ protected:
|
|||||||
};
|
};
|
||||||
|
|
||||||
private:
|
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;
|
bool wasMovementRestricted = false;
|
||||||
void DoMovePoint(Unit* unit, float x, float y, float z, bool generatePath, bool backwards);
|
void DoMovePoint(Unit* unit, float x, float y, float z, bool generatePath, bool backwards);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,21 +11,20 @@
|
|||||||
|
|
||||||
bool LootAvailableTrigger::IsActive()
|
bool LootAvailableTrigger::IsActive()
|
||||||
{
|
{
|
||||||
bool distanceCheck = false;
|
// Strategy is non-combat-only — the engine state separation is the
|
||||||
if (botAI->HasStrategy("stay", BOT_STATE_NON_COMBAT))
|
// safety net. Don't gate on hostiles-in-sight: that locked out
|
||||||
{
|
// looting in zones with continuous respawns (e.g. cave farms).
|
||||||
distanceCheck =
|
// If a new enemy aggros mid-loot the combat engine takes over, loot
|
||||||
ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"), CONTACT_DISTANCE);
|
// resumes on the next non-combat window.
|
||||||
}
|
if (!AI_VALUE(bool, "has available loot"))
|
||||||
else
|
return false;
|
||||||
{
|
|
||||||
distanceCheck = ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"),
|
|
||||||
INTERACTION_DISTANCE - 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if loot target if empty, always pass distance check
|
// "stay" strategy is restrictive: only loot if corpse is at our feet.
|
||||||
return AI_VALUE(bool, "has available loot") &&
|
if (botAI->HasStrategy("stay", BOT_STATE_NON_COMBAT))
|
||||||
(distanceCheck || AI_VALUE(GuidVector, "all targets").empty());
|
return ServerFacade::instance().IsDistanceLessOrEqualThan(
|
||||||
|
AI_VALUE2(float, "distance", "loot target"), CONTACT_DISTANCE);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FarFromCurrentLootTrigger::IsActive()
|
bool FarFromCurrentLootTrigger::IsActive()
|
||||||
|
|||||||
@ -12,64 +12,48 @@ LastMovement::LastMovement() { clear(); }
|
|||||||
LastMovement::LastMovement(LastMovement& other)
|
LastMovement::LastMovement(LastMovement& other)
|
||||||
: taxiNodes(other.taxiNodes),
|
: taxiNodes(other.taxiNodes),
|
||||||
taxiMaster(other.taxiMaster),
|
taxiMaster(other.taxiMaster),
|
||||||
lastFollow(other.lastFollow),
|
|
||||||
lastAreaTrigger(other.lastAreaTrigger),
|
lastAreaTrigger(other.lastAreaTrigger),
|
||||||
lastMoveToX(other.lastMoveToX),
|
|
||||||
lastMoveToY(other.lastMoveToY),
|
|
||||||
lastMoveToZ(other.lastMoveToZ),
|
|
||||||
lastMoveToOri(other.lastMoveToOri),
|
|
||||||
lastFlee(other.lastFlee)
|
lastFlee(other.lastFlee)
|
||||||
{
|
{
|
||||||
lastMoveShort = other.lastMoveShort;
|
lastMoveShort = other.lastMoveShort;
|
||||||
nextTeleport = other.nextTeleport;
|
nextTeleport = other.nextTeleport;
|
||||||
lastPath = other.lastPath;
|
lastPath = other.lastPath;
|
||||||
priority = other.priority;
|
priority = other.priority;
|
||||||
|
lastTransportEntry = other.lastTransportEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LastMovement::clear()
|
void LastMovement::clear()
|
||||||
{
|
{
|
||||||
lastMoveShort = WorldPosition();
|
lastMoveShort = WorldPosition();
|
||||||
lastPath.clear();
|
lastPath.clear();
|
||||||
lastMoveToMapId = 0;
|
|
||||||
lastMoveToX = 0;
|
|
||||||
lastMoveToY = 0;
|
|
||||||
lastMoveToZ = 0;
|
|
||||||
lastMoveToOri = 0;
|
|
||||||
lastFollow = nullptr;
|
|
||||||
lastAreaTrigger = 0;
|
lastAreaTrigger = 0;
|
||||||
lastFlee = 0;
|
lastFlee = 0;
|
||||||
nextTeleport = 0;
|
nextTeleport = 0;
|
||||||
msTime = 0;
|
msTime = 0;
|
||||||
lastdelayTime = 0;
|
|
||||||
priority = MovementPriority::MOVEMENT_NORMAL;
|
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);
|
Set(0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
setShort(WorldPosition());
|
setShort(WorldPosition());
|
||||||
setPath(TravelPath());
|
setPath(TravelPath());
|
||||||
lastFollow = follow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LastMovement::Set(uint32 mapId, float x, float y, float z, float ori, float delayTime, MovementPriority pri)
|
void LastMovement::Set(uint32 mapId, float x, float y, float z, float ori, float delayTime, MovementPriority pri)
|
||||||
{
|
{
|
||||||
lastMoveToMapId = mapId;
|
|
||||||
lastMoveToX = x;
|
|
||||||
lastMoveToY = y;
|
|
||||||
lastMoveToZ = z;
|
|
||||||
lastMoveToOri = ori;
|
|
||||||
lastFollow = nullptr;
|
|
||||||
lastMoveShort = WorldPosition(mapId, x, y, z, ori);
|
lastMoveShort = WorldPosition(mapId, x, y, z, ori);
|
||||||
msTime = getMSTime();
|
msTime = getMSTime();
|
||||||
lastdelayTime = delayTime;
|
|
||||||
priority = pri;
|
priority = pri;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LastMovement::setShort(WorldPosition point)
|
void LastMovement::setShort(WorldPosition point)
|
||||||
{
|
{
|
||||||
lastMoveShort = point;
|
lastMoveShort = point;
|
||||||
lastFollow = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LastMovement::setPath(TravelPath path) { lastPath = path; }
|
void LastMovement::setPath(TravelPath path) { lastPath = path; }
|
||||||
|
|||||||
@ -33,12 +33,12 @@ public:
|
|||||||
{
|
{
|
||||||
taxiNodes = other.taxiNodes;
|
taxiNodes = other.taxiNodes;
|
||||||
taxiMaster = other.taxiMaster;
|
taxiMaster = other.taxiMaster;
|
||||||
lastFollow = other.lastFollow;
|
|
||||||
lastAreaTrigger = other.lastAreaTrigger;
|
lastAreaTrigger = other.lastAreaTrigger;
|
||||||
lastMoveShort = other.lastMoveShort;
|
lastMoveShort = other.lastMoveShort;
|
||||||
lastPath = other.lastPath;
|
lastPath = other.lastPath;
|
||||||
nextTeleport = other.nextTeleport;
|
nextTeleport = other.nextTeleport;
|
||||||
priority = other.priority;
|
priority = other.priority;
|
||||||
|
lastTransportEntry = other.lastTransportEntry;
|
||||||
return *this;
|
return *this;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,21 +52,17 @@ public:
|
|||||||
|
|
||||||
std::vector<uint32> taxiNodes;
|
std::vector<uint32> taxiNodes;
|
||||||
ObjectGuid taxiMaster;
|
ObjectGuid taxiMaster;
|
||||||
Unit* lastFollow;
|
|
||||||
uint32 lastAreaTrigger;
|
uint32 lastAreaTrigger;
|
||||||
time_t lastFlee;
|
time_t lastFlee;
|
||||||
uint32 lastMoveToMapId;
|
|
||||||
float lastMoveToX;
|
|
||||||
float lastMoveToY;
|
|
||||||
float lastMoveToZ;
|
|
||||||
float lastMoveToOri;
|
|
||||||
float lastdelayTime;
|
|
||||||
WorldPosition lastMoveShort;
|
WorldPosition lastMoveShort;
|
||||||
uint32 msTime;
|
uint32 msTime;
|
||||||
MovementPriority priority;
|
MovementPriority priority;
|
||||||
TravelPath lastPath;
|
TravelPath lastPath;
|
||||||
time_t nextTeleport;
|
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&>
|
class LastMovementValue : public ManualSetValue<LastMovement&>
|
||||||
|
|||||||
@ -47,7 +47,13 @@ public:
|
|||||||
{
|
{
|
||||||
if (allowedGOFlags.empty())
|
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_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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1462,15 +1462,6 @@ bool HodirBitingColdJumpAction::Execute(Event /*event*/)
|
|||||||
// float speed = 7.96f;
|
// float speed = 7.96f;
|
||||||
|
|
||||||
// UpdateMovementState();
|
// UpdateMovementState();
|
||||||
// if (!IsMovingAllowed(mapId, x, y, z))
|
|
||||||
//{
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// MovementPriority priority;
|
|
||||||
// if (IsWaitingForLastMove(priority))
|
|
||||||
//{
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// MotionMaster& mm = *bot->GetMotionMaster();
|
// MotionMaster& mm = *bot->GetMotionMaster();
|
||||||
// mm.Clear();
|
// mm.Clear();
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
#include "PathGenerator.h"
|
#include "PathGenerator.h"
|
||||||
#include "Player.h"
|
#include "Player.h"
|
||||||
#include "PlayerbotAI.h"
|
#include "PlayerbotAI.h"
|
||||||
|
#include "Playerbots.h"
|
||||||
#include "QuestDef.h"
|
#include "QuestDef.h"
|
||||||
#include "Random.h"
|
#include "Random.h"
|
||||||
#include "SharedDefines.h"
|
#include "SharedDefines.h"
|
||||||
@ -120,17 +121,8 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case RPG_TRAVEL_FLIGHT:
|
// RPG_TRAVEL_FLIGHT arrival is handled inside NewRpgTravelFlightAction
|
||||||
{
|
// so the flight action owns both take-off and landing transitions.
|
||||||
auto& data = std::get<NewRpgInfo::TravelFlight>(info.data);
|
|
||||||
if (data.inFlight && !bot->IsInFlight())
|
|
||||||
{
|
|
||||||
// flight arrival
|
|
||||||
info.ChangeToIdle();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RPG_REST:
|
case RPG_REST:
|
||||||
{
|
{
|
||||||
// REST -> IDLE
|
// REST -> IDLE
|
||||||
@ -163,11 +155,22 @@ bool NewRpgGoGrindAction::Execute(Event /*event*/)
|
|||||||
if (auto* data = std::get_if<NewRpgInfo::GoGrind>(&botAI->rpgInfo.data))
|
if (auto* data = std::get_if<NewRpgInfo::GoGrind>(&botAI->rpgInfo.data))
|
||||||
{
|
{
|
||||||
if (MoveFarTo(data->pos))
|
if (MoveFarTo(data->pos))
|
||||||
|
{
|
||||||
|
botAI->rpgInfo.moveRetryCount = 0;
|
||||||
return true;
|
return true;
|
||||||
// Small nudge so the next tick's MoveFarTo starts from a
|
}
|
||||||
// slightly different position. Kept small so it doesn't look
|
// Reference pattern (TravelTarget retry counter): count
|
||||||
// like the bot is abandoning its destination.
|
// consecutive MoveFarTo failures, give up after N tries by
|
||||||
return MoveRandomNear(10.0f);
|
// 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;
|
return false;
|
||||||
@ -181,8 +184,18 @@ bool NewRpgGoCampAction::Execute(Event /*event*/)
|
|||||||
if (auto* data = std::get_if<NewRpgInfo::GoCamp>(&botAI->rpgInfo.data))
|
if (auto* data = std::get_if<NewRpgInfo::GoCamp>(&botAI->rpgInfo.data))
|
||||||
{
|
{
|
||||||
if (MoveFarTo(data->pos))
|
if (MoveFarTo(data->pos))
|
||||||
|
{
|
||||||
|
botAI->rpgInfo.moveRetryCount = 0;
|
||||||
return true;
|
return true;
|
||||||
return MoveRandomNear(10.0f);
|
}
|
||||||
|
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 false;
|
return false;
|
||||||
@ -238,11 +251,23 @@ bool NewRpgWanderNpcAction::Execute(Event /*event*/)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (MoveWorldObjectTo(data.npcOrGo))
|
if (MoveWorldObjectTo(data.npcOrGo))
|
||||||
|
{
|
||||||
|
botAI->rpgInfo.moveRetryCount = 0;
|
||||||
return true;
|
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
|
// Retry counter (reference pattern): give up after N failures
|
||||||
// different spot instead of staring at the NPC from afar.
|
// by clearing the picked NPC so next tick picks a different
|
||||||
return MoveRandomNear(15.0f);
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -275,14 +300,23 @@ bool NewRpgDoQuestAction::Execute(Event /*event*/)
|
|||||||
|
|
||||||
bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
||||||
{
|
{
|
||||||
uint32 questId = data.questId;
|
uint32 const questId = data.questId;
|
||||||
if (data.pos != WorldPosition())
|
|
||||||
|
// === 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 const currentObjective = data.objectiveIdx;
|
||||||
int32 currentObjective = data.objectiveIdx;
|
|
||||||
// check if the objective has completed
|
|
||||||
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
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;
|
bool completed = true;
|
||||||
if (currentObjective < QUEST_OBJECTIVES_COUNT)
|
if (currentObjective < QUEST_OBJECTIVES_COUNT)
|
||||||
{
|
{
|
||||||
@ -295,67 +329,109 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
|||||||
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
|
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
|
||||||
completed = false;
|
completed = false;
|
||||||
}
|
}
|
||||||
// the current objective is completed, clear and find a new objective later
|
|
||||||
if (completed)
|
if (completed)
|
||||||
{
|
{
|
||||||
|
data.candidateSpawns.clear();
|
||||||
|
data.currentSpawnIdx = 0;
|
||||||
data.lastReachPOI = 0;
|
data.lastReachPOI = 0;
|
||||||
data.pos = WorldPosition();
|
|
||||||
data.objectiveIdx = 0;
|
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;
|
std::vector<WorldPosition> spawns;
|
||||||
if (!GetQuestPOIPosAndObjectiveIdx(questId, poiInfo))
|
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();
|
botAI->rpgInfo.ChangeToIdle();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
uint32 rndIdx = urand(0, poiInfo.size() - 1);
|
data.candidateSpawns = std::move(spawns);
|
||||||
G3D::Vector2 nearestPoi = poiInfo[rndIdx].pos;
|
data.currentSpawnIdx = 0;
|
||||||
int32 objectiveIdx = poiInfo[rndIdx].objectiveIdx;
|
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
|
WorldPosition const& target = data.candidateSpawns[data.currentSpawnIdx];
|
||||||
float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy));
|
|
||||||
|
|
||||||
// double check for GetQuestPOIPosAndObjectiveIdx
|
// 4. Walk to the current target spawn. Yield to attack-anything
|
||||||
if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE)
|
// 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;
|
return false;
|
||||||
|
|
||||||
WorldPosition pos(bot->GetMapId(), dx, dy, dz);
|
if (MoveFarTo(target))
|
||||||
data.lastReachPOI = 0;
|
{
|
||||||
data.pos = pos;
|
botAI->rpgInfo.moveRetryCount = 0;
|
||||||
data.objectiveIdx = objectiveIdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
|
|
||||||
{
|
|
||||||
if (MoveFarTo(data.pos))
|
|
||||||
return true;
|
return true;
|
||||||
// Long-range sampler couldn't land a candidate — nudge the
|
}
|
||||||
// bot a short distance so the next tick retries from a
|
// Retry counter: on N consecutive MoveFarTo failures, advance
|
||||||
// different position instead of sitting idle.
|
// to the next candidate spawn rather than sit on an unreachable
|
||||||
return MoveRandomNear(10.0f);
|
// 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;
|
||||||
}
|
}
|
||||||
// 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)
|
if (!data.lastReachPOI)
|
||||||
{
|
{
|
||||||
data.lastReachPOI = getMSTime();
|
data.lastReachPOI = getMSTime();
|
||||||
return true;
|
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;
|
bool hasProgression = false;
|
||||||
int32 currentObjective = data.objectiveIdx;
|
int32 const currentObjective = data.objectiveIdx;
|
||||||
// check if the objective has progression
|
|
||||||
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
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 (currentObjective < QUEST_OBJECTIVES_COUNT)
|
||||||
{
|
{
|
||||||
if (q_status.CreatureOrGOCount[currentObjective] != 0 && quest->RequiredNpcOrGoCount[currentObjective])
|
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])
|
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
|
||||||
hasProgression = true;
|
hasProgression = true;
|
||||||
}
|
}
|
||||||
if (!hasProgression)
|
if (hasProgression)
|
||||||
{
|
{
|
||||||
// we has reach the poi for more than 5 mins but no progession
|
// Refresh: re-fetch candidates so the list reflects what's
|
||||||
// may not be able to complete this quest, marked as abandoned
|
// still needed and is sorted from the bot's new position.
|
||||||
/// @TODO: It may be better to make lowPriorityQuest a global set shared by all bots (or saved in db)
|
data.candidateSpawns.clear();
|
||||||
botAI->lowPriorityQuest.insert(questId);
|
data.currentSpawnIdx = 0;
|
||||||
botAI->rpgStatistic.questAbandoned++;
|
data.lastReachPOI = 0;
|
||||||
LOG_DEBUG("playerbots", "[New RPG] {} marked as abandoned quest {}", bot->GetName(), questId);
|
|
||||||
botAI->rpgInfo.ChangeToIdle();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// clear and select another poi later
|
// No progression at this spawn — advance to the next candidate.
|
||||||
|
++data.currentSpawnIdx;
|
||||||
data.lastReachPOI = 0;
|
data.lastReachPOI = 0;
|
||||||
data.pos = WorldPosition();
|
data.pursuedLootGO.Clear();
|
||||||
data.objectiveIdx = 0;
|
data.pursuedUseGO.Clear();
|
||||||
|
data.pursuedUseTarget.Clear();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// At the POI: keep the bot actively placed but avoid large
|
// 7. At spawn, within timeout: drive toward specific objectives.
|
||||||
// random 20yd hops that look like pacing back and forth. A small
|
// Combat strategy engages adjacent quest mobs; loot/use
|
||||||
// ~8yd wander reads as the bot looking around while grind/loot
|
// actions handle quest GOs and quest items.
|
||||||
// strategies do their work.
|
if (TryUseQuestItem(data.pursuedUseGO, data.pursuedUseTarget))
|
||||||
return MoveRandomNear(8.0f);
|
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)
|
bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
|
||||||
@ -423,6 +508,15 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
|
|||||||
data.lastReachPOI = 0;
|
data.lastReachPOI = 0;
|
||||||
data.pos = pos;
|
data.pos = pos;
|
||||||
data.objectiveIdx = -1;
|
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())
|
if (data.pos == WorldPosition())
|
||||||
@ -431,8 +525,21 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
|
|||||||
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
|
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
|
||||||
{
|
{
|
||||||
if (MoveFarTo(data.pos))
|
if (MoveFarTo(data.pos))
|
||||||
|
{
|
||||||
|
botAI->rpgInfo.moveRetryCount = 0;
|
||||||
return true;
|
return true;
|
||||||
return MoveRandomNear(10.0f);
|
}
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we are near the qoi of reward
|
// Now we are near the qoi of reward
|
||||||
@ -453,7 +560,9 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
|
|||||||
botAI->rpgInfo.ChangeToIdle();
|
botAI->rpgInfo.ChangeToIdle();
|
||||||
return true;
|
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*/)
|
bool NewRpgTravelFlightAction::Execute(Event /*event*/)
|
||||||
@ -464,6 +573,22 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
auto& data = *dataPtr;
|
auto& data = *dataPtr;
|
||||||
|
|
||||||
|
// Arrival: we had boarded a flight (data.inFlight) and we're no longer in
|
||||||
|
// it → we just landed. Special-case Rut'theran: walk to the portal GO so
|
||||||
|
// it teleports the bot into Darnassus, flipping the zone to AREA_DARNASSUS
|
||||||
|
// so this branch falls through to ChangeToIdle on the next tick.
|
||||||
|
if (data.inFlight && !bot->IsInFlight())
|
||||||
|
{
|
||||||
|
if (bot->GetZoneId() == AREA_TELDRASSIL)
|
||||||
|
{
|
||||||
|
static WorldPosition const rutTheranPortalEntrance(1, 8799.41f, 969.787f, 26.2409f, 0.0f);
|
||||||
|
return MoveFarTo(rutTheranPortalEntrance);
|
||||||
|
}
|
||||||
|
info.ChangeToIdle();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (bot->IsInFlight())
|
if (bot->IsInFlight())
|
||||||
{
|
{
|
||||||
data.inFlight = true;
|
data.inFlight = true;
|
||||||
@ -479,19 +604,9 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
|
|||||||
info.ChangeToIdle();
|
info.ChangeToIdle();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
|
|
||||||
return MoveFarTo(flightMaster);
|
|
||||||
|
|
||||||
std::vector<uint32> nodes = data.path;
|
if (!TakeFlight(data.path, flightMaster))
|
||||||
|
|
||||||
botAI->RemoveShapeshift();
|
|
||||||
if (bot->IsMounted())
|
|
||||||
bot->Dismount();
|
|
||||||
|
|
||||||
if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0))
|
|
||||||
{
|
{
|
||||||
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
|
|
||||||
flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);
|
|
||||||
info.ChangeToIdle();
|
info.ChangeToIdle();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,11 +47,11 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
// static NewRpgStatusTransitionProb transitionMat;
|
// static NewRpgStatusTransitionProb transitionMat;
|
||||||
const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS ;
|
const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS;
|
||||||
const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS ;
|
const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS;
|
||||||
const int32 statusRestDuration = 30 * IN_MILLISECONDS ;
|
const int32 statusRestDuration = 30 * IN_MILLISECONDS;
|
||||||
const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS ;
|
const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS;
|
||||||
const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS ;
|
const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS;
|
||||||
};
|
};
|
||||||
|
|
||||||
class NewRpgGoGrindAction : public NewRpgBaseAction
|
class NewRpgGoGrindAction : public NewRpgBaseAction
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -33,6 +33,7 @@ protected:
|
|||||||
bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE);
|
bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE);
|
||||||
bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr);
|
bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr);
|
||||||
bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
||||||
|
bool TakeFlight(std::vector<uint32> const& taxiNodes, Creature* flightMaster);
|
||||||
|
|
||||||
/* QUEST RELATED CHECK */
|
/* QUEST RELATED CHECK */
|
||||||
ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f);
|
ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f);
|
||||||
@ -50,25 +51,47 @@ protected:
|
|||||||
bool TurnInQuest(Quest const* quest, ObjectGuid guid);
|
bool TurnInQuest(Quest const* quest, ObjectGuid guid);
|
||||||
bool OrganizeQuestLog();
|
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:
|
protected:
|
||||||
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
|
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 SelectRandomGrindPos(Player* bot);
|
||||||
static WorldPosition SelectRandomCampPos(Player* bot);
|
static WorldPosition SelectRandomCampPos(Player* bot);
|
||||||
bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path);
|
bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path);
|
||||||
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
|
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
|
||||||
bool CheckRpgStatusAvailable(NewRpgStatus status);
|
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
|
#endif
|
||||||
|
|||||||
@ -9,7 +9,7 @@ bool NewRpgOutdoorPvpAction::Execute(Event event)
|
|||||||
botAI->rpgInfo.ChangeToIdle();
|
botAI->rpgInfo.ChangeToIdle();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL) || !bot->IsOutdoorPvPActive())
|
if (!bot->IsOutdoorPvPActive())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
uint32 zoneId = bot->GetZoneId();
|
uint32 zoneId = bot->GetZoneId();
|
||||||
@ -113,9 +113,6 @@ OPvPCapturePoint* NewRpgOutdoorPvpAction::SelectNewObjective(OutdoorPvP::OPvPCap
|
|||||||
|
|
||||||
bool NewRpgOutdoorPvpAction::PatrolCapturePoint(GameObject* objectiveGO, float radius)
|
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
|
// Randomly pause at the current spot before picking a new patrol point
|
||||||
if (urand(0, 2) == 0)
|
if (urand(0, 2) == 0)
|
||||||
return ForceToWait(urand(3000, 6000));
|
return ForceToWait(urand(3000, 6000));
|
||||||
|
|||||||
@ -6,31 +6,31 @@
|
|||||||
|
|
||||||
void NewRpgInfo::ChangeToGoGrind(WorldPosition pos)
|
void NewRpgInfo::ChangeToGoGrind(WorldPosition pos)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = GoGrind{pos};
|
data = GoGrind{pos};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToGoCamp(WorldPosition pos)
|
void NewRpgInfo::ChangeToGoCamp(WorldPosition pos)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = GoCamp{pos};
|
data = GoCamp{pos};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToWanderNpc()
|
void NewRpgInfo::ChangeToWanderNpc()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = WanderNpc{};
|
data = WanderNpc{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToWanderRandom()
|
void NewRpgInfo::ChangeToWanderRandom()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = WanderRandom{};
|
data = WanderRandom{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
|
void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
DoQuest do_quest;
|
DoQuest do_quest;
|
||||||
do_quest.questId = questId;
|
do_quest.questId = questId;
|
||||||
do_quest.quest = quest;
|
do_quest.quest = quest;
|
||||||
@ -39,7 +39,7 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
|
|||||||
|
|
||||||
void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path)
|
void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
TravelFlight flight;
|
TravelFlight flight;
|
||||||
flight.flightMasterEntry = flightMasterEntry;
|
flight.flightMasterEntry = flightMasterEntry;
|
||||||
flight.flightMasterPos = flightMasterPos;
|
flight.flightMasterPos = flightMasterPos;
|
||||||
@ -58,13 +58,13 @@ void NewRpgInfo::ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId)
|
|||||||
|
|
||||||
void NewRpgInfo::ChangeToRest()
|
void NewRpgInfo::ChangeToRest()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = Rest{};
|
data = Rest{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToIdle()
|
void NewRpgInfo::ChangeToIdle()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = Idle{};
|
data = Idle{};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,14 +77,7 @@ void NewRpgInfo::Reset()
|
|||||||
{
|
{
|
||||||
data = Idle{};
|
data = Idle{};
|
||||||
startT = getMSTime();
|
startT = getMSTime();
|
||||||
}
|
moveRetryCount = 0;
|
||||||
|
|
||||||
void NewRpgInfo::SetMoveFarTo(WorldPosition pos)
|
|
||||||
{
|
|
||||||
nearestMoveFarDis = FLT_MAX;
|
|
||||||
stuckTs = 0;
|
|
||||||
stuckAttempts = 0;
|
|
||||||
moveFarPos = pos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NewRpgStatus NewRpgInfo::GetStatus()
|
NewRpgStatus NewRpgInfo::GetStatus()
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
#ifndef _PLAYERBOT_NEWRPGINFO_H
|
#ifndef _PLAYERBOT_NEWRPGINFO_H
|
||||||
#define _PLAYERBOT_NEWRPGINFO_H
|
#define _PLAYERBOT_NEWRPGINFO_H
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
#include "Define.h"
|
#include "Define.h"
|
||||||
#include "ObjectGuid.h"
|
#include "ObjectGuid.h"
|
||||||
#include "ObjectMgr.h"
|
#include "ObjectMgr.h"
|
||||||
@ -8,6 +10,7 @@
|
|||||||
#include "Strategy.h"
|
#include "Strategy.h"
|
||||||
#include "Timer.h"
|
#include "Timer.h"
|
||||||
#include "TravelMgr.h"
|
#include "TravelMgr.h"
|
||||||
|
#include "TravelNode.h"
|
||||||
|
|
||||||
using NewRpgStatusTransitionProb = std::vector<std::vector<int>>;
|
using NewRpgStatusTransitionProb = std::vector<std::vector<int>>;
|
||||||
|
|
||||||
@ -43,8 +46,23 @@ struct NewRpgInfo
|
|||||||
const Quest* quest{nullptr};
|
const Quest* quest{nullptr};
|
||||||
uint32 questId{0};
|
uint32 questId{0};
|
||||||
int32 objectiveIdx{0};
|
int32 objectiveIdx{0};
|
||||||
|
// Turn-in POI (DoCompletedQuest). Kept POI-based since this is
|
||||||
|
// the quest-giver location, not a mob spawn.
|
||||||
WorldPosition pos{};
|
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};
|
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
|
// RPG_TRAVEL_FLIGHT
|
||||||
struct TravelFlight
|
struct TravelFlight
|
||||||
@ -70,12 +88,13 @@ struct NewRpgInfo
|
|||||||
|
|
||||||
uint32 startT{0}; // start timestamp of the current status
|
uint32 startT{0}; // start timestamp of the current status
|
||||||
|
|
||||||
// MOVE_FAR
|
// Counts consecutive MoveFarTo failures for the current state.
|
||||||
float nearestMoveFarDis{FLT_MAX};
|
// Reset on every status change (via Reset) and on every successful
|
||||||
uint32 stuckTs{0};
|
// MoveFarTo. When it crosses MAX_MOVE_RETRIES the failing action
|
||||||
uint32 stuckAttempts{0};
|
// gives up and transitions out of the current state instead of
|
||||||
WorldPosition moveFarPos;
|
// sitting on a stuck objective forever.
|
||||||
// END MOVE_FAR
|
uint8 moveRetryCount{0};
|
||||||
|
static constexpr uint8 MAX_MOVE_RETRIES = 10;
|
||||||
|
|
||||||
using RpgData = std::variant<
|
using RpgData = std::variant<
|
||||||
Idle,
|
Idle,
|
||||||
@ -103,7 +122,6 @@ struct NewRpgInfo
|
|||||||
void ChangeToIdle();
|
void ChangeToIdle();
|
||||||
bool CanChangeTo(NewRpgStatus status);
|
bool CanChangeTo(NewRpgStatus status);
|
||||||
void Reset();
|
void Reset();
|
||||||
void SetMoveFarTo(WorldPosition pos);
|
|
||||||
std::string ToString();
|
std::string ToString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,11 +5,66 @@
|
|||||||
|
|
||||||
#include "NewRpgStrategy.h"
|
#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) {}
|
NewRpgStrategy::NewRpgStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
|
||||||
|
|
||||||
std::vector<NextAction> NewRpgStrategy::getDefaultActions()
|
std::vector<NextAction> NewRpgStrategy::getDefaultActions()
|
||||||
{
|
{
|
||||||
// the releavance should be greater than grind
|
// must outrank grind
|
||||||
return {
|
return {
|
||||||
NextAction("new rpg status update", 11.0f)
|
NextAction("new rpg status update", 11.0f)
|
||||||
};
|
};
|
||||||
@ -53,7 +108,8 @@ void NewRpgStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
|
|||||||
new TriggerNode(
|
new TriggerNode(
|
||||||
"do quest status",
|
"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));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,13 @@
|
|||||||
|
|
||||||
class PlayerbotAI;
|
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
|
class NewRpgStrategy : public Strategy
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@ -767,6 +767,21 @@ void PlayerbotAI::HandleCommand(uint32 type, const std::string& text, Player& fr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerbotAI::TeleportTo(WorldLocation loc, bool resetAI)
|
||||||
|
{
|
||||||
|
if (!bot || bot->IsBeingTeleported() || !bot->IsInWorld())
|
||||||
|
return;
|
||||||
|
|
||||||
|
bot->GetMotionMaster()->Clear();
|
||||||
|
if (resetAI)
|
||||||
|
Reset(true);
|
||||||
|
else
|
||||||
|
InterruptSpell();
|
||||||
|
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
||||||
|
bot->TeleportTo(loc.GetMapId(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ(), 0);
|
||||||
|
bot->SendMovementFlagUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
void PlayerbotAI::HandleTeleportAck()
|
void PlayerbotAI::HandleTeleportAck()
|
||||||
{
|
{
|
||||||
if (!bot || !bot->GetSession())
|
if (!bot || !bot->GetSession())
|
||||||
@ -809,7 +824,7 @@ void PlayerbotAI::HandleTeleportAck()
|
|||||||
bot->StopMoving();
|
bot->StopMoving();
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulate far teleport latency (cmangos-style)
|
// simulate far teleport latency
|
||||||
SetNextCheckDelay(urand(2000, 5000));
|
SetNextCheckDelay(urand(2000, 5000));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -396,6 +396,7 @@ public:
|
|||||||
void HandleMasterIncomingPacket(WorldPacket const& packet);
|
void HandleMasterIncomingPacket(WorldPacket const& packet);
|
||||||
void HandleMasterOutgoingPacket(WorldPacket const& packet);
|
void HandleMasterOutgoingPacket(WorldPacket const& packet);
|
||||||
void HandleTeleportAck();
|
void HandleTeleportAck();
|
||||||
|
void TeleportTo(WorldLocation loc, bool resetAI = false);
|
||||||
void ChangeEngine(BotState type);
|
void ChangeEngine(BotState type);
|
||||||
void ChangeEngineOnCombat();
|
void ChangeEngineOnCombat();
|
||||||
void ChangeEngineOnNonCombat();
|
void ChangeEngineOnNonCombat();
|
||||||
|
|||||||
@ -1697,14 +1697,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
bot->GetMotionMaster()->Clear();
|
botAI->TeleportTo(WorldLocation(loc.GetMapId(), x, y, z, 0), true);
|
||||||
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
||||||
if (botAI)
|
|
||||||
botAI->Reset(true);
|
|
||||||
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
|
||||||
bot->TeleportTo(loc.GetMapId(), x, y, z, 0);
|
|
||||||
bot->SendMovementFlagUpdate();
|
|
||||||
|
|
||||||
if (pmo)
|
if (pmo)
|
||||||
pmo->finish();
|
pmo->finish();
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
Refresh(bot, guid);
|
||||||
}
|
}
|
||||||
@ -55,6 +56,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
|||||||
skillId = SKILL_NONE;
|
skillId = SKILL_NONE;
|
||||||
reqSkillValue = 0;
|
reqSkillValue = 0;
|
||||||
reqItem = 0;
|
reqItem = 0;
|
||||||
|
isNeededQuestItem = false;
|
||||||
guid.Clear();
|
guid.Clear();
|
||||||
|
|
||||||
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
||||||
@ -101,6 +103,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
|||||||
if (IsNeededForQuest(bot, itemId))
|
if (IsNeededForQuest(bot, itemId))
|
||||||
{
|
{
|
||||||
this->guid = lootGUID;
|
this->guid = lootGUID;
|
||||||
|
this->isNeededQuestItem = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,10 +138,21 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
|||||||
if (!proto)
|
if (!proto)
|
||||||
continue;
|
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)
|
if (proto->Class != ITEM_CLASS_QUEST)
|
||||||
{
|
{
|
||||||
onlyHasQuestItems = false;
|
onlyHasQuestItems = false;
|
||||||
break;
|
// keep scanning — a later item may be needed
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this item references another loot table, process it
|
// If this item references another loot table, process it
|
||||||
@ -157,11 +171,15 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
|||||||
if (!refProto)
|
if (!refProto)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (refProto->Class != ITEM_CLASS_QUEST)
|
if (IsNeededForQuest(bot, refItemId))
|
||||||
{
|
{
|
||||||
onlyHasQuestItems = false;
|
this->guid = lootGUID;
|
||||||
break;
|
this->isNeededQuestItem = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (refProto->Class != ITEM_CLASS_QUEST)
|
||||||
|
onlyHasQuestItems = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -270,6 +288,7 @@ LootObject::LootObject(LootObject const& other)
|
|||||||
skillId = other.skillId;
|
skillId = other.skillId;
|
||||||
reqSkillValue = other.reqSkillValue;
|
reqSkillValue = other.reqSkillValue;
|
||||||
reqItem = other.reqItem;
|
reqItem = other.reqItem;
|
||||||
|
isNeededQuestItem = other.isNeededQuestItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LootObject::IsLootPossible(Player* bot)
|
bool LootObject::IsLootPossible(Player* bot)
|
||||||
@ -299,10 +318,13 @@ bool LootObject::IsLootPossible(Player* bot)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent bot from running to chests that are unlootable (e.g. Gunship Armory before completing the event) or on
|
// Block event-gated chests (Gunship Armory pre-event) and unspawned
|
||||||
// respawn time
|
// 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);
|
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;
|
return false;
|
||||||
|
|
||||||
if (skillId == SKILL_NONE)
|
if (skillId == SKILL_NONE)
|
||||||
@ -340,6 +362,13 @@ bool LootObject::IsLootPossible(Player* bot)
|
|||||||
|
|
||||||
bool LootObjectStack::Add(ObjectGuid guid)
|
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)
|
if (availableLoot.size() >= MAX_LOOT_OBJECT_COUNT)
|
||||||
{
|
{
|
||||||
availableLoot.shrink(time(nullptr) - 30);
|
availableLoot.shrink(time(nullptr) - 30);
|
||||||
@ -363,7 +392,17 @@ void LootObjectStack::Remove(ObjectGuid guid)
|
|||||||
availableLoot.erase(i);
|
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)
|
bool LootObjectStack::CanLoot(float maxDistance)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -26,7 +26,7 @@ public:
|
|||||||
class LootObject
|
class LootObject
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
LootObject() : skillId(0), reqSkillValue(0), reqItem(0) {}
|
LootObject() : skillId(0), reqSkillValue(0), reqItem(0), isNeededQuestItem(false) {}
|
||||||
LootObject(Player* bot, ObjectGuid guid);
|
LootObject(Player* bot, ObjectGuid guid);
|
||||||
LootObject(LootObject const& other);
|
LootObject(LootObject const& other);
|
||||||
LootObject& operator=(LootObject const& other) = default;
|
LootObject& operator=(LootObject const& other) = default;
|
||||||
@ -40,6 +40,9 @@ public:
|
|||||||
uint32 skillId;
|
uint32 skillId;
|
||||||
uint32 reqSkillValue;
|
uint32 reqSkillValue;
|
||||||
uint32 reqItem;
|
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:
|
private:
|
||||||
static bool IsNeededForQuest(Player* bot, uint32 itemId);
|
static bool IsNeededForQuest(Player* bot, uint32 itemId);
|
||||||
@ -73,6 +76,7 @@ public:
|
|||||||
|
|
||||||
bool Add(ObjectGuid guid);
|
bool Add(ObjectGuid guid);
|
||||||
void Remove(ObjectGuid guid);
|
void Remove(ObjectGuid guid);
|
||||||
|
void MarkCompleted(ObjectGuid guid);
|
||||||
void Clear();
|
void Clear();
|
||||||
bool CanLoot(float maxDistance);
|
bool CanLoot(float maxDistance);
|
||||||
LootObject GetLoot(float maxDistance = 0);
|
LootObject GetLoot(float maxDistance = 0);
|
||||||
@ -82,6 +86,9 @@ private:
|
|||||||
|
|
||||||
Player* bot;
|
Player* bot;
|
||||||
LootTargetList availableLoot;
|
LootTargetList availableLoot;
|
||||||
|
// Guids we already opened loot on; blocks "add all loot" from
|
||||||
|
// re-adding the same corpse before it despawns.
|
||||||
|
LootTargetList completedLoot;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
60
src/Mgr/Quest/QuestSpawnIndex.cpp
Normal file
60
src/Mgr/Quest/QuestSpawnIndex.cpp
Normal 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;
|
||||||
|
}
|
||||||
70
src/Mgr/Quest/QuestSpawnIndex.h
Normal file
70
src/Mgr/Quest/QuestSpawnIndex.h
Normal 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
@ -7,6 +7,7 @@
|
|||||||
#define _PLAYERBOT_TRAVELMGR_H
|
#define _PLAYERBOT_TRAVELMGR_H
|
||||||
|
|
||||||
#include <boost/functional/hash.hpp>
|
#include <boost/functional/hash.hpp>
|
||||||
|
#include <cmath>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
@ -18,6 +19,7 @@
|
|||||||
|
|
||||||
class Creature;
|
class Creature;
|
||||||
class GuidPosition;
|
class GuidPosition;
|
||||||
|
class PathGenerator;
|
||||||
class ObjectGuid;
|
class ObjectGuid;
|
||||||
class Quest;
|
class Quest;
|
||||||
class Player;
|
class Player;
|
||||||
@ -136,6 +138,9 @@ public:
|
|||||||
bool isOverworld();
|
bool isOverworld();
|
||||||
bool isInWater();
|
bool isInWater();
|
||||||
bool isUnderWater();
|
bool isUnderWater();
|
||||||
|
// Snap Z to the water surface (level + 0.5y). Returns false if the
|
||||||
|
// point isn't in/under water or the water level can't be sampled.
|
||||||
|
bool setAtWaterSurface();
|
||||||
bool IsValid();
|
bool IsValid();
|
||||||
|
|
||||||
WorldPosition relPoint(WorldPosition* center);
|
WorldPosition relPoint(WorldPosition* center);
|
||||||
@ -227,6 +232,28 @@ public:
|
|||||||
|
|
||||||
float getAngleBetween(WorldPosition dir1, WorldPosition dir2) { return abs(getAngleTo(dir1) - getAngleTo(dir2)); }
|
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 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);
|
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<mGridCoord> getmGridCoords(WorldPosition secondPos);
|
||||||
std::vector<WorldPosition> frommGridCoord(mGridCoord GridCoord);
|
std::vector<WorldPosition> frommGridCoord(mGridCoord GridCoord);
|
||||||
|
|
||||||
void loadMapAndVMap(uint32 mapId, uint8 x, uint8 y);
|
|
||||||
|
|
||||||
void loadMapAndVMap() { loadMapAndVMap(GetMapId(), getmGridCoord().first, getmGridCoord().second); }
|
|
||||||
|
|
||||||
void loadMapAndVMaps(WorldPosition secondPos);
|
|
||||||
|
|
||||||
// Display functions
|
// Display functions
|
||||||
WorldPosition getDisplayLocation();
|
WorldPosition getDisplayLocation();
|
||||||
float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; }
|
float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; }
|
||||||
@ -288,6 +309,7 @@ public:
|
|||||||
|
|
||||||
// Pathfinding
|
// Pathfinding
|
||||||
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, Unit* bot);
|
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> getPathFromPath(std::vector<WorldPosition> startPath, Unit* bot, uint8 maxAttempt = 40);
|
||||||
|
|
||||||
std::vector<WorldPosition> getPathFrom(WorldPosition startPos, Unit* bot)
|
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); }
|
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 cropPathTo(std::vector<WorldPosition>& path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance);
|
||||||
bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); }
|
bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); }
|
||||||
|
|
||||||
@ -507,9 +545,15 @@ public:
|
|||||||
radiusMin = radiusMin1;
|
radiusMin = radiusMin1;
|
||||||
radiusMax = radiusMax1;
|
radiusMax = radiusMax1;
|
||||||
}
|
}
|
||||||
virtual ~TravelDestination() = default;
|
virtual ~TravelDestination();
|
||||||
|
|
||||||
void addPoint(WorldPosition* pos) { points.push_back(pos); }
|
void addPoint(WorldPosition* pos)
|
||||||
|
{
|
||||||
|
if (!pos)
|
||||||
|
return;
|
||||||
|
|
||||||
|
points.push_back(new WorldPosition(*pos));
|
||||||
|
}
|
||||||
|
|
||||||
void setExpireDelay(uint32 delay) { expireDelay = delay; }
|
void setExpireDelay(uint32 delay) { expireDelay = delay; }
|
||||||
|
|
||||||
@ -673,7 +717,7 @@ public:
|
|||||||
bool isActive(Player* bot) override;
|
bool isActive(Player* bot) override;
|
||||||
virtual CreatureTemplate const* GetCreatureTemplate();
|
virtual CreatureTemplate const* GetCreatureTemplate();
|
||||||
std::string const getName() override { return "RpgTravelDestination"; }
|
std::string const getName() override { return "RpgTravelDestination"; }
|
||||||
int32 getEntry() override { return 0; }
|
int32 getEntry() override { return entry; }
|
||||||
std::string const getTitle() override;
|
std::string const getTitle() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@ -985,18 +1029,14 @@ private:
|
|||||||
bool InsideBracket(uint32 val) const { return val >= low && val <= high; }
|
bool InsideBracket(uint32 val) const { return val >= low && val <= high; }
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BankerLocation
|
|
||||||
{
|
|
||||||
WorldLocation loc;
|
|
||||||
uint32 entry;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Navigation caches
|
// Navigation caches
|
||||||
std::map<uint32, FlightMasterInfo> allianceFlightMasterCache;
|
std::map<uint32, FlightMasterInfo> allianceFlightMasterCache;
|
||||||
std::map<uint32, FlightMasterInfo> hordeFlightMasterCache;
|
std::map<uint32, FlightMasterInfo> hordeFlightMasterCache;
|
||||||
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
|
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
|
||||||
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
|
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
|
||||||
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;
|
std::map<uint8, std::vector<NpcLocation>> bankerLocsPerLevelCache;
|
||||||
|
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> hordeAuctioneerCache;
|
||||||
|
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> allianceAuctioneerCache;
|
||||||
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
|
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
|
||||||
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
|
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
|
||||||
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;
|
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -8,12 +8,12 @@
|
|||||||
|
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
|
|
||||||
|
#include "G3D/Vector3.h"
|
||||||
#include "TravelMgr.h"
|
#include "TravelMgr.h"
|
||||||
|
|
||||||
// THEORY
|
// THEORY
|
||||||
//
|
//
|
||||||
// Pathfinding in (c)mangos is based on detour recast an opensource nashmesh creation and pathfinding codebase.
|
// Pathfinding uses the detour recast navmesh engine for mob, npc, and bot movement.
|
||||||
// This system is used for mob and npc pathfinding and in this codebase also for bots.
|
|
||||||
// Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y.
|
// Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y.
|
||||||
// This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a
|
// This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a
|
||||||
// straight path. Bots would get stuck moving from Northshire to Stormwind because there is no 296y path that doesn't
|
// 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>
|
// <S> ---> [N1] ---> [N2] ---> [N3] ---> <E>
|
||||||
//
|
//
|
||||||
// Bot at <S> wants to move to <E>
|
// Bot at <S> wants to move to <E>
|
||||||
// [N1],[N2],[N3] are predefined nodes for wich we know we can move from [N1] to [N2] and from [N2] to [N3] but not
|
// [N1],[N2],[N3] are predefined nodes for which we know we can move from [N1] to [N2] and from [N2] to [N3] but not
|
||||||
// from [N1] to [N3] If we can move fom [S] to [N1] and from [N3] to [E] we have a complete route to travel.
|
// from [N1] to [N3]. If we can move from [S] to [N1] and from [N3] to [E] we have a complete route to travel.
|
||||||
//
|
//
|
||||||
// Termonology:
|
// Terminology:
|
||||||
// Node: a location on a map for which we know bots are likely to want to travel to or need to travel past to reach
|
// Node: A location on a map for which we know bots are likely to want to travel to or need to travel past to reach
|
||||||
// other nodes. Link: the connection between two nodes. A link signifies that the bot can travel from one node to
|
// other nodes. Stored in DB table `playerbots_travelnode`.
|
||||||
// another. A link is one-directional. Path: the waypointpath returned by the standard PathGenerator to move from one
|
// Link: The connection between two nodes. A link signifies that the bot can travel from one node to another.
|
||||||
// node (or position) to another. A path can be imcomplete or empty which means there is no link. Route: the list of
|
// A link is one-directional. Stored in `playerbots_travelnode_link`.
|
||||||
// nodes that give the shortest route from a node to a distant node. Routes are calculated using a standard A* search
|
// Path: The waypoint path returned by the standard PathGenerator to move from one node (or position) to another.
|
||||||
// based on links.
|
// A path can be incomplete or empty which means there is no link. Stored in `playerbots_travelnode_path`.
|
||||||
|
// Route: The list of nodes that give the shortest route from a node to a distant node. Routes are calculated using
|
||||||
|
// a standard A* search based on links.
|
||||||
//
|
//
|
||||||
// On server start saved nodes and links are loaded. Paths and routes are calculated on the fly but saved for future
|
// Edge types (TravelNodePathType):
|
||||||
// use. Nodes can be added and removed realtime however because bots access the nodes from different threads this
|
// walk(1) — Walk via navmesh waypoints (stored in DB)
|
||||||
// requires a locking mechanism.
|
// 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:
|
// Initially the current nodes have been made:
|
||||||
// Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route
|
// Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route
|
||||||
// calculation) WorldBosses and Unique bosses in instances (These are a logical places bots might want to go in
|
// calculation) WorldBosses and Unique bosses in instances (These are logical places bots might want to go in
|
||||||
// instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with
|
// instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with
|
||||||
// teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators
|
// teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators
|
||||||
// (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is
|
// (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is
|
||||||
// good for global coverage)
|
// good for global coverage).
|
||||||
//
|
//
|
||||||
// To increase coverage/linking extra nodes can be automatically be created.
|
// To increase coverage/linking extra nodes must be manually created via the "playerbot travel generatenode"
|
||||||
// Current implentation places nodes on paths (including complete) at sub-zone transitions or randomly.
|
// console command after importing the specified node. Current implementation places nodes on paths (including
|
||||||
// After calculating possible links the node is removed if it does not create local coverage.
|
// complete) at sub-zone transitions or randomly. After calculating possible links the node is removed if it
|
||||||
|
// does not create local coverage (.fullgenerate only).
|
||||||
|
//
|
||||||
|
// Travel Flow:
|
||||||
|
//
|
||||||
|
// GetFullPath finds nearest nodes (zone-indexed), runs A* to get a node route, then
|
||||||
|
// BuildPath assembles a flat TravelPath with typed waypoints (walk, portal, transport, flight).
|
||||||
|
// 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
|
enum class TravelNodePathType : uint8
|
||||||
{
|
{
|
||||||
none = 0,
|
none = 0,
|
||||||
walk = 1,
|
walk = 1,
|
||||||
portal = 2,
|
areaTrigger = 2,
|
||||||
transport = 3,
|
transport = 3,
|
||||||
flightPath = 4,
|
flightPath = 4,
|
||||||
teleportSpell = 5
|
// teleportSpell = 5 // maybe someday
|
||||||
|
staticPortal = 6
|
||||||
};
|
};
|
||||||
|
|
||||||
// A connection between two nodes.
|
// A connection between two nodes.
|
||||||
class TravelNodePath
|
class TravelNodePath
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Legacy Constructor for travelnodestore
|
|
||||||
// TravelNodePath(float distance1, float extraCost1, bool portal1 = false, uint32 portalId1 = 0, bool transport1 =
|
|
||||||
// false, bool calculated = false, uint8 maxLevelMob1 = 0, uint8 maxLevelAlliance1 = 0, uint8 maxLevelHorde1 = 0,
|
|
||||||
// float swimDistance1 = 0, bool flightPath1 = false);
|
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
TravelNodePath(float distance = 0.1f, float extraCost = 0, uint8 pathType = (uint8)TravelNodePathType::walk,
|
TravelNodePath(float distance = 0.1f, float extraCost = 0,
|
||||||
uint32 pathObject = 0, bool calculated = false, std::vector<uint8> maxLevelCreature = {0, 0, 0},
|
uint8 pathType = (uint8)TravelNodePathType::walk,
|
||||||
|
uint32 pathObject = 0, bool calculated = false,
|
||||||
|
std::vector<uint8> maxLevelCreature = {0, 0, 0},
|
||||||
float swimDistance = 0)
|
float swimDistance = 0)
|
||||||
: extraCost(extraCost),
|
: extraCost(extraCost),
|
||||||
calculated(calculated),
|
calculated(calculated),
|
||||||
@ -85,7 +117,7 @@ public:
|
|||||||
{
|
{
|
||||||
if (pathType != (uint8)TravelNodePathType::walk)
|
if (pathType != (uint8)TravelNodePathType::walk)
|
||||||
complete = true;
|
complete = true;
|
||||||
};
|
}
|
||||||
|
|
||||||
TravelNodePath(TravelNodePath* basePath)
|
TravelNodePath(TravelNodePath* basePath)
|
||||||
{
|
{
|
||||||
@ -98,11 +130,11 @@ public:
|
|||||||
swimDistance = basePath->swimDistance;
|
swimDistance = basePath->swimDistance;
|
||||||
pathType = basePath->pathType;
|
pathType = basePath->pathType;
|
||||||
pathObject = basePath->pathObject;
|
pathObject = basePath->pathObject;
|
||||||
};
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
bool getComplete() { return complete || pathType != TravelNodePathType::walk; }
|
bool getComplete() { return complete || pathType != TravelNodePathType::walk; }
|
||||||
std::vector<WorldPosition> getPath() { return path; }
|
std::vector<WorldPosition> GetPath() { return path; }
|
||||||
|
|
||||||
TravelNodePathType getPathType() { return pathType; }
|
TravelNodePathType getPathType() { return pathType; }
|
||||||
uint32 getPathObject() { return pathObject; }
|
uint32 getPathObject() { return pathObject; }
|
||||||
@ -130,9 +162,6 @@ public:
|
|||||||
extraCost = distance / speed;
|
extraCost = distance / speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// void setPortal(bool portal1, uint32 portalId1 = 0) { portal = portal1; portalId = portalId1; }
|
|
||||||
// void setTransport(bool transport1) { transport = transport1; }
|
|
||||||
|
|
||||||
void setPathType(TravelNodePathType pathType1) { pathType = pathType1; }
|
void setPathType(TravelNodePathType pathType1) { pathType = pathType1; }
|
||||||
|
|
||||||
void setPathObject(uint32 pathObject1) { pathObject = pathObject1; }
|
void setPathObject(uint32 pathObject1) { pathObject = pathObject1; }
|
||||||
@ -186,9 +215,10 @@ class TravelNode
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Constructors
|
// Constructors
|
||||||
TravelNode(){};
|
TravelNode() {}
|
||||||
|
|
||||||
TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node", bool important1 = false)
|
TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node",
|
||||||
|
bool important1 = false)
|
||||||
{
|
{
|
||||||
nodeName = nodeName1;
|
nodeName = nodeName1;
|
||||||
point = point1;
|
point = point1;
|
||||||
@ -207,11 +237,11 @@ public:
|
|||||||
void setPoint(WorldPosition point1) { point = point1; }
|
void setPoint(WorldPosition point1) { point = point1; }
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
std::string const getName() { return nodeName; };
|
std::string const getName() { return nodeName; }
|
||||||
WorldPosition* getPosition() { return &point; };
|
WorldPosition* getPosition() { return &point; }
|
||||||
std::unordered_map<TravelNode*, TravelNodePath>* getPaths() { return &paths; }
|
std::unordered_map<TravelNode*, TravelNodePath>* getPaths() { return &paths; }
|
||||||
std::unordered_map<TravelNode*, TravelNodePath*>* getLinks() { return &links; }
|
std::unordered_map<TravelNode*, TravelNodePath*>* getLinks() { return &links; }
|
||||||
bool isImportant() { return important; };
|
bool isImportant() { return important; }
|
||||||
bool isLinked() { return linked; }
|
bool isLinked() { return linked; }
|
||||||
|
|
||||||
bool isTransport()
|
bool isTransport()
|
||||||
@ -235,9 +265,9 @@ public:
|
|||||||
bool isPortal()
|
bool isPortal()
|
||||||
{
|
{
|
||||||
for (auto const& link : *getLinks())
|
for (auto const& link : *getLinks())
|
||||||
if (link.second->getPathType() == TravelNodePathType::portal)
|
if (link.second->getPathType() == TravelNodePathType::areaTrigger ||
|
||||||
|
link.second->getPathType() == TravelNodePathType::staticPortal)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,17 +281,25 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WorldLocation shortcuts
|
// WorldLocation shortcuts
|
||||||
uint32 getMapId() { return point.GetMapId(); }
|
uint32 GetMapId() { return point.GetMapId(); }
|
||||||
float getX() { return point.GetPositionX(); }
|
float getX() { return point.GetPositionX(); }
|
||||||
float getY() { return point.GetPositionY(); }
|
float getY() { return point.GetPositionY(); }
|
||||||
float getZ() { return point.GetPositionZ(); }
|
float getZ() { return point.GetPositionZ(); }
|
||||||
float getO() { return point.GetOrientation(); }
|
float getO() { return point.GetOrientation(); }
|
||||||
float getDistance(WorldPosition pos) { return point.distance(pos); }
|
float getDistance(WorldPosition pos) { return point.distance(pos); }
|
||||||
float getDistance(TravelNode* node) { return point.distance(node->getPosition()); }
|
float getDistance(TravelNode* node)
|
||||||
float fDist(TravelNode* node) { return point.fDist(node->getPosition()); }
|
{
|
||||||
|
return point.distance(node->getPosition());
|
||||||
|
}
|
||||||
|
float fDist(TravelNode* node)
|
||||||
|
{
|
||||||
|
return point.fDist(node->getPosition());
|
||||||
|
}
|
||||||
float fDist(WorldPosition pos) { return point.fDist(pos); }
|
float fDist(WorldPosition pos) { return point.fDist(pos); }
|
||||||
|
|
||||||
TravelNodePath* setPathTo(TravelNode* node, TravelNodePath path = TravelNodePath(), bool isLink = true)
|
TravelNodePath* setPathTo(TravelNode* node,
|
||||||
|
TravelNodePath path = TravelNodePath(),
|
||||||
|
bool isLink = true)
|
||||||
{
|
{
|
||||||
if (this != node)
|
if (this != node)
|
||||||
{
|
{
|
||||||
@ -275,10 +313,20 @@ public:
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasPathTo(TravelNode* node) { return paths.find(node) != paths.end(); }
|
bool hasPathTo(TravelNode* node)
|
||||||
TravelNodePath* getPathTo(TravelNode* node) { return &paths[node]; }
|
{
|
||||||
bool hasCompletePathTo(TravelNode* node) { return hasPathTo(node) && getPathTo(node)->getComplete(); }
|
return paths.find(node) != paths.end();
|
||||||
TravelNodePath* buildPath(TravelNode* endNode, Unit* bot, bool postProcess = false);
|
}
|
||||||
|
TravelNodePath* getPathTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return &paths[node];
|
||||||
|
}
|
||||||
|
bool hasCompletePathTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return hasPathTo(node) && getPathTo(node)->getComplete();
|
||||||
|
}
|
||||||
|
TravelNodePath* BuildPath(TravelNode* endNode, Unit* bot,
|
||||||
|
bool postProcess = false);
|
||||||
|
|
||||||
void setLinkTo(TravelNode* node, float distance = 0.1f)
|
void setLinkTo(TravelNode* node, float distance = 0.1f)
|
||||||
{
|
{
|
||||||
@ -291,20 +339,27 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasLinkTo(TravelNode* node) { return links.find(node) != links.end(); }
|
bool hasLinkTo(TravelNode* node)
|
||||||
float linkCostTo(TravelNode* node) { return paths.find(node)->second.getDistance(); }
|
{
|
||||||
float linkDistanceTo(TravelNode* node) { return paths.find(node)->second.getDistance(); }
|
return links.find(node) != links.end();
|
||||||
|
}
|
||||||
|
float linkCostTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return paths.find(node)->second.getDistance();
|
||||||
|
}
|
||||||
|
float linkDistanceTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return paths.find(node)->second.getDistance();
|
||||||
|
}
|
||||||
void removeLinkTo(TravelNode* node, bool removePaths = false);
|
void removeLinkTo(TravelNode* node, bool removePaths = false);
|
||||||
|
|
||||||
bool isEqual(TravelNode* compareNode);
|
|
||||||
|
|
||||||
// Removes links to other nodes that can also be reached by passing another node.
|
// Removes links to other nodes that can also be reached by passing another node.
|
||||||
bool isUselessLink(TravelNode* farNode);
|
bool isUselessLink(TravelNode* farNode);
|
||||||
void cropUselessLink(TravelNode* farNode);
|
|
||||||
bool cropUselessLinks();
|
bool cropUselessLinks();
|
||||||
|
|
||||||
// Returns all nodes that can be reached from this node.
|
// Returns all nodes that can be reached from this node.
|
||||||
std::vector<TravelNode*> getNodeMap(bool importantOnly = false, std::vector<TravelNode*> ignoreNodes = {});
|
std::vector<TravelNode*> getNodeMap(bool importantOnly = false,
|
||||||
|
std::vector<TravelNode*> ignoreNodes = {});
|
||||||
|
|
||||||
// Checks if it is even possible to route to this node.
|
// Checks if it is even possible to route to this node.
|
||||||
bool hasRouteTo(TravelNode* node)
|
bool hasRouteTo(TravelNode* node)
|
||||||
@ -314,7 +369,10 @@ public:
|
|||||||
routes[mNode] = true;
|
routes[mNode] = true;
|
||||||
|
|
||||||
return routes.find(node) != routes.end();
|
return routes.find(node) != routes.end();
|
||||||
};
|
}
|
||||||
|
|
||||||
|
void clearRoutes() { routes.clear(); }
|
||||||
|
void setRouteTo(TravelNode* node) { routes[node] = true; }
|
||||||
|
|
||||||
void print(bool printFailed = true);
|
void print(bool printFailed = true);
|
||||||
|
|
||||||
@ -344,63 +402,65 @@ protected:
|
|||||||
// uint32 transportId = 0;
|
// uint32 transportId = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PortalNode : public TravelNode
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PortalNode(TravelNode* baseNode) : TravelNode(baseNode){};
|
|
||||||
|
|
||||||
void SetPortal(TravelNode* baseNode, TravelNode* endNode, uint32 portalSpell)
|
|
||||||
{
|
|
||||||
nodeName = baseNode->getName();
|
|
||||||
point = *baseNode->getPosition();
|
|
||||||
paths.clear();
|
|
||||||
links.clear();
|
|
||||||
TravelNodePath path(0.1f, 0.1f, (uint8)TravelNodePathType::teleportSpell, portalSpell, true);
|
|
||||||
setPathTo(endNode, path);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Route step type
|
// Route step type
|
||||||
enum PathNodeType
|
enum class PathNodeType : uint8
|
||||||
{
|
{
|
||||||
NODE_PREPATH = 0,
|
NODE_PREPATH = 0,
|
||||||
NODE_PATH = 1,
|
NODE_PATH = 1,
|
||||||
NODE_NODE = 2,
|
NODE_NODE = 2,
|
||||||
NODE_PORTAL = 3,
|
NODE_AREA_TRIGGER = 3,
|
||||||
NODE_TRANSPORT = 4,
|
NODE_TRANSPORT = 4,
|
||||||
NODE_FLIGHTPATH = 5,
|
NODE_FLIGHTPATH = 5,
|
||||||
NODE_TELEPORT = 6
|
// value 6 reserved (was NODE_TELEPORT — removed with teleportSpell)
|
||||||
|
NODE_STATIC_PORTAL = 7
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PathNodePoint
|
struct PathNodePoint
|
||||||
{
|
{
|
||||||
WorldPosition point;
|
WorldPosition point;
|
||||||
PathNodeType type = NODE_PATH;
|
PathNodeType type = PathNodeType::NODE_PATH;
|
||||||
uint32 entry = 0;
|
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.
|
// A complete list of points the bots has to walk to or teleport to.
|
||||||
class TravelPath
|
class TravelPath
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TravelPath(){};
|
TravelPath() {}
|
||||||
TravelPath(std::vector<PathNodePoint> fullPath1) { fullPath = fullPath1; }
|
TravelPath(std::vector<PathNodePoint> fullPath1)
|
||||||
TravelPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0)
|
{
|
||||||
|
fullPath = fullPath1;
|
||||||
|
}
|
||||||
|
TravelPath(std::vector<WorldPosition> path,
|
||||||
|
PathNodeType type = PathNodeType::NODE_PATH,
|
||||||
|
uint32 entry = 0)
|
||||||
{
|
{
|
||||||
addPath(path, type, entry);
|
addPath(path, type, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addPoint(PathNodePoint point) { fullPath.push_back(point); }
|
void addPoint(PathNodePoint point) { fullPath.push_back(point); }
|
||||||
void addPoint(WorldPosition point, PathNodeType type = NODE_PATH, uint32 entry = 0)
|
void addPoint(WorldPosition point,
|
||||||
|
PathNodeType type = PathNodeType::NODE_PATH,
|
||||||
|
uint32 entry = 0)
|
||||||
{
|
{
|
||||||
fullPath.push_back(PathNodePoint{point, type, entry});
|
fullPath.push_back(PathNodePoint{point, type, entry});
|
||||||
}
|
}
|
||||||
void addPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0)
|
void addPath(std::vector<WorldPosition> path,
|
||||||
|
PathNodeType type = PathNodeType::NODE_PATH,
|
||||||
|
uint32 entry = 0)
|
||||||
{
|
{
|
||||||
for (auto& p : path)
|
for (auto& p : path)
|
||||||
{
|
|
||||||
fullPath.push_back(PathNodePoint{p, type, entry});
|
fullPath.push_back(PathNodePoint{p, type, entry});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
void addPath(std::vector<PathNodePoint> newPath)
|
void addPath(std::vector<PathNodePoint> newPath)
|
||||||
{
|
{
|
||||||
@ -408,8 +468,11 @@ public:
|
|||||||
}
|
}
|
||||||
void clear() { fullPath.clear(); }
|
void clear() { fullPath.clear(); }
|
||||||
|
|
||||||
bool empty() { return fullPath.empty(); }
|
bool empty() const { return fullPath.empty(); }
|
||||||
std::vector<PathNodePoint> getPath() { return fullPath; }
|
size_t size() const { return fullPath.size(); }
|
||||||
|
const PathNodePoint& operator[](size_t idx) const { return fullPath[idx]; }
|
||||||
|
std::vector<PathNodePoint> GetPath() { return fullPath; }
|
||||||
|
const std::vector<PathNodePoint>& GetPathRef() const { return fullPath; }
|
||||||
WorldPosition getFront() { return fullPath.front().point; }
|
WorldPosition getFront() { return fullPath.front().point; }
|
||||||
WorldPosition getBack() { return fullPath.back().point; }
|
WorldPosition getBack() { return fullPath.back().point; }
|
||||||
|
|
||||||
@ -419,17 +482,64 @@ public:
|
|||||||
for (auto const& p : fullPath)
|
for (auto const& p : fullPath)
|
||||||
retVec.push_back(p.point);
|
retVec.push_back(p.point);
|
||||||
return retVec;
|
return retVec;
|
||||||
};
|
}
|
||||||
|
|
||||||
bool makeShortCut(WorldPosition startPos, float maxDist);
|
bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr);
|
||||||
bool shouldMoveToNextPoint(WorldPosition startPos, std::vector<PathNodePoint>::iterator beg,
|
|
||||||
std::vector<PathNodePoint>::iterator ed, std::vector<PathNodePoint>::iterator p,
|
// For each waypoint that's in/under water, snap its Z to the water
|
||||||
float& moveDist, float maxDist);
|
// surface. No-op when destination is itself underwater (caller wants
|
||||||
WorldPosition getNextPoint(WorldPosition startPos, float maxDist, TravelNodePathType& pathType, uint32& entry);
|
// 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();
|
std::ostringstream const print();
|
||||||
|
|
||||||
private:
|
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;
|
std::vector<PathNodePoint> fullPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -438,17 +548,24 @@ class TravelNodeRoute
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TravelNodeRoute() {}
|
TravelNodeRoute() {}
|
||||||
TravelNodeRoute(std::vector<TravelNode*> nodes1) { nodes = nodes1; /*currentNode = route.begin();*/ }
|
TravelNodeRoute(std::vector<TravelNode*> nodes1)
|
||||||
|
{
|
||||||
|
nodes = nodes1;
|
||||||
|
}
|
||||||
bool isEmpty() { return nodes.empty(); }
|
bool isEmpty() { return nodes.empty(); }
|
||||||
|
|
||||||
bool hasNode(TravelNode* node) { return findNode(node) != nodes.end(); }
|
bool hasNode(TravelNode* node)
|
||||||
|
{
|
||||||
|
return findNode(node) != nodes.end();
|
||||||
|
}
|
||||||
float getTotalDistance();
|
float getTotalDistance();
|
||||||
|
|
||||||
std::vector<TravelNode*> getNodes() { return nodes; }
|
std::vector<TravelNode*> getNodes() { return nodes; }
|
||||||
|
|
||||||
TravelPath buildPath(std::vector<WorldPosition> pathToStart = {}, std::vector<WorldPosition> pathToEnd = {},
|
TravelPath BuildPath(
|
||||||
Unit* bot = nullptr);
|
std::vector<WorldPosition> pathToStart = {},
|
||||||
|
std::vector<WorldPosition> pathToEnd = {},
|
||||||
|
Unit* bot = nullptr);
|
||||||
|
|
||||||
std::ostringstream const print();
|
std::ostringstream const print();
|
||||||
|
|
||||||
@ -467,8 +584,11 @@ public:
|
|||||||
TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; }
|
TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; }
|
||||||
|
|
||||||
TravelNode* dataNode;
|
TravelNode* dataNode;
|
||||||
float m_f = 0.0, m_g = 0.0, m_h = 0.0;
|
float totalCost = 0.0;
|
||||||
bool open = false, close = false;
|
float costFromStart = 0.0;
|
||||||
|
float heuristic = 0.0;
|
||||||
|
bool open = false;
|
||||||
|
bool closed = false;
|
||||||
TravelNodeStub* parent = nullptr;
|
TravelNodeStub* parent = nullptr;
|
||||||
uint32 currentGold = 0;
|
uint32 currentGold = 0;
|
||||||
};
|
};
|
||||||
@ -484,14 +604,18 @@ public:
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelNode* addNode(WorldPosition pos, std::string const preferedName = "Travel Node", bool isImportant = false,
|
TravelNode* addNode(WorldPosition pos,
|
||||||
bool checkDuplicate = true, bool transport = false, uint32 transportId = 0);
|
std::string const preferedName = "Travel Node",
|
||||||
|
bool isImportant = false,
|
||||||
|
bool checkDuplicate = true,
|
||||||
|
bool transport = false,
|
||||||
|
uint32 transportId = 0);
|
||||||
void removeNode(TravelNode* node);
|
void removeNode(TravelNode* node);
|
||||||
bool removeNodes()
|
bool removeNodes()
|
||||||
{
|
{
|
||||||
if (m_nMapMtx.try_lock_for(std::chrono::seconds(10)))
|
if (m_nMapMtx.try_lock_for(std::chrono::seconds(10)))
|
||||||
{
|
{
|
||||||
for (auto& node : m_nodes)
|
for (auto& node : nodes)
|
||||||
removeNode(node);
|
removeNode(node);
|
||||||
|
|
||||||
m_nMapMtx.unlock();
|
m_nMapMtx.unlock();
|
||||||
@ -499,28 +623,32 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
void fullLinkNode(TravelNode* startNode, Unit* bot);
|
void fullLinkNode(TravelNode* startNode, Unit* bot);
|
||||||
|
|
||||||
// Get all nodes
|
// Get all nodes
|
||||||
std::vector<TravelNode*> getNodes() { return m_nodes; }
|
std::vector<TravelNode*> getNodes() { return nodes; }
|
||||||
std::vector<TravelNode*> getNodes(WorldPosition pos, float range = -1);
|
std::vector<TravelNode*> getNodes(WorldPosition pos, float range = -1);
|
||||||
|
|
||||||
// Find nearest node.
|
// Find nearest node.
|
||||||
TravelNode* getNode(TravelNode* sameNode)
|
TravelNode* getNode(TravelNode* sameNode)
|
||||||
{
|
{
|
||||||
for (auto& node : m_nodes)
|
for (auto& node : nodes)
|
||||||
{
|
{
|
||||||
if (node->getName() == sameNode->getName() && node->getPosition() == sameNode->getPosition())
|
if (node->getName() == sameNode->getName()
|
||||||
|
&& node->getPosition() == sameNode->getPosition())
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelNode* getNode(WorldPosition pos, std::vector<WorldPosition>& ppath, Unit* bot = nullptr, float range = -1);
|
TravelNode* getNode(WorldPosition pos,
|
||||||
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr, float range = -1)
|
std::vector<WorldPosition>& ppath,
|
||||||
|
Unit* bot = nullptr, float range = -1);
|
||||||
|
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr,
|
||||||
|
float range = -1)
|
||||||
{
|
{
|
||||||
std::vector<WorldPosition> ppath;
|
std::vector<WorldPosition> ppath;
|
||||||
return getNode(pos, ppath, bot, range);
|
return getNode(pos, ppath, bot, range);
|
||||||
@ -536,18 +664,16 @@ public:
|
|||||||
return rNodes[urand(0, rNodes.size() - 1)];
|
return rNodes[urand(0, rNodes.size() - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds the best nodePath between two nodes
|
// Finds the best nodePath between two nodes (A* over the node graph)
|
||||||
TravelNodeRoute getRoute(TravelNode* start, TravelNode* goal, Player* bot = nullptr);
|
TravelNodeRoute GetNodeRoute(TravelNode* start, TravelNode* goal,
|
||||||
|
Player* bot);
|
||||||
|
|
||||||
// Find the best node between two positions
|
// Picks the nearest start/end nodes for two world positions and runs A*
|
||||||
TravelNodeRoute getRoute(WorldPosition startPos, WorldPosition endPos, std::vector<WorldPosition>& startPath,
|
// over the node graph to return a full route between them.
|
||||||
Player* bot = nullptr);
|
TravelNodeRoute FindRouteNearestNodes(WorldPosition startPos,
|
||||||
|
WorldPosition endPos,
|
||||||
// Find the full path between those locations
|
std::vector<WorldPosition>& startPath,
|
||||||
static TravelPath getFullPath(WorldPosition startPos, WorldPosition endPos, Player* bot = nullptr);
|
Player* bot = nullptr);
|
||||||
|
|
||||||
// Manage/update nodes
|
|
||||||
void manageNodes(Unit* bot, bool mapFull = false);
|
|
||||||
|
|
||||||
void setHasToGen() { hasToGen = true; }
|
void setHasToGen() { hasToGen = true; }
|
||||||
|
|
||||||
@ -563,19 +689,18 @@ public:
|
|||||||
void removeUselessPaths();
|
void removeUselessPaths();
|
||||||
void calculatePathCosts();
|
void calculatePathCosts();
|
||||||
void generateTaxiPaths();
|
void generateTaxiPaths();
|
||||||
void generatePaths();
|
void generatePaths(bool fullGen = false);
|
||||||
|
|
||||||
void generateAll();
|
void generateAll();
|
||||||
|
|
||||||
|
void Init();
|
||||||
|
|
||||||
void printMap();
|
void printMap();
|
||||||
|
|
||||||
void printNodeStore();
|
void printNodeStore();
|
||||||
void saveNodeStore();
|
void saveNodeStore();
|
||||||
void loadNodeStore();
|
void LoadNodeStore();
|
||||||
|
|
||||||
bool cropUselessNode(TravelNode* startNode);
|
|
||||||
TravelNode* addZoneLinkNode(TravelNode* startNode);
|
|
||||||
TravelNode* addRandomExtNode(TravelNode* startNode);
|
|
||||||
|
|
||||||
void calcMapOffset();
|
void calcMapOffset();
|
||||||
WorldPosition getMapOffset(uint32 mapId);
|
WorldPosition getMapOffset(uint32 mapId);
|
||||||
@ -584,8 +709,23 @@ public:
|
|||||||
void InitTaxiGraph();
|
void InitTaxiGraph();
|
||||||
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
|
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::shared_timed_mutex m_nMapMtx;
|
||||||
std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TravelNodeMap() = default;
|
TravelNodeMap() = default;
|
||||||
@ -601,13 +741,15 @@ private:
|
|||||||
void BuildTaxiGraph();
|
void BuildTaxiGraph();
|
||||||
void ComputeAllPaths();
|
void ComputeAllPaths();
|
||||||
std::unordered_map<uint32, uint32> BFS(uint32 startNode);
|
std::unordered_map<uint32, uint32> BFS(uint32 startNode);
|
||||||
std::vector<uint32> BuildPath(uint32 fromNode, uint32 toNode,
|
std::vector<uint32> BuildPath(
|
||||||
const std::unordered_map<uint32, uint32>& parentMap);
|
uint32 fromNode, uint32 toNode,
|
||||||
|
const std::unordered_map<uint32, uint32>& parentMap);
|
||||||
|
|
||||||
std::unordered_map<uint32, std::vector<uint32>> taxiGraph;
|
std::unordered_map<uint32, std::vector<uint32>> m_taxiGraph;
|
||||||
std::map<uint32, std::map<uint32, std::vector<uint32>>> taxiPathCache;
|
std::map<uint32, std::map<uint32, std::vector<uint32>>>
|
||||||
|
m_taxiPathCache;
|
||||||
|
|
||||||
std::vector<TravelNode*> m_nodes;
|
std::vector<TravelNode*> nodes;
|
||||||
|
|
||||||
std::vector<std::pair<uint32, WorldPosition>> mapOffsets;
|
std::vector<std::pair<uint32, WorldPosition>> mapOffsets;
|
||||||
|
|
||||||
|
|||||||
@ -72,7 +72,6 @@ bool PlayerbotAIConfig::Initialize()
|
|||||||
globalCoolDown = sConfigMgr->GetOption<int32>("AiPlayerbot.GlobalCooldown", 500);
|
globalCoolDown = sConfigMgr->GetOption<int32>("AiPlayerbot.GlobalCooldown", 500);
|
||||||
maxWaitForMove = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxWaitForMove", 5000);
|
maxWaitForMove = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxWaitForMove", 5000);
|
||||||
disableMoveSplinePath = sConfigMgr->GetOption<int32>("AiPlayerbot.DisableMoveSplinePath", 0);
|
disableMoveSplinePath = sConfigMgr->GetOption<int32>("AiPlayerbot.DisableMoveSplinePath", 0);
|
||||||
maxMovementSearchTime = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxMovementSearchTime", 3);
|
|
||||||
expireActionTime = sConfigMgr->GetOption<int32>("AiPlayerbot.ExpireActionTime", 5000);
|
expireActionTime = sConfigMgr->GetOption<int32>("AiPlayerbot.ExpireActionTime", 5000);
|
||||||
dispelAuraDuration = sConfigMgr->GetOption<int32>("AiPlayerbot.DispelAuraDuration", 700);
|
dispelAuraDuration = sConfigMgr->GetOption<int32>("AiPlayerbot.DispelAuraDuration", 700);
|
||||||
reactDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReactDelay", 100);
|
reactDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReactDelay", 100);
|
||||||
@ -89,6 +88,7 @@ bool PlayerbotAIConfig::Initialize()
|
|||||||
|
|
||||||
farDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FarDistance", 20.0f);
|
farDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FarDistance", 20.0f);
|
||||||
sightDistance = sConfigMgr->GetOption<float>("AiPlayerbot.SightDistance", 100.0f);
|
sightDistance = sConfigMgr->GetOption<float>("AiPlayerbot.SightDistance", 100.0f);
|
||||||
|
transportSkipRide = sConfigMgr->GetOption<bool>("AiPlayerbot.TransportSkipRide", false);
|
||||||
spellDistance = sConfigMgr->GetOption<float>("AiPlayerbot.SpellDistance", 28.5f);
|
spellDistance = sConfigMgr->GetOption<float>("AiPlayerbot.SpellDistance", 28.5f);
|
||||||
shootDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ShootDistance", 5.0f);
|
shootDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ShootDistance", 5.0f);
|
||||||
healDistance = sConfigMgr->GetOption<float>("AiPlayerbot.HealDistance", 38.5f);
|
healDistance = sConfigMgr->GetOption<float>("AiPlayerbot.HealDistance", 38.5f);
|
||||||
@ -678,6 +678,7 @@ bool PlayerbotAIConfig::Initialize()
|
|||||||
autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false);
|
autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false);
|
||||||
autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true);
|
autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true);
|
||||||
enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true);
|
enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true);
|
||||||
|
enableTravelNodes = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableTravelNodes", false);
|
||||||
|
|
||||||
RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15);
|
RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15);
|
||||||
RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20);
|
RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20);
|
||||||
|
|||||||
@ -91,8 +91,14 @@ public:
|
|||||||
bool EnableICCBuffs;
|
bool EnableICCBuffs;
|
||||||
bool allowAccountBots, allowGuildBots, allowTrustedAccountBots;
|
bool allowAccountBots, allowGuildBots, allowTrustedAccountBots;
|
||||||
bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat;
|
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;
|
dispelAuraDuration, passiveDelay, repeatDelay, errorDelay, rpgDelay, sitDelay, returnDelay, lootDelay;
|
||||||
|
// Transport handling:
|
||||||
|
// false (default) = teleport-board, ride the transport, teleport-disembark
|
||||||
|
// true = skip the ride entirely (teleport directly across)
|
||||||
|
// AC has no transport-surface mmap so an in-deck walking mode can't be
|
||||||
|
// faithfully implemented — the on-board phase always teleport-snaps.
|
||||||
|
bool transportSkipRide;
|
||||||
bool dynamicReactDelay;
|
bool dynamicReactDelay;
|
||||||
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
|
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
|
||||||
tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance,
|
tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance,
|
||||||
@ -375,6 +381,7 @@ public:
|
|||||||
bool autoLearnTrainerSpells;
|
bool autoLearnTrainerSpells;
|
||||||
bool autoDoQuests;
|
bool autoDoQuests;
|
||||||
bool enableNewRpgStrategy;
|
bool enableNewRpgStrategy;
|
||||||
|
bool enableTravelNodes;
|
||||||
std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight;
|
std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight;
|
||||||
bool syncLevelWithPlayers;
|
bool syncLevelWithPlayers;
|
||||||
bool autoLearnQuestSpells;
|
bool autoLearnQuestSpells;
|
||||||
|
|||||||
@ -16,10 +16,12 @@
|
|||||||
#include "BattleGroundTactics.h"
|
#include "BattleGroundTactics.h"
|
||||||
#include "Chat.h"
|
#include "Chat.h"
|
||||||
#include "GuildTaskMgr.h"
|
#include "GuildTaskMgr.h"
|
||||||
|
#include "MapMgr.h"
|
||||||
#include "PerfMonitor.h"
|
#include "PerfMonitor.h"
|
||||||
#include "PlayerbotMgr.h"
|
#include "PlayerbotMgr.h"
|
||||||
#include "RandomPlayerbotMgr.h"
|
#include "RandomPlayerbotMgr.h"
|
||||||
#include "ScriptMgr.h"
|
#include "ScriptMgr.h"
|
||||||
|
#include "TravelNode.h"
|
||||||
|
|
||||||
using namespace Acore::ChatCommands;
|
using namespace Acore::ChatCommands;
|
||||||
|
|
||||||
@ -32,6 +34,7 @@ public:
|
|||||||
{
|
{
|
||||||
static ChatCommandTable playerbotsDebugCommandTable = {
|
static ChatCommandTable playerbotsDebugCommandTable = {
|
||||||
{"bg", HandleDebugBGCommand, SEC_GAMEMASTER, Console::Yes},
|
{"bg", HandleDebugBGCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
|
{"zone", HandleDebugZoneCommand, SEC_GAMEMASTER, Console::No},
|
||||||
};
|
};
|
||||||
|
|
||||||
static ChatCommandTable playerbotsAccountCommandTable = {
|
static ChatCommandTable playerbotsAccountCommandTable = {
|
||||||
@ -41,11 +44,16 @@ public:
|
|||||||
{"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No},
|
{"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static ChatCommandTable playerbotsTravelCommandTable = {
|
||||||
|
{"generatenode", HandleGenerateTravelNodesCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
|
};
|
||||||
|
|
||||||
static ChatCommandTable playerbotsCommandTable = {
|
static ChatCommandTable playerbotsCommandTable = {
|
||||||
{"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No},
|
{"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No},
|
||||||
{"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes},
|
{"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
{"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes},
|
{"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
{"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes},
|
{"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
|
{"travel", playerbotsTravelCommandTable},
|
||||||
{"debug", playerbotsDebugCommandTable},
|
{"debug", playerbotsDebugCommandTable},
|
||||||
{"account", playerbotsAccountCommandTable},
|
{"account", playerbotsAccountCommandTable},
|
||||||
};
|
};
|
||||||
@ -106,11 +114,194 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool HandleGenerateTravelNodesCommand(ChatHandler* handler, char const* /*args*/)
|
||||||
|
{
|
||||||
|
handler->PSendSysMessage("Regenerating travel node paths...");
|
||||||
|
LOG_INFO("playerbots", "Manual travel node regeneration started via console command.");
|
||||||
|
sTravelNodeMap.generateAll();
|
||||||
|
handler->PSendSysMessage("Travel node regeneration complete. Paths saved to database.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool HandleDebugBGCommand(ChatHandler* handler, char const* args)
|
static bool HandleDebugBGCommand(ChatHandler* handler, char const* args)
|
||||||
{
|
{
|
||||||
return BGTactics::HandleConsoleCommand(handler, args);
|
return BGTactics::HandleConsoleCommand(handler, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
static bool HandleSetSecurityKeyCommand(ChatHandler* handler, char const* args)
|
||||||
{
|
{
|
||||||
if (!args || !*args)
|
if (!args || !*args)
|
||||||
|
|||||||
24
todo.md
Normal file
24
todo.md
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user