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.
113 lines
8.6 KiB
Python
113 lines
8.6 KiB
Python
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
root=Path(__file__).resolve().parents[2]
|
|
h=(root/'src/server/game/World/ClientlessTravelGraphMgr.h').read_text()
|
|
s=(root/'src/server/game/World/ClientlessTravelGraphMgr.cpp').read_text()
|
|
structs=h[h.index('struct TC_GAME_API ClientlessTravelNode'):h.index('class TC_GAME_API ClientlessTravelGraphMgr')].replace('TC_GAME_API ','')
|
|
method=s[s.index('bool ClientlessTravelGraphMgr::PlanAdaptiveRoute('):s.index('bool ClientlessTravelGraphMgr::PlanRouteToPosition(')]
|
|
signature=h[h.index(' bool PlanAdaptiveRoute('):h.index(' bool FindNearestNodeToPosition(')]
|
|
preamble=r'''
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <queue>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
using uint32=uint32_t; using int32=int32_t;
|
|
#define TC_LOG_INFO(...) do {} while(0)
|
|
#define TC_LOG_DEBUG(...) do {} while(0)
|
|
#define TC_LOG_WARN(...) do {} while(0)
|
|
constexpr uint32 NAV_GROUND=1, PATHFIND_INCOMPLETE=2, PATHFIND_NOPATH=4;
|
|
struct Position { float x=0,y=0,z=0; void Relocate(float a,float b,float c,float=0){x=a;y=b;z=c;} };
|
|
struct Player { float x=0,y=0,z=0; bool IsInWorld() const{return true;} uint32 GetMapId()const{return 0;} uint32 GetInstanceId()const{return 0;} float GetPositionX()const{return x;} float GetPositionY()const{return y;} float GetPositionZ()const{return z;} };
|
|
float dist(Position a,Position b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}
|
|
struct ClientlessPathDiagnostics {bool Calculated=true, Complete=true,Suspicious=false; uint32 PathType=1,PointCount=2; float DirectDistance=0,PathLength=0,MaxSegmentLength=0,MaxZDelta=0,FinalDistance=0; std::string Reason;};
|
|
struct Travel {void AnalyzePath(Player* p,Position b,ClientlessPathDiagnostics& n){Position a;a.Relocate(p->x,p->y,p->z);n.PathLength=n.DirectDistance=dist(a,b);}} travel;
|
|
auto sClientlessTravelMgr=&travel;
|
|
struct Flight {
|
|
bool enabled=false;float minimumX=-10000;
|
|
void BeginPlanning(){}
|
|
bool Enabled(Player const*){return enabled;}
|
|
float Estimate(Player const*,uint32 map,Position a,Position b){
|
|
float d=dist(a,b);return enabled&&map==0&&a.x>=minimumX&&d>=60?180+d*1.5f:std::numeric_limits<float>::infinity();
|
|
}
|
|
} flight;
|
|
auto sClientlessFlightMgr=&flight;
|
|
struct VcorePositionPathResult {bool complete=false; uint32 Result=2;float FinalDistance=0,PathLength=0;bool IsComplete()const{return complete;}};
|
|
struct PathGenerator {static inline bool available=false; static inline float maxDistance=10000;static VcorePositionPathResult AnalyzePositions(uint32,uint32,uint32,Position a,Position b,uint32){float d=dist(a,b);return {available && d<=maxDistance,2,0,d};}};
|
|
std::string MakeLocalBridgeKey(std::string const&a,std::string const&b){return a+"->"+b;}
|
|
'''
|
|
cls='class ClientlessTravelGraphMgr { public: bool EnsureLoaded(){return true;}\n'+signature+'''
|
|
std::unordered_map<std::string,ClientlessTravelNode> _nodes;
|
|
std::unordered_map<std::string,std::vector<ClientlessTravelEdge>> _edges;
|
|
std::unordered_map<uint32,std::vector<ClientlessTravelEdgePoint>> _edgePoints;
|
|
};
|
|
'''
|
|
tests=r'''
|
|
void node(ClientlessTravelGraphMgr& g,std::string k,float x,float y=0){ClientlessTravelNode n;n.Key=k;n.X=x;n.Y=y;g._nodes[k]=n;}
|
|
void edge(ClientlessTravelGraphMgr&g,std::string a,std::string b,uint32 id,float cost,bool rev=false,std::string type="ground"){g._edges[a].push_back({id,a,b,type,cost,rev});}
|
|
std::vector<ClientlessTravelRouteStep> plan(ClientlessTravelGraphMgr&g,Player p,ClientlessTravelLocationGoal goal,float radius,std::unordered_set<std::string> blocked={}){
|
|
std::vector<ClientlessTravelRouteStep> r;bool complete=false;std::string reached,error;
|
|
assert(g.PlanAdaptiveRoute(&p,"B",radius,200,r,complete,reached,error,nullptr,&blocked,&goal));if(!complete){std::cerr<<"incomplete x="<<p.x<<" goal="<<goal.X<<" blocked="<<blocked.size()<<" error="<<error<<"\n";}assert(complete);return r;}
|
|
int main(){
|
|
ClientlessTravelGraphMgr g;node(g,"A",0);node(g,"B",1000);edge(g,"A","B",1,1000);
|
|
for(float x:{0.f,100.f,700.f,900.f,1000.f}){ClientlessTravelEdgePoint p;p.X=x;g._edgePoints[1].push_back(p);}
|
|
ClientlessTravelLocationGoal goal;goal.X=700;goal.Y=20;goal.AnchorRadius=50;
|
|
// A stored path exists. Its interior exit must still win without remote MMAP.
|
|
auto r=plan(g,{},goal,1);assert(r.back().GroundEndIndex==2);assert(r.back().X==700);
|
|
// Preserve a valid entry even when another entry reaches the endpoint cheaper.
|
|
Player p;p.x=100;r=plan(g,p,goal,900);assert(r.front().NodeKey=="virtual_ground_entry");assert(r.back().GroundStartIndex==1);assert(r.back().GroundEndIndex==2);
|
|
// Reverse direction uses execution-order indices.
|
|
ClientlessTravelGraphMgr rev=g;rev._edges.clear();edge(rev,"B","A",1,1000,true);p.x=900;goal.X=100;r=plan(rev,p,goal,1);assert(r.back().EdgeReverse);assert(r.back().GroundStartIndex==1);assert(r.back().GroundEndIndex==3);
|
|
// Blacklist the nearest failed exit: select the next eligible waypoint.
|
|
goal.X=700;goal.AnchorRadius=250;r=plan(g,{},goal,1,{"exit:1:2F"});assert(r.back().GroundEndIndex==3);
|
|
// Regular anchors use the same estimated local cost without remote MMAP.
|
|
goal.X=1000;goal.Y=0;goal.AnchorRadius=10;r=plan(g,{},goal,1);assert(r.back().NodeKey=="B");assert(r.back().GroundEndIndex==-1);
|
|
// Forward entry after a transport can exit on the same road before its endpoint.
|
|
ClientlessTravelGraphMgr f=g;node(f,"S",-1000);node(f,"T",100);edge(f,"S","T",2,1,false,"taxi");p.x=-1000;goal.X=700;goal.Y=20;goal.AnchorRadius=50;PathGenerator::available=true;PathGenerator::maxDistance=150;
|
|
r=plan(f,p,goal,1);assert(r.back().GroundStartIndex==1);assert(r.back().GroundEndIndex==2);assert(r[r.size()-2].NodeKey=="virtual_ground_entry");
|
|
// A regular node outside the exit radius must not win an optimistic local leg.
|
|
ClientlessTravelGraphMgr detour=g;node(detour,"N",300,0);edge(detour,"A","N",5,1,false,"taxi");goal.X=700;goal.Y=20;goal.AnchorRadius=1000;p.x=0;PathGenerator::available=false;
|
|
r=plan(detour,p,goal,1);assert(r.back().NodeKey=="virtual_ground_exit");
|
|
// Restore the directional test's narrow goal radius.
|
|
goal.AnchorRadius=50;
|
|
// A point beyond the exit cannot traverse the directed road backwards.
|
|
PathGenerator::available=false;p.x=900;r=plan(g,p,goal,1);assert(r.back().GroundStartIndex<0); // uses ordinary B anchor when expanded below
|
|
// New flight alternatives are transient and preserve the existing road preference.
|
|
flight.enabled=true;Player start;
|
|
ClientlessTravelGraphMgr air;node(air,"A",0);node(air,"B",1000);
|
|
std::vector<ClientlessTravelRouteStep> route;bool done=false;std::string endKey,why;
|
|
assert(air.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why));
|
|
assert(done&&route.size()==2&&route.back().EdgeType=="free_flight"&&route.back().EdgeDatabaseId==0);
|
|
assert(air._edges.empty()&&air._nodes.size()==2);
|
|
ClientlessTravelGraphMgr road=air;edge(road,"A","B",9,1000);
|
|
assert(road.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why));
|
|
assert(done&&route.back().EdgeType=="ground");
|
|
ClientlessTravelLocationGoal far;far.X=2000;far.AnchorRadius=50;
|
|
assert(air.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,&far));
|
|
assert(done&&route.back().NodeKey=="flight_location"&&route.back().EdgeType=="free_flight");
|
|
// A permissible takeoff halfway along a real road may beat its far endpoint.
|
|
ClientlessTravelGraphMgr exits;node(exits,"A",0);node(exits,"B",1000);node(exits,"D",500,800);
|
|
edge(exits,"A","B",8,1000);for(float x:{0.f,100.f,500.f,900.f,1000.f}){ClientlessTravelEdgePoint pt;pt.X=x;exits._edgePoints[8].push_back(pt);}
|
|
flight.minimumX=400;
|
|
assert(exits.PlanAdaptiveRoute(&start,"D",1,200,route,done,endKey,why));
|
|
assert(done&&route.back().NodeKey=="D"&&route.back().EdgeType=="free_flight");
|
|
assert(route[route.size()-2].NodeKey=="virtual_ground_exit");
|
|
for(auto const& step:route)if(step.EdgeType=="free_flight")assert(step.NodeKey!="virtual_ground_entry");
|
|
std::cout<<"ground planner regressions and dynamic flight/node/location/virtual-exit integration passed\n";
|
|
}
|
|
'''
|
|
# Last direction case must be unreachable: explicitly inspect a partial result.
|
|
tests=tests.replace('r=plan(g,p,goal,1);assert(r.back().GroundStartIndex<0); // uses ordinary B anchor when expanded below','std::vector<ClientlessTravelRouteStep> partial;bool complete;std::string reached,error;assert(g.PlanAdaptiveRoute(&p,"B",1,200,partial,complete,reached,error,nullptr,nullptr,&goal));assert(!complete);')
|
|
with tempfile.TemporaryDirectory(prefix="vcore-flight-planner-") as folder:
|
|
cpp=Path(folder)/"test.cpp";binary=Path(folder)/"test"
|
|
cpp.write_text(preamble+structs+cls+method+tests)
|
|
subprocess.run(["c++","-std=c++17","-O0",str(cpp),"-o",str(binary)],check=True)
|
|
subprocess.run([str(binary)],check=True)
|