Finish capital waiting travel and adaptive flight entry up to 10,000 yards. Preserve checked vertical climbs, recover blocked ground departures and find validated dry landings. Retain group routes and Undercity arrival constraints. Restore defense from native combat references, coordinate instance rendezvous and browser reentry, finalize clientless sessions before map destruction and transfer Halaa capture-point ownership safely. Include the associated regression tests, investigation reports and reusable diagnostic scripts. Keep generated runtime evidence and server binaries local through scoped ignore rules. Validation/build/live evidence is recorded in the session checkpoint and individual reports.
294 lines
23 KiB
Python
294 lines
23 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 "ClientlessTravelCost.h"
|
|
#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 {
|
|
float rejectBelow=-1e9;float closedFromX=1e9;bool rejectNearbyTargets=false;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&&b.x>=rejectBelow&&std::fabs(a.x-closedFromX)>0.1f&&!(rejectNearbyTargets&&b.x>1&&b.x<450)&&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)&&done);
|
|
assert(route[1].NodeKey=="B"&&route[1].EdgeType=="free_flight");
|
|
// 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;
|
|
|
|
// Replanning AT an interactive portal must traverse it, even when flying
|
|
// directly from the indoor source looks cheaper to the estimate.
|
|
ClientlessTravelGraphMgr rootPortal;node(rootPortal,"P",500);node(rootPortal,"Q",-1000);node(rootPortal,"B",2000);
|
|
edge(rootPortal,"P","Q",91,1,false,"portal_use");
|
|
Player atPortal;atPortal.x=500;travel.complete=false;route.clear();
|
|
assert(rootPortal.PlanAdaptiveRoute(&atPortal,"B",1,200,route,done,endKey,why)&&done);
|
|
assert(route.size()>=3&&route[1].EdgeType=="portal_use"&&route[1].NodeKey=="Q");
|
|
// A blocked portal does not trap the actor: its recorded escape road remains.
|
|
edge(rootPortal,"P","B",92,400,false,"ground");
|
|
std::unordered_set<uint32> failedPortal{91};flight.enabled=false;route.clear();
|
|
assert(rootPortal.PlanAdaptiveRoute(&atPortal,"B",1,200,route,done,endKey,why,&failedPortal)&&done);
|
|
assert(route.back().EdgeDatabaseId==92);
|
|
// Many nearby inaccessible anchors must leave budget for an isolated portal
|
|
// outside the local-entry radius and the flight from its destination.
|
|
flight.enabled=true;flight.rejectNearbyTargets=true;
|
|
flight.forbiddenPairFromX=0;flight.forbiddenPairTargetX=2000;
|
|
ClientlessTravelGraphMgr crowded;node(crowded,"A",0);node(crowded,"P",500);node(crowded,"Q",1500);node(crowded,"B",2000);
|
|
edge(crowded,"P","Q",93,1,false,"portal_use");
|
|
for(unsigned i=1;i<=40;++i)node(crowded,"indoor"+std::to_string(i),float(i*10));
|
|
route.clear();assert(crowded.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why)&&done);
|
|
bool usedIsolatedPortal=false;for(auto const& step:route)if(step.EdgeDatabaseId==93)usedIsolatedPortal=true;
|
|
assert(usedIsolatedPortal);
|
|
flight.rejectNearbyTargets=false;flight.forbiddenPairFromX=flight.forbiddenPairTargetX=1e9;
|
|
|
|
// Speculative road exits and an unusable initial departure must not starve a
|
|
// nearby flight-only portal. The cheap complete macro route must beat the
|
|
// long native fallback, not merely appear as a considered candidate.
|
|
flight.enabled=true;flight.closedFromX=0;travel.complete=true;
|
|
ClientlessTravelGraphMgr portalBudget;node(portalBudget,"A",0);node(portalBudget,"C",100);
|
|
node(portalBudget,"P",500);node(portalBudget,"Q",1500);node(portalBudget,"B",2000);
|
|
edge(portalBudget,"P","Q",94,1,false,"portal_use");
|
|
edge(portalBudget,"A","B",95,10000,false,"taxi");
|
|
for(unsigned i=0;i<4;++i){
|
|
auto key="remote"+std::to_string(i);node(portalBudget,key,3000+float(i*100));
|
|
edge(portalBudget,"A",key,100+i,10000);
|
|
for(float x:{0.f,1000.f,2000.f,3000.f}){ClientlessTravelEdgePoint pt;pt.X=x;pt.Y=float(i*20);portalBudget._edgePoints[100+i].push_back(pt);}
|
|
}
|
|
route.clear();assert(portalBudget.PlanAdaptiveRoute(&start,"B",150,200,route,done,endKey,why)&&done);
|
|
bool budgetPortal=false;for(auto const& step:route)if(step.EdgeDatabaseId==94)budgetPortal=true;
|
|
assert(budgetPortal);flight.closedFromX=1e9;
|
|
|
|
// Apply modal factors to ALL persisted edges and keep a profile stable over
|
|
// replanning. Independent journeys may choose different near-equal modes.
|
|
flight.enabled=false;travel.complete=true;PathGenerator::available=false;
|
|
ClientlessTravelGraphMgr modal;node(modal,"A",0);node(modal,"B",1000);
|
|
edge(modal,"A","B",200,1000,false,"ground");edge(modal,"A","B",201,1050,false,"taxi");
|
|
bool sawGround=false,sawTaxi=false;
|
|
for(unsigned seed=1;seed<=100;++seed){
|
|
auto profile=ClientlessTravelCost::Sample({},seed,.2f);
|
|
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,nullptr,&profile)&&done);
|
|
auto picked=route.back().EdgeDatabaseId;sawGround|=picked==200;sawTaxi|=picked==201;
|
|
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,nullptr,&profile)&&done);
|
|
assert(route.back().EdgeDatabaseId==picked);
|
|
}assert(sawGround&&sawTaxi);
|
|
// A same-map flight is a competing route, never an unconditional override.
|
|
flight.enabled=true;ClientlessTravelCost::Profile expensiveFlight;expensiveFlight.Factors[ClientlessTravelCost::Flight]=100;
|
|
ClientlessTravelLocationGoal sameMap;sameMap.X=1000;sameMap.AnchorRadius=5;
|
|
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,&sameMap,&expensiveFlight)&&done);
|
|
assert(route.back().NodeKey!="flight_location");
|
|
expensiveFlight.Factors[ClientlessTravelCost::Flight]=.25f;
|
|
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,&sameMap,&expensiveFlight)&&done);
|
|
assert(route.back().NodeKey=="flight_location"&&route.back().EdgeType=="free_flight");
|
|
// Adaptive start radius: nearest permitted usable ring wins; no distant MMAP probe.
|
|
flight.enabled=true;travel.complete=false;PathGenerator::available=false;
|
|
ClientlessTravelGraphMgr expanded;node(expanded,"R",952);node(expanded,"G",12000);
|
|
edge(expanded,"R","G",501,100,false,"taxi");
|
|
route.clear();assert(expanded.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why)&&done);
|
|
assert(route.front().NodeKey=="flight_departure"&&route[1].NodeKey=="R"&&route[1].EdgeType=="free_flight");
|
|
// Ground-only bots retain the original radius.
|
|
flight.enabled=false;route.clear();assert(!expanded.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why));
|
|
flight.enabled=true;
|
|
// The exact 10,000-yard boundary is included; farther nodes are not seeded.
|
|
ClientlessTravelGraphMgr boundary;node(boundary,"R",10000);node(boundary,"G",12000);
|
|
edge(boundary,"R","G",502,100,false,"taxi");
|
|
route.clear();assert(boundary.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why)&&done&&route[1].NodeKey=="R");
|
|
ClientlessTravelGraphMgr beyond;node(beyond,"R",10001);node(beyond,"G",12000);
|
|
edge(beyond,"R","G",503,100,false,"taxi");
|
|
route.clear();assert(!beyond.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why));
|
|
// Native-ineligible inner nodes do not prevent inspection of the outermost ring.
|
|
ClientlessTravelGraphMgr rings;node(rings,"R",10000);node(rings,"G",12000);
|
|
edge(rings,"R","G",504,100,false,"taxi");
|
|
for(unsigned ring=1;ring<20;++ring)for(unsigned n=0;n<4;++n)
|
|
node(rings,"bad"+std::to_string(ring)+"_"+std::to_string(n),ring*500.f-20+n);
|
|
flight.rejectNearbyTargets=false;flight.forbiddenTargetX=1e9;
|
|
flight.rejectBelow=9999;
|
|
route.clear();assert(rings.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why)&&done&&route[1].NodeKey=="R");
|
|
flight.rejectBelow=-1e9;
|
|
// Rejected origins and area-denied roots remain forbidden throughout expansion.
|
|
std::unordered_set<std::string> failedOrigin{"flight_origin:flight_departure"};
|
|
route.clear();assert(!expanded.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why,nullptr,&failedOrigin));
|
|
access.blocked.insert("R");route.clear();assert(!expanded.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why));access.blocked.clear();
|
|
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)
|