implemented usage of virtual exit and entry nodes
clang PCH / build (push) Waiting to run
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-25 12:09:36 +01:00
1 parent 56da0d60cf
commit 01a9ba4b0b
4 files changed
+442 -131

No files matched your search

@@ -387,7 +387,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
std::string& reachedNode,
std::string& error,
std::unordered_set<uint32> const* blockedEdgeIds,
std::unordered_set<std::string> const* blockedBridgeKeys)
std::unordered_set<std::string> const* blockedBridgeKeys,
ClientlessTravelLocationGoal const* locationGoal)
{
// VCORE_C819_DEFERRED_START_NODES: run the unchanged C8.18 search in two tiers.
// A partial ordinary route must not suppress a complete route via a deferred root.
@@ -418,7 +419,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
// 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;
constexpr float MaxVirtualNodeBridge = 200.0f; // C8.20 empirical cap
if (bridgeRadius <= 0.0f)
bridgeRadius = MaxVirtualNodeBridge;
bridgeRadius = std::min(bridgeRadius, MaxVirtualNodeBridge);
@@ -468,6 +469,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
uint32 PointIndex = 0; // original order of _edgePoints
bool Reverse = false;
ClientlessTravelEdgePoint Point;
float AccessCost = 0.0f;
};
std::unordered_map<std::string, VirtualEntry> virtualRoots;
@@ -642,7 +644,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
persistentHops[key] = 1;
previous.erase(key);
virtualRoots[key] = { candidate.Edge.DatabaseId, candidate.Index,
candidate.Edge.Reverse, candidate.Point };
candidate.Edge.Reverse, candidate.Point,
alreadyThere ? 0.0f : nav.PathLength };
open.push({ newCost, state });
++accepted;
}
@@ -657,44 +660,12 @@ 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);
// C8.20: virtual node bridges are equal-cost ground alternatives and
// participate even when stored graph routes also exist.
bool const repairRequired = true;
TC_LOG_INFO("server.adventurebot.navigation",
"C818_ROUTE_MODE destination='%s' mode='%s' roots=%u",
destinationNode.c_str(), repairRequired ? "repair" : "persistent",
destinationNode.c_str(), locationGoal ? "location" : "all-edges",
uint32(startCount + accepted));
// Dynamic candidates may be examined many times during Dijkstra. Avoid
@@ -719,7 +690,198 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
player->GetInstanceId(), a, b, NAV_GROUND);
};
// VCORE_C820_VIRTUAL_EXIT_NODES: preflight a bounded, geographical
// shortlist of directed recorded-road exits. Candidate generation does
// not mutate nodes, edges or stored path points. Costs use travelled
// arclength, not a flat per-bridge penalty or waypoint index fraction.
struct ExitCandidate
{
std::string FromNode;
uint32 EdgeId = 0;
bool Reverse = false;
uint32 EndIndex = 0; // in execution-direction original road points
float RoadCost = 0.0f;
float LocalCost = 0.0f;
float DistanceSq = 0.0f;
ClientlessTravelEdgePoint Point;
};
std::vector<ExitCandidate> rawExitCandidates;
std::unordered_map<std::string, std::vector<ExitCandidate>> exitsByFrom;
uint32 exitTests = 0, exitAccepted = 0;
if (locationGoal)
{
float const radius = std::min(300.0f, locationGoal->AnchorRadius);
std::unordered_set<std::string> scannedDirected;
for (auto const& edgePair : _edges)
{
for (ClientlessTravelEdge const& edge : edgePair.second)
{
if (edge.Type != "ground" || !edge.DatabaseId ||
(blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId)))
continue;
std::string const identity = std::to_string(edge.DatabaseId) +
(edge.Reverse ? "R" : "F");
if (!scannedDirected.insert(identity).second)
continue;
auto pointsItr = _edgePoints.find(edge.DatabaseId);
if (pointsItr == _edgePoints.end() || pointsItr->second.size() < 3)
continue;
std::vector<ClientlessTravelEdgePoint> ordered = pointsItr->second;
if (edge.Reverse)
std::reverse(ordered.begin(), ordered.end());
float totalLength = 0.0f;
for (std::size_t i = 1; i < ordered.size(); ++i)
{
float dx = ordered[i].X - ordered[i - 1].X;
float dy = ordered[i].Y - ordered[i - 1].Y;
float dz = ordered[i].Z - ordered[i - 1].Z;
totalLength += std::sqrt(dx * dx + dy * dy + dz * dz);
}
if (totalLength < 0.1f)
continue;
float travelled = 0.0f;
for (uint32 i = 1; i + 1 < ordered.size(); ++i)
{
float dx = ordered[i].X - ordered[i - 1].X;
float dy = ordered[i].Y - ordered[i - 1].Y;
float dz = ordered[i].Z - ordered[i - 1].Z;
travelled += std::sqrt(dx * dx + dy * dy + dz * dz);
ClientlessTravelEdgePoint const& point = ordered[i];
if (point.MapId != locationGoal->MapId)
continue;
dx = point.X - locationGoal->X;
dy = point.Y - locationGoal->Y;
dz = point.Z - locationGoal->Z;
float const dsq = dx * dx + dy * dy + dz * dz;
if (dsq > radius * radius)
continue;
std::string const exitKey = "exit:" + std::to_string(edge.DatabaseId) +
":" + std::to_string(i) + (edge.Reverse ? "R" : "F");
if (blockedBridgeKeys && blockedBridgeKeys->count(exitKey))
continue;
ExitCandidate candidate;
candidate.FromNode = edge.FromNode;
candidate.EdgeId = edge.DatabaseId;
candidate.Reverse = edge.Reverse;
candidate.EndIndex = i;
candidate.RoadCost = edge.Cost * (travelled / totalLength);
candidate.DistanceSq = dsq;
candidate.Point = point;
rawExitCandidates.push_back(std::move(candidate));
}
}
}
std::sort(rawExitCandidates.begin(), rawExitCandidates.end(),
[](ExitCandidate const& a, ExitCandidate const& b)
{ return a.DistanceSq < b.DistanceSq; });
// Bound Detour preflight work, not the number of ground edges.
constexpr uint32 MaxExitPreflights = 96;
for (ExitCandidate candidate : rawExitCandidates)
{
if (exitTests >= MaxExitPreflights)
break;
++exitTests;
Position fromPosition, goalPosition;
fromPosition.Relocate(candidate.Point.X, candidate.Point.Y,
candidate.Point.Z, candidate.Point.O);
goalPosition.Relocate(locationGoal->X, locationGoal->Y,
locationGoal->Z, 0.0f);
if (candidate.DistanceSq <= 2.25f)
candidate.LocalCost = 0.0f;
else
{
VcorePositionPathResult const probe = PathGenerator::AnalyzePositions(
locationGoal->MapId, locationGoal->MapId,
player->GetInstanceId(), fromPosition, goalPosition, NAV_GROUND);
if (!probe.IsComplete() || probe.FinalDistance > 1.5f ||
!std::isfinite(probe.PathLength) || probe.PathLength > 500.0f)
continue;
candidate.LocalCost = probe.PathLength;
}
exitsByFrom[candidate.FromNode].push_back(std::move(candidate));
++exitAccepted;
}
TC_LOG_INFO("server.adventurebot.navigation",
"C820_EXIT_PREFLIGHT candidates=%u tested=%u accepted=%u radius=%.1f",
uint32(rawExitCandidates.size()), exitTests, exitAccepted, radius);
}
// Exact road-section cost from recorded arclength; handles reverse
// travel as well as virtual entry + exit within the SAME edge.
auto roadSectionCost = [&](uint32 edgeId, bool reverse, uint32 begin,
uint32 end, float fullCost) -> float
{
auto itr = _edgePoints.find(edgeId);
if (itr == _edgePoints.end() || end >= itr->second.size() || begin > end)
return std::numeric_limits<float>::infinity();
std::vector<ClientlessTravelEdgePoint> ordered = itr->second;
if (reverse)
std::reverse(ordered.begin(), ordered.end());
float total = 0.0f, section = 0.0f;
for (uint32 i = 1; i < ordered.size(); ++i)
{
float dx = ordered[i].X - ordered[i - 1].X;
float dy = ordered[i].Y - ordered[i - 1].Y;
float dz = ordered[i].Z - ordered[i - 1].Z;
float const segment = std::sqrt(dx * dx + dy * dy + dz * dz);
total += segment;
if (i > begin && i <= end)
section += segment;
}
return total > 0.1f ? fullCost * section / total
: std::numeric_limits<float>::infinity();
};
struct ExitSelection
{
bool Active = false;
bool RootPartial = false;
ExitCandidate Candidate;
} bestExit;
float bestGoalCost = std::numeric_limits<float>::infinity();
std::string bestGoalState;
// C8.20: a virtual entry is represented in the queue by its FAR
// endpoint. Check exits BEFORE that endpoint independently: otherwise
// an exit halfway along the road would pay for the entire remaining edge.
if (locationGoal)
{
for (auto const& root : virtualRoots)
{
if (previous.find(root.first) != previous.end())
continue;
for (auto const& exitGroup : exitsByFrom)
{
for (ExitCandidate const& exit : exitGroup.second)
{
if (exit.EdgeId != root.second.EdgeId ||
exit.Reverse != root.second.Reverse)
continue;
auto pointsItr = _edgePoints.find(exit.EdgeId);
if (pointsItr == _edgePoints.end())
continue;
uint32 const startIndex = root.second.Reverse ?
uint32(pointsItr->second.size() - 1 - root.second.PointIndex) :
root.second.PointIndex;
if (exit.EndIndex <= startIndex)
continue;
float fullEdgeCost = 0.0f;
auto fromNodeItr = _edges.find(exit.FromNode);
if (fromNodeItr != _edges.end())
for (ClientlessTravelEdge const& edge : fromNodeItr->second)
if (edge.DatabaseId == exit.EdgeId && edge.Reverse == exit.Reverse)
fullEdgeCost = edge.Cost;
if (fullEdgeCost <= 0.0f)
continue;
float const partialCost = roadSectionCost(exit.EdgeId,
exit.Reverse, startIndex, exit.EndIndex, fullEdgeCost);
float const score = root.second.AccessCost + partialCost + exit.LocalCost;
if (!std::isfinite(score) || score >= bestGoalCost)
continue;
bestGoalCost = score;
bestGoalState = root.first;
bestExit = { true, true, exit };
}
}
}
}
while (!open.empty())
{
@@ -731,12 +893,71 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (distanceItr == distance.end() || current.Cost != distanceItr->second)
continue;
if (current.Value.Node == destinationNode)
if (!locationGoal && current.Value.Node == destinationNode)
{
bestGoalState = currentKey;
break;
}
// C8.20: location goal is a first-class search terminator.
// Node endpoints and road exits compete by the SAME full route cost.
if (locationGoal)
{
if (current.Cost >= bestGoalCost)
break; // Dijkstra lower bound; all remaining edges are nonnegative
ClientlessTravelNode const& candidateNode = _nodes.at(current.Value.Node);
bool const sourcePortal = candidateNode.Key.rfind("portal_at_", 0) == 0 &&
candidateNode.Key.size() >= 7 &&
candidateNode.Key.compare(candidateNode.Key.size() - 7, 7, "_source") == 0;
if (!sourcePortal && candidateNode.MapId == locationGoal->MapId)
{
float dx = candidateNode.X - locationGoal->X;
float dy = candidateNode.Y - locationGoal->Y;
float dz = candidateNode.Z - locationGoal->Z;
float const direct = std::sqrt(dx*dx + dy*dy + dz*dz);
if (direct <= locationGoal->AnchorRadius)
{
Position a, b;
a.Relocate(candidateNode.X, candidateNode.Y, candidateNode.Z, candidateNode.O);
b.Relocate(locationGoal->X, locationGoal->Y, locationGoal->Z, 0.0f);
float finalCost = std::numeric_limits<float>::infinity();
if (direct <= 1.5f)
finalCost = 0.0f;
else
{
VcorePositionPathResult const probe = PathGenerator::AnalyzePositions(
candidateNode.MapId, candidateNode.MapId,
player->GetInstanceId(), a, b, NAV_GROUND);
if (probe.IsComplete() && probe.FinalDistance <= 1.5f &&
std::isfinite(probe.PathLength) && probe.PathLength <= 500.0f)
finalCost = probe.PathLength;
}
if (current.Cost + finalCost < bestGoalCost)
{
bestGoalCost = current.Cost + finalCost;
bestGoalState = currentKey;
bestExit = {};
}
}
}
if (!sourcePortal)
{
auto exitsItr = exitsByFrom.find(current.Value.Node);
if (exitsItr != exitsByFrom.end())
{
for (ExitCandidate const& exit : exitsItr->second)
{
float const score = current.Cost + exit.RoadCost + exit.LocalCost;
if (score >= bestGoalCost)
continue;
bestGoalCost = score;
bestGoalState = currentKey;
bestExit = { true, false, exit };
}
}
}
}
// VCORE_C816_PORTAL_MOUNT_GUARD: a source node is not ordinary
// ground transit. Once reached during a planned route, its only legal
// continuation is the portal edge itself. Physical AreaTriggers may
@@ -960,9 +1181,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
State next{ to.Key, true };
std::string const nextKey = stateKey(next);
// 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 + measuredLength;
// C8.20: dynamic node-to-node MMAP bridges are ordinary local
// ground connections, not penalized fallback routes.
float const candidate = current.Cost + measuredLength;
auto old = distance.find(nextKey);
if (old != distance.end() && candidate >= old->second)
continue;
@@ -977,6 +1198,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
}
}
if (locationGoal && bestExit.Active && bestExit.RootPartial &&
!bestGoalState.empty())
previous.erase(bestGoalState);
std::string selectedState = bestGoalState;
if (!selectedState.empty())
complete = true;
@@ -1170,6 +1394,57 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
route.push_back(std::move(step));
}
if (locationGoal && complete && bestExit.Active)
{
ExitCandidate const& exit = bestExit.Candidate;
if (bestExit.RootPartial)
{
// The reconstructed root already contains the virtual entry
// waypoint and the matching road. Stop that SAME edge early.
if (route.empty() || route.back().EdgeType != "ground" ||
route.back().EdgeDatabaseId != exit.EdgeId)
{
error = "virtual entry/exit reconstruction mismatch";
route.clear();
return false;
}
route.back().GroundEndIndex = int32(exit.EndIndex);
route.back().NodeKey = "virtual_ground_exit";
route.back().MapId = exit.Point.MapId;
route.back().X = exit.Point.X;
route.back().Y = exit.Point.Y;
route.back().Z = exit.Point.Z;
route.back().O = exit.Point.O;
}
else
{
ClientlessTravelRouteStep end;
end.NodeKey = "virtual_ground_exit";
end.NodeType = "road";
end.EdgeType = "ground";
end.EdgeDatabaseId = exit.EdgeId;
end.EdgeReverse = exit.Reverse;
end.GroundEndIndex = int32(exit.EndIndex);
end.MapId = exit.Point.MapId;
end.X = exit.Point.X;
end.Y = exit.Point.Y;
end.Z = exit.Point.Z;
end.O = exit.Point.O;
route.push_back(std::move(end));
}
reachedNode = "virtual_ground_exit";
TC_LOG_INFO("server.adventurebot.navigation",
"C820_EXIT_SELECTED edgeDbId=%u reverse=%u endIndex=%u "
"road=%.1f local=%.1f total=%.1f rootPartial=%u",
exit.EdgeId, exit.Reverse ? 1u : 0u, exit.EndIndex,
exit.RoadCost, exit.LocalCost, bestGoalCost,
bestExit.RootPartial ? 1u : 0u);
}
else if (locationGoal && complete)
TC_LOG_INFO("server.adventurebot.navigation",
"C820_NODE_FINISH node='%s' fullCost=%.1f",
reachedNode.c_str(), bestGoalCost);
TC_LOG_INFO("server.adventurebot.navigation",
"C818_PREFLIGHT complete=%u bridgeTests=%u bridgeAccepted=%u bridgeRejected=%u "
"distanceRejected=%u forwardTests=%u forwardAccepted=%u steps=%u",