From b37a0ddcc4e9f42d8b156b954f9154fdcf2ae243 Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 19 Oct 2026 17:03:42 +0200 Subject: [PATCH] implemented temporary entry nodes to be considered using playerless MMAP check --- .../game/World/ClientlessTravelGraphMgr.cpp | 254 +++++++++++++++++- 1 file changed, 248 insertions(+), 6 deletions(-) diff --git a/src/server/game/World/ClientlessTravelGraphMgr.cpp b/src/server/game/World/ClientlessTravelGraphMgr.cpp index cdba556f21..a9e74527c7 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.cpp +++ b/src/server/game/World/ClientlessTravelGraphMgr.cpp @@ -4,6 +4,7 @@ #include "Log.h" #include "Player.h" #include "ClientlessTravelMgr.h" +#include "PathGenerator.h" // VCORE_C818_PREDICTIVE_GRAPH #include #include @@ -410,8 +411,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( if (startRadius <= 0.0f) startRadius = 500.0f; + // C8.18: this cap applies ONLY to virtual node-to-node bridges. + // Start access, destination location access and virtual ground entries retain + // their distinct travel/search limits. + constexpr float MaxVirtualNodeBridge = 100.0f; if (bridgeRadius <= 0.0f) - bridgeRadius = 300.0f; + bridgeRadius = MaxVirtualNodeBridge; + bridgeRadius = std::min(bridgeRadius, MaxVirtualNodeBridge); struct State { @@ -438,6 +444,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( std::string EdgeType; uint32 EdgeDatabaseId = 0; bool EdgeReverse = false; + // An intermediate virtual entry is represented by the landing waypoint + // followed by the remaining portion of its REAL recorded ground edge. + bool VirtualGroundEntry = false; + uint32 GroundStartIndex = 0; + ClientlessTravelEdgePoint EntryPoint; }; std::priority_queue, std::greater> open; @@ -469,6 +480,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( float const localDistance = std::sqrt(dx * dx + dy * dy + dz * dz); if (localDistance > startRadius) continue; + float startCost = localDistance; // VCORE_VIRTUAL_EDGE_V11_PORTAL_APPROACH: never seed an unreachable node. if (blockedBridgeKeys && blockedBridgeKeys->count("start:" + node.Key)) @@ -488,14 +500,15 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( node.Key.c_str(), localDistance, entryNav.Reason.c_str()); continue; } + startCost = entryNav.PathLength; } State state{ node.Key, false }; std::string const key = stateKey(state); auto found = distance.find(key); - if (found != distance.end() && found->second <= localDistance) + if (found != distance.end() && found->second <= startCost) continue; - distance[key] = localDistance; + distance[key] = startCost; states[key] = state; persistentHops[key] = 0; open.push({ localDistance, state }); @@ -623,6 +636,68 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( return false; } + // C8.18: do not create intermediate shortcuts when a complete path already + // exists through persistent graph edges. All valid START roots (ordinary + // nodes and virtual ground entries) participate in this reachability test. + // The repair mode below is only enabled when this baseline has no goal. + std::unordered_set persistentReachable; + std::queue persistentOpen; + for (auto const& pair : distance) + { + if (previous.find(pair.first) == previous.end() && + std::isfinite(pair.second) && persistentReachable.insert(states.at(pair.first).Node).second) + persistentOpen.push(states.at(pair.first).Node); + } + while (!persistentOpen.empty()) + { + std::string const nodeKey = persistentOpen.front(); + persistentOpen.pop(); + auto outgoing = _edges.find(nodeKey); + if (outgoing == _edges.end()) + continue; + for (ClientlessTravelEdge const& edge : outgoing->second) + { + // A portal trigger source reached as a transit node may only + // continue through its explicitly stored portal edge. + bool const sourcePortal = nodeKey.rfind("portal_at_", 0) == 0 && + nodeKey.size() >= 7 && + nodeKey.compare(nodeKey.size() - 7, 7, "_source") == 0; + if (sourcePortal && edge.Type != "portal") + continue; + if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId)) + continue; + if (persistentReachable.insert(edge.ToNode).second) + persistentOpen.push(edge.ToNode); + } + } + bool const repairRequired = !persistentReachable.count(destinationNode); + TC_LOG_INFO("server.adventurebot.navigation", + "C818_ROUTE_MODE destination='%s' mode='%s' roots=%u", + destinationNode.c_str(), repairRequired ? "repair" : "persistent", + uint32(startCount + accepted)); + + // Dynamic candidates may be examined many times during Dijkstra. Avoid + // duplicating costly Detour queries for identical directed connections. + std::unordered_map validBridgeLengths; + std::unordered_set rejectedBridges; + uint32 bridgeTests = 0, bridgeAccepted = 0, bridgeRejected = 0; + uint32 bridgeDistanceRejected = 0; + uint32 forwardEntryTests = 0, forwardEntryAccepted = 0; + + auto analyze = [&](ClientlessTravelNode const& from, + uint32 targetMap, float x, float y, float z) + { + Position a, b; + a.Relocate(from.X, from.Y, from.Z, from.O); + b.Relocate(x, y, z, 0.0f); + // The map is deliberately taken from the future graph node, never + // from Arthoran's current position. No Player/WorldObject is required. + if (targetMap != from.MapId) + return VcorePositionPathResult{}; + return PathGenerator::AnalyzePositions(from.MapId, from.MapId, + player->GetInstanceId(), a, b, NAV_GROUND); + }; + std::string bestGoalState; while (!open.empty()) @@ -685,10 +760,118 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // local bridge. This prevents MMAP from silently becoming a long-range // planner while still allowing unlimited bridges over a long journey // when real transport/ground edges lie between them. - if (current.Value.LastWasBridge || portalArrival) + if (!repairRequired || current.Value.LastWasBridge || portalArrival) continue; ClientlessTravelNode const& from = _nodes.at(current.Value.Node); + + // C8.18: if a future node is disconnected, attach it to a recorded + // road at an MMAP-reachable waypoint. Only remaining portions of + // stored, directed ground edges are admitted; no off-graph shortcut. + // A portal source may not be treated as an ordinary intermediate node. + bool const futurePortalSource = + from.Key.rfind("portal_at_", 0) == 0 && + from.Key.size() >= 7 && + from.Key.compare(from.Key.size() - 7, 7, "_source") == 0; + if (!futurePortalSource && persistentHops[currentKey] > 0 && + !(blockedBridgeKeys && + blockedBridgeKeys->count(MakeLocalBridgeKey(from.Key, "virtual_ground_entry")))) + { + // Check a bounded set of the geographically closest waypoint + // candidates first to avoid O(all-points * Detour) work. + struct ForwardCandidate + { + float DistanceSq; + ClientlessTravelEdge const* Edge; + uint32 Index; + ClientlessTravelEdgePoint const* Point; + uint32 PointCount; + }; + std::vector candidates; + constexpr float ForwardEntryRadius = 200.0f; + for (auto const& edgePair : _edges) + { + for (ClientlessTravelEdge const& edge : edgePair.second) + { + if (edge.Type != "ground" || !edge.DatabaseId || + (blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId))) + continue; + auto pts = _edgePoints.find(edge.DatabaseId); + if (pts == _edgePoints.end() || pts->second.size() < 3) + continue; + for (uint32 i = 0; i < pts->second.size(); ++i) + { + if ((!edge.Reverse && i + 1 >= pts->second.size()) || + (edge.Reverse && i == 0)) + continue; + ClientlessTravelEdgePoint const& pt = pts->second[i]; + if (pt.MapId != from.MapId) + continue; + float dx = pt.X - from.X, dy = pt.Y - from.Y, dz = pt.Z - from.Z; + float dsq = dx * dx + dy * dy + dz * dz; + if (dsq <= ForwardEntryRadius * ForwardEntryRadius) + candidates.push_back({ dsq, &edge, i, &pt, uint32(pts->second.size()) }); + } + } + } + std::sort(candidates.begin(), candidates.end(), + [](ForwardCandidate const& a, ForwardCandidate const& b) + { return a.DistanceSq < b.DistanceSq; }); + // One candidate per directed recorded edge is enough for a first + // robust repair pass; use the closest admissible point on it. + std::unordered_set testedDirectedEdges; + uint32 localTests = 0; + for (ForwardCandidate const& entry : candidates) + { + if (localTests >= 24) + break; + std::string directionKey = std::to_string(entry.Edge->DatabaseId) + + (entry.Edge->Reverse ? "R" : "F"); + if (!testedDirectedEdges.insert(directionKey).second) + continue; + ++localTests; + ++forwardEntryTests; + VcorePositionPathResult const probe = analyze(from, entry.Point->MapId, + entry.Point->X, entry.Point->Y, entry.Point->Z); + if (!probe.IsComplete() || probe.FinalDistance > 1.5f || + probe.PathLength > 500.0f) + continue; + + if (entry.Edge->ToNode == from.Key) + continue; + State next{ entry.Edge->ToNode, false }; + std::string const nextKey = stateKey(next); + float remainingFraction = entry.Edge->Reverse + ? float(entry.Index) / float(entry.PointCount - 1) + : float(entry.PointCount - entry.Index - 1) / float(entry.PointCount - 1); + float const cost = current.Cost + 1000.0f + probe.PathLength + + entry.Edge->Cost * remainingFraction; + auto old = distance.find(nextKey); + if (old != distance.end() && cost >= old->second) + continue; + distance[nextKey] = cost; + states[nextKey] = next; + persistentHops[nextKey] = persistentHops[currentKey] + 1; + Previous link; + link.StateKey = currentKey; + link.Node = current.Value.Node; + link.EdgeType = "ground"; + link.EdgeDatabaseId = entry.Edge->DatabaseId; + link.EdgeReverse = entry.Edge->Reverse; + link.VirtualGroundEntry = true; + link.GroundStartIndex = entry.Edge->Reverse + ? entry.PointCount - 1 - entry.Index : entry.Index; + link.EntryPoint = *entry.Point; + previous[nextKey] = link; + open.push({ cost, next }); + ++forwardEntryAccepted; + TC_LOG_INFO("server.adventurebot.navigation", + "C818_FORWARD_ENTRY_PLAN from='%s' viaEdge=%u point=%u reverse=%u to='%s' path=%.1f", + from.Key.c_str(), entry.Edge->DatabaseId, entry.Index, + entry.Edge->Reverse ? 1u : 0u, entry.Edge->ToNode.c_str(), probe.PathLength); + } + } + for (auto const& pair : _nodes) { ClientlessTravelNode const& to = pair.second; @@ -706,6 +889,35 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( if (blockedBridgeKeys && blockedBridgeKeys->count(bridgeKey)) continue; + // C8.18: even in repair mode, a virtual node bridge is not + // eligible until the future-node -> future-node MMAP is complete. + // The geometry cap above is a cheap first-stage filter. + float measuredLength = 0.0f; + auto cached = validBridgeLengths.find(bridgeKey); + if (cached != validBridgeLengths.end()) + measuredLength = cached->second; + else + { + if (rejectedBridges.count(bridgeKey)) + continue; + ++bridgeTests; + VcorePositionPathResult const probe = analyze(from, to.MapId, to.X, to.Y, to.Z); + if (!probe.IsComplete() || probe.FinalDistance > 1.5f || + !std::isfinite(probe.PathLength) || probe.PathLength > bridgeRadius * 2.0f) + { + rejectedBridges.insert(bridgeKey); + ++bridgeRejected; + TC_LOG_DEBUG("server.adventurebot.navigation", + "C818_BRIDGE_REJECT from='%s' to='%s' direct=%.1f status=%u final=%.1f", + from.Key.c_str(), to.Key.c_str(), direct, + unsigned(probe.Result), probe.FinalDistance); + continue; + } + measuredLength = probe.PathLength; + validBridgeLengths.emplace(bridgeKey, measuredLength); + ++bridgeAccepted; + } + // A bridge must lead back into real TravelGraph infrastructure. // The sole exception is the actual requested destination, where a // local bridge may be the final leg of the graph journey. @@ -729,7 +941,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // Large fixed penalty makes a real graph path preferable whenever // one exists; distance then ranks alternative local bridges. - float const candidate = current.Cost + 1000.0f + direct; + float const candidate = current.Cost + 1000.0f + measuredLength; auto old = distance.find(nextKey); if (old != distance.end() && candidate >= old->second) continue; @@ -830,6 +1042,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( std::string IncomingEdgeType; uint32 EdgeDatabaseId = 0; bool EdgeReverse = false; + bool VirtualGroundEntry = false; + uint32 GroundStartIndex = 0; + ClientlessTravelEdgePoint EntryPoint; }; std::vector reversed; @@ -848,7 +1063,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( state, prevItr->second.EdgeType, prevItr->second.EdgeDatabaseId, - prevItr->second.EdgeReverse + prevItr->second.EdgeReverse, + prevItr->second.VirtualGroundEntry, + prevItr->second.GroundStartIndex, + prevItr->second.EntryPoint }); cursor = prevItr->second.StateKey; } @@ -882,6 +1100,23 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( for (Reconstructed const& item : reversed) { + if (item.VirtualGroundEntry) + { + ClientlessTravelRouteStep connector; + connector.NodeKey = "virtual_ground_entry"; + connector.NodeType = "road"; + connector.EdgeType = "local_bridge"; + connector.MapId = item.EntryPoint.MapId; + connector.X = item.EntryPoint.X; + connector.Y = item.EntryPoint.Y; + connector.Z = item.EntryPoint.Z; + connector.O = item.EntryPoint.O; + route.push_back(std::move(connector)); + TC_LOG_INFO("server.adventurebot.navigation", + "C818_FORWARD_ENTRY_SELECTED waypoint=(%.2f %.2f %.2f) edgeDbId=%u offset=%u", + item.EntryPoint.X, item.EntryPoint.Y, item.EntryPoint.Z, + item.EdgeDatabaseId, item.GroundStartIndex); + } ClientlessTravelNode const& node = _nodes.at(item.Value.Node); ClientlessTravelRouteStep step; step.NodeKey = node.Key; @@ -890,6 +1125,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( step.EdgeType = item.IncomingEdgeType; step.EdgeDatabaseId = item.EdgeDatabaseId; step.EdgeReverse = item.EdgeReverse; + if (item.VirtualGroundEntry) + step.GroundStartIndex = int32(item.GroundStartIndex); if (route.size() == 1 && route.front().NodeKey == "virtual_ground_entry" && item.IncomingEdgeType == "start") { @@ -912,6 +1149,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( route.push_back(std::move(step)); } + TC_LOG_INFO("server.adventurebot.navigation", + "C818_PREFLIGHT complete=%u bridgeTests=%u bridgeAccepted=%u bridgeRejected=%u " + "distanceRejected=%u forwardTests=%u forwardAccepted=%u steps=%u", + complete ? 1u : 0u, bridgeTests, bridgeAccepted, bridgeRejected, + bridgeDistanceRejected, forwardEntryTests, forwardEntryAccepted, uint32(route.size())); if (!complete) error = "destination not fully reachable; returning best-effort partial route";