implemented temporary entry nodes to be considered using playerless MMAP check
This commit is contained in:
1 parent
366615bcb9
commit
b37a0ddcc4
1 file changed
+248
-6
@@ -4,6 +4,7 @@
|
||||
#include "Log.h"
|
||||
#include "Player.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "PathGenerator.h" // VCORE_C818_PREDICTIVE_GRAPH
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -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<QueueItem, std::vector<QueueItem>, std::greater<QueueItem>> 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<std::string> persistentReachable;
|
||||
std::queue<std::string> 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<std::string, float> validBridgeLengths;
|
||||
std::unordered_set<std::string> 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<ForwardCandidate> 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<std::string> 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<Reconstructed> 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";
|
||||
|
||||
|
||||
Reference in new issue
Block a user