Smooth convoy startup and record Workshop 0.0.153
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-10 15:34:24 +01:00
parent d8352136e8
commit 2390f77554
72 changed files with 15088 additions and 161 deletions
+104
View File
@@ -0,0 +1,104 @@
'''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<vector> positions, out array<vector> angles, out ERR_ForceRoute route, array<vector> 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<BaseRoad> 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<vector> points = {}; road.GetPoints(points);\n for (int candidateIndex = 0; candidateIndex < 33; candidateIndex++)\n {\n array<vector> 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<vector>','Array<vector>&').replace('array<vector>','Array<vector>').replace('array<BaseRoad>','Array<BaseRoad*>')
code=code.replace('out ERR_ForceRoute','ERR_ForceRoute*&')
code=re.sub(r'(?<![\w:])ERR_ForceRoute (\w+)',r'ERR_ForceRoute* \1',code)
code=re.sub(r'(?<![\w:])BaseRoad (\w+)',r'BaseRoad* \1',code)
code=code.replace('SCR_AIWorld world','SCR_AIWorld* world').replace('SCR_AIWorld.Cast(GetGame().GetAIWorld())','GetGame()->GetAIWorld()')
code=code.replace('foreach (','for (').replace('null','nullptr').replace('protected ','').replace('ref ','')
code=code.replace('Array<vector> excludedStarts = nullptr','Array<vector> 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<vector> points;float GetWidth(){return width;}void GetPoints(Array<vector>& 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<BaseRoad*> roads;
void BaseRoad::GetPoints(Array<vector>& out){++pointLoads;out=points;}
struct Network{void GetRoadsInAABB(vector,vector,Array<BaseRoad*>& 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<vector> starts,float d){for(auto s:starts)if(vector::DistanceXZ(p,s)<d)return false;return true;}}
namespace ERR_ConvoyRules{bool Plan(Array<vector> p,Array<vector> a,ERR_ForceRoute* r){return p.Count()==6&&a.Count()==6&&r&&r->Length()>=200;}bool GroundClear(Array<vector> p,Array<vector>){++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<vector> a,Array<vector> b){assert(a.size()==b.size());for(size_t i=0;i<a.size();i++)assert(equal(a[i],b[i]));}
void compare(vector seed,Array<vector> excluded={},float separation=0){
Array<vector> 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<vector> 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<BaseRoad*>(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<vector> 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')