Improve AdventureBot planning and travel performance

This commit is contained in:
Nico committed 2026-10-31 15:35:46 +01:00
1 parent 69b5a1c4a1
commit d1ef007cef
18 files changed
+906 -27

No files matched your search

@@ -574,6 +574,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
// Reuse all start-point observations only WITHIN this immutable planning
// call, including its two search tiers. Never cache live Player paths globally.
std::map<ClientlessGraphProbeCache::Key,ClientlessPathDiagnostics> startProbes;
std::map<std::pair<ClientlessGraphProbeCache::Key, bool>, float> flightEstimates;
auto analyzeStart=[&](Position const& target,ClientlessPathDiagnostics& result)
{
auto key=ClientlessGraphProbeCache::MakeKey(player->GetMapId(),player->GetInstanceId(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ(),target.GetPositionX(),target.GetPositionY(),target.GetPositionZ());
@@ -621,12 +622,26 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
{ error="destination location denied by area catalog"; return false; }
}
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
constexpr std::uint64_t MaxFlightEstimatesPerPlan = 16;
auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z, bool access = false)
{
if (!flightEnabled || from.MapId != map ||
(blockedBridgeKeys && blockedBridgeKeys->count("flight_origin:"+from.Key)))
return std::numeric_limits<float>::infinity();
Position a,b; a.Relocate(from.X,from.Y,from.Z,from.O); b.Relocate(x,y,z);
auto const estimateKey = std::make_pair(
ClientlessGraphProbeCache::MakeKey(map, player->GetInstanceId(),
from.X, from.Y, from.Z, x, y, z), access);
auto cachedEstimate = flightEstimates.find(estimateKey);
if (cachedEstimate != flightEstimates.end())
return cachedEstimate->second;
// Flight links are optional accelerators around the persistent graph.
// A pathological graph must not monopolize the world thread while
// constructing an implicit complete air graph. Cached estimates remain
// usable after the cap; unexamined links simply stay unavailable in
// this plan and ordinary graph travel can still win.
if (trace.FlightCalls >= MaxFlightEstimatesPerPlan)
return std::numeric_limits<float>::infinity();
auto begin=std::chrono::steady_clock::now();++trace.FlightCalls;
float result=sClientlessFlightMgr->Estimate(player,map,a,b,access);
if(access && std::isfinite(result))
@@ -637,6 +652,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
40+distance*0.4f:180+distance*1.5f;
}
trace.FlightUs+=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-begin).count();
flightEstimates.emplace(estimateKey, result);
return result;
};
if (startRadius <= 0.0f)
@@ -649,6 +665,47 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
bridgeRadius = MaxVirtualNodeBridge;
bridgeRadius = std::min(bridgeRadius, MaxVirtualNodeBridge);
// The overwhelmingly common free-flight case is a same-map Location. Do
// not construct a macro graph first: Estimate already enforces licenses,
// mount flags, area policy, distance and the complete catalog segment.
// Native departure/landing validation still happens in FlightMgr::Start.
if (locationGoal && locationGoal->MapId == player->GetMapId())
{
ClientlessTravelNode departureNode;
departureNode.Key = "flight_departure";
departureNode.MapId = player->GetMapId();
departureNode.X = player->GetPositionX();
departureNode.Y = player->GetPositionY();
departureNode.Z = player->GetPositionZ();
float const directFlight = flightCost(departureNode, locationGoal->MapId,
locationGoal->X, locationGoal->Y, locationGoal->Z);
if (std::isfinite(directFlight))
{
ClientlessTravelRouteStep departure;
departure.NodeKey = "flight_departure";
departure.NodeType = "ground";
departure.EdgeType = "start";
departure.MapId = player->GetMapId();
departure.X = player->GetPositionX();
departure.Y = player->GetPositionY();
departure.Z = player->GetPositionZ();
route.push_back(std::move(departure));
ClientlessTravelRouteStep flight;
flight.NodeKey = "flight_location";
flight.NodeType = "location";
flight.EdgeType = "free_flight";
flight.MapId = locationGoal->MapId;
flight.X = locationGoal->X;
flight.Y = locationGoal->Y;
flight.Z = locationGoal->Z;
route.push_back(std::move(flight));
complete = true;
reachedNode = "flight_location";
return true;
}
}
struct State
{
std::string Node;
@@ -1005,6 +1062,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
? std::min(300.0f, locationGoal->AnchorRadius) : 0.0f;
std::vector<ExitCandidate> rawExitCandidates;
std::unordered_map<std::string, std::vector<ExitCandidate>> exitsByFrom;
std::size_t flightExitProbes = 0;
if (locationGoal || flightEnabled)
{
auto const& targetNode = destinationItr->second;
@@ -1046,6 +1104,12 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
float travelled = 0.0f;
ExitCandidate nearest;
nearest.DistanceSq = std::numeric_limits<float>::infinity();
struct RankedFlightExit
{
float LowerBound = 0.0f;
ExitCandidate Candidate;
};
std::vector<RankedFlightExit> rankedFlightExits;
for (uint32 i = 1; i + 1 < ordered.size(); ++i)
{
float dx = ordered[i].X - ordered[i - 1].X;
@@ -1078,18 +1142,59 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
blockedBridgeKeys->count("ground_exit_edge:" + identity);
candidate.LocalCost = locationGoal && !groundExitExhausted && dsq <= locationAccessRadius*locationAccessRadius
? std::sqrt(dsq) : std::numeric_limits<float>::infinity();
// A recorded edge can contain hundreds of samples. Calling
// FlightMgr::Estimate for every sample made the synchronous
// world-thread planner quadratic in road geometry (13k+
// area checks for one route in production). Keep an exact
// ground candidate, but validate only the geometrically best
// flight exits below. Estimate still performs all catalog and
// native flight-permission checks for those candidates.
if (std::isfinite(candidate.LocalCost) &&
(!std::isfinite(nearest.DistanceSq) ||
candidate.RoadCost + candidate.LocalCost < nearest.RoadCost + nearest.LocalCost))
nearest = candidate;
if (flightEnabled && !(blockedBridgeKeys && blockedBridgeKeys->count("flight_exit:" + exitKey)))
{
float const direct = std::sqrt(dsq);
// Forty plus 0.4 * distance is the cheapest possible
// free-flight cost. It is an admissible ordering key;
// rejected area segments simply advance to the next item.
rankedFlightExits.push_back({ candidate.RoadCost + 40.0f + direct * 0.4f,
std::move(candidate) });
}
}
std::sort(rankedFlightExits.begin(), rankedFlightExits.end(),
[](RankedFlightExit const& left, RankedFlightExit const& right)
{ return left.LowerBound < right.LowerBound; });
constexpr std::size_t MaxFlightExitProbesPerEdge = 2;
for (std::size_t i = 0; i < std::min(MaxFlightExitProbesPerEdge, rankedFlightExits.size()); ++i)
{
constexpr std::size_t MaxFlightExitProbesPerPlan = 8;
if (flightExitProbes >= MaxFlightExitProbesPerPlan)
break;
++flightExitProbes;
RankedFlightExit const& ranked = rankedFlightExits[i];
ExitCandidate candidate = ranked.Candidate;
ClientlessTravelNode exitNode;
exitNode.MapId=point.MapId;exitNode.X=point.X;exitNode.Y=point.Y;exitNode.Z=point.Z;
float const air = blockedBridgeKeys && blockedBridgeKeys->count("flight_exit:"+exitKey)
? std::numeric_limits<float>::infinity() : flightCost(exitNode,goalMap,goalX,goalY,goalZ);
if (air < candidate.LocalCost) { candidate.LocalCost=air;candidate.Flight=true; }
if (!std::isfinite(candidate.LocalCost)) continue;
// Never replace a node goal with an unvalidated ground exit.
if (!locationGoal && !candidate.Flight) continue;
exitNode.MapId = candidate.Point.MapId;
exitNode.X = candidate.Point.X;
exitNode.Y = candidate.Point.Y;
exitNode.Z = candidate.Point.Z;
float const air = flightCost(exitNode, goalMap, goalX, goalY, goalZ);
if (!std::isfinite(air))
continue;
candidate.LocalCost = air;
candidate.Flight = true;
if (std::isfinite(nearest.DistanceSq) &&
candidate.RoadCost+candidate.LocalCost >= nearest.RoadCost+nearest.LocalCost) continue;
candidate.RoadCost + candidate.LocalCost >= nearest.RoadCost + nearest.LocalCost)
continue;
nearest = std::move(candidate);
}
// Never replace a node goal with an unvalidated ground exit.
if (!locationGoal && std::isfinite(nearest.DistanceSq) && !nearest.Flight)
nearest.DistanceSq = std::numeric_limits<float>::infinity();
if (std::isfinite(nearest.DistanceSq))
rawExitCandidates.push_back(std::move(nearest));
}
@@ -1115,6 +1220,29 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
float bestGoalCost = std::numeric_limits<float>::infinity();
std::string bestGoalState;
bool bestFinalFlight = false;
// Free flight is a macro connector, not a reason to turn every graph map
// into a complete directed graph. A flyer only needs direct links to the
// requested destination and to nodes that enter a map-changing or physical
// transport edge (portal, ship, zeppelin or tram). Taxi nodes are excluded:
// a free flyer can already reach their same-map destination directly, and
// adding every taxi source recreates an almost-complete graph. Ordinary
// same-map ground
// nodes remain reachable through their recorded graph, while a location
// goal is evaluated directly from every expanded node below.
std::unordered_set<std::string> flightConnectorNodes;
flightConnectorNodes.insert(destinationNode);
for (auto const& edgeGroup : _edges)
for (ClientlessTravelEdge const& edge : edgeGroup.second)
{
auto fromItr = _nodes.find(edge.FromNode);
auto toItr = _nodes.find(edge.ToNode);
bool const changesMap = fromItr != _nodes.end() && toItr != _nodes.end() &&
fromItr->second.MapId != toItr->second.MapId;
bool const physicalTransport = edge.Type == "portal" || edge.Type == "ship" ||
edge.Type == "zeppelin" || edge.Type == "tram" || edge.Type == "transport";
if (changesMap || physicalTransport)
flightConnectorNodes.insert(edge.FromNode);
}
// 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.
@@ -1259,6 +1387,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
// Persistent TravelGraph edges always reset the bridge guard.
auto edgeItr = _edges.find(current.Value.Node);
bool hasPersistentContinuation = false;
if (edgeItr != _edges.end())
{
for (ClientlessTravelEdge const& edge : edgeItr->second)
@@ -1268,6 +1397,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
continue;
if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))
continue;
hasPersistentContinuation = true;
State next{ edge.ToNode, false };
std::string const nextKey = stateKey(next);
@@ -1294,9 +1424,12 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (flightEnabled && !portalArrival)
{
auto const& origin=_nodes.at(current.Value.Node);
for (auto const& pair:_nodes)
for (std::string const& targetKey : flightConnectorNodes)
{
auto const& target=pair.second;
auto targetItr = _nodes.find(targetKey);
if (targetItr == _nodes.end())
continue;
auto const& target=targetItr->second;
if (target.Key==origin.Key || target.MapId!=origin.MapId) continue;
State next{target.Key,false};std::string key=stateKey(next);
auto old=distance.find(key);
@@ -1335,7 +1468,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
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 &&
if (!futurePortalSource && !hasPersistentContinuation && persistentHops[currentKey] > 0 &&
!(blockedBridgeKeys &&
blockedBridgeKeys->count(MakeLocalBridgeKey(from.Key, "virtual_ground_entry"))))
{