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:
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",
|
||||
|
||||
Reference in new issue
Block a user