implemented usage of virtual exit and entry nodes
This commit is contained in:
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",
|
||||
|
||||
Reference in new issue
Block a user