Compare commits

...

11 Commits

36 changed files with 151625 additions and 1239 deletions

51
.claude/settings.json Normal file
View File

@ -0,0 +1,51 @@
{
"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 \"^//\")"
],
"additionalDirectories": [
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\common\\Collision\\Maps",
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\tools\\mmaps_generator",
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\server\\game\\Movement\\MovementGenerators"
]
}
}

View File

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

View File

@ -343,10 +343,6 @@ AiPlayerbot.MaxWaitForMove = 5000
# 2 - MoveSplinePath disabled everywhere
AiPlayerbot.DisableMoveSplinePath = 0
# Max search time for movement (higher for better movement on slopes)
# Default: 3
AiPlayerbot.MaxMovementSearchTime = 3
# Action expiration time
AiPlayerbot.ExpireActionTime = 5000
@ -1054,6 +1050,12 @@ AiPlayerbot.RestrictedHealerDPSMaps = "33,34,36,43,47,48,70,90,109,129,209,229,2
# Default: 1 (enabled)
AiPlayerbot.EnableNewRpgStrategy = 1
# Use pre-computed travel node paths for long-distance movement (>300 yards).
# When enabled, bots use the travel node graph (A*, flight paths, transports)
# instead of repeated mmap hops. Experimental.
# Default: 0 (disabled)
AiPlayerbot.EnableTravelNodes = 0
# Control probability weights for RPG status of bots. Takes effect only when the status meets its premise.
# Sum of weights need not be 100. Set to 0 to disable the status.
#

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

351
extract_ac_client_data.sh Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
#include "Action.h"
#include "LastMovementValue.h"
#include "PathGenerator.h"
#include "PlayerbotAIConfig.h"
class Player;
@ -22,12 +23,33 @@ class Position;
#define ANGLE_90_DEG M_PI_2
#define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f)
// Default acceptable path types for GeneratePath
constexpr uint32 DEFAULT_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE;
constexpr uint32 RELAXED_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
struct PathResult
{
Movement::PointsArray points;
G3D::Vector3 actualEnd;
G3D::Vector3 end;
PathType pathType;
bool reachable;
};
class MovementAction : public Action
{
public:
MovementAction(PlayerbotAI* botAI, std::string const name);
protected:
// Emit a one-line trace describing the imminent movement. No-op
// unless the bot has the "debug move" non-combat strategy.
// Subclasses (e.g. NewRpgBaseAction) may override to append richer
// context such as RPG status and target name. Optional `extra`
// is appended verbatim (use it to attach hop labels like
// "node:Stormwind innkeeper" or fallback reasons).
virtual void EmitDebugMove(char const* method, char const* generator, float x, float y, float z, char const* extra = nullptr);
bool JumpTo(uint32 mapId, float x, float y, float z, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool MoveNear(uint32 mapId, float x, float y, float z, float distance = sPlayerbotAIConfig.contactDistance,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
@ -66,6 +88,31 @@ protected:
bool FleePosition(Position pos, float radius, uint32 minInterval = 1000);
bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList);
PathResult GeneratePath(float x, float y, float z, uint32 acceptMask = DEFAULT_PATH_ACCEPT_MASK, bool forceDestination = false);
bool GetTravelPlan(TravelPlan& plan, WorldPosition destination);
bool ExecuteTravelPlan(TravelPlan& state);
// Transport boarding helpers (shared by FollowAction and travel plan)
static Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref,
uint32 phaseMask, float x, float y, float z);
static bool FindBoardingPointOnTransport(Map* map, Transport* transport,
WorldObject* ref, float refX, float refY, float refZ,
float botX, float botY, float botZ,
float& outX, float& outY, float& outZ);
bool BoardTransport(Transport* transport);
private:
bool LaunchWalkSpline(TravelPlan& state);
bool CheckSplineProgress(TravelPlan& state);
bool MoveToSpline(TravelPlan& state, WorldPosition target);
// Per-segment mmap refinement of a travel-node-graph walk batch.
// The graph stores offline-baked coords whose straight-line
// interpolation may pass through geometry the bot can't actually
// traverse. Returns false if any segment is unwalkable per the
// live navmesh, in which case the caller should abort the plan.
bool RefineWalkPoints(std::vector<G3D::Vector3>& walkPoints);
protected:
struct CheckAngle
{
@ -74,10 +121,6 @@ protected:
};
private:
// float SearchBestGroundZForPath(float x, float y, float z, bool generatePath, float range = 20.0f, bool
// normal_only = false, float step = 8.0f);
const Movement::PointsArray SearchForBestPath(float x, float y, float z, float& modified_z, int maxSearchCount = 5,
bool normal_only = false, float step = 8.0f);
bool wasMovementRestricted = false;
void DoMovePoint(Unit* unit, float x, float y, float z, bool generatePath, bool backwards);
};

