200 lines
16 KiB
Python
200 lines
16 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 "ClientlessGraphProbeCache.h"
|
|
#include <chrono>
|
|
#include <map>
|
|
#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 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<float>::infinity();
|
|
}
|
|
} flight;
|
|
auto sClientlessFlightMgr=&flight;
|
|
struct AreaAccess {
|
|
bool active=true;std::unordered_set<std::string> blocked;float forbiddenMin=1e9,forbiddenMax=1e9;
|
|
bool Applies(Player const*)const{return active;}
|
|
template<class Node>bool AllowsNode(Player const*,Node const& n)const{return !blocked.count(n.Key);}
|
|
template<class Edge>bool 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)<forbiddenMin||std::min(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<Point> 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<std::string,std::vector<std::string>> _bridgeNeighborhoods;
|
|
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);assert(r.back().GroundContinuationNode=="B");
|
|
// 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);assert(r.back().GroundContinuationNode=="A");
|
|
// 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);
|
|
// Once a whole direction has been traversed without a usable exit, its stale
|
|
// estimate must not cause another optimistic exit plan. The actual road stays usable.
|
|
{auto blocked=std::unordered_set<std::string>{"ground_exit_edge:1F"};
|
|
std::vector<ClientlessTravelRouteStep> 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<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");
|
|
// 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<std::string> 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<std::string> 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<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","-I"+str(root/"src/server/game/World"),str(cpp),"-o",str(binary)],check=True)
|
|
subprocess.run([str(binary)],check=True)
|