From ae2a4a0f085379192386cf5b7424f2c44ef28ff7 Mon Sep 17 00:00:00 2001 From: nico Date: Sat, 17 Oct 2026 16:45:58 +0000 Subject: [PATCH] added virtual entry nodes --- .../World/ClientlessActionExecutorMgr.cpp | 17 ++ .../game/World/ClientlessTravelGraphMgr.cpp | 168 +++++++++++++++++- .../game/World/ClientlessTravelGraphMgr.h | 3 + 3 files changed, 185 insertions(+), 3 deletions(-) diff --git a/src/server/game/World/ClientlessActionExecutorMgr.cpp b/src/server/game/World/ClientlessActionExecutorMgr.cpp index 2a786aaee8..b8d090d1a8 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.cpp +++ b/src/server/game/World/ClientlessActionExecutorMgr.cpp @@ -1244,6 +1244,23 @@ bool ClientlessActionExecutorMgr::StartRouteStep( record.EdgePath = *points; if (step.EdgeReverse) std::reverse(record.EdgePath.begin(), record.EdgePath.end()); + // VCORE_VIRTUAL_GROUND_EDGE_ENTRY_V1: keep original recorded points, + // only discard the portion before the validated virtual entry. + if (step.GroundStartIndex >= 0) + { + std::size_t const offset = std::size_t(step.GroundStartIndex); + if (offset >= record.EdgePath.size()) + { + record.Status.State = ClientlessActionExecutionState::Failed; + record.Status.Detail = "virtual ground entry index outside recorded edge"; + record.Active = false; + return false; + } + record.EdgePath.erase(record.EdgePath.begin(), record.EdgePath.begin() + offset); + TC_LOG_INFO("server.adventurebot.navigation", + "VIRTUAL_EDGE_EXEC edgeDbId=%u pointIndex=%u remainingPoints=%u", + step.EdgeDatabaseId, uint32(offset), uint32(record.EdgePath.size())); + } } } diff --git a/src/server/game/World/ClientlessTravelGraphMgr.cpp b/src/server/game/World/ClientlessTravelGraphMgr.cpp index ac07f947dc..8ca1ac886a 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.cpp +++ b/src/server/game/World/ClientlessTravelGraphMgr.cpp @@ -3,6 +3,7 @@ #include "DatabaseEnv.h" #include "Log.h" #include "Player.h" +#include "ClientlessTravelMgr.h" #include #include @@ -445,6 +446,16 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( std::unordered_map states; std::unordered_map persistentHops; + // VCORE_VIRTUAL_GROUND_EDGE_ENTRY_V1: a root may be a virtual recorded-road entry. + struct VirtualEntry + { + uint32 EdgeId = 0; + uint32 PointIndex = 0; // original order of _edgePoints + bool Reverse = false; + ClientlessTravelEdgePoint Point; + }; + std::unordered_map virtualRoots; + uint32 startCount = 0; for (auto const& pair : _nodes) { @@ -472,9 +483,122 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( ++startCount; } - if (!startCount) + // Use the nearest ten nodes merely to shortlist real recorded ground edges. + // All distances below are on the player's current map; no database mutation. + std::vector> nearestNodes; + for (auto const& pair : _nodes) { - error = "no travel node near current position"; + ClientlessTravelNode const& node = pair.second; + if (node.MapId != player->GetMapId()) + continue; + float dx = node.X - player->GetPositionX(); + float dy = node.Y - player->GetPositionY(); + float dz = node.Z - player->GetPositionZ(); + nearestNodes.emplace_back(dx * dx + dy * dy + dz * dz, pair.first); + } + std::sort(nearestNodes.begin(), nearestNodes.end()); + if (nearestNodes.size() > 10) + nearestNodes.resize(10); + + struct EntryCandidate + { + float DistanceSq; + ClientlessTravelEdge Edge; + uint32 Index; + ClientlessTravelEdgePoint Point; + uint32 Count; + }; + std::vector entryCandidates; + std::unordered_set visitedDirectedEdges; + for (auto const& near : nearestNodes) + { + // Both outgoing and incoming records are considered for spatial discovery. + // A directed entry still follows only the direction present in _edges. + for (auto const& edgePair : _edges) + { + for (ClientlessTravelEdge const& edge : edgePair.second) + { + if (edge.Type != "ground" || !edge.DatabaseId || + (edge.FromNode != near.second && edge.ToNode != near.second)) + continue; + if (blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId)) + continue; + std::string key = std::to_string(edge.DatabaseId) + (edge.Reverse ? "R" : "F"); + if (!visitedDirectedEdges.insert(key).second) + continue; + auto pointIt = _edgePoints.find(edge.DatabaseId); + if (pointIt == _edgePoints.end() || pointIt->second.size() < 3) + continue; + std::vector const& points = pointIt->second; + // Candidates are stored in original point order, even for reverse travel. + for (uint32 i = 0; i < points.size(); ++i) + { + if ((!edge.Reverse && i + 1 >= points.size()) || (edge.Reverse && i == 0)) + continue; + ClientlessTravelEdgePoint const& point = points[i]; + if (point.MapId != player->GetMapId()) + continue; + float dx = point.X - player->GetPositionX(); + float dy = point.Y - player->GetPositionY(); + float dz = point.Z - player->GetPositionZ(); + float dsq = dx * dx + dy * dy + dz * dz; + // Keep local MMAP checks bounded; startRadius is a planning limit. + if (dsq > startRadius * startRadius) + continue; + entryCandidates.push_back({ dsq, edge, i, point, uint32(points.size()) }); + } + } + } + } + std::sort(entryCandidates.begin(), entryCandidates.end(), + [](EntryCandidate const& a, EntryCandidate const& b) { return a.DistanceSq < b.DistanceSq; }); + + uint32 validated = 0; + uint32 accepted = 0; + for (EntryCandidate const& candidate : entryCandidates) + { + if (validated >= 24) + break; + ++validated; + Position target; + target.Relocate(candidate.Point.X, candidate.Point.Y, candidate.Point.Z, candidate.Point.O); + ClientlessPathDiagnostics nav; + // A point on top of the player needs no pathfinder round trip. + bool const alreadyThere = candidate.DistanceSq <= 2.25f; + if (!alreadyThere) + { + sClientlessTravelMgr->AnalyzePath(const_cast(player), target, nav); + if (!nav.Calculated || !nav.Complete || nav.Suspicious || + nav.FinalDistance > 1.5f || nav.PathLength > 500.0f) + continue; + } + + State state{ candidate.Edge.ToNode, false }; + std::string key = stateKey(state); + float remainingFraction = candidate.Edge.Reverse + ? float(candidate.Index) / float(candidate.Count - 1) + : float(candidate.Count - candidate.Index - 1) / float(candidate.Count - 1); + float newCost = (alreadyThere ? 0.0f : nav.PathLength) + candidate.Edge.Cost * remainingFraction; + auto old = distance.find(key); + if (old != distance.end() && old->second <= newCost) + continue; + distance[key] = newCost; + states[key] = state; + persistentHops[key] = 1; + previous.erase(key); + virtualRoots[key] = { candidate.Edge.DatabaseId, candidate.Index, + candidate.Edge.Reverse, candidate.Point }; + open.push({ newCost, state }); + ++accepted; + } + if (accepted) + TC_LOG_INFO("server.adventurebot.navigation", + "VIRTUAL_EDGE_CANDIDATES map=%u tested=%u accepted=%u", + player->GetMapId(), validated, accepted); + + if (!startCount && !accepted) + { + error = "no reachable travel node or recorded ground edge near current position"; return false; } @@ -684,7 +808,31 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( } std::reverse(reversed.begin(), reversed.end()); - route.reserve(reversed.size()); + route.reserve(reversed.size() + 1); + // If the shortest root entered a recorded edge mid-way, precede the actual + // endpoint with an MMAP-reachable virtual waypoint; do NOT rewrite the DB edge. + if (!reversed.empty()) + { + std::string rootKey = stateKey(reversed.front().Value); + auto root = virtualRoots.find(rootKey); + if (root != virtualRoots.end() && previous.find(rootKey) == previous.end()) + { + VirtualEntry const& entry = root->second; + ClientlessTravelRouteStep initial; + initial.NodeKey = "virtual_ground_entry"; + initial.NodeType = "road"; + initial.EdgeType = "start"; + initial.MapId = entry.Point.MapId; + initial.X = entry.Point.X; + initial.Y = entry.Point.Y; + initial.Z = entry.Point.Z; + initial.O = entry.Point.O; + route.push_back(initial); + TC_LOG_INFO("server.adventurebot.navigation", + "VIRTUAL_EDGE_SELECTED edgeDbId=%u pointIndex=%u reverse=%u", + entry.EdgeId, entry.PointIndex, entry.Reverse ? 1u : 0u); + } + } for (Reconstructed const& item : reversed) { @@ -696,6 +844,20 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( step.EdgeType = item.IncomingEdgeType; step.EdgeDatabaseId = item.EdgeDatabaseId; step.EdgeReverse = item.EdgeReverse; + if (route.size() == 1 && route.front().NodeKey == "virtual_ground_entry" && + item.IncomingEdgeType == "start") + { + auto root = virtualRoots.find(stateKey(item.Value)); + if (root != virtualRoots.end()) + { + step.EdgeType = "ground"; + step.EdgeDatabaseId = root->second.EdgeId; + step.EdgeReverse = root->second.Reverse; + step.GroundStartIndex = root->second.Reverse + ? int32(_edgePoints.at(root->second.EdgeId).size() - 1 - root->second.PointIndex) + : int32(root->second.PointIndex); + } + } step.MapId = node.MapId; step.X = node.X; step.Y = node.Y; diff --git a/src/server/game/World/ClientlessTravelGraphMgr.h b/src/server/game/World/ClientlessTravelGraphMgr.h index 9b796f7c59..75fc73cf33 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.h +++ b/src/server/game/World/ClientlessTravelGraphMgr.h @@ -66,6 +66,9 @@ struct TC_GAME_API ClientlessTravelRouteStep std::string EdgeType; uint32 EdgeDatabaseId = 0; bool EdgeReverse = false; + // VCORE_VIRTUAL_GROUND_EDGE_ENTRY_V1: index in execution-direction edge points. + // -1 means replay the entire recorded ground edge as before. + int32 GroundStartIndex = -1; uint32 MapId = 0; float X = 0.0f;