From 0e0dd961f7dfe9a93e380aff255559869a74461f Mon Sep 17 00:00:00 2001 From: bash Date: Fri, 1 May 2026 15:17:53 +0200 Subject: [PATCH] feat(Core/Debug): Visualize travel nodes and walk paths via .playerbots debug zone showpath --- src/Mgr/Travel/TravelNode.cpp | 9 ++ src/Mgr/Travel/TravelNode.h | 4 + src/Script/PlayerbotCommandScript.cpp | 158 ++++++++++++++++++++++++++ 3 files changed, 171 insertions(+) diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index 77b3af5ae..02f0fd1d1 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -2454,6 +2454,15 @@ TravelNode* TravelNodeMap::GetNearestNodeInZone(WorldPosition pos, uint32 zoneId return bestNode; } +std::vector const& TravelNodeMap::GetNodesInZone(uint32 zoneId) const +{ + static std::vector const empty; + auto it = m_zoneIndex.find(zoneId); + if (it == m_zoneIndex.end()) + return empty; + return it->second; +} + TravelNode* TravelNodeMap::GetNearestNodeOnMap(WorldPosition pos) { auto it = m_mapIndex.find(pos.GetMapId()); diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index 7c966762f..e37abd251 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -697,6 +697,10 @@ public: 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 const& GetNodesInZone(uint32 zoneId) const; + bool GetFullPath(TravelPlan& plan, WorldPosition botPos, uint32 botZoneId, WorldPosition destination); diff --git a/src/Script/PlayerbotCommandScript.cpp b/src/Script/PlayerbotCommandScript.cpp index 105f8a28f..38f59bad9 100644 --- a/src/Script/PlayerbotCommandScript.cpp +++ b/src/Script/PlayerbotCommandScript.cpp @@ -33,6 +33,7 @@ public: { static ChatCommandTable playerbotsDebugCommandTable = { {"bg", HandleDebugBGCommand, SEC_GAMEMASTER, Console::Yes}, + {"zone", HandleDebugZoneCommand, SEC_GAMEMASTER, Console::No}, }; static ChatCommandTable playerbotsAccountCommandTable = { @@ -126,6 +127,163 @@ public: 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)