Smooth convoy startup and record Workshop 0.0.153
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -24,7 +24,7 @@ run(r'''
|
||||
#include <vector>
|
||||
#include <string>
|
||||
using string=std::string;struct vector{};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}int Count(){return this->size();}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}int Count(){return this->size();}bool IsEmpty(){return this->empty();}};
|
||||
struct Faction{string key="USSR";string GetFactionKey(){return key;}};Faction enemy;
|
||||
struct IEntity{virtual ~IEntity()=default;};
|
||||
struct FactionAffiliationComponent{void SetAffiliatedFaction(Faction*){}};
|
||||
@@ -72,7 +72,7 @@ int main(){
|
||||
''')
|
||||
# Execute the seat-selection and creation body. Failed capacity cannot shrink
|
||||
# the plan, and every native successful character is immediately owned.
|
||||
fill=body(path,'protected bool FillCrew(').replace('protected ','')
|
||||
fill='\n'.join(body(path,method) for method in ('protected bool SelectCrewSeats(', 'protected bool SpawnCrewMember(', 'protected bool FillCrew(')).replace('protected ','')
|
||||
fill=re.sub(r'//[^\n]*','',fill).replace('array<BaseCompartmentSlot>','Array<BaseCompartmentSlot*>')
|
||||
for cls in ['ERR_ConvoyVehicle','BaseCompartmentSlot','AIGroup','IEntity']:
|
||||
fill=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',fill)
|
||||
@@ -85,7 +85,7 @@ run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
using ResourceName=int;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}int Count(){return this->size();}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}int Count(){return this->size();}bool IsEmpty(){return this->empty();}};
|
||||
enum class ECompartmentType{PILOT,TURRET,CARGO};struct IEntity{};struct AIGroup{};
|
||||
struct BaseCompartmentSlot{ECompartmentType type;bool occupied=false,accessible=true,fail=false;int spawns=0;ECompartmentType GetType(){return type;}bool IsCompartmentAccessible(){return accessible;}bool IsOccupied(){return occupied;}IEntity* SpawnCharacterInCompartment(ResourceName,AIGroup*&,ResourceName){spawns++;return fail?nullptr:new IEntity();}};
|
||||
struct Vehicle{Array<BaseCompartmentSlot*>seats;};
|
||||
|
||||
@@ -111,7 +111,7 @@ int main(){
|
||||
'''.replace(';if(', ';\n if('))
|
||||
service=(D/'ERR_SupplyConvoyService.c').read_text();task=(D/'ERR_SupplyConvoyTactics.c').read_text()
|
||||
issue=body(D/'ERR_SupplyConvoyService.c','protected override void IssueCargo(')
|
||||
assert issue.index('supply.CargoIssued = true;')<issue.index('supply.MineStock = new ERR_ConvoyMineStock')<issue.index('TrySpawnPrefabToStorage')
|
||||
assert issue.index('supply.CargoIssued = true;')<issue.index('supply.MineStock = new ERR_ConvoyMineStock')<issue.index('QueueCargo(supply, inventory, operation)')
|
||||
assert 'if (task.MineStock) task.MineStock.Tick(now, MountedStockOwner(task, 1, ERR_HQ.Cast(m_Mode.Command())));' in service
|
||||
assert 'ref ERR_ConvoyMineStock MineStock;' in task and 'if (MineStock) MineStock.Stop();' in task
|
||||
print('PASS: exact optional PMN4 startup, primary callback independence, original owner,128 capacity/callback limits, terminal no-retry and callback-proven freeze')
|
||||
|
||||
@@ -77,7 +77,10 @@ int main(){
|
||||
}
|
||||
''')
|
||||
build=body(p,'static bool Build(')
|
||||
assert 'candidateIndex < 33' in build
|
||||
assert build.index('ERR_SupplyConvoyRules.Separated(') < build.index('Prefer(distance,')
|
||||
assert build.index('Prefer(distance,') < build.index('ERR_ConvoyRules.GroundClear(')
|
||||
consider=body(p,'protected void Consider(')
|
||||
step=body(p,'bool Step(')
|
||||
assert 'while (!survey.Step())' in build
|
||||
assert 'm_CandidateIndex >= 33' in step
|
||||
assert consider.index('ERR_SupplyConvoyRules.Separated(') < consider.index('Prefer(distance,')
|
||||
assert consider.index('Prefer(distance,') < consider.index('ERR_ConvoyRules.GroundClear(')
|
||||
print('PASS: actual road interpolation/projection/orientation, six35m starts,200m forward tail, preserved bends, short/far/degenerate/nonfinite/overdense/hairpin refusal and deterministic candidate ties')
|
||||
|
||||
@@ -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')
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Execute the shipped queued convoy transaction against deterministic native doubles."""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
BASE=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c'
|
||||
SUPPLY=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyService.c'
|
||||
methods=['override ERR_ConvoyTask Dispatch(', 'protected void FinishCreation(', 'protected bool CreationOwned(', 'protected void CreationPiece(', 'override void Stop(']
|
||||
code='\n'.join(body(SUPPLY,x) for x in methods)
|
||||
code+='\n'+'\n'.join(body(BASE,x) for x in ['protected bool SelectCrewSeats(', 'protected bool SpawnCrewMember('])
|
||||
code=re.sub(r'//[^\n]*','',code).replace('protected ','').replace('override ','').replace('null','nullptr')
|
||||
code=code.replace('array<vector> positions','Array<Vec>& positions').replace('array<vector> angles','Array<Vec>& angles')
|
||||
for a,b in [('array<ResourceName>','Array<string>'),('array<BaseCompartmentSlot>','Array<BaseCompartmentSlot*>'),('array<AIAgent>','Array<AIAgent*>')]:code=code.replace(a,b)
|
||||
code=code.replace('ResourceName','string').replace('foreach (','for (')
|
||||
for cast,typ in [('ERR_SupplyConvoyTask','ERR_SupplyConvoyTask'),('SCR_AIGroup','SCR_AIGroup'),('Vehicle','Vehicle')]:
|
||||
code=code.replace(cast+'.Cast(', 'static_cast<'+typ+'*>(')
|
||||
code=code.replace('FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent))','&unit->Entity->affiliation').replace('AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent))','&unit->Entity->movement')
|
||||
for cls in ['ERR_ConvoyTask','ERR_ConvoyReceipt','ERR_ForceRoute','ERR_ConvoyVehicle','ERR_SupplyConvoyTask','ERR_SupplyConvoyCreation','FactionAffiliationComponent','AICarMovementComponent','SCR_AIGroup','AIGroup','BaseCompartmentSlot','IEntity','AIAgent']:
|
||||
code=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',code)
|
||||
code=code.replace('ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();','ERR_ConvoyPlacement placement;')
|
||||
for obj in ['m_Mode','m_Pool','task','unit','creation','actor','parent','agent','slot','seat','affiliation','movement']:
|
||||
code=re.sub(r'\b'+obj+r'\.',obj+'->',code)
|
||||
for cls in ['ERR_ForceRules','ERR_ConvoyRules','ERR_ConvoyResources','ERR_Actors','ERR_HQ','ERR_VehicleSelection','ERR_ConvoyPool','ECompartmentType']:
|
||||
code=code.replace(cls+'.',cls+'::')
|
||||
for field in ['Group','Entity','Receipt','Task']:
|
||||
code=code.replace('->'+field+'.','->'+field+'->')
|
||||
code=code.replace('unit->Seats[member].','unit->Seats[member]->')
|
||||
code=code.replace('unit->Entity->FindComponent(InventoryStorageManagerComponent)','unit->Entity->storage')
|
||||
code=code.replace('prefab.IsEmpty()','prefab.empty()')
|
||||
code=code.replace('GetGame().GetCallqueue().Remove(CargoStep)', 'cargoQueue.Remove()')
|
||||
code=code.replace('GetGame().GetCallqueue().CallLater(CreationStep, 50, true)','queue.Schedule()').replace('GetGame().GetCallqueue().Remove(CreationStep)','queue.Remove()')
|
||||
code=code.replace('GetGame().GetWorld().GetWorldTime()','clockNow').replace('GetGame().GetPlayerManager().GetPlayerCount()','players').replace('GetGame().ERR_ServerVehicles().Reject','Reject').replace('if (GetGame())','if (true)').replace('super.Stop()','BaseStop()')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string=std::string;
|
||||
struct Vec {float v[3]={};float operator[](int i)const{return v[i];}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T x){this->push_back(x);}int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Copy(const Array<T>& a){*this=a;}void RemoveItem(T a){auto i=std::find(this->begin(),this->end(),a);if(i!=this->end())this->erase(i);}bool Contains(T a){return std::find(this->begin(),this->end(),a)!=this->end();}};
|
||||
struct SCR_AIGroup;
|
||||
struct IEntity{virtual ~IEntity()=default;bool alive=true,player=false;SCR_AIGroup* parent=nullptr;};
|
||||
struct AIAgent{IEntity* actor;IEntity* GetControlledEntity(){return actor;}};
|
||||
struct SCR_AIGroup:IEntity{int faction=1,players=0;bool empty=true;Array<AIAgent*>agents;int GetFaction(){return faction;}int GetPlayerCount(bool){return players;}void GetAgents(Array<AIAgent*>& out){out=agents;}};using AIGroup=SCR_AIGroup;
|
||||
enum class ECompartmentType{PILOT,TURRET,CARGO};
|
||||
int spawnCalls=0,failSpawn=0;bool changeNativeGroup=false;
|
||||
struct BaseCompartmentSlot{ECompartmentType type;bool accessible=true;IEntity* occupant=nullptr;ECompartmentType GetType(){return type;}bool IsCompartmentAccessible(){return accessible;}bool IsOccupied(){return occupant!=nullptr;}IEntity* GetOccupant(){return occupant;}
|
||||
IEntity* SpawnCharacterInCompartment(string,AIGroup*& group,string){++spawnCalls;if(spawnCalls==failSpawn)return nullptr;occupant=new IEntity();occupant->parent=group;group->agents.Insert(new AIAgent{occupant});if(changeNativeGroup)group=new SCR_AIGroup();return occupant;}};
|
||||
struct FactionAffiliationComponent{void SetAffiliatedFaction(int){}};struct AICarMovementComponent{int speed=0;void SetCruiseSpeed(int n){speed=n;}};
|
||||
struct Vehicle:IEntity{bool storage=true,captured=false;Vec position;FactionAffiliationComponent affiliation;AICarMovementComponent movement;Array<BaseCompartmentSlot*>seats;Vec GetOrigin(){return position;}};
|
||||
struct ERR_GameMode{Array<IEntity*>owned;int EnemyFaction(){return 1;}void Own(IEntity* e){assert(e&&!owned.Contains(e));owned.Insert(e);}};
|
||||
struct ERR_ForceRoute{};
|
||||
struct ERR_ConvoyReceipt{bool Consumed=false;};
|
||||
struct ERR_ConvoyPool{int consumes=0;bool Consume(ERR_ConvoyReceipt* r){if(!r||r->Consumed)return false;r->Consumed=true;consumes++;return true;}static int CrewCount(int i){return i==1?1:(i==2?0:(i==4?3:2));}};
|
||||
struct ERR_ConvoyVehicle{bool Released=false;int Index=0;Vehicle* Entity=nullptr;SCR_AIGroup* Group=nullptr;Array<BaseCompartmentSlot*>Seats;Array<IEntity*>Members;IEntity* Driver=nullptr;};
|
||||
struct ERR_ConvoyTask{virtual ~ERR_ConvoyTask()=default;ERR_ConvoyReceipt* Receipt=nullptr;ERR_ForceRoute* Route=nullptr;float Deadline=0,NextCheck=0;Array<ERR_ConvoyVehicle*>Vehicles;bool Stopped=false,Creating=false;string reason;void Stop(ERR_GameMode*,string why){Stopped=true;reason=why;for(auto*v:Vehicles)v->Released=true;}};
|
||||
struct ERR_SupplyConvoyTask:ERR_ConvoyTask{Array<Vec>Starts;};
|
||||
struct ERR_SupplyConvoyCreation{ERR_SupplyConvoyTask* Task=nullptr;Array<Vec>Positions,Angles;Array<string>Prefabs;float Expires=0;int VehicleIndex=0,Phase=0;};
|
||||
namespace ERR_ForceRules{bool Finite(float f){return std::isfinite(f);}}
|
||||
namespace ERR_ConvoyRules{bool valid=true,clear=true;bool EmptyCrewTemplate(SCR_AIGroup*g){return g&&g->empty&&g->agents.empty();}bool GroundClear(Array<Vec>&,Array<Vec>&){return clear;}bool Plan(Array<Vec>&,Array<Vec>&,ERR_ForceRoute*){return valid;}bool CollectSeats(Vehicle*v,Array<BaseCompartmentSlot*>&out){out=v->seats;return true;}}
|
||||
namespace ERR_ConvoyResources{bool available=true;bool Available(){return available;}string GroupPrefab(){return "group";}string Soldier(){return "soldier";}string VehiclePrefab(int i){return "vehicle"+std::to_string(i);}}
|
||||
namespace ERR_HQ{void Event(string,string){}}
|
||||
namespace ERR_Actors{IEntity* Spawn(string prefab,Vec position,Vec=Vec{}){++spawnCalls;if(spawnCalls==failSpawn)return nullptr;if(prefab=="group")return new SCR_AIGroup();auto*v=new Vehicle();v->position=position;v->seats={new BaseCompartmentSlot{ECompartmentType::PILOT},new BaseCompartmentSlot{ECompartmentType::TURRET},new BaseCompartmentSlot{ECompartmentType::TURRET}};return v;}bool Alive(IEntity*a){return a&&a->alive;}bool Player(IEntity*a){return a&&a->player;}SCR_AIGroup* Group(IEntity*a){return a?a->parent:nullptr;}}
|
||||
namespace ERR_VehicleSelection{bool empty=false;bool fit=true;string ForRole(int,string preferred){return empty?"":preferred;}bool RoadFit(Vehicle*v){return v&&fit;}}
|
||||
struct ERR_ConvoyPlacement{static bool clear;bool Clear(Vec,float){return clear;}};bool ERR_ConvoyPlacement::clear=true;
|
||||
void Reject(string,int){}
|
||||
struct Queue{bool queued=false;int schedules=0,removes=0;void Schedule(){assert(!queued);queued=true;schedules++;}void Remove(){queued=false;removes++;}};
|
||||
struct Service{ERR_GameMode* m_Mode;ERR_ConvoyPool* m_Pool;Array<ERR_ConvoyTask*>m_Tasks;Array<ERR_SupplyConvoyCreation*>m_Creations;bool m_CreationQueued=false;bool current=true;float clockNow=100;int players=1;Queue queue;
|
||||
ERR_ConvoyTask* NewTask(){return new ERR_SupplyConvoyTask();}ERR_ConvoyVehicle* NewVehicle(){return new ERR_ConvoyVehicle();}bool Current(){return current;}
|
||||
bool Captured(ERR_SupplyConvoyTask*t){for(auto*v:t->Vehicles)if((v->Entity&&v->Entity->captured)||(v->Group&&v->Group->players))return true;return false;}
|
||||
Queue cargoQueue;bool m_CargoQueued=false;void CancelCargo(ERR_SupplyConvoyTask*){}
|
||||
void BaseStop(){current=false;for(auto*t:m_Tasks)t->Stop(m_Mode,"service stopped");}
|
||||
''' + code + r'''
|
||||
void Advance(){int before=spawnCalls;clockNow+=50;CreationPiece();assert(spawnCalls-before<=1);}
|
||||
};
|
||||
struct Fixture{ERR_GameMode mode;ERR_ConvoyPool pool;Service service{&mode,&pool};ERR_ConvoyReceipt receipt;ERR_ForceRoute route;Array<Vec>positions{6},angles{6};Fixture(){spawnCalls=0;failSpawn=0;changeNativeGroup=false;ERR_ConvoyPlacement::clear=true;ERR_ConvoyRules::valid=true;ERR_ConvoyRules::clear=true;ERR_ConvoyResources::available=true;ERR_VehicleSelection::empty=false;}ERR_ConvoyTask* Dispatch(){return service.Dispatch(&receipt,positions,angles,&route,service.clockNow);}void Drain(){for(int n=0;n<100&&service.queue.queued;n++)service.Advance();assert(!service.queue.queued);}};
|
||||
int main(){
|
||||
{Fixture f;auto*t=f.Dispatch();assert(t&&t->Creating&&t->Vehicles.Count()==6&&t->Deadline==0&&spawnCalls==0&&f.pool.consumes==1&&f.service.queue.schedules==1);assert(t->Vehicles[2]->Released);assert(!f.Dispatch());f.Drain();assert(!t->Creating&&!t->Stopped&&spawnCalls==20&&f.mode.owned.Count()==20);assert(t->Deadline==f.service.clockNow+30000);assert(static_cast<ERR_SupplyConvoyTask*>(t)->Starts.Count()==5);for(auto*v:t->Vehicles)if(v->Index!=2)assert(v->Members.Count()==ERR_ConvoyPool::CrewCount(v->Index)&&v->Driver==v->Members[0]);}
|
||||
// Every native allocation boundary is finite and immediately owned on success.
|
||||
for(int failure=1;failure<=20;failure++){Fixture f;failSpawn=failure;auto*t=f.Dispatch();f.Drain();assert(t->Stopped&&!t->Creating&&f.pool.consumes==1&&spawnCalls==failure&&f.mode.owned.Count()==failure-1);int before=spawnCalls;assert(!f.Dispatch());f.service.CreationPiece();assert(spawnCalls==before);}
|
||||
{Fixture f;ERR_ConvoyRules::clear=false;assert(!f.Dispatch()&&!f.receipt.Consumed&&spawnCalls==0);}
|
||||
{Fixture f;ERR_VehicleSelection::empty=true;assert(!f.Dispatch()&&!f.receipt.Consumed&&spawnCalls==0);}
|
||||
{Fixture f;auto*t=f.Dispatch();f.service.Advance();assert(spawnCalls==1);ERR_ConvoyPlacement::clear=false;f.Drain();assert(t->Stopped&&spawnCalls==1&&f.mode.owned.Count()==1);}
|
||||
{Fixture f;auto*t=f.Dispatch();f.service.Advance();f.service.Advance();t->Vehicles[0]->Entity->captured=true;f.Drain();assert(t->Stopped&&spawnCalls==2);}
|
||||
{Fixture f;auto*t=f.Dispatch();f.service.Advance();t->Vehicles[0]->Group->faction=2;f.Drain();assert(t->Stopped&&spawnCalls==1);}
|
||||
{Fixture f;auto*t=f.Dispatch();f.service.Advance();f.service.Advance();f.service.Advance();t->Vehicles[0]->Seats[0]->occupant=new IEntity();f.Drain();assert(t->Stopped&&spawnCalls==2);}
|
||||
{Fixture f;auto*t=f.Dispatch();f.service.Advance();f.service.Advance();f.service.Advance();f.service.Advance();assert(spawnCalls==3);t->Vehicles[0]->Members[0]->parent=new SCR_AIGroup();f.Drain();assert(t->Stopped&&spawnCalls==3);}
|
||||
{Fixture f;auto*t=f.Dispatch();f.service.Advance();f.service.Advance();f.service.Advance();changeNativeGroup=true;f.Drain();assert(t->Stopped&&spawnCalls==3&&f.mode.owned.Count()==3);}
|
||||
{Fixture f;auto*t=f.Dispatch();f.service.Advance();t->Stopped=true;f.Drain();assert(!t->Creating&&spawnCalls==1);}
|
||||
{Fixture f;auto*t=f.Dispatch();f.service.players=0;for(int i=0;i<10;i++)f.service.Advance();assert(t->Creating&&spawnCalls==0&&f.service.queue.queued);f.service.players=1;f.Drain();assert(!t->Stopped&&spawnCalls==20);}
|
||||
{Fixture f;auto*t=f.Dispatch();f.service.players=0;f.service.clockNow=61000;f.service.Advance();assert(spawnCalls==0&&t->Creating);f.service.players=1;f.Drain();assert(t->Stopped&&spawnCalls==0);}
|
||||
{Fixture f;auto*t=f.Dispatch();f.service.Advance();f.service.Stop();assert(t->Stopped&&!t->Creating&&!f.service.queue.queued&&spawnCalls==1);f.service.CreationPiece();assert(spawnCalls==1);}
|
||||
{Fixture f;auto*a=f.Dispatch();ERR_ConvoyReceipt second;auto*b=f.service.Dispatch(&second,f.positions,f.angles,&f.route,100);assert(a&&b&&spawnCalls==0&&f.service.queue.schedules==1);f.Drain();assert(!a->Stopped&&!b->Stopped&&spawnCalls==40&&f.pool.consumes==2);}
|
||||
}
|
||||
''')
|
||||
# Pending physical creation must not enter readiness, cargo, combat or travel.
|
||||
tick=body(SUPPLY,'override void Tick(')
|
||||
assert tick.index('task.Creating') < tick.index('Captured(task)') < tick.index('Admit(task, now)')
|
||||
print('PASS: shipped staged startup limits each callback to one native allocation; finite receipt, exact20 allocations/10 crew, ownership, placement/capture changes, no-player pause, deadline, stop and serialized transactions verified')
|
||||
|
||||
# Execute the actual global cargo queue and owner gate. Planning keeps immutable
|
||||
# callback identities; accepted native callbacks are not part of cancellation.
|
||||
cargo='\n'.join(body(SUPPLY,x) for x in ['protected void QueueCargo(', 'protected bool CargoPending(', 'protected void CancelCargo(', 'protected bool CargoOwner(', 'protected void CargoPiece('])
|
||||
cargo=re.sub(r'//[^\n]*','',cargo).replace('protected ','').replace('null','nullptr').replace('foreach (','for (')
|
||||
for cls in ['ERR_SupplyConvoyLoad','ERR_SupplyConvoyTask','InventoryStorageManagerComponent','ERR_ConvoyCargoCallback']:
|
||||
cargo=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',cargo)
|
||||
for obj in ['load','task','operation']:
|
||||
cargo=re.sub(r'\b'+obj+r'\.',obj+'->',cargo)
|
||||
for field in ['Operation','OriginalVehicle','Inventory','Truck']:
|
||||
cargo=cargo.replace('->'+field+'.','->'+field+'->')
|
||||
cargo=cargo.replace('load->Truck->FindComponent(InventoryStorageManagerComponent)','load->Truck->inventory')
|
||||
cargo=cargo.replace('GetGame().GetCallqueue().CallLater(CargoStep, 50, true)','queue.Schedule()').replace('GetGame().GetCallqueue().Remove(CargoStep)','queue.Remove()').replace('GetGame().GetPlayerManager().GetPlayerCount()','players').replace('EStoragePurpose.PURPOSE_DEPOSIT','0')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string=std::string;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T x){this->push_back(x);}int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Remove(int i){this->erase(this->begin()+i);}};
|
||||
struct InventoryStorageManagerComponent;
|
||||
struct IEntity{InventoryStorageManagerComponent* inventory=nullptr;};
|
||||
struct ERR_ConvoyCargoCallback{string Prefab;bool Finished=false,Succeeded=false;};
|
||||
struct ERR_ConvoyVehicle{IEntity* Entity=nullptr;bool Released=false,fit=true;bool CrewValid(void*){return fit;}};
|
||||
struct ERR_SupplyConvoyTask{bool Stopped=false,Ready=true,Creating=false,captured=false;Array<ERR_ConvoyVehicle*>Vehicles;};
|
||||
struct InventoryStorageManagerComponent{IEntity* owner=nullptr;Array<ERR_ConvoyCargoCallback*> accepted;Array<string>prefabs;bool accept=true,inlineDone=false;IEntity* GetOwner(){return owner;}bool TrySpawnPrefabToStorage(string prefab,void*,int,int,ERR_ConvoyCargoCallback* op){prefabs.Insert(prefab);if(inlineDone){op->Finished=true;op->Succeeded=true;}if(accept)accepted.Insert(op);return accept;}};
|
||||
struct ERR_SupplyConvoyLoad{ERR_SupplyConvoyTask* Task=nullptr;ERR_ConvoyVehicle* OriginalVehicle=nullptr;IEntity* Truck=nullptr;InventoryStorageManagerComponent* Inventory=nullptr;ERR_ConvoyCargoCallback* Operation=nullptr;};
|
||||
struct Queue{bool queued=false;int schedules=0;void Schedule(){assert(!queued);queued=true;schedules++;}void Remove(){queued=false;}};
|
||||
struct Service{Array<ERR_SupplyConvoyLoad*>m_Loads;bool m_CargoQueued=false,current=true;void*m_Mode=nullptr;int players=1;Queue queue;bool Current(){return current;}bool Captured(ERR_SupplyConvoyTask*t){return t->captured;}void Stop(){current=false;CancelCargo(nullptr);queue.Remove();m_CargoQueued=false;}
|
||||
''' + cargo + r'''
|
||||
};
|
||||
struct Fixture{IEntity truck;InventoryStorageManagerComponent inventory;ERR_ConvoyVehicle vehicle;ERR_SupplyConvoyTask task;Service service;Array<ERR_ConvoyCargoCallback*>ops;
|
||||
Fixture(){truck.inventory=&inventory;inventory.owner=&truck;vehicle.Entity=&truck;task.Vehicles.resize(6,&vehicle);}
|
||||
void Plan(int n){for(int i=0;i<n;i++){auto*op=new ERR_ConvoyCargoCallback();op->Prefab="original-"+std::to_string(i);ops.Insert(op);service.QueueCargo(&task,&inventory,op);}}
|
||||
void Piece(){int before=inventory.prefabs.Count();service.CargoPiece();assert(inventory.prefabs.Count()-before<=2);}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;f.Plan(96);assert(f.inventory.prefabs.IsEmpty()&&f.service.CargoPending(&f.task)&&f.service.queue.schedules==1);for(int i=0;i<48;i++)f.Piece();assert(!f.service.CargoPending(&f.task)&&!f.service.queue.queued&&f.inventory.prefabs.Count()==96);for(int i=0;i<96;i++){assert(f.inventory.prefabs[i]==f.ops[i]->Prefab&&f.inventory.accepted[i]==f.ops[i]&&!f.ops[i]->Finished);}f.Piece();assert(f.inventory.prefabs.Count()==96);}
|
||||
{Fixture f;f.Plan(8);f.Piece();f.task.captured=true;f.Piece();assert(f.inventory.prefabs.Count()==2&&!f.service.CargoPending(&f.task));for(int i=0;i<8;i++)assert(f.ops[i]->Finished==(i>=2));assert(!f.task.Stopped);f.inventory.accepted[0]->Finished=true;f.inventory.accepted[0]->Succeeded=true;assert(f.ops[0]->Succeeded&&!f.ops[1]->Finished);}
|
||||
{Fixture f;f.Plan(8);f.Piece();f.vehicle.fit=false;f.Piece();assert(f.inventory.prefabs.Count()==2&&!f.task.Stopped&&!f.service.queue.queued);}
|
||||
{Fixture f;f.Plan(8);f.task.Stopped=true;f.Piece();assert(f.inventory.prefabs.Count()==0);for(auto*op:f.ops)assert(op->Finished&&!op->Succeeded);}
|
||||
{Fixture f;f.Plan(8);IEntity other;f.inventory.owner=&other;f.Piece();assert(f.inventory.prefabs.IsEmpty()&&!f.service.CargoPending(&f.task));}
|
||||
{Fixture f;f.Plan(8);InventoryStorageManagerComponent replacement;f.truck.inventory=&replacement;f.Piece();assert(f.inventory.prefabs.IsEmpty());}
|
||||
{Fixture f;f.Plan(8);ERR_ConvoyVehicle replacement;f.task.Vehicles[1]=&replacement;f.Piece();assert(f.inventory.prefabs.IsEmpty());}
|
||||
{Fixture f;f.Plan(8);f.service.players=0;f.Piece();assert(f.inventory.prefabs.IsEmpty()&&f.service.CargoPending(&f.task));f.service.players=1;f.Piece();assert(f.inventory.prefabs.Count()==2);}
|
||||
{Fixture f;f.Plan(8);f.Piece();f.service.Stop();assert(!f.service.queue.queued&&!f.service.CargoPending(&f.task));for(int i=0;i<8;i++)assert(f.ops[i]->Finished==(i>=2));}
|
||||
{Fixture f;f.inventory.accept=false;f.Plan(8);for(int i=0;i<4;i++)f.Piece();assert(f.inventory.prefabs.Count()==8);for(auto*op:f.ops)assert(op->Finished&&!op->Succeeded);f.Piece();assert(f.inventory.prefabs.Count()==8);}
|
||||
{Fixture f;f.inventory.inlineDone=true;f.Plan(8);for(int i=0;i<4;i++)f.Piece();assert(f.inventory.prefabs.Count()==8);for(auto*op:f.ops)assert(op->Finished&&op->Succeeded);}
|
||||
{Fixture f;f.Plan(4);ERR_SupplyConvoyTask another=f.task;ERR_ConvoyCargoCallback op;op.Prefab="second-task";f.service.QueueCargo(&another,&f.inventory,&op);f.service.CancelCargo(&f.task);assert(f.service.CargoPending(&another)&&!f.service.CargoPending(&f.task));f.Piece();assert(f.inventory.prefabs.Count()==1&&f.inventory.prefabs[0]=="second-task");}
|
||||
}
|
||||
''')
|
||||
magazines=body(SUPPLY,'protected void IssueMagazines(')
|
||||
assert magazines.index('CargoPending(task)') < magazines.index('task.MagazinesIssued = true;')
|
||||
stop=body(SUPPLY,'override void Stop(')
|
||||
assert 'Remove(CargoStep)' in stop and 'CancelCargo(null)' in stop
|
||||
print('PASS: shipped cargo queue submits at most2 frozen requests per callback; capture/truck loss/stop cancel only unsubmitted callbacks, preserve accepted callbacks and surviving escorts, never retry failures, and pause on empty servers')
|
||||
@@ -86,15 +86,16 @@ raw=service.read_text()
|
||||
assert raw.index('CaptureMountedStock(supply)') < raw.index('for (int i = 0; i < 8; i++)')
|
||||
assert raw.index('task.MagazinesIssued = true;',raw.index('protected void IssueMagazines')) < raw.index('IssueMountedStock(task, inventory, issued,')
|
||||
assert 'ref array<ref ERR_MountedMagazineSeed> MountedStock' in (D/'ERR_SupplyConvoyTactics.c').read_text()
|
||||
code='\n'.join(body(service,x) for x in ['protected bool MountedStockOwner(', 'protected void CaptureMountedStock(', 'protected int IssueMountedStock(', 'protected void IssueMagazines('])
|
||||
code='\n'.join(body(service,x) for x in ['protected bool MountedStockOwner(', 'protected void CaptureMountedStock(', 'protected int IssueMountedStock(', 'protected bool CargoPending(', 'protected void IssueMagazines('])
|
||||
code=re.sub(r'//[^\n]*','',code).replace('protected ','').replace('null','nullptr').replace('foreach (','for (').replace('array<','Array<')
|
||||
for old,new in {
|
||||
'ERR_HQ.Cast(m_Mode.Command())':'m_Mode->Command()',
|
||||
'InventoryStorageManagerComponent.Cast(task.Vehicles[1].Entity.FindComponent(InventoryStorageManagerComponent))':'task->Vehicles[1]->Entity->inventory',
|
||||
'InventoryStorageManagerComponent.Cast(vehicle.Entity.FindComponent(InventoryStorageManagerComponent))':'vehicle->Entity->inventory',
|
||||
'BaseWeaponComponent.Cast(item.FindComponent(BaseWeaponComponent))':'item->weapon',
|
||||
'existing.FindComponent(BaseMagazineComponent)':'existing->magazine',
|
||||
}.items():code=code.replace(old,new)
|
||||
for typ in ['ERR_SupplyConvoyTask','ERR_HQ','ERR_ConvoyVehicle','ERR_ForceRecord','InventoryStorageManagerComponent','ERR_MountedMagazineSeed','ERR_ConvoyCargoCallback','BaseWeaponComponent','BaseMuzzleComponent']:
|
||||
for typ in ['ERR_SupplyConvoyTask','ERR_HQ','ERR_ConvoyVehicle','ERR_ForceRecord','InventoryStorageManagerComponent','ERR_MountedMagazineSeed','ERR_ConvoyCargoCallback','BaseWeaponComponent','BaseMuzzleComponent','ERR_SupplyConvoyLoad']:
|
||||
code=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',code)
|
||||
code=code.replace('Array<BaseMuzzleComponent>', 'Array<BaseMuzzleComponent*>')
|
||||
for name in ['m_Mode','task','vehicle','hq','record','inventory','seed','load','operation','item','weaponOperation','weapon','muzzle']:
|
||||
@@ -121,38 +122,57 @@ struct ERR_HQ{static void Event(string,string){}bool ValidateBoundVehicle(Group*
|
||||
struct Mode{Ledger ledger;ERR_HQ hq;Ledger* Forces(){return &ledger;}ERR_HQ* Command(){return &hq;}};
|
||||
struct ERR_ConvoyVehicle{int Index=0;bool Registered=true,Released=false,crew=true;IEntity Entity=nullptr;struct Group* Group=nullptr;bool CrewValid(Mode*){return crew;}};
|
||||
struct Receipt{bool Consumed=true;Array<int>Records={0,1,2,3,4,5};string SourceId="convoy";};
|
||||
struct ERR_ConvoyCargoCallback{string Prefab;bool Finished=true,Succeeded=true;};
|
||||
struct ERR_ConvoyCargoCallback{string Prefab;bool Finished=false,Succeeded=false;};
|
||||
struct ERR_MountedMagazineSeed{IEntity Root=nullptr;int VehicleIndex=0;string Prefab="PKM-box";bool valid=true;bool Current(){return valid;}static inline int captures=0;static void Capture(IEntity,int,Array<ERR_MountedMagazineSeed*>&){captures++;}};
|
||||
struct ERR_SupplyConvoyTask{bool Ready=true,Stopped=false,MagazinesIssued=false,CargoIssued=true;float Deadline=100;struct Receipt receipt;struct Receipt* Receipt=&receipt;Array<ERR_ConvoyVehicle*>Vehicles;Array<ERR_ConvoyCargoCallback*>Cargo;Array<ERR_MountedMagazineSeed*>MountedStock;Array<IEntity>CargoBefore;};
|
||||
struct EStoragePurpose{static const int PURPOSE_DEPOSIT=1;};
|
||||
struct InventoryStorageManagerComponent{IEntity owner=nullptr;Array<IEntity>items;Array<string>requests;bool accept=true,inlineDone=false;Entity spawned;IEntity GetOwner(){return owner;}
|
||||
bool TrySpawnPrefabToStorage(string p,void*,int,int,ERR_ConvoyCargoCallback* cb){requests.Insert(p);cb->Finished=inlineDone;if(inlineDone){spawned.magazine=true;items.Insert(&spawned);}return accept;}};
|
||||
struct InventoryStorageManagerComponent{IEntity owner=nullptr;Array<IEntity>items;Array<string>planned;int nativeCalls=0;bool accept=true,inlineDone=false;Entity spawned;IEntity GetOwner(){return owner;}
|
||||
bool TrySpawnPrefabToStorage(string p,void*,int,int,ERR_ConvoyCargoCallback* cb){nativeCalls++;cb->Finished=inlineDone;if(inlineDone){spawned.magazine=true;items.Insert(&spawned);}return accept;}};
|
||||
struct ERR_Inventory{static void Items(InventoryStorageManagerComponent* i,Array<IEntity>& items){items=i->items;}};
|
||||
struct Service{Mode* m_Mode;
|
||||
struct ERR_SupplyConvoyLoad{ERR_SupplyConvoyTask* Task;ERR_ConvoyCargoCallback* Operation;};
|
||||
struct Service{Mode* m_Mode;Array<ERR_SupplyConvoyLoad*>m_Loads;
|
||||
void QueueCargo(ERR_SupplyConvoyTask* task,InventoryStorageManagerComponent* inventory,ERR_ConvoyCargoCallback* operation){
|
||||
inventory->planned.Insert(operation->Prefab);m_Loads.Insert(new ERR_SupplyConvoyLoad{task,operation});}
|
||||
void CompletePlanned(){for(auto load:m_Loads){load->Operation->Finished=true;load->Operation->Succeeded=true;}m_Loads.Clear();}
|
||||
'''+code+r'''
|
||||
};
|
||||
struct Fixture{Mode mode;Service service{&mode};ERR_SupplyConvoyTask task;InventoryStorageManagerComponent inventory;Array<Entity>roots{6},cargo{8},extra{129};Array<Group>groups{6};Array<ERR_ConvoyVehicle>vehicles{6};Array<BaseWeaponComponent>weapons{8};BaseMuzzleComponent muzzle;Array<ERR_ConvoyCargoCallback>callbacks{8};Array<ERR_MountedMagazineSeed>seeds{9};
|
||||
Fixture(){mode.ledger.records.resize(6);for(int i=0;i<6;i++){groups[i].id=i;groups[i].bound=&roots[i];vehicles[i].Index=i;vehicles[i].Entity=&roots[i];vehicles[i].Group=&groups[i];task.Vehicles.Insert(&vehicles[i]);mode.ledger.records[i].Id=i;}
|
||||
roots[1].inventory=&inventory;inventory.owner=&roots[1];
|
||||
for(int i=0;i<8;i++){cargo[i].prefab.name="weapon"+std::to_string(i);cargo[i].weapon=&weapons[i];weapons[i].muzzles={&muzzle};callbacks[i].Prefab=cargo[i].prefab.name;task.Cargo.Insert(&callbacks[i]);inventory.items.Insert(&cargo[i]);}
|
||||
for(int i=0;i<8;i++){cargo[i].prefab.name="weapon"+std::to_string(i);cargo[i].weapon=&weapons[i];weapons[i].muzzles={&muzzle};callbacks[i].Prefab=cargo[i].prefab.name;callbacks[i].Finished=true;callbacks[i].Succeeded=true;task.Cargo.Insert(&callbacks[i]);inventory.items.Insert(&cargo[i]);}
|
||||
for(int i=0;i<9;i++){seeds[i].Root=&roots[0];seeds[i].Prefab="box"+std::to_string(i);task.MountedStock.Insert(&seeds[i]);}}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;f.service.IssueMagazines(&f.task,0);assert(f.task.MagazinesIssued&&f.inventory.requests.Count()==64&&f.task.Cargo.Count()==72&&f.task.MountedStock.Count()==0);
|
||||
for(int i=0;i<48;i++)assert(f.inventory.requests[i]=="rifle-mag");for(int i=48;i<64;i++)assert(f.inventory.requests[i]=="box"+std::to_string((i-48)/2));
|
||||
f.service.IssueMagazines(&f.task,99999);assert(f.inventory.requests.Count()==64);}
|
||||
{Fixture f;f.callbacks[0].Finished=false;f.service.IssueMagazines(&f.task,0);assert(!f.task.MagazinesIssued&&f.inventory.requests.Count()==0);f.callbacks[0].Finished=true;f.service.IssueMagazines(&f.task,1);assert(f.inventory.requests.Count()==64);}
|
||||
// Lost cargo authority ends this one-time plan, without stopping escorts or
|
||||
// touching any native inventory. Includes a physically deleted original truck.
|
||||
for(int fault=0;fault<5;fault++){Fixture f;f.task.CargoBefore.Insert(&f.extra[0]);
|
||||
if(fault==0)f.task.Vehicles.resize(5);if(fault==1)f.task.Vehicles[1]=nullptr;
|
||||
if(fault==2)f.vehicles[1].Entity=nullptr;if(fault==3)f.vehicles[1].Released=true;if(fault==4)f.vehicles[1].crew=false;
|
||||
f.service.IssueMagazines(&f.task,0);assert(f.task.MagazinesIssued&&!f.task.Stopped&&f.task.MountedStock.Count()==0&&f.task.CargoBefore.Count()==0&&f.inventory.planned.Count()==0&&f.inventory.nativeCalls==0);
|
||||
f.service.IssueMagazines(&f.task,99999);assert(f.inventory.planned.Count()==0&&!f.task.Stopped);}
|
||||
{Fixture f;f.service.IssueMagazines(nullptr,0);assert(f.inventory.planned.Count()==0);}
|
||||
// Even past the callback deadline, unsent weapon requests block magazine
|
||||
// planning. Another task's pending requests must not block this task.
|
||||
{Fixture f;f.callbacks[0].Finished=false;f.service.QueueCargo(&f.task,&f.inventory,&f.callbacks[0]);
|
||||
f.service.IssueMagazines(&f.task,99999);assert(!f.task.MagazinesIssued&&f.inventory.planned.Count()==1&&f.inventory.nativeCalls==0);
|
||||
f.service.CompletePlanned();f.inventory.planned.Clear();f.service.IssueMagazines(&f.task,100000);assert(f.task.MagazinesIssued&&f.inventory.planned.Count()==64&&f.inventory.nativeCalls==0);}
|
||||
{Fixture f;ERR_SupplyConvoyTask other;ERR_ConvoyCargoCallback operation;f.service.QueueCargo(&other,&f.inventory,&operation);f.inventory.planned.Clear();
|
||||
f.service.IssueMagazines(&f.task,0);assert(f.task.MagazinesIssued&&f.inventory.planned.Count()==64&&f.inventory.nativeCalls==0);}
|
||||
{Fixture f;f.service.IssueMagazines(&f.task,0);assert(f.task.MagazinesIssued&&f.inventory.planned.Count()==64&&f.task.Cargo.Count()==72&&f.task.MountedStock.Count()==0);
|
||||
for(int i=0;i<48;i++)assert(f.inventory.planned[i]=="rifle-mag");for(int i=48;i<64;i++)assert(f.inventory.planned[i]=="box"+std::to_string((i-48)/2));
|
||||
f.service.IssueMagazines(&f.task,99999);assert(f.inventory.planned.Count()==64);}
|
||||
{Fixture f;f.callbacks[0].Finished=false;f.service.IssueMagazines(&f.task,0);assert(!f.task.MagazinesIssued&&f.inventory.planned.Count()==0);f.callbacks[0].Finished=true;f.service.IssueMagazines(&f.task,1);assert(f.inventory.planned.Count()==64);}
|
||||
for(int missing=0;missing<2;missing++){Fixture f;
|
||||
if(missing==0)f.callbacks[0].Finished=false;else f.task.Cargo[0]=nullptr;
|
||||
f.service.IssueMagazines(&f.task,30100);
|
||||
assert(f.task.MagazinesIssued&&f.inventory.requests.Count()==42&&f.task.MountedStock.Count()==0);
|
||||
for(auto request:f.inventory.requests){assert(request=="rifle-mag");}
|
||||
assert(f.task.MagazinesIssued&&f.inventory.planned.Count()==42&&f.task.MountedStock.Count()==0);
|
||||
for(auto request:f.inventory.planned){assert(request=="rifle-mag");}
|
||||
f.callbacks[0].Finished=true;f.task.Cargo[0]=&f.callbacks[0];
|
||||
f.service.IssueMagazines(&f.task,99999);assert(f.inventory.requests.Count()==42);}
|
||||
{Fixture f;f.inventory.accept=false;f.service.IssueMagazines(&f.task,0);assert(f.inventory.requests.Count()==64);for(auto cb:f.task.Cargo){assert(cb->Finished);}f.service.IssueMagazines(&f.task,99999);assert(f.inventory.requests.Count()==64);}
|
||||
{Fixture f;f.inventory.inlineDone=true;f.service.IssueMagazines(&f.task,0);assert(f.inventory.requests.Count()==64);}
|
||||
{Fixture f;f.service.IssueMountedStock(&f.task,&f.inventory,80,8,20);assert(f.inventory.requests.Count()==8);} //96 existing+attempts
|
||||
{Fixture f;int total=f.service.IssueMountedStock(&f.task,&f.inventory,48,0,79);assert(total==49&&f.inventory.requests.Count()==1);} //128allitems
|
||||
f.service.IssueMagazines(&f.task,99999);assert(f.inventory.planned.Count()==42);}
|
||||
{Fixture f;f.inventory.accept=false;f.service.IssueMagazines(&f.task,0);assert(f.inventory.planned.Count()==64);assert(f.inventory.nativeCalls==0);for(int i=8;i<f.task.Cargo.Count();i++){assert(!f.task.Cargo[i]->Finished);}f.service.IssueMagazines(&f.task,99999);assert(f.inventory.planned.Count()==64);}
|
||||
{Fixture f;f.inventory.inlineDone=true;f.service.IssueMagazines(&f.task,0);assert(f.inventory.planned.Count()==64&&f.inventory.nativeCalls==0&&f.inventory.items.Count()==8);}
|
||||
{Fixture f;f.service.IssueMountedStock(&f.task,&f.inventory,80,8,20);assert(f.inventory.planned.Count()==8);} //96 existing+attempts
|
||||
{Fixture f;int total=f.service.IssueMountedStock(&f.task,&f.inventory,48,0,79);assert(total==49&&f.inventory.planned.Count()==1);} //128allitems
|
||||
{Fixture f;f.task.Cargo.resize(127,&f.callbacks[0]);assert(f.service.IssueMountedStock(&f.task,&f.inventory,48,0,8)==49&&f.task.Cargo.Count()==128);}
|
||||
{Fixture f;for(auto& seed:f.seeds){seed.Prefab="same";}assert(f.service.IssueMountedStock(&f.task,&f.inventory,48,0,8)==50);}
|
||||
for(int bad=0;bad<8;bad++){Fixture f;
|
||||
@@ -160,13 +180,13 @@ int main(){
|
||||
if(bad==3)f.task.Ready=false;if(bad==4)f.task.Receipt->Consumed=false;
|
||||
if(bad==5)for(auto& seed:f.seeds)seed.valid=false;
|
||||
if(bad==6)f.task.Vehicles[0]=&f.vehicles[3];if(bad==7)f.groups[0].valid=false;
|
||||
assert(f.service.IssueMountedStock(&f.task,&f.inventory,48,0,8)==48&&f.inventory.requests.Count()==0);}
|
||||
assert(f.service.IssueMountedStock(&f.task,&f.inventory,48,0,8)==48&&f.inventory.planned.Count()==0);}
|
||||
{Fixture f;f.inventory.items.clear();for(auto& e:f.extra){f.inventory.items.Insert(&e);}f.service.IssueMagazines(&f.task,0);
|
||||
assert(f.task.MagazinesIssued&&f.inventory.requests.Count()==0&&f.task.MountedStock.Count()==0&&f.task.CargoBefore.Count()==0);}
|
||||
assert(f.task.MagazinesIssued&&f.inventory.planned.Count()==0&&f.task.MountedStock.Count()==0&&f.task.CargoBefore.Count()==0);}
|
||||
{Fixture f;f.service.CaptureMountedStock(&f.task);assert(ERR_MountedMagazineSeed::captures==6);f.groups[1].valid=false;f.service.CaptureMountedStock(&f.task);assert(ERR_MountedMagazineSeed::captures==6);}
|
||||
}
|
||||
'''.replace(';if(', ';\n if(').replace(';for(', ';\n for('))
|
||||
print('PASS: actual primary-before-mounted one-shot batch, shared96/128 caps, physical snapshot headroom, original ownership, callback refusal and admission pinning')
|
||||
print('PASS: actual primary-before-mounted one-shot planning, shared96/128 caps, physical snapshot headroom, original ownership, pending-weapon barrier and zero native insertions during planning')
|
||||
lookup=body(D/'ERR_HQ.c','bool ValidateBoundVehicle(SCR_AIGroup group, IEntity vehicle)')
|
||||
lookup=lookup.replace('Replication.','Replication::').replace('null','nullptr')
|
||||
for typ in ['SCR_AIGroup','IEntity','ERR_HQUnit']:lookup=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',lookup)
|
||||
|
||||
@@ -3,7 +3,7 @@ from policy_harness import ROOT, body, run
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
P=Path(os.environ.get('ERR_SUPPLY_SYSTEM_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoySystem.c'))
|
||||
s='\n'.join(body(P,sig) for sig in ('bool StartOne(', 'protected string RefusalReason(',
|
||||
s='\n'.join(body(P,sig) for sig in ('bool StartOne(', 'protected void SearchPiece(', 'protected void FinishSearch(', 'protected string RefusalReason(',
|
||||
'protected string BudgetStatus(', 'protected void NoteFailure(', 'void Tick(', 'string Status('))
|
||||
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('null','nullptr')
|
||||
for name in ('Replication','ERR_GameMode','Math','ERR_Actors','ERR_ConvoyRoadPlan','ERR_SupplyConvoyRules','ERR_HQ','ERR_ForceRules','string'):
|
||||
@@ -11,10 +11,13 @@ for name in ('Replication','ERR_GameMode','Math','ERR_Actors','ERR_ConvoyRoadPla
|
||||
s=s.replace('string::Empty','string{}').replace('array<vector>','Array<vector>')
|
||||
for typ in ('ERR_ForceRoute','ERR_ConvoyReceipt','ERR_ConvoyTask'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
for name in ('m_Mode','m_Pool','m_Service','receipt','task'):
|
||||
for name in ('m_Mode','m_Pool','m_Service','m_Survey','receipt','task'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetWorldEntity().','GetWorldEntity()->').replace('GetWorld().','GetWorld()->')
|
||||
s=s.replace('(m_Service->Count() + 1).ToString()', 'std::to_string(m_Service->Count() + 1)')
|
||||
s=s.replace('(m_StartingCount + 1).ToString()', 'std::to_string(m_StartingCount + 1)')
|
||||
s=s.replace('GetCallqueue().CallLater(SearchStep, 20, true)', 'queue.Schedule()').replace('GetCallqueue().Remove(SearchStep)', 'queue.Remove()')
|
||||
s=s.replace('System.GetTickCount()', '0').replace('bool complete;', 'bool complete=false;')
|
||||
s=s.replace('ERR_ForceRoute* route;', 'ERR_ForceRoute* route=nullptr;')
|
||||
B=Path(os.environ.get('ERR_CONVOY_BUDGET',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBudget.c'))
|
||||
# Execute the real reservation/replay/accounting/consume methods alongside StartOne.
|
||||
pool=B.read_text();pool=re.sub(r'//[^\n]*','',pool).replace('protected ','').replace('ref ','').replace('class ','struct ')
|
||||
@@ -39,10 +42,11 @@ struct string:std::string{using std::string::string;string(std::string s):std::s
|
||||
struct vector{float x=0,y=0,z=0;float operator[](int i)const{return i==0?x:i==1?y:z;}};
|
||||
std::ostream& operator<<(std::ostream& out,vector p){return out<<p.x<<","<<p.y<<","<<p.z;}
|
||||
vector Vector(float x,float y,float z){return {x,y,z};}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T x){this->push_back(x);}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T x){this->push_back(x);}void Clear(){this->clear();}};
|
||||
struct Counts{int world=0,bounds=0,bases=0,ground=0,random=0,road=0,reserve=0,dispatch=0;} calls;
|
||||
struct World{void GetWorldBounds(vector& low,vector& high){calls.bounds++;low={0,0,0};high={10000,100,10000};}};
|
||||
struct Game{World world;bool available=true;float now=0;World* GetWorldEntity(){calls.world++;return available?&world:nullptr;}Game* GetWorld(){return this;}float GetWorldTime(){return now;}} game;
|
||||
struct Queue{bool active=false;void Schedule(){assert(!active);active=true;}void Remove(){active=false;}};
|
||||
struct Game{Queue queue;World world;bool available=true;float now=0;World* GetWorldEntity(){calls.world++;return available?&world:nullptr;}Game* GetWorld(){return this;}float GetWorldTime(){return now;}} game;
|
||||
Game* GetGame(){return &game;}
|
||||
enum class ERR_ForceKind{GARRISON,CONVOY};
|
||||
struct ERR_ForceRecord{int Id=0,InitialStrength=0,Strength=0;bool Physical=true,Eliminated=false;void* Group=this;ERR_ForceKind Kind=ERR_ForceKind::CONVOY;string SourceId;};
|
||||
@@ -60,19 +64,25 @@ using Pool=ERR_ConvoyPool;
|
||||
struct ERR_ConvoyTask{bool Stopped=false;};
|
||||
struct Service{Pool* pool=nullptr;int count=0;bool fail=false;ERR_ConvoyTask task;int Count(){return count;}
|
||||
ERR_ConvoyTask* Dispatch(ERR_ConvoyReceipt* r,Array<vector>,Array<vector>,ERR_ForceRoute*,float){calls.dispatch++;if(fail||!pool->Consume(r))return nullptr;count++;return &task;}};
|
||||
namespace ERR_ConvoyRoadPlan{bool clear=true;ERR_ForceRoute found;
|
||||
bool Build(vector,Array<vector>& positions,Array<vector>& angles,ERR_ForceRoute*& route,Array<vector>,float){calls.road++;if(!clear)return false;positions={{100,0,100}};angles={{0,0,0}};route=&found;return true;}}
|
||||
namespace ERR_ConvoyRoadPlan{bool clear=true;ERR_ForceRoute found;}
|
||||
struct ERR_ConvoyRoadSurvey{int progress=0;void Begin(vector,Array<vector>,float){calls.road++;}bool Step(){return ++progress==9;}
|
||||
bool Result(Array<vector>& positions,Array<vector>& angles,ERR_ForceRoute*& route){if(!ERR_ConvoyRoadPlan::clear)return false;positions={{100,0,100}};angles={{0,0,0}};route=&ERR_ConvoyRoadPlan::found;return true;}};
|
||||
namespace ERR_SupplyConvoyRules{bool Separated(vector,Array<vector>,float){return true;}}
|
||||
struct Startup{Mode* m_Mode;Pool* m_Pool;Service* m_Service;Array<vector> m_Bases,m_Starts;int m_Target=3,m_Attempts=0;bool m_Stopped=false,m_Automatic=true;
|
||||
float m_Began=-1,m_Next=0;string m_Reason,m_LastFailure;void FindBases(){calls.bases++;m_Bases={{0,0,0},{1000,0,0}};}
|
||||
float m_Began=-1,m_Next=0;string m_Reason,m_LastFailure;
|
||||
bool m_Searching=false,m_FindingBases=false;int m_BaseCursor=0,m_SeedAttempt=0,m_StartingCount=0;
|
||||
string m_Source;vector m_Low,m_High;float m_Separation=0;ERR_ConvoyRoadSurvey* m_Survey=nullptr;
|
||||
bool FindBaseStep(){calls.bases++;m_Bases={{0,0,0},{1000,0,0}};return true;}
|
||||
'''+s+r'''
|
||||
void Drain(){for(int i=0;i<100&&m_Searching;i++){int before=calls.reserve;SearchPiece();if(m_Searching)assert(calls.reserve==before);}assert(!m_Searching&&!game.queue.active);}
|
||||
bool StartAndDrain(){int before=m_Service->Count();if(!StartOne())return false;assert(calls.dispatch==0||m_Service->Count()==before);Drain();return m_Service->Count()>before;}
|
||||
};
|
||||
void Reset(){calls={};ERR_HQ::events.clear();ERR_ConvoyRoadPlan::clear=true;game.available=true;}
|
||||
void Reset(){calls={};ERR_HQ::events.clear();ERR_ConvoyRoadPlan::clear=true;game.available=true;game.queue.active=false;}
|
||||
void NoSurvey(){assert(calls.bases==0&&calls.world==0&&calls.bounds==0&&calls.ground==0&&calls.random==0&&calls.road==0&&calls.reserve==0&&calls.dispatch==0);}
|
||||
struct Fixture{Mode mode;ERR_ForceRecord baseline;Pool pool;Service service;Startup startup;
|
||||
Fixture(int bodies):pool(&mode,3,true),startup{&mode,&pool,&service}{baseline.Strength=bodies;mode.ledger.records.Insert(&baseline);ERR_GameMode::current=&mode;service.pool=&pool;Reset();}};
|
||||
int main(){
|
||||
{Fixture f(126);auto& blocked=f.startup;assert(!blocked.StartOne());NoSurvey();
|
||||
{Fixture f(126);auto& blocked=f.startup;assert(!blocked.StartAndDrain());NoSurvey();
|
||||
assert(blocked.m_Reason=="insufficient shared body budget for another original convoy");
|
||||
assert(blocked.m_Starts.empty()&&blocked.m_Attempts==0&&f.service.count==0&&f.pool.m_Receipts.empty());
|
||||
blocked.Tick(0,true);blocked.Tick(30000,true);blocked.Tick(34999,true);blocked.Tick(35000,true);
|
||||
@@ -81,35 +91,39 @@ int main(){
|
||||
for(int i=2;i<150;i++)blocked.Tick(30000+5000*i,true);
|
||||
NoSurvey();assert(blocked.m_Attempts==120&&ERR_HQ::events.size()==2);
|
||||
assert(ERR_HQ::events.back().find("attempts exhausted")!=string::npos);}
|
||||
{Fixture f(125);assert(f.startup.StartOne());assert(calls.bases==1&&calls.bounds==1&&calls.road==1&&calls.reserve==1&&calls.dispatch==1);
|
||||
{Fixture f(125);assert(f.startup.StartAndDrain());assert(calls.bases==1&&calls.bounds==1&&calls.road==1&&calls.reserve==1&&calls.dispatch==1);
|
||||
assert(f.service.count==1&&f.startup.m_Starts.Count()==1&&f.pool.m_Receipts[0]->Consumed&&f.pool.CommittedBodies()==135);
|
||||
// A lost task counter must never turn the same consumed source into a replacement.
|
||||
f.service.count=0;assert(!f.startup.StartOne());assert(calls.dispatch==1&&f.startup.m_Reason.find("already consumed")!=string::npos);}
|
||||
{Fixture f(100);ERR_ConvoyRoadPlan::clear=false;assert(!f.startup.StartOne());assert(calls.road==8&&calls.reserve==0&&calls.dispatch==0);
|
||||
f.service.count=0;assert(!f.startup.StartAndDrain());assert(calls.dispatch==1&&f.startup.m_Reason.find("already consumed")!=string::npos);}
|
||||
{Fixture f(100);ERR_ConvoyRoadPlan::clear=false;assert(!f.startup.StartAndDrain());assert(calls.road==8&&calls.reserve==0&&calls.dispatch==0);
|
||||
assert(f.startup.m_Reason=="no clear separated road formation in this bounded survey");}
|
||||
{Fixture f(126);f.startup.Tick(0,true);f.startup.Tick(30000,true);NoSurvey();f.baseline.Strength=125;
|
||||
f.startup.Tick(35000,true);assert(f.service.count==1&&calls.dispatch==1&&f.startup.m_Attempts==2&&f.startup.m_LastFailure.empty());}
|
||||
f.startup.Tick(35000,true);assert(f.startup.m_Searching&&calls.dispatch==0);f.startup.Drain();assert(f.service.count==1&&calls.dispatch==1&&f.startup.m_Attempts==2&&f.startup.m_LastFailure.empty());}
|
||||
// Critical real-pool replay: failed road preflight retains the unconsumed receipt.
|
||||
{Fixture f(123);f.service.fail=true;assert(!f.startup.StartOne());
|
||||
{Fixture f(123);f.service.fail=true;assert(!f.startup.StartAndDrain());
|
||||
assert(f.service.count==0&&f.pool.m_Receipts.Count()==1&&f.pool.CommittedBodies()==133);
|
||||
auto original=f.pool.m_Receipts[0];assert(!original->Consumed&&original->OutstandingBodies==10);
|
||||
assert(f.pool.BodyBudgetAvailable("supply-convoy-1")&&!f.pool.BodyBudgetAvailable("supply-convoy-2"));
|
||||
assert(f.pool.Reserve("supply-convoy-1")==original&&!f.pool.Reserve("supply-convoy-2"));
|
||||
f.service.fail=false;Reset();assert(f.startup.StartOne());
|
||||
f.service.fail=false;Reset();assert(f.startup.StartAndDrain());
|
||||
assert(calls.road==1&&calls.reserve==1&&calls.dispatch==1&&f.pool.m_Receipts.Count()==1);
|
||||
assert(f.pool.m_Receipts[0]==original&&original->Consumed&&f.service.count==1&&f.pool.CommittedBodies()==133);}
|
||||
{Fixture f(123);f.service.fail=true;assert(!f.startup.StartOne());auto original=f.pool.m_Receipts[0];
|
||||
{Fixture f(123);f.service.fail=true;assert(!f.startup.StartAndDrain());auto original=f.pool.m_Receipts[0];
|
||||
// Precheck intentionally cannot grant admission: actual Consume rechecks budget growth.
|
||||
f.baseline.Strength=126;f.service.fail=false;assert(!f.startup.StartOne()&&!original->Consumed&&f.service.count==0);
|
||||
f.baseline.Strength=125;assert(f.startup.StartOne()&&original->Consumed&&f.pool.CommittedBodies()==135);}
|
||||
f.baseline.Strength=126;f.service.fail=false;assert(!f.startup.StartAndDrain()&&!original->Consumed&&f.service.count==0);
|
||||
f.baseline.Strength=125;assert(f.startup.StartAndDrain()&&original->Consumed&&f.pool.CommittedBodies()==135);}
|
||||
// Real budget grows while an admitted survey is still pending: no reservation.
|
||||
{Fixture f(125);assert(f.startup.StartOne());assert(f.startup.m_Searching&&calls.road==0&&calls.reserve==0&&calls.dispatch==0);
|
||||
f.startup.SearchPiece();f.startup.SearchPiece();assert(calls.road==1&&calls.reserve==0);f.baseline.Strength=126;f.startup.Drain();
|
||||
assert(f.service.count==0&&calls.reserve==0&&calls.dispatch==0&&f.pool.m_Receipts.empty());}
|
||||
{Fixture f(0);assert(!f.pool.BodyBudgetAvailable("")&&!f.pool.BodyBudgetAvailable(string(65,'x')));
|
||||
Mode other;ERR_GameMode::current=&other;assert(!f.pool.BodyBudgetAvailable("supply-convoy-1"));
|
||||
ERR_GameMode::current=&f.mode;Replication::server=false;assert(!f.pool.BodyBudgetAvailable("supply-convoy-1"));Replication::server=true;
|
||||
f.mode.active=false;assert(!f.pool.BodyBudgetAvailable("supply-convoy-1"));}
|
||||
{Fixture f(0);f.startup.m_Pool=nullptr;assert(!f.startup.StartOne());NoSurvey();assert(f.startup.m_Reason=="convoy allocator unavailable");}
|
||||
{Fixture f(0);f.startup.m_Target=0;assert(!f.startup.StartOne());NoSurvey();f.startup.m_Target=3;
|
||||
f.startup.m_Stopped=true;assert(!f.startup.StartOne());NoSurvey();}
|
||||
{Fixture f(0);f.startup.m_Pool=nullptr;assert(!f.startup.StartAndDrain());NoSurvey();assert(f.startup.m_Reason=="convoy allocator unavailable");}
|
||||
{Fixture f(0);f.startup.m_Target=0;assert(!f.startup.StartAndDrain());NoSurvey();f.startup.m_Target=3;
|
||||
f.startup.m_Stopped=true;assert(!f.startup.StartAndDrain());NoSurvey();}
|
||||
|
||||
}
|
||||
''')
|
||||
print('PASS: actual StartOne plus actual pool preserve unconsumed receipt retry, reject consumed replay, skip impossible new-budget surveys, retain equality, retry caps and journal dedup; no FPS claim')
|
||||
print('PASS: actual queued StartOne/SearchPiece/FinishSearch plus actual pool preserve unconsumed receipt retry, reject consumed replay, skip impossible new-budget surveys, retain equality, retry caps and journal dedup; no FPS claim')
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Execute production asynchronous survey orchestration; native road geometry has its own differential harness."""
|
||||
import re
|
||||
from policy_harness import ROOT,body,run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoySystem.c'
|
||||
s='\n'.join(body(p,x) for x in ('protected bool FindBaseStep(', 'bool StartOne(', 'protected void FinishSearch(', 'protected void SearchStep(', 'protected void SearchPiece(', 'void Stop('))
|
||||
s=s.replace('protected ','').replace('(m_StartingCount + 1).ToString()', 'std::to_string(m_StartingCount + 1)')
|
||||
s=s.replace('GetGame().', 'GetGame()->').replace('GetCallqueue().CallLater(SearchStep, 20, true)', 'queue.Schedule(20)').replace('GetCallqueue().Remove(SearchStep)', 'queue.Remove()')
|
||||
for name in ('GetWorldEntity()','GetPlayerManager()','GetWorld()'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('GetGame()->GetWorld()->GetWorldTime()', 'clockMs').replace('System.GetTickCount()', 'clockMs')
|
||||
s=s.replace('array<vector>', 'Array<vector>').replace('m_Mode.Sites().', 'm_Mode->Sites()->')
|
||||
for typ in ('ERR_SiteRuntime','ERR_Performance','ERR_ForceRoute','ERR_ConvoyTask','ERR_ConvoyReceipt'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
s=s.replace('ERR_ConvoyRoadSurvey()', 'ERR_ConvoyRoadSurvey()')
|
||||
for name in ('m_Mode','m_Service','m_Pool','m_Survey','site','task','receipt','perf'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('->Definition.', '->Definition->').replace('->Group.', '->Group->')
|
||||
for name in ('Replication','ERR_GameMode','Math','ERR_LocationKind','ERR_SupplyConvoyRules','ERR_Actors','ERR_HQ','ERR_PerfMetric'):
|
||||
s=s.replace(name+'.',name+'::')
|
||||
s=s.replace('string.Format','string::Format').replace('string.Empty','string{}').replace('null','nullptr')
|
||||
s=s.replace('bool complete;', 'bool complete=false;').replace('bool failed = true','bool failed = true')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}template<class...T>static string Format(T...){return "event";}};
|
||||
struct vector{float v[3]={};float& operator[](int i){return v[i];}};
|
||||
vector Vector(float x,float y,float z){return {{x,y,z}};}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Clear(){this->clear();}void Insert(T v){this->push_back(v);}void Copy(Array<T>&v){*this=v;}};
|
||||
int clockMs=0;
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace Math{float Min(float a,float b){return std::min(a,b);}float Max(float a,float b){return std::max(a,b);}float RandomFloat(float a,float){return a;}}
|
||||
namespace ERR_Actors{vector Ground(vector v){return v;}}
|
||||
namespace ERR_HQ{int events=0;void Event(string,string){events++;}}
|
||||
namespace ERR_PerfMetric{int CONVOY_SURVEY=1;}
|
||||
struct ERR_Performance{int marks=0;void Mark(int,int){marks++;}};
|
||||
namespace ERR_LocationKind{enum Value{OUTPOST,DEPOT,RADIO,OTHER};}using ERR_LocationKind_t=ERR_LocationKind::Value;
|
||||
struct Definition{ERR_LocationKind_t Kind=ERR_LocationKind::OUTPOST;vector Position;};
|
||||
struct Group{int faction=1;int GetFaction(){return faction;}};
|
||||
struct ERR_SiteRuntime{struct Definition* Definition=nullptr;struct Group* Group=nullptr;};
|
||||
struct Sites{Array<ERR_SiteRuntime*>sites;int Count(){return sites.Count();}ERR_SiteRuntime* At(int i){return sites.at(i);}};
|
||||
struct ERR_ForceRoute{};struct ERR_ConvoyReceipt{bool Consumed=false;};struct ERR_ConvoyTask{bool Stopped=false;};
|
||||
struct Pool{bool available=true,reserve=true;int reserves=0;ERR_ConvoyReceipt receipt;bool BodyBudgetAvailable(string){return available;}ERR_ConvoyReceipt* Reserve(string){reserves++;return reserve?&receipt:nullptr;}};
|
||||
struct Service{int count=0,dispatches=0,bases=0;bool success=true;ERR_ConvoyTask task;int Count(){return count;}void SetBases(Array<vector>&b){bases=b.Count();}ERR_ConvoyTask* Dispatch(ERR_ConvoyReceipt*,Array<vector>&,Array<vector>&,ERR_ForceRoute*,int){dispatches++;if(!success)return nullptr;count++;return &task;}};
|
||||
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;struct Sites sites;ERR_Performance perf;static ERR_GameMode* Get(){return current;}bool Active(){return active;}struct Sites* Sites(){return &sites;}int EnemyFaction(){return 1;}ERR_Performance* Performance(){return &perf;}};
|
||||
struct Queue{bool active=false;int scheduled=0,removed=0;void Schedule(int n){assert(n==20);active=true;scheduled++;}void Remove(){active=false;removed++;}};
|
||||
struct World{void GetWorldBounds(vector&l,vector&h){l=Vector(0,0,0);h=Vector(10000,0,10000);}};
|
||||
struct Players{int count=1;int GetPlayerCount(){return count;}};
|
||||
struct Game{Queue queue;World world;Players players;bool haveWorld=true;World* GetWorldEntity(){return haveWorld?&world:nullptr;}Players* GetPlayerManager(){return &players;}}game;
|
||||
Game* GetGame(){return &game;}
|
||||
namespace ERR_SupplyConvoyRules{bool separated=true;bool Separated(vector,Array<vector>&,float){return separated;}}
|
||||
struct ERR_ConvoyRoadSurvey{static inline int calls=0,begins=0,stepsNeeded=19,cost=0;static inline bool found=true;int progress=0;
|
||||
void Begin(vector,Array<vector>&,float){begins++;progress=0;}bool Step(){calls++;clockMs+=cost;return ++progress>=stepsNeeded;}
|
||||
bool Result(Array<vector>&p,Array<vector>&a,ERR_ForceRoute*&r){if(!found)return false;p.Insert(Vector(10,0,10));a.Insert(Vector(0,0,0));r=new ERR_ForceRoute;return true;}};
|
||||
struct SurveySystem{
|
||||
ERR_GameMode* m_Mode;Pool* m_Pool;Service* m_Service;
|
||||
Array<vector>m_Bases,m_Starts;bool m_Stopped=false,m_Searching=false,m_FindingBases=false;
|
||||
int m_Target=3,m_Attempts=1,m_BaseCursor=0,m_SeedAttempt=0,m_StartingCount=0;
|
||||
string m_Source,m_Reason,m_LastFailure;vector m_Low,m_High;float m_Separation=0,m_Next=0;
|
||||
ERR_ConvoyRoadSurvey* m_Survey=nullptr;int approaches=0,notes=0;
|
||||
bool Approach(vector center,vector&position){approaches++;position=center;return true;}
|
||||
string RefusalReason(ERR_ConvoyReceipt*){return "budget refusal";}void NoteFailure(){notes++;}string Status(){return m_Reason;}
|
||||
'''+s.replace('ERR_LocationKind kind','ERR_LocationKind_t kind')+r'''
|
||||
};
|
||||
struct Fixture{ERR_GameMode mode;Pool pool;Service service;SurveySystem s{&mode,&pool,&service};
|
||||
Fixture(){ERR_GameMode::current=&mode;game=Game{};Replication::server=true;ERR_ConvoyRoadSurvey::found=true;ERR_ConvoyRoadSurvey::cost=0;ERR_ConvoyRoadSurvey::stepsNeeded=19;ERR_SupplyConvoyRules::separated=true;s.m_Bases={Vector(0,0,0),Vector(1000,0,0)};}
|
||||
void Finish(){for(int i=0;s.m_Searching&&i<300;i++)s.SearchStep();assert(!s.m_Searching);}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;assert(f.s.StartOne());assert(f.s.m_Searching&&game.queue.active&&f.pool.reserves==0&&f.service.count==0);assert(!f.s.StartOne()&&game.queue.scheduled==1);
|
||||
game.players.count=0;f.s.SearchStep();assert(!f.s.m_Survey&&f.pool.reserves==0);game.players.count=1;
|
||||
f.s.SearchStep();assert(f.s.m_SeedAttempt==1&&f.s.m_Survey);int calls=ERR_ConvoyRoadSurvey::calls;f.s.SearchStep();assert(ERR_ConvoyRoadSurvey::calls==calls+8&&f.pool.reserves==0);
|
||||
f.Finish();assert(f.pool.reserves==1&&f.service.dispatches==1&&f.service.count==1&&f.s.m_Starts.Count()==1&&!game.queue.active);
|
||||
f.s.SearchStep();assert(f.service.dispatches==1);}
|
||||
// The cooperative budget yields before eight pieces when the native timer advances.
|
||||
{Fixture f;ERR_ConvoyRoadSurvey::cost=1;f.s.StartOne();f.s.SearchStep();int before=ERR_ConvoyRoadSurvey::calls;f.s.SearchStep();assert(ERR_ConvoyRoadSurvey::calls==before+2);f.s.Stop();}
|
||||
// Exhaust every original eight-seed attempt without reserving or spawning.
|
||||
{Fixture f;ERR_ConvoyRoadSurvey::found=false;f.s.StartOne();f.Finish();assert(f.s.m_SeedAttempt==8&&f.pool.reserves==0&&f.service.count==0&&f.s.notes==1);}
|
||||
// Current capacity is checked again after yield; no stale plan spends another source's slot.
|
||||
for(int reason=0;reason<4;reason++){Fixture f;f.s.StartOne();f.s.SearchStep();if(reason==0)f.pool.available=false;if(reason==1)f.service.count=1;if(reason==2)f.mode.active=false;if(reason==3)ERR_GameMode::current=nullptr;
|
||||
f.s.SearchStep();assert(!f.s.m_Searching&&!game.queue.active&&f.pool.reserves==0&&f.service.dispatches==0);}
|
||||
// A reservation or final native preflight refusal does not record a physical start.
|
||||
for(int failure=0;failure<3;failure++){Fixture f;if(failure==0)f.pool.reserve=false;if(failure==1)f.pool.receipt.Consumed=true;if(failure==2)f.service.success=false;f.s.StartOne();f.Finish();assert(f.service.count==0&&f.s.m_Starts.Count()==0&&f.s.notes==1);}
|
||||
{Fixture f;f.service.task.Stopped=true;f.s.StartOne();f.Finish();assert(f.service.count==1&&f.s.m_Starts.Count()==1);}
|
||||
// No invalid caller queues work or consumes budget.
|
||||
for(int failure=0;failure<5;failure++){Fixture f;if(failure==0)Replication::server=false;if(failure==1)f.mode.active=false;if(failure==2)f.pool.available=false;if(failure==3)game.haveWorld=false;if(failure==4)f.service.count=3;
|
||||
assert(!f.s.StartOne()&&!game.queue.active&&f.pool.reserves==0);}
|
||||
// Base search is one site per callback; no full-site sweep in the HQ tick.
|
||||
{Fixture f;f.s.m_Bases.Clear();Definition a,b,skip;skip.Kind=ERR_LocationKind::OTHER;a.Position=Vector(100,0,100);b.Position=Vector(1000,0,1000);ERR_SiteRuntime sa{&a},sb{&b},sc{&skip};f.mode.sites.sites={&sa,&sc,&sb};
|
||||
f.s.StartOne();assert(f.s.approaches==0);f.s.SearchStep();assert(f.s.approaches==1&&f.s.m_Bases.Count()==1);f.s.SearchStep();assert(f.s.approaches==1);f.s.SearchStep();assert(f.s.approaches==2&&f.s.m_Bases.Count()==2);f.s.SearchStep();assert(!f.s.m_FindingBases&&f.service.bases==2);f.Finish();}
|
||||
{Fixture f;f.s.m_Bases.Clear();f.s.StartOne();f.Finish();assert(f.s.notes==1&&f.pool.reserves==0);}
|
||||
{Fixture f;f.s.StartOne();f.s.SearchStep();f.s.Stop();f.s.SearchStep();assert(!f.s.m_Survey&&!game.queue.active&&f.pool.reserves==0);}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual queued supply survey yields by piece/time, pauses empty worlds, scans one base per callback, keeps eight seeds, rechecks capacity and stops without native admission on failure')
|
||||
@@ -105,7 +105,7 @@ int main(){
|
||||
startup=cpp(body(D/'ERR_SupplyConvoySystem.c','void Tick('))
|
||||
run(common+r'''
|
||||
struct Service{int count=0;int Count(){return count;}};
|
||||
struct Startup{Service* m_Service;bool m_Stopped=false,m_Automatic=true,create=false;int m_Target=3,m_Attempts=0,calls=0;
|
||||
struct Startup{Service* m_Service;bool m_Stopped=false,m_Searching=false,m_Automatic=true,create=false;int m_Target=3,m_Attempts=0,calls=0;
|
||||
float m_Began=-1,m_Next=0;string m_Reason,m_LastFailure;int notes=0;void NoteFailure(){notes++;}string Status(){return m_Reason;}
|
||||
bool StartOne(){calls++;if(create)m_Service->count++;return create;}
|
||||
'''+startup+r'''};
|
||||
@@ -321,7 +321,7 @@ namespace ERR_ForceRules{bool Finite(float v){return std::isfinite(v);}}
|
||||
struct Pool{int committed=108,limit=135,planned=25;int CommittedBodies(){return committed;}int BodyLimit(){return limit;}int PlannedBodies(){return planned;}};
|
||||
struct ERR_ConvoyReceipt{bool Consumed=false;};
|
||||
struct Service{int count=0;int Count(){return count;}};
|
||||
struct Startup{Service* m_Service;Pool* m_Pool;bool m_Stopped=false,m_Automatic=true,create=false;
|
||||
struct Startup{Service* m_Service;Pool* m_Pool;bool m_Stopped=false,m_Searching=false,m_Automatic=true,create=false;
|
||||
int m_Target=3,m_Attempts=0,calls=0;float m_Began=-1,m_Next=0;string m_Reason,m_LastFailure,nextReason="no clear separated road formation in this bounded survey";
|
||||
bool StartOne(){calls++;m_Reason=nextReason;if(create)m_Service->count++;return create;}
|
||||
'''+diag+r'''};
|
||||
|
||||
Reference in New Issue
Block a user