View File

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

View File

@ -19,6 +19,7 @@
#include "PathGenerator.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "QuestDef.h"
#include "Random.h"
#include "SharedDefines.h"
@ -120,17 +121,8 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/)
}
break;
}
case RPG_TRAVEL_FLIGHT:
{
auto& data = std::get<NewRpgInfo::TravelFlight>(info.data);
if (data.inFlight && !bot->IsInFlight())
{
// flight arrival
info.ChangeToIdle();
return true;
}
break;
}
// RPG_TRAVEL_FLIGHT arrival is handled inside NewRpgTravelFlightAction
// so the flight action owns both take-off and landing transitions.
case RPG_REST:
{
// REST -> IDLE
@ -301,6 +293,9 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0;
data.pos = WorldPosition();
data.objectiveIdx = 0;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
}
}
if (data.pos == WorldPosition())
@ -329,15 +324,31 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0;
data.pos = pos;
data.objectiveIdx = objectiveIdx;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
}
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
{
// yield to attack-anything if a quest mob is right next to us
if (HasNearbyQuestMob(15.0f))
return false;
// Note: previously yielded ~10%/tick when any hostile was
// within 25y. That overrode the do-quest multiplier in
// practice (combined with bots getting aggroed on the way,
// which ALSO bypasses the multiplier via combat engine) and
// bots ended up grinding their way to POIs instead of
// travelling. Quest-mob exception above is kept so we don't
// walk past a quest target while gathering. Anything else
// hostile is the multiplier's job to throttle — and bots
// that DO get aggroed switch to combat engine where the
// class strategy handles it.
if (MoveFarTo(data.pos))
return true;
// Long-range sampler couldn't land a candidate — nudge the
// bot a short distance so the next tick retries from a
// different position instead of sitting idle.
// sampler found nothing — nudge so next tick tries a new pos
return MoveRandomNear(10.0f);
}
// Now we are near the quest objective
@ -382,14 +393,72 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0;
data.pos = WorldPosition();
data.objectiveIdx = 0;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
return true;
}
// At the POI: keep the bot actively placed but avoid large
// random 20yd hops that look like pacing back and forth. A small
// ~8yd wander reads as the bot looking around while grind/loot
// strategies do their work.
return MoveRandomNear(8.0f);
// at POI: drive toward specific objectives first
if (TryUseQuestItem(data.pursuedUseGO, data.pursuedUseTarget))
return true;
if (TryLootQuestGO(data.pursuedLootGO))
return true;
if (TryUseQuestGO(data.pursuedUseGO))
return true;
// gather quests: roam for spawns. kill quests: yield to grind.
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (quest)
{
int32 obj = data.objectiveIdx;
bool isGatherObjective = false;
if (obj < QUEST_OBJECTIVES_COUNT)
{
int32 entry = quest->RequiredNpcOrGo[obj];
if (entry < 0) // GO objective
isGatherObjective = true;
if (entry == 0 && obj < QUEST_ITEM_OBJECTIVES_COUNT && quest->RequiredItemId[obj])
isGatherObjective = true;
}
else if (obj < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT)
{
isGatherObjective = true;
}
// source-item quest: need to find the target to use it on
if (quest->GetSrcItemId())
isGatherObjective = true;
if (isGatherObjective)
return MoveRandomNear(20.0f);
}
// kill quest: walk toward the marker before handing off to grind.
// lastReachPOI trips at ~10y so without this the bot fights on the
// edge and never reaches the dense cluster. Skip if a quest mob is
// in sight (might be the target) or a hostile is mid-pull.
if (bot->GetDistance(data.pos) > 5.0f)
{
if (HasNearbyQuestMob(30.0f))
return false;
GuidVector nearby = AI_VALUE(GuidVector, "possible targets");
bool hostileClose = false;
for (ObjectGuid guid : nearby)
{
Unit* u = botAI->GetUnit(guid);
if (u && u->IsAlive() && bot->GetDistance(u) < 15.0f)
{
hostileClose = true;
break;
}
}
if (!hostileClose)
return MoveFarTo(data.pos);
}
// yield to grind
return false;
}
bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
@ -423,6 +492,15 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0;
data.pos = pos;
data.objectiveIdx = -1;
// Drop the spline + lastPath that DoIncompleteQuest committed
// to the now-completed objective. Without this, MoveFarTo on
// the next tick hits the bot->isMoving() / lastPath-reuse
// early-exits at the top of MoveFarTo and rides the stale
// path instead of replanning toward the turn-in POI. (This
// is what `.playerbot bot self` masks by recreating the AI.)
bot->GetMotionMaster()->Clear();
AI_VALUE(LastMovement&, "last movement").clear();
}
if (data.pos == WorldPosition())
@ -453,7 +531,9 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
botAI->rpgInfo.ChangeToIdle();
return true;
}
return false;
// waiting for SearchQuestGiverAndAcceptOrReward to pick up the NPC;
// wander instead of false so we don't fall through to grind
return MoveRandomNear(15.0f);
}
bool NewRpgTravelFlightAction::Execute(Event /*event*/)
@ -464,6 +544,22 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
return false;
auto& data = *dataPtr;
// Arrival: we had boarded a flight (data.inFlight) and we're no longer in
// it → we just landed. Special-case Rut'theran: walk to the portal GO so
// it teleports the bot into Darnassus, flipping the zone to AREA_DARNASSUS
// so this branch falls through to ChangeToIdle on the next tick.
if (data.inFlight && !bot->IsInFlight())
{
if (bot->GetZoneId() == AREA_TELDRASSIL)
{
static WorldPosition const rutTheranPortalEntrance(1, 8799.41f, 969.787f, 26.2409f, 0.0f);
return MoveFarTo(rutTheranPortalEntrance);
}
info.ChangeToIdle();
return true;
}
if (bot->IsInFlight())
{
data.inFlight = true;
@ -479,19 +575,9 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
info.ChangeToIdle();
return true;
}
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
return MoveFarTo(flightMaster);
std::vector<uint32> nodes = data.path;
botAI->RemoveShapeshift();
if (bot->IsMounted())
bot->Dismount();
if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0))
if (!TakeFlight(data.path, flightMaster))
{
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);
info.ChangeToIdle();
return true;
}

