'''Actual resumable road survey versus the pre-change synchronous Build. Native world/terrain/placement calls are deterministic doubles, not engine tests. ''' import os,re,runpy,contextlib,io from pathlib import Path import policy_harness from policy_harness import ROOT,body,run p=Path(os.environ.get('ERR_CONVOY_ROAD_PLAN',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyRoadPlan.c'))) # Reuse the existing actual-source geometry translation and its native type doubles. captured=[] original_run=policy_harness.run policy_harness.run=captured.append try: with contextlib.redirect_stdout(io.StringIO()): runpy.run_path(str(ROOT/'tests/test_convoy_road_plan.py')) finally: policy_harness.run=original_run prefix=captured[0].split('int main(){')[0] prefix=prefix.replace('struct Plan{','int candidateCalls=0;\nstruct Plan{') prefix=prefix.replace('positions = {}; angles = {}; route = nullptr; proximity', '++candidateCalls; positions = {}; angles = {}; route = nullptr; proximity') prefix=prefix.replace('bool IsEmpty()const', 'void Clear(){this->clear();}bool IsEmpty()const') legacy=' static bool Build(vector seed, out array positions, out array angles, out ERR_ForceRoute route, array excludedStarts = null, float separation = 0)\n {\n positions = {}; angles = {}; route = null;\n if (!Replication.IsServer() || !ERR_ForceRules.FiniteVector(seed)) return false;\n SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());\n if (!world || !world.GetRoadNetworkManager()) return false;\n array roads = {};\n world.GetRoadNetworkManager().GetRoadsInAABB(seed + "-500 -1000 -500", seed + "500 1000 500", roads);\n if (roads.Count() > 128) return false;\n float bestDistance = 1e20;\n foreach (BaseRoad road : roads)\n {\n if (!road || road.GetWidth() < 4) continue;\n array points = {}; road.GetPoints(points);\n for (int candidateIndex = 0; candidateIndex < 33; candidateIndex++)\n {\n array candidatePositions, candidateAngles;\n ERR_ForceRoute polyline;\n float distance;\n if (!FromPolyline(points, seed, candidatePositions, candidateAngles, polyline, distance, candidateIndex)) continue;\n // Exclude occupied deployment areas before selecting the best formation.\n // Returning the nearest clear-but-too-close road used to hide valid\n // alternatives from startup\'s outer separation check.\n if (excludedStarts && !ERR_SupplyConvoyRules.Separated(candidatePositions[0], excludedStarts, separation)) continue;\n for (int i = 0; i < candidatePositions.Count(); i++) candidatePositions[i] = ERR_Actors.Ground(candidatePositions[i]);\n ERR_ForceRoute grounded = new ERR_ForceRoute();\n bool valid = true;\n for (int i = 0; i < polyline.Count(); i++)\n {\n vector point = ERR_Actors.Ground(polyline.At(i));\n if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point) || !grounded.Add(point)) { valid = false; break; }\n }\n if (!valid || !grounded.Seal() || !ERR_ConvoyRules.Plan(candidatePositions, candidateAngles, grounded)) continue;\n if (!Prefer(distance, candidatePositions, grounded, bestDistance, positions, route)) continue;\n if (!ERR_ConvoyRules.GroundClear(candidatePositions, candidateAngles)) continue;\n positions = candidatePositions; angles = candidateAngles; route = grounded; bestDistance = distance;\n }\n }\n return route != null;\n }' def translate(code): code=re.sub(r'//[^\n]*','',code) code=code.replace('out array','Array&').replace('array','Array').replace('array','Array') code=code.replace('out ERR_ForceRoute','ERR_ForceRoute*&') code=re.sub(r'(?GetAIWorld()') code=code.replace('foreach (','for (').replace('null','nullptr').replace('protected ','').replace('ref ','') code=code.replace('Array excludedStarts = nullptr','Array excludedStarts = {}') for cls in ('ERR_ForceRules','ERR_SupplyConvoyRules','ERR_Actors','ChimeraWorldUtils','ERR_ConvoyRules','Replication'): code=code.replace(cls+'.',cls+'::') code=code.replace('ERR_ConvoyRoadPlan.','Plan::') for obj in ('world','road','polyline','grounded'):code=code.replace(obj+'.',obj+'->') code=code.replace('GetGame().','GetGame()->').replace('GetRoadNetworkManager().','GetRoadNetworkManager()->') code=code.replace('"-500 -1000 -500"','Vector(-500,-1000,-500)').replace('"500 1000 500"','Vector(500,1000,500)') return code survey=p.read_text().split('class ERR_ConvoyRoadSurvey\n',1)[1] survey=translate(survey) survey=re.sub(r'(?m)^( (?:vector|float|int|bool|ERR_ForceRoute\*) m_\w+);',r'\1{};',survey) survey='struct ERR_ConvoyRoadSurvey\n'+survey+';\n' legacy=translate(legacy).replace('static bool Build(', 'bool LegacyBuild(') legacy=legacy.replace('!FromPolyline(', '!Plan::FromPolyline(').replace('!Prefer(', '!Plan::Prefer(') wrapper=translate(body(p,'static bool Build(')).replace('static bool Build(', 'bool CurrentBuild(').replace('ERR_ConvoyRoadSurvey survey = new ERR_ConvoyRoadSurvey();','ERR_ConvoyRoadSurvey survey;') run(prefix+r''' struct BaseRoad{float width=6;Array points;float GetWidth(){return width;}void GetPoints(Array& out);}; int queries=0,pointLoads=0,grounds=0,clearChecks=0,waterChecks=0;bool server=true,worldAvailable=true,networkAvailable=true; int obstruction=0;bool water=false;Array roads; void BaseRoad::GetPoints(Array& out){++pointLoads;out=points;} struct Network{void GetRoadsInAABB(vector,vector,Array& out){++queries;out=roads;}} network; struct SCR_AIWorld{Network* GetRoadNetworkManager(){return networkAvailable?&network:nullptr;}} world; struct Game{SCR_AIWorld* GetAIWorld(){return worldAvailable?&world:nullptr;}int GetWorld(){return 0;}} game; Game* GetGame(){return &game;} namespace Replication{bool IsServer(){return server;}} namespace ERR_Actors{vector Ground(vector v){++grounds;v.y=std::sin(v.x*.01f)*.1f;return v;}} namespace ChimeraWorldUtils{bool TryGetWaterSurfaceSimple(int,vector v){++waterChecks;return water&&v.z>0;}} namespace ERR_SupplyConvoyRules{bool Separated(vector p,Array starts,float d){for(auto s:starts)if(vector::DistanceXZ(p,s) p,Array a,ERR_ForceRoute* r){return p.Count()==6&&a.Count()==6&&r&&r->Length()>=200;}bool GroundClear(Array p,Array){++clearChecks;if(obstruction==2)return false;if(obstruction==1)for(auto v:p)if(std::abs(v.x-587.5)<10)return false;return true;}} '''+survey+legacy+wrapper+r''' void reset(){queries=pointLoads=grounds=clearChecks=waterChecks=candidateCalls=0;} bool equal(vector a,vector b){return a.x==b.x&&a.y==b.y&&a.z==b.z;} void same(Array a,Array b){assert(a.size()==b.size());for(size_t i=0;i excluded={},float separation=0){ Array lp,la,sp,sa;ERR_ForceRoute *lr=nullptr,*sr=nullptr; reset();bool expected=LegacyBuild(seed,lp,la,lr,excluded,separation); int expectedCandidates=candidateCalls,expectedGrounds=grounds,expectedClear=clearChecks,expectedQueries=queries,expectedPoints=pointLoads,expectedWater=waterChecks; reset();ERR_ConvoyRoadSurvey survey;survey.Begin(seed,excluded,separation);assert(queries==0&&candidateCalls==0); assert(!survey.Result(sp,sa,sr)&&sp.empty()&&sa.empty()&&sr==nullptr); int steps=0; while(true){int c=candidateCalls,q=queries,p=pointLoads;bool done=survey.Step();++steps; assert(candidateCalls-c<=1&&queries-q<=1&&pointLoads-p<=1); assert((candidateCalls-c)+(queries-q)+(pointLoads-p)<=1); assert(steps<=4354); if(done)break; assert(!survey.Result(sp,sa,sr)); } bool got=survey.Result(sp,sa,sr);assert(got==expected);same(lp,sp);same(la,sa); if(expected)same(lr->points,sr->points);else assert(sr==nullptr); assert(candidateCalls==expectedCandidates&&grounds==expectedGrounds&&clearChecks==expectedClear&&queries==expectedQueries&&pointLoads==expectedPoints&&waterChecks==expectedWater); int finishedCalls=candidateCalls;assert(survey.Step()&&survey.Step()&&candidateCalls==finishedCalls); reset();Array wp,wa;ERR_ForceRoute* wr=nullptr;assert(CurrentBuild(seed,wp,wa,wr,excluded,separation)==expected);same(lp,wp);same(la,wa);if(expected)same(lr->points,wr->points); } int main(){ BaseRoad a,b,c,shortRoad,narrow,dense,invalid; a.points={{0,10,0},{2000,10,0}};b.points={{0,10,40},{2000,10,40}}; c.points={{0,10,-40},{2000,10,-40}};shortRoad.points={{0,0,0},{389,0,0}};narrow.width=3;narrow.points=a.points; for(int i=0;i<1025;i++){dense.points.Insert({float(i),0,0});}invalid.points={{0,0,0},{NAN,0,0}}; roads={};compare({500,0,0});roads={nullptr,&narrow,&shortRoad,&dense,&invalid};compare({500,0,0}); roads={&a,&b,&c};compare({500,0,0});compare({500,0,0},{{587.5,0,0}},100);compare({500,0,0},{{587.5,0,0}},10000); obstruction=1;compare({500,0,0});obstruction=2;compare({500,0,0});obstruction=0; water=true;compare({500,0,0});water=false; // Equal-distance roads retain exact tie ordering, including terrain Y. roads={&b,&c};compare({500,0,0});roads={&c,&b};compare({500,0,0}); roads={&a};compare({1800,0,0});compare({500,0,501});compare({NAN,0,0}); server=false;compare({500,0,0});server=true;worldAvailable=false;compare({500,0,0});worldAvailable=true;networkAvailable=false;compare({500,0,0});networkAvailable=true; roads=Array(128,&a);compare({500,0,0});roads.push_back(&a);compare({500,0,0}); // Begin resets partially consumed state; exclusion input is snapshotted. roads={&a};ERR_ConvoyRoadSurvey s;Array ex={{587.5,0,0}},p,h;ERR_ForceRoute* r=nullptr; s.Begin({500,0,0},ex,10000);ex.clear();s.Step();s.Step();while(!s.Step()){}assert(!s.Result(p,h,r)); s.Begin({500,0,0});s.Step();s.Step();s.Step();s.Begin({500,0,0});while(!s.Step()){}assert(s.Result(p,h,r)); // Abandoning a job has no spawn/reservation side effects: this class only surveys. } ''') assert not any(token in p.read_text().split('class ERR_ConvoyRoadSurvey',1)[1] for token in ('SpawnEntity','Reserve(','Consume(','CallLater(')) print('PASS: actual resumable survey matches original synchronous Build, exact candidate/native-query counts, per-step bounded work, road caps, ties, failures, exclusions, reset and legacy wrapper')