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 "ClientlessGraphProbeCache.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using uint8=unsigned char;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;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;} float GetExactDist(Position const* p)const{return std::sqrt((x-p->x)*(x-p->x)+(y-p->y)*(y-p->y)+(z-p->z)*(z-p->z));} 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 {bool complete=true;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);n.Complete=complete;}} travel; auto sClientlessTravelMgr=&travel; struct Flight { bool enabled=false;float minimumX=-10000;float forbiddenTargetX=1e9; float forbiddenPairFromX=1e9,forbiddenPairTargetX=1e9; void BeginPlanning(Player const* = nullptr){} bool Enabled(Player const*){return enabled;} bool Preferred(Player const*,uint32,Position const&){return false;} float Estimate(Player const*,uint32 map,Position a,Position b,bool recovery=false){ bool forbiddenPair=std::fabs(a.x-forbiddenPairFromX)<0.1f&&std::fabs(b.x-forbiddenPairTargetX)<0.1f; float d=dist(a,b);return enabled&&map==0&&a.x>=minimumX&&std::fabs(b.x-forbiddenTargetX)>0.1f&&!forbiddenPair&&(recovery||d>=60)?(recovery?d:180+d*1.5f):std::numeric_limits::infinity(); } } flight; auto sClientlessFlightMgr=&flight; struct AreaAccess { bool active=true;std::unordered_set blocked;float forbiddenMin=1e9,forbiddenMax=1e9; bool Applies(Player const*)const{return active;} templatebool AllowsNode(Player const*,Node const& n)const{return !blocked.count(n.Key);} templatebool AllowsEdge(Player const*,Edge const& e)const{return !blocked.count(e.FromNode)&&!blocked.count(e.ToNode);} bool AllowsPosition(Player const*,uint32,Position const& p)const{return !(p.x>=forbiddenMin&&p.x<=forbiddenMax);} bool AllowsSegment(Player const*,uint32,Position const& a,Position const& b)const{return std::max(a.x,b.x)forbiddenMax;} } access;auto sAdventureAreaMgr=&access; struct VcorePositionPathResult {struct Point{float x,y,z;};enum class Status{Complete,Partial};Status Result=Status::Partial;float FinalDistance=0,PathLength=0,DirectDistance=0;std::vector Points;bool IsComplete()const{return Result==Status::Complete;}}; struct PathGenerator {static inline unsigned probes=0;static inline bool available=false; static inline float maxDistance=10000;static VcorePositionPathResult AnalyzePositions(uint32,uint32,uint32,Position a,Position b,uint32){++probes;float d=dist(a,b);return {available && d<=maxDistance?VcorePositionPathResult::Status::Complete:VcorePositionPathResult::Status::Partial,0,d,d,{{a.x,a.y,a.z},{b.x,b.y,b.z}}};}}; std::string MakeLocalBridgeKey(std::string const&a,std::string const&b){return a+"->"+b;} ''' cls='class ClientlessTravelGraphMgr { public: bool EnsureLoaded(){return true;}\n'+signature+''' bool _rescuePlanning=false; bool RescueNodeAllowed(ClientlessTravelNode const& n)const{return _rescuePlanning || n.Type!="rescue";} bool RescueEdgeAllowed(ClientlessTravelEdge const& e)const{return RescueNodeAllowed(_nodes.at(e.FromNode))&&RescueNodeAllowed(_nodes.at(e.ToNode));} ClientlessGraphProbeCache _positionProbes;std::unordered_map> _bridgeNeighborhoods; std::unordered_map _nodes; std::unordered_map> _edges; std::unordered_map> _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 plan(ClientlessTravelGraphMgr&g,Player p,ClientlessTravelLocationGoal goal,float radius,std::unordered_set blocked={}){ std::vector 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="<{"ground_exit_edge:1F"}; std::vector partial;bool complete;std::string reached,error; goal.AnchorRadius=50; Player startAtA;assert(g.PlanAdaptiveRoute(&startAtA,"B",1,200,partial,complete,reached,error,nullptr,&blocked,&goal)); assert(!complete);for(auto const& step:partial)assert(step.NodeKey!="virtual_ground_exit");} // 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.back().GroundContinuationNode=="B");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 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"); // A second plan reuses static proofs, but still honors query-specific failures. flight.enabled=false;PathGenerator::available=true;PathGenerator::maxDistance=10000; ClientlessTravelGraphMgr cached;node(cached,"A",0);node(cached,"B",150); assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done); auto calls=PathGenerator::probes;auto size=route.size(); assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done&&route.size()==size); assert(PathGenerator::probes==calls&&cached._positionProbes.Hits>0); std::unordered_set forbidden{"A->B"}; assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,&forbidden)&&!done); cached._positionProbes.Clear();cached._bridgeNeighborhoods.clear();PathGenerator::available=false; assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&!done); // Catalog restrictions apply even to geometry cached for a previously legal route. PathGenerator::available=true;cached._positionProbes.Clear();cached._bridgeNeighborhoods.clear(); assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done); calls=PathGenerator::probes; access.forbiddenMin=60;access.forbiddenMax=90; assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&!done); assert(PathGenerator::probes==calls); access.forbiddenMin=access.forbiddenMax=1e9; assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done); access.blocked.insert("B"); assert(!cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)); access.blocked.clear(); // A failed local MMAP approach must not exclude a native-capable flight root. travel.complete=false;flight.enabled=true;flight.minimumX=-10000;PathGenerator::available=false; ClientlessTravelGraphMgr accessFlight;node(accessFlight,"A",300);node(accessFlight,"B",2000);edge(accessFlight,"A","B",77,1,false,"taxi"); assert(accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why)&&done); assert(route.size()==3&&route.front().NodeKey=="flight_departure"&&route[1].NodeKey=="A"&&route[1].EdgeType=="free_flight"); std::unordered_set deniedFlightRoots={"flight:flight_departure->A"}; route.clear(); assert(!accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots)); // Reject all flight targets from a failed physical departure, not just one target. deniedFlightRoots={"flight_origin:flight_departure"};route.clear(); assert(!accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots)); // The same origin restriction still allows a real local ground approach. travel.complete=true;route.clear(); assert(accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots)); assert(route.front().NodeKey!="flight_departure"); travel.complete=true; // Any allowed graph node can be a bounded, on-demand flight re-entry. The // planner must not require a portal/transport node type or an MMAP bridge. flight.forbiddenTargetX=2000; ClientlessTravelGraphMgr genericEntry;node(genericEntry,"A",0);node(genericEntry,"R",500);node(genericEntry,"B",2000); edge(genericEntry,"R","B",88,10,false,"ground"); travel.complete=false;route.clear(); assert(genericEntry.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done); assert(route.size()==3&&route[0].NodeKey=="A"&&route[1].NodeKey=="R"); assert(route[1].EdgeType=="free_flight"&&route[2].EdgeDatabaseId==88); flight.forbiddenTargetX=1e9;travel.complete=true; // Keep probe budget for a later graph state: fly to a generic portal source, // cross it, and then fly to the destination. A rejected direct flight must // not consume all estimates at the initial node. flight.forbiddenPairFromX=0;flight.forbiddenPairTargetX=2000; ClientlessTravelGraphMgr portalEntry;node(portalEntry,"A",0);node(portalEntry,"P",500); node(portalEntry,"Q",1500);node(portalEntry,"B",2000); edge(portalEntry,"P","Q",89,1,false,"portal_use"); travel.complete=false;route.clear(); assert(portalEntry.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done); assert(route.size()==4&&route[1].NodeKey=="P"&&route[1].EdgeType=="free_flight"); assert(route[2].NodeKey=="Q"&&route[2].EdgeType=="portal_use"); assert(route[3].NodeKey=="B"&&route[3].EdgeType=="free_flight"); flight.forbiddenPairFromX=flight.forbiddenPairTargetX=1e9;travel.complete=true; 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 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","-I"+str(root/"src/server/game/World"),str(cpp),"-o",str(binary)],check=True) subprocess.run([str(binary)],check=True)