Implement dynamic clientless free-flight travel

Integrate transient flight alternatives with graph routing, native flying mounts, phased terrain and collision checks, bounded avoidance and confirmed landing. Add safe-return recovery, node and location integration, profile settings, diagnostic commands, regression tests and flight documentation.
This commit is contained in:
Nico committed 2026-10-26 17:35:58 +01:00
1 parent 4474714c00
commit 8df9591c99
21 files changed
+1536 -16

No files matched your search

@@ -1,5 +1,6 @@
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessTransportMgr.h"
#include "ClientlessFlightMgr.h"
#include "DatabaseEnv.h"
#include "Log.h"
@@ -509,6 +510,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
std::unordered_set<std::string> const* blockedBridgeKeys,
ClientlessTravelLocationGoal const* locationGoal)
{
sClientlessFlightMgr->BeginPlanning();
struct FlightPlanningScope
{
~FlightPlanningScope() { sClientlessFlightMgr->BeginPlanning(); }
} flightPlanningScope;
// 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.
auto runSearch = [&](bool allowDeferred) -> bool
@@ -533,6 +539,14 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
return false;
}
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z)
{
if (!flightEnabled || from.MapId != map)
return std::numeric_limits<float>::infinity();
Position a,b; a.Relocate(from.X,from.Y,from.Z,from.O); b.Relocate(x,y,z);
return sClientlessFlightMgr->Estimate(player,map,a,b);
};
if (startRadius <= 0.0f)
startRadius = 500.0f;
// C8.18: this cap applies ONLY to virtual node-to-node bridges.
@@ -853,14 +867,20 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
float LocalCost = 0.0f;
float DistanceSq = 0.0f;
ClientlessTravelEdgePoint Point;
bool Flight = false;
};
float const locationAccessRadius = locationGoal
? std::min(300.0f, locationGoal->AnchorRadius) : 0.0f;
std::vector<ExitCandidate> rawExitCandidates;
std::unordered_map<std::string, std::vector<ExitCandidate>> exitsByFrom;
if (locationGoal)
if (locationGoal || flightEnabled)
{
float const radius = locationAccessRadius;
auto const& targetNode = destinationItr->second;
uint32 const goalMap = locationGoal ? locationGoal->MapId : targetNode.MapId;
float const goalX = locationGoal ? locationGoal->X : targetNode.X;
float const goalY = locationGoal ? locationGoal->Y : targetNode.Y;
float const goalZ = locationGoal ? locationGoal->Z : targetNode.Z;
float const radius = flightEnabled ? 20000.0f : locationAccessRadius;
std::unordered_set<std::string> scannedDirected;
for (auto const& edgePair : _edges)
{
@@ -899,13 +919,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
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)
if (point.MapId != goalMap)
continue;
dx = point.X - locationGoal->X;
dy = point.Y - locationGoal->Y;
dz = point.Z - locationGoal->Z;
dx = point.X - goalX;
dy = point.Y - goalY;
dz = point.Z - goalZ;
float const dsq = dx * dx + dy * dy + dz * dz;
if (dsq > radius * radius || dsq >= nearest.DistanceSq)
if (dsq > radius * radius)
continue;
std::string const exitKey = "exit:" + std::to_string(edge.DatabaseId) +
":" + std::to_string(i) + (edge.Reverse ? "R" : "F");
@@ -919,7 +939,18 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
candidate.RoadCost = edge.Cost * (travelled / totalLength);
candidate.DistanceSq = dsq;
candidate.Point = point;
candidate.LocalCost = std::sqrt(dsq);
candidate.LocalCost = locationGoal && dsq <= locationAccessRadius*locationAccessRadius
? std::sqrt(dsq) : std::numeric_limits<float>::infinity();
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;
if (std::isfinite(nearest.DistanceSq) &&
candidate.RoadCost+candidate.LocalCost >= nearest.RoadCost+nearest.LocalCost) continue;
nearest = std::move(candidate);
}
if (std::isfinite(nearest.DistanceSq))
@@ -946,10 +977,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
} bestExit;
float bestGoalCost = std::numeric_limits<float>::infinity();
std::string bestGoalState;
bool bestFinalFlight = false;
// 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)
if (locationGoal || flightEnabled)
{
for (auto const& root : virtualRoots)
{
@@ -1001,9 +1033,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (distanceItr == distance.end() || current.Cost != distanceItr->second)
continue;
if (current.Cost >= bestGoalCost) break;
if (!locationGoal && current.Value.Node == destinationNode)
{
bestGoalState = currentKey;
bestGoalCost = current.Cost;
bestExit = {};
bestFinalFlight = false;
break;
}
@@ -1025,18 +1061,22 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
float const direct = std::sqrt(dx*dx + dy*dy + dz*dz);
// Use the same local access limit for nodes and road exits.
// Otherwise a distant node can bypass a closer usable exit.
if (direct <= locationAccessRadius)
float const air = blockedBridgeKeys && blockedBridgeKeys->count("flight_finish:"+candidateNode.Key)
? std::numeric_limits<float>::infinity() : flightCost(candidateNode,locationGoal->MapId,locationGoal->X,locationGoal->Y,locationGoal->Z);
if (direct <= locationAccessRadius || std::isfinite(air))
{
// Node anchors and virtual exits use the same estimate.
// Neither is rejected because a remote tile is unloaded.
std::string const finishKey = "finish:" + candidateNode.Key;
float const finalCost = blockedBridgeKeys && blockedBridgeKeys->count(finishKey)
? std::numeric_limits<float>::infinity() : direct;
? std::numeric_limits<float>::infinity() :
std::min(direct<=locationAccessRadius ? direct : std::numeric_limits<float>::infinity(),air);
if (current.Cost + finalCost < bestGoalCost)
{
bestGoalCost = current.Cost + finalCost;
bestGoalState = currentKey;
bestExit = {};
bestFinalFlight = air <= finalCost;
}
}
}
@@ -1058,6 +1098,18 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
}
}
if (!locationGoal && flightEnabled)
{
auto exits=exitsByFrom.find(current.Value.Node);
if (exits!=exitsByFrom.end())
for (auto const& exit:exits->second)
if (current.Cost+exit.RoadCost+exit.LocalCost<bestGoalCost)
{
bestGoalCost=current.Cost+exit.RoadCost+exit.LocalCost;
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
@@ -1098,6 +1150,28 @@ 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)
{
auto const& origin=_nodes.at(current.Value.Node);
for (auto const& pair:_nodes)
{
auto const& target=pair.second;
if (target.Key==origin.Key || target.MapId!=origin.MapId) continue;
if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue;
float cost=flightCost(origin,target.MapId,target.X,target.Y,target.Z);
if (!std::isfinite(cost)) continue;
State next{target.Key,false};std::string key=stateKey(next);
float score=current.Cost+cost;
auto old=distance.find(key);if(old!=distance.end()&&old->second<=score)continue;
distance[key]=score;states[key]=next;persistentHops[key]=persistentHops[currentKey]+1;
previous[key]={currentKey,current.Value.Node,"free_flight",0,false};
open.push({score,next});
}
}
// A local bridge is deliberately impossible immediately after another
// local bridge. This prevents MMAP from silently becoming a long-range
// planner while still allowing unlimited bridges over a long journey
@@ -1319,7 +1393,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
}
}
if (locationGoal && bestExit.Active && bestExit.RootPartial &&
if ((locationGoal || flightEnabled) && bestExit.Active && bestExit.RootPartial &&
!bestGoalState.empty())
previous.erase(bestGoalState);
std::string selectedState = bestGoalState;
@@ -1515,7 +1589,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
route.push_back(std::move(step));
}
if (locationGoal && complete && bestExit.Active)
if ((locationGoal || flightEnabled) && complete && bestExit.Active)
{
ExitCandidate const& exit = bestExit.Candidate;
if (bestExit.RootPartial)
@@ -1581,6 +1655,22 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
"C820_NODE_FINISH node='%s' fullCost=%.1f",
reachedNode.c_str(), bestGoalCost);
if (complete && (bestExit.Active ? bestExit.Candidate.Flight : bestFinalFlight))
{
ClientlessTravelRouteStep flight;
auto const& target=destinationItr->second;
flight.NodeKey=locationGoal ? "flight_location" : target.Key;
flight.NodeType=locationGoal ? "location" : target.Type;
flight.EdgeType="free_flight";
flight.MapId=locationGoal ? locationGoal->MapId : target.MapId;
flight.X=locationGoal ? locationGoal->X : target.X;
flight.Y=locationGoal ? locationGoal->Y : target.Y;
flight.Z=locationGoal ? locationGoal->Z : target.Z;
flight.O=locationGoal ? 0 : target.O;
route.push_back(std::move(flight));
reachedNode=locationGoal ? "flight_location" : target.Key;
}
TC_LOG_INFO("server.adventurebot.navigation",
"C818_PREFLIGHT complete=%u bridgeTests=%u bridgeAccepted=%u bridgeRejected=%u "
"distanceRejected=%u forwardTests=%u forwardAccepted=%u steps=%u",