Files
Viano-Core/tests/clientless/test_free_flight_planner.py
T
Nico 49712aadfe
GCC PCH / build (push) Waiting to run
clang PCH / build (push) Canceled after 0s
Improve AdventureBot travel recovery and destination eligibility
2026-10-29 11:10:27 +01:00

165 lines
13 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 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;
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){
float d=dist(a,b);return enabled&&map==0&&a.x>=minimumX&&(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+'''
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));
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)