Improve AdventureBot instance and flight navigation
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-11-01 17:00:59 +01:00
1 parent be049baacc
commit 3c361e435a
14 files changed
+525 -63

No files matched your search

@@ -3,6 +3,7 @@
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessTransportMgr.h"
#include "ClientlessFlightMgr.h"
#include "ClientlessLuaEngine.h"
#include "DatabaseEnv.h"
#include "Log.h"
@@ -125,7 +126,7 @@ ClientlessPortalUse const* ClientlessTravelGraphMgr::GetPortalUse(uint32 edgeDat
return itr != _portalUses.end() ? &itr->second : nullptr;
}
bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const
bool ClientlessTravelGraphMgr::PortalNativePrerequisites(Player const* player,uint32 edgeDatabaseId) const
{
auto portal=GetPortalUse(edgeDatabaseId);
auto info=portal?sObjectMgr->GetGameObjectTemplate(portal->Entry):nullptr;
@@ -136,6 +137,23 @@ bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 e
return !portal->Requirements.empty();
}
bool ClientlessTravelGraphMgr::AdventurePortalUnlockAllowed(Player const* player,
uint32 edgeDatabaseId) const
{
// AdventureBots cannot acquire quest progress. Once they have reached the
// Cataclysm level range, graph-bound public travel portals are therefore
// considered unlocked. This does not bypass faction/area/node policy or
// make arbitrary, unbound GameObjects usable.
return player && player->getLevel() >= 80 && GetPortalUse(edgeDatabaseId) &&
sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
}
bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const
{
return PortalNativePrerequisites(player,edgeDatabaseId) ||
AdventurePortalUnlockAllowed(player,edgeDatabaseId);
}
bool ClientlessTravelGraphMgr::Reload()
{
_positionProbes.Clear();
@@ -623,6 +641,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
}
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
constexpr std::uint64_t MaxFlightEstimatesPerPlan = 16;
// Preserve part of the global budget for nodes reached later in the
// search. In particular, a flight to a portal source must still leave
// enough probes for the flight from its destination map to the goal.
constexpr std::uint64_t MaxFlightEstimatesPerOrigin = 6;
auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z, bool access = false)
{
if (!flightEnabled || from.MapId != map ||
@@ -815,9 +837,15 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
ClientlessTravelNode departure;
departure.Key="flight_departure";
departure.MapId=player->GetMapId();departure.X=player->GetPositionX();departure.Y=player->GetPositionY();departure.Z=player->GetPositionZ();
float air=blockedBridgeKeys && blockedBridgeKeys->count("flight:flight_departure->"+node.Key)?
std::numeric_limits<float>::infinity():flightCost(departure,node.MapId,node.X,node.Y,node.Z,!reachable);
if(std::isfinite(air) && (!reachable || air<entryNav.PathLength))
// A valid local MMAP entry is the normal, organic graph access and
// must not spend the bounded flight-estimate budget. Free flight is
// the recovery/re-entry mode for nodes that local navigation cannot
// reach, not a shortcut racing every nearby pavement node.
float air = std::numeric_limits<float>::infinity();
if (!reachable && !(blockedBridgeKeys &&
blockedBridgeKeys->count("flight:flight_departure->"+node.Key)))
air=flightCost(departure,node.MapId,node.X,node.Y,node.Z,true);
if(std::isfinite(air) && !reachable)
{flightRoots.insert(node.Key);startCost=air;}
else
{
@@ -1220,29 +1248,37 @@ 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)
// Every permitted node is a potential way back into the persistent graph.
// Do not materialize an all-to-all air graph, though: candidates are ranked
// on demand below and the shared per-plan Estimate cap remains authoritative.
// This is important for isolated anchors such as the capital Cataclysm
// portals, but is deliberately not portal-specific.
auto flightRemainderLowerBound = [&](ClientlessTravelNode const& candidate)
{
ClientlessTravelNode const& destination = destinationItr->second;
auto lowerBoundToGoal = [&](ClientlessTravelNode const& node)
{
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);
}
if (node.MapId != destination.MapId)
return std::numeric_limits<float>::infinity();
float dx = destination.X-node.X, dy = destination.Y-node.Y, dz = destination.Z-node.Z;
return 40.0f + std::sqrt(dx*dx+dy*dy+dz*dz)*0.4f;
};
float best = lowerBoundToGoal(candidate);
auto edges = _edges.find(candidate.Key);
if (edges != _edges.end())
for (ClientlessTravelEdge const& edge : edges->second)
{
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge)))
continue;
if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))
continue;
auto next = _nodes.find(edge.ToNode);
if (next == _nodes.end())
continue;
best = std::min(best, edge.Cost + lowerBoundToGoal(next->second));
}
return best;
};
// 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.
@@ -1379,10 +1415,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
// ground transit. Once reached during a planned route, its only legal
// continuation is the portal edge itself. Physical AreaTriggers may
// fire even before the route executor advances to that edge.
bool const isPortalSource =
current.Value.Node.rfind("portal_at_", 0) == 0 &&
current.Value.Node.size() >= 7 &&
current.Value.Node.compare(current.Value.Node.size() - 7, 7, "_source") == 0;
auto const sourceEdges = _edges.find(current.Value.Node);
bool const isPortalSource = sourceEdges != _edges.end() &&
std::any_of(sourceEdges->second.begin(), sourceEdges->second.end(),
[](ClientlessTravelEdge const& edge)
{ return edge.Type == "portal" || edge.Type == "portal_use"; });
bool const portalArrival = isPortalSource && previous.find(currentKey) != previous.end();
// Persistent TravelGraph edges always reset the bridge guard.
@@ -1393,7 +1430,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
for (ClientlessTravelEdge const& edge : edgeItr->second)
{
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue;
if (portalArrival && edge.Type != "portal")
if (portalArrival && edge.Type != "portal" && edge.Type != "portal_use")
continue;
if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))
continue;
@@ -1421,15 +1458,15 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
// Dynamic flights connect real nodes; never the synthetic entry waypoints.
// Include them before the local-bridge guard: a flight is a separate
// macro travel mode with its own native departure and landing checks.
if (flightEnabled && !portalArrival)
if (flightEnabled && !portalArrival && trace.FlightCalls < MaxFlightEstimatesPerPlan)
{
auto const& origin=_nodes.at(current.Value.Node);
for (std::string const& targetKey : flightConnectorNodes)
std::vector<std::pair<float, std::string>> macroFlightCandidates;
std::vector<std::pair<float, std::string>> flightCandidates;
flightCandidates.reserve(_nodes.size());
for (auto const& candidate : _nodes)
{
auto targetItr = _nodes.find(targetKey);
if (targetItr == _nodes.end())
continue;
auto const& target=targetItr->second;
auto const& target=candidate.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);
@@ -1441,6 +1478,58 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if(old!=distance.end() && old->second<=current.Cost+lowerBound)continue;
if ((!RescueNodeAllowed(target) || !sAdventureAreaMgr->AllowsNode(player,target))) continue;
if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue;
float const remainder = flightRemainderLowerBound(target);
if (!std::isfinite(remainder))
continue;
bool macroConnector = target.Key == destinationNode;
auto outgoing = _edges.find(target.Key);
if (outgoing != _edges.end())
for (ClientlessTravelEdge const& edge : outgoing->second)
{
auto next = _nodes.find(edge.ToNode);
bool const changesMap = next != _nodes.end() &&
next->second.MapId != target.MapId;
bool const physical = edge.Type == "portal" ||
edge.Type == "portal_use" || edge.Type == "ship" ||
edge.Type == "zeppelin" || edge.Type == "tram" ||
edge.Type == "transport";
if (changesMap || physical)
{
macroConnector = true;
break;
}
}
auto& candidates = macroConnector ? macroFlightCandidates : flightCandidates;
candidates.emplace_back(lowerBound + remainder, target.Key);
}
std::sort(macroFlightCandidates.begin(), macroFlightCandidates.end(),
[&](auto const& left, auto const& right)
{
// Every newly reached portal/transport endpoint must get
// one chance to finish the requested journey before the
// bounded probe window is spent on further connectors.
bool const leftIsDestination = left.second == destinationNode;
bool const rightIsDestination = right.second == destinationNode;
if (leftIsDestination != rightIsDestination)
return leftIsDestination;
return left < right;
});
std::sort(flightCandidates.begin(), flightCandidates.end());
macroFlightCandidates.insert(macroFlightCandidates.end(),
flightCandidates.begin(), flightCandidates.end());
std::uint64_t const callsAtOrigin = trace.FlightCalls;
for (auto const& ranked : macroFlightCandidates)
{
if (trace.FlightCalls >= MaxFlightEstimatesPerPlan ||
trace.FlightCalls - callsAtOrigin >= MaxFlightEstimatesPerOrigin)
break;
auto const& target = _nodes.at(ranked.second);
State next{target.Key,false};std::string key=stateKey(next);
auto old=distance.find(key);
auto const blockedFlight = blockedBridgeKeys &&
blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key);
if (blockedFlight)
continue;
float cost=flightCost(origin,target.MapId,target.X,target.Y,target.Z);
if (!std::isfinite(cost)) continue;
float score=current.Cost+cost;
@@ -1464,10 +1553,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
// 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;
auto const futureEdges = _edges.find(from.Key);
bool const futurePortalSource = futureEdges != _edges.end() &&
std::any_of(futureEdges->second.begin(), futureEdges->second.end(),
[](ClientlessTravelEdge const& edge)
{ return edge.Type == "portal" || edge.Type == "portal_use"; });
if (!futurePortalSource && !hasPersistentContinuation && persistentHops[currentKey] > 0 &&
!(blockedBridgeKeys &&
blockedBridgeKeys->count(MakeLocalBridgeKey(from.Key, "virtual_ground_entry"))))