View File

@ -47,11 +47,11 @@ public:
protected:
// static NewRpgStatusTransitionProb transitionMat;
const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS ;
const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS ;
const int32 statusRestDuration = 30 * IN_MILLISECONDS ;
const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS ;
const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS ;
const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS;
const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS;
const int32 statusRestDuration = 30 * IN_MILLISECONDS;
const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS;
const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS;
};
class NewRpgGoGrindAction : public NewRpgBaseAction

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ protected:
bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE);
bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr);
bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool TakeFlight(std::vector<uint32> const& taxiNodes, Creature* flightMaster);
/* QUEST RELATED CHECK */
ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f);
@ -50,6 +51,23 @@ protected:
bool TurnInQuest(Quest const* quest, ObjectGuid guid);
bool OrganizeQuestLog();
/* QUEST PROGRESSION HELPERS (at POI) */
// Walk to a GO that drops a needed quest item. The loot strategy
// opens and loots it once in range.
bool TryLootQuestGO(ObjectGuid& pursuedGO, float searchRange = 60.0f);
// Walk to / use a GO that is itself the objective (rune, lever,
// altar, coffin — RequiredNpcOrGo with a negative entry).
bool TryUseQuestGO(ObjectGuid& pursuedGO, float searchRange = 60.0f);
// Fire a quest item's OnUse spell at the right target: a spell-focus
// GO (moonwell), a required creature, or the bot itself.
bool TryUseQuestItem(ObjectGuid& pursuedGO, ObjectGuid& pursuedTarget, float searchRange = 60.0f);
// True when a quest-relevant mob is within range — used during
// travel so we yield to attack-anything instead of running past.
bool HasNearbyQuestMob(float range = 20.0f);
protected:
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
static WorldPosition SelectRandomGrindPos(Player* bot);
@ -58,17 +76,18 @@ protected:
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status);
protected:
/* FOR MOVE FAR */
const float pathFinderDis = 70.0f;
// Time without real progress toward dest before MoveFarTo
// falls back to teleport recovery. Kept short enough that a
// bot truly oscillating around an unreachable destination
// (mmap returning non-progressing partial paths, or NOPATH +
// cone fallback wandering) doesn't spin for 5 minutes before
// the teleport fires, but long enough that a genuine long
// walk that is slowly making progress never triggers it.
const uint32 stuckTime = 90 * 1000;
private:
void StartTravelPlan(WorldPosition dest);
bool UpdateTravelPlan();
// Centralized dispatch helper. Applies underwater fixup, ClipPath
// (truncate at first hostile in attack range with LOS, level+5 cap),
// inactive-bot teleport (with self-bot carve-out), masterWalking
// mode, pre-dispatch state cleanup, then dispatches via
// MoveSplinePath and schedules via WaitForReach formula.
bool DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label);
};
#endif

