diff --git a/README.md b/README.md index 1b5205168..aaa5f746f 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,16 @@ Then build the server following the platform-specific instructions in our **[Ins > **Testing branch:** A `test-staging` branch is available with the latest features and fixes before they are merged into `master`. To use it, clone with `--branch=test-staging` instead. Note that this branch may contain unstable or breaking changes — use it at your own risk and only if you are comfortable troubleshooting issues. +### Required server configuration + +In `worldserver.conf` (AzerothCore core config), set: + +```ini +PreloadAllNonInstancedMapGrids = 1 +``` + +This is required for `mod-playerbots`. + ### Detailed Guides | Guide | Description | diff --git a/src/Ai/Base/Actions/DebugAction.cpp b/src/Ai/Base/Actions/DebugAction.cpp index edf1ef925..1ea8302f9 100644 --- a/src/Ai/Base/Actions/DebugAction.cpp +++ b/src/Ai/Base/Actions/DebugAction.cpp @@ -76,7 +76,7 @@ bool DebugAction::Execute(Event event) return false; std::vector 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 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 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 ppath = l.second->getPath(); + std::vector ppath = l.second->GetPath(); for (auto p : ppath) { diff --git a/src/Script/PlayerbotCommandScript.cpp b/src/Script/PlayerbotCommandScript.cpp index a7a073952..38f59bad9 100644 --- a/src/Script/PlayerbotCommandScript.cpp +++ b/src/Script/PlayerbotCommandScript.cpp @@ -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(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 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)