View File

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

View File

@ -1,6 +1,8 @@
#ifndef _PLAYERBOT_NEWRPGINFO_H
#define _PLAYERBOT_NEWRPGINFO_H
#include <deque>
#include "Define.h"
#include "ObjectGuid.h"
#include "ObjectMgr.h"
@ -8,6 +10,7 @@
#include "Strategy.h"
#include "Timer.h"
#include "TravelMgr.h"
#include "TravelNode.h"
using NewRpgStatusTransitionProb = std::vector<std::vector<int>>;
@ -45,6 +48,11 @@ struct NewRpgInfo
int32 objectiveIdx{0};
WorldPosition pos{};
uint32 lastReachPOI{0};
// committed target per objective type. stops zig-zagging in
// dense spawn clusters when "nearest" would flip each tick.
ObjectGuid pursuedLootGO{}; // GOs we loot (lilies, eggs)
ObjectGuid pursuedUseGO{}; // GOs we click or focus on
ObjectGuid pursuedUseTarget{}; // creature we apply an item to
};
// RPG_TRAVEL_FLIGHT
struct TravelFlight
@ -70,12 +78,10 @@ struct NewRpgInfo
uint32 startT{0}; // start timestamp of the current status
// MOVE_FAR
float nearestMoveFarDis{FLT_MAX};
uint32 stuckTs{0};
uint32 stuckAttempts{0};
WorldPosition moveFarPos;
// END MOVE_FAR
// Travel Node System
TravelPlan travelPlan;
bool HasActiveTravelPlan() const { return travelPlan.IsActive(); }
void ClearTravel() { travelPlan.Reset(); }
using RpgData = std::variant<
Idle,
@ -103,7 +109,6 @@ struct NewRpgInfo
void ChangeToIdle();
bool CanChangeTo(NewRpgStatus status);
void Reset();
void SetMoveFarTo(WorldPosition pos);
std::string ToString();
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@
#include "ChatHelper.h"
#include "MapCollisionData.h"
#include "MapMgr.h"
#include "ModelIgnoreFlags.h"
#include "PathGenerator.h"
#include "Playerbots.h"
#include "RaceMgr.h"
@ -681,93 +682,6 @@ std::vector<WorldPosition> WorldPosition::frommGridCoord(mGridCoord GridCoord)
return retVec;
}
// TODO: Cleanup — make this actually work.
void WorldPosition::loadMapAndVMap(uint32 mapId, uint8 x, uint8 y)
{
std::string const fileName = "load_map_grid.csv";
/*
if (isOverworld() && false || false)
{
if (!MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(mapId, x, y))
if (sPlayerbotAIConfig.hasLog(fileName))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr();
out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1")
<< ",";
printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true);
sPlayerbotAIConfig.log(fileName, out.str().c_str());
}
}
else
{
// This needs to be disabled or maps will not load.
// Needs more testing to check for impact on movement.
if (false)
if (!TravelMgr::instance().isBadVmap(mapId, x, y))
{
// load VMAPs for current map/grid...
const MapEntry* i_mapEntry = sMapStore.LookupEntry(mapId);
//const char* mapName = i_mapEntry ? i_mapEntry->name[sWorld->GetDefaultDbcLocale()] : "UNNAMEDMAP\x0"; //not used, (usage are commented out below), line marked for removal.
int vmapLoadResult = VMAP::VMapFactory::createOrGetVMapMgr()->loadMap(
(sWorld->GetDataPath() + "vmaps").c_str(), mapId, x, y);
switch (vmapLoadResult)
{
case VMAP::VMAP_LOAD_RESULT_OK:
// LOG_ERROR("playerbots", "VMAP loaded name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})",
// mapName, mapId, x, y, x, y);
break;
case VMAP::VMAP_LOAD_RESULT_ERROR:
// LOG_ERROR("playerbots", "Could not load VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{},
// y:{})", mapName, mapId, x, y, x, y);
TravelMgr::instance().addBadVmap(mapId, x, y);
break;
case VMAP::VMAP_LOAD_RESULT_IGNORED:
TravelMgr::instance().addBadVmap(mapId, x, y);
// LOG_INFO("playerbots", "Ignored VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})",
// mapName, mapId, x, y, x, y);
break;
}
if (sPlayerbotAIConfig.hasLog(fileName))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr();
out << "+00,\"vmap\", " << x << "," << y << ", " << (TravelMgr::instance().isBadVmap(mapId, x, y) ? "0" : "1")
<< ",";
printWKT(frommGridCoord(mGridCoord(x, y)), out, 1, true);
sPlayerbotAIConfig.log(fileName, out.str().c_str());
}
}
*/
if (!TravelMgr::instance().isBadMmap(mapId, x, y))
{
// load navmesh
Map* map = getMap();
if (map && map->GetMapCollisionData().LoadMMapTile(x, y) == MMAP::MMAP_LOAD_RESULT_ERROR)
TravelMgr::instance().addBadMmap(mapId, x, y);
if (sPlayerbotAIConfig.hasLog(fileName))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr();
out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1")
<< ",";
printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true);
sPlayerbotAIConfig.log(fileName, out.str().c_str());
}
}
}
void WorldPosition::loadMapAndVMaps(WorldPosition secondPos)
{
for (auto& grid : getmGridCoords(secondPos))
{
loadMapAndVMap(GetMapId(), grid.first, grid.second);
}
}
std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vector3> path)
{
std::vector<WorldPosition> retVec;
@ -780,39 +694,101 @@ std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vecto
// A single pathfinding attempt from one position to another. Returns pathfinding status and path.
std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, Unit* bot)
{
if (!bot)
return {};
Unit* pathUnit = bot;
Creature* tempCreature = nullptr;
// Load mmaps and vmaps between the two points.
loadMapAndVMaps(startPos);
if (!pathUnit)
{
Map* map = sMapMgr->FindBaseMap(startPos.GetMapId());
if (!map)
return {};
PathGenerator path(bot);
path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ());
tempCreature = new Creature();
if (!tempCreature->Create(map->GenerateLowGuid<HighGuid::Unit>(), map,
PHASEMASK_NORMAL, 1 /*entry*/, 0,
startPos.GetPositionX(), startPos.GetPositionY(),
startPos.GetPositionZ(), 0))
{
delete tempCreature;
return {};
}
pathUnit = tempCreature;
map->EnsureGridCreated(Acore::ComputeGridCoord(startPos.GetPositionX(), startPos.GetPositionY()));
map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY()));
}
PathGenerator path(pathUnit);
auto result = getPathStepFrom(startPos, path);
if (tempCreature)
delete tempCreature;
return result;
}
// Pathfinder-reuse overload — caller owns the PathGenerator and any
// per-call configuration. Used by getPathFromPath to thread one
// PathGenerator through the whole 40-step chain instead of
// constructing a new one per step.
std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, PathGenerator& path)
{
// Explicit-start overload. Without this, the chain begins from the
// unit's current position every step and never advances.
path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ(),
GetPositionX(), GetPositionY(), GetPositionZ(), false);
Movement::PointsArray points = path.GetPath();
PathType type = path.GetPathType();
if (sPlayerbotAIConfig.hasLog("pathfind_attempt_point.csv"))
// PathType is a bitmask. AC's PathGenerator returns
// NORMAL | NOT_USING_PATH when start/end poly is INVALID_POLYREF
// (BuildShortcut produces a 2-point straight line through whatever's
// in the way). Reject those to avoid silently dispatching a
// geometry-ignoring shortcut.
if (!(type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) ||
(type & PATHFIND_NOT_USING_PATH))
return {};
std::vector<WorldPosition> retvec = fromPointsArray(points);
// Underwater path-extension. When PATHFIND_INCOMPLETE ends within
// 50y of dest and both endpoints are underwater with LOS, extend
// by one 5y step (or straight to dest if <5y). Lets bots traverse
// navmesh-poor water volumes.
if (type & PATHFIND_INCOMPLETE)
{
std::ostringstream out;
out << std::fixed << std::setprecision(1);
printWKT({startPos, *this}, out);
sPlayerbotAIConfig.log("pathfind_attempt_point.csv", out.str().c_str());
WorldPosition end = *this;
WorldPosition lastPoint = retvec.back();
float dist = lastPoint.distance(&end);
if (dist < 50.0f && lastPoint.isUnderWater() && end.isUnderWater())
{
Map* m = end.getMap();
bool inLos = m && m->isInLineOfSight(
lastPoint.GetPositionX(), lastPoint.GetPositionY(), lastPoint.GetPositionZ() + 1.0f,
end.GetPositionX(), end.GetPositionY(), end.GetPositionZ() + 1.0f,
PHASEMASK_NORMAL, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing);
if (inLos)
{
if (dist < 5.0f)
retvec.push_back(end);
else
{
float dx = end.GetPositionX() - lastPoint.GetPositionX();
float dy = end.GetPositionY() - lastPoint.GetPositionY();
float dz = end.GetPositionZ() - lastPoint.GetPositionZ();
float scale = 5.0f / dist;
retvec.emplace_back(end.GetMapId(),
lastPoint.GetPositionX() + dx * scale,
lastPoint.GetPositionY() + dy * scale,
lastPoint.GetPositionZ() + dz * scale);
}
}
}
}
if (sPlayerbotAIConfig.hasLog("pathfind_attempt.csv") && (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr() << "+00,";
out << std::fixed << std::setprecision(1) << type << ",";
printWKT(fromPointsArray(points), out, 1);
sPlayerbotAIConfig.log("pathfind_attempt.csv", out.str().c_str());
}
if (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL)
return fromPointsArray(points);
return {};
return retvec;
}
bool WorldPosition::cropPathTo(std::vector<WorldPosition>& path, float maxDistance)
@ -848,27 +824,59 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
std::vector<WorldPosition> subPath, fullPath = startPath;
// Construct ONE PathGenerator and thread it through every step
// to avoid the per-step alloc cost. AC's BuildPolyPath has a
// subpath-prefix optimization that can bend chained probes, so
// call Clear() before each step to reset the poly cache.
Unit* pathUnit = bot;
Creature* tempCreature = nullptr;
if (!pathUnit)
{
Map* map = sMapMgr->FindBaseMap(GetMapId());
if (!map)
return fullPath;
tempCreature = new Creature();
if (!tempCreature->Create(map->GenerateLowGuid<HighGuid::Unit>(), map,
PHASEMASK_NORMAL, 1 /*entry*/, 0,
currentPos.GetPositionX(), currentPos.GetPositionY(),
currentPos.GetPositionZ(), 0))
{
delete tempCreature;
return fullPath;
}
pathUnit = tempCreature;
map->EnsureGridCreated(Acore::ComputeGridCoord(currentPos.GetPositionX(), currentPos.GetPositionY()));
map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY()));
}
PathGenerator path(pathUnit);
// Limit the pathfinding attempts
for (uint32 i = 0; i < maxAttempt; i++)
{
// Try to pathfind to this position.
subPath = getPathStepFrom(currentPos, bot);
// Reset cached poly state from the previous step so each call
// is a fresh A* (otherwise the prefix-recycling at
// PathGenerator.cpp BuildPolyPath snaps the start to the
// cached corridor, bending the chain).
path.Clear();
subPath = getPathStepFrom(currentPos, path);
// If we could not find a path return what we have now.
if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance)
break;
// Append the path excluding the start (this should be the same as the end of the startPath)
fullPath.insert(fullPath.end(), std::next(subPath.begin(), 1), subPath.end());
// Are we there yet?
if (isPathTo(subPath))
break;
// Continue pathfinding.
currentPos = subPath.back();
}
if (tempCreature)
delete tempCreature;
return fullPath;
}
@ -1073,6 +1081,14 @@ GuidPosition::GuidPosition(GameObjectData const& goData)
loadedFromDB = true;
}
TravelDestination::~TravelDestination()
{
for (WorldPosition* point : points)
delete point;
points.clear();
}
std::vector<WorldPosition*> TravelDestination::getPoints(bool ignoreFull)
{
if (ignoreFull)
@ -2379,9 +2395,7 @@ void TravelMgr::LoadQuestTravelTable()
sPlayerbotAIConfig.openLog("unload_grid.csv", "w");
sPlayerbotAIConfig.openLog("unload_obj.csv", "w");
TravelNodeMap::instance().loadNodeStore();
TravelNodeMap::instance().generateAll();
// Node loading/generation is handled by TravelNodeMap::Init() called from TravelMgr::Init().
/*
bool fullNavPointReload = false;
@ -2772,7 +2786,7 @@ void TravelMgr::LoadQuestTravelTable()
//if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode))
// continue;
startNode->buildPath(endNode, nullptr, false);
startNode->BuildPath(endNode, nullptr, false);
//if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete())
//startNode->removeLinkTo(endNode);
@ -2896,7 +2910,7 @@ void TravelMgr::LoadQuestTravelTable()
TravelNodePath nodePath = *path.second;
std::vector<WorldPosition> pPath = nodePath.getPath();
std::vector<WorldPosition> pPath = nodePath.GetPath();
std::reverse(pPath.begin(), pPath.end());
nodePath.setPath(pPath);
@ -4359,8 +4373,7 @@ void TravelMgr::Init()
PrepareZone2LevelBracket();
PrepareDestinationCache();
}
sTravelNodeMap.InitTaxiGraph();
LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built.");
sTravelNodeMap.Init();
}
TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
@ -4407,7 +4420,7 @@ std::vector<std::vector<uint32>> TravelMgr::GetOptimalFlightDestinations(Player*
std::vector<std::vector<uint32>> validDestinations;
FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot);
if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster->pos) > 500.0f)
if (!nearestFlightMaster)
return validDestinations;
uint32 fromNode = nearestFlightMaster->taxiNodeId;
@ -4426,9 +4439,9 @@ std::vector<std::vector<uint32>> TravelMgr::GetOptimalFlightDestinations(Player*
if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId()))
botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0;
//Simplify destination delection. Its either target cities (Based on config value) or target world.
std::vector<uint32> candidateZones;
if (botLevel >= 10 && !botInCapital && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
if (botLevel >= 10 && !botInCapital &&
urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
{
TeamId botTeam = bot->GetTeamId();
for (Capital const& capital : capitals)
@ -4555,6 +4568,34 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
return fallbackLocations;
}
bool TravelMgr::SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer)
{
uint16 botMapId = bot->GetMapId();
auto const& cache = (bot->GetTeamId() == TEAM_HORDE) ? hordeAuctioneerCache : allianceAuctioneerCache;
auto mapIt = cache.find(botMapId);
if (mapIt == cache.end() || mapIt->second.empty())
return false;
// Collect all areas on this map that have auctioneers
std::vector<uint32> areaIds;
areaIds.reserve(mapIt->second.size());
for (auto const& [areaId, npcs] : mapIt->second)
{
if (!npcs.empty())
areaIds.push_back(areaId);
}
if (areaIds.empty())
return false;
// Pick a random area, then a random auctioneer in that area
uint32 selectedArea = areaIds[urand(0, areaIds.size() - 1)];
auto const& auctioneers = mapIt->second.at(selectedArea);
outAuctioneer = auctioneers[urand(0, auctioneers.size() - 1)];
return true;
}
void TravelMgr::PrepareZone2LevelBracket()
{
// Classic WoW - starter zones
@ -4641,6 +4682,7 @@ void TravelMgr::PrepareDestinationCache()
uint32 flightMastersCount = 0;
uint32 innkeepersCount = 0;
uint32 bankerCount = 0;
uint32 auctioneerCount = 0;
LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel);
// Temporary map to group creatures by entry and area
@ -4687,18 +4729,18 @@ void TravelMgr::PrepareDestinationCache()
(creatureTemplate->unit_flags & 4096) == 0 &&
creatureTemplate->rank == 0)
{
int32 roundX = static_cast<int32>(std::lround(x / 50.0f));
int32 roundY = static_cast<int32>(std::lround(y / 50.0f));
int32 roundZ = static_cast<int32>(std::lround(z / 50.0f));
uint32 roundX = static_cast<uint32>(std::round(x / 50.0f));
uint32 roundY = static_cast<uint32>(std::round(y / 50.0f));
uint32 roundZ = static_cast<uint32>(std::round(z / 50.0f));
tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData);
tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z));
}
// FLIGHT MASTERS
// Entry 29480 is Grimwing (Storm Peaks)
// Entry 3838 is Vesprystus in Rut'Theran. Need Travel Node system to resolve this one.
// Entry 29480 is Grimwing (Storm Peaks) — has FLIGHTMASTER flag but
// isn't a real usable flight master; skip it.
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480)
creatureTemplate->Entry != 29480)
{
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
bool forHorde = !(factionEntry->hostileMask & 4);
@ -4783,7 +4825,7 @@ void TravelMgr::PrepareDestinationCache()
creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 &&
creatureTemplate->Entry != 29282)
{
BankerLocation bLoc;
NpcLocation bLoc;
bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI);
bLoc.entry = templateEntry;
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
@ -4806,6 +4848,31 @@ void TravelMgr::PrepareDestinationCache()
}
bankerCount++;
}
// === AUCTIONEERS ===
else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_AUCTIONEER)
{
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
if (!factionEntry)
continue;
bool forHorde = !(factionEntry->hostileMask & 4);
bool forAlliance = !(factionEntry->hostileMask & 2);
if (!forHorde && !forAlliance)
continue;
NpcLocation aLoc;
aLoc.loc = WorldLocation(mapId, x + cos(orient) * 3.0f, y + sin(orient) * 3.0f, z + 0.5f, orient + M_PI);
aLoc.entry = templateEntry;
if (forHorde)
hordeAuctioneerCache[mapId][areaId].push_back(aLoc);
if (forAlliance)
allianceAuctioneerCache[mapId][areaId].push_back(aLoc);
auctioneerCount++;
}
}
// Process temporary caches
@ -4815,16 +4882,29 @@ void TravelMgr::PrepareDestinationCache()
{
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1);
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
float totalX = 0.0f;
float totalY = 0.0f;
float totalZ = 0.0f;
for (CreatureData const& creatureData : creatureDataList)
{
totalX += creatureData.posX;
totalY += creatureData.posY;
totalZ += creatureData.posZ;
}
float avgX = totalX / creatureDataList.size();
float avgY = totalY / creatureDataList.size();
float avgZ = totalZ / creatureDataList.size();
uint32 mapId = std::get<0>(gridTuple);
for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel;
l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++)
{
if (l < 1 || l > maxLevel)
continue;
locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple),
static_cast<float>(std::get<1>(gridTuple)) * 50.0f,
static_cast<float>(std::get<2>(gridTuple)) * 50.0f,
static_cast<float>(std::get<3>(gridTuple)) * 50.0f));
locsPerLevelCache[(uint8)l].push_back(WorldLocation(mapId, avgX, avgY, avgZ, 0.0f));
}
}
}
@ -4870,5 +4950,5 @@ void TravelMgr::PrepareDestinationCache()
break;
}
}
LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount);
LOG_INFO("playerbots", ">> {} flight masters, {} innkeepers, {} bankers, {} auctioneers collected.", flightMastersCount, innkeepersCount, bankerCount, auctioneerCount);
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -71,7 +71,6 @@ bool PlayerbotAIConfig::Initialize()
globalCoolDown = sConfigMgr->GetOption<int32>("AiPlayerbot.GlobalCooldown", 500);
maxWaitForMove = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxWaitForMove", 5000);
disableMoveSplinePath = sConfigMgr->GetOption<int32>("AiPlayerbot.DisableMoveSplinePath", 0);
maxMovementSearchTime = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxMovementSearchTime", 3);
expireActionTime = sConfigMgr->GetOption<int32>("AiPlayerbot.ExpireActionTime", 5000);
dispelAuraDuration = sConfigMgr->GetOption<int32>("AiPlayerbot.DispelAuraDuration", 700);
reactDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReactDelay", 100);
@ -99,6 +98,7 @@ bool PlayerbotAIConfig::Initialize()
tooCloseDistance = sConfigMgr->GetOption<float>("AiPlayerbot.TooCloseDistance", 5.0f);
meleeDistance = sConfigMgr->GetOption<float>("AiPlayerbot.MeleeDistance", 0.75f);
followDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FollowDistance", 1.5f);
walkDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WalkDistance", 5.0f);
whisperDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WhisperDistance", 6000.0f);
contactDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ContactDistance", 0.45f);
aoeRadius = sConfigMgr->GetOption<float>("AiPlayerbot.AoeRadius", 10.0f);
@ -652,6 +652,7 @@ bool PlayerbotAIConfig::Initialize()
autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false);
autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true);
enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true);
enableTravelNodes = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableTravelNodes", false);
RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15);
RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20);

View File

@ -84,12 +84,12 @@ public:
bool EnableICCBuffs;
bool allowAccountBots, allowGuildBots, allowTrustedAccountBots;
bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat;
uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, maxMovementSearchTime, expireActionTime,
uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, expireActionTime,
dispelAuraDuration, passiveDelay, repeatDelay, errorDelay, rpgDelay, sitDelay, returnDelay, lootDelay;
bool dynamicReactDelay;
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance,
targetPosRecalcDistance, farDistance, healDistance, aggroDistance;
targetPosRecalcDistance, farDistance, healDistance, aggroDistance, walkDistance;
uint32 criticalHealth, lowHealth, mediumHealth, almostFullHealth;
uint32 lowMana, mediumMana, highMana;
bool autoSaveMana;
@ -372,6 +372,7 @@ public:
bool autoLearnTrainerSpells;
bool autoDoQuests;
bool enableNewRpgStrategy;
bool enableTravelNodes;
std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight;
bool syncLevelWithPlayers;
bool autoLearnQuestSpells;

View File

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

24
todo.md Normal file
View File

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