"""Run the shipped initial/background scheduler with fake native work and clock.""" import re from policy_harness import ROOT, body, run p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c' s='\n'.join(body(p,signature) for signature in ('protected ERR_PrepareResult Finish(', 'ERR_PrepareResult Prepare(', 'protected void SearchStep(', 'protected void BackgroundStep()', 'void Stop()')) s=re.sub(r'#ifdef WORKBENCH.*?#endif','',s,flags=re.S) s=s.replace('protected ', '').replace('ERR_ScenarioDefinition definition', 'Definition* definition') s=s.replace('array positions = {};', 'Array positions;').replace('array covers = {};', 'Array covers;') s=s.replace('GetGame().GetCallqueue().CallLater(BackgroundStep, 200, true)', 'queue.Schedule(200)').replace('GetGame().GetCallqueue().Remove(BackgroundStep)', 'queue.Remove()') s=s.replace('GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High)', 'Bounds(m_Low, m_High)').replace('GetGame().GetPlayerManager().GetPlayerCount()', 'players') s=s.replace('m_Mode.Settings().Get("largeRoadblockCount")', 'm_Mode->largeTarget').replace('m_Mode.Settings().Get("mainRoadWidthMetres")', '6') s=s.replace('m_Mode.Settings().Get("roadblockCount")', 'm_Mode->target').replace('m_Mode.Forces().Count()', 'm_Mode->ledger') s=s.replace('m_Mode.', 'm_Mode->').replace('System.GetTickCount()', 'Now()').replace('Replication.IsServer()', 'server').replace('ERR_GameMode.Get()', 'current') s=s.replace('ERR_PrepareResult.', 'Result::').replace('ERR_Phase.', 'Phase::').replace('ERR_RoadblockRules.', 'Rules::').replace('ERR_TerrainSites.Bounds', 'ValidBounds').replace('ERR_HQ.Event','Event').replace('Math.Max','std::max').replace('null','nullptr') rules='\n'.join(body(p,sig) for sig in ('static int SearchMilliseconds(', 'static int SearchAttempts(')) run(r''' #include #include #include #include using string=std::string; namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value; namespace Phase{enum{PREPARING,PRISON,ESCAPED,ENDED};} namespace Rules{ '''+rules+r''' } int clockMs=0,players=1;bool server=true; int Now(){return clockMs;}bool GetGame(){return true;}void Bounds(int&,int&){}bool ValidBounds(int,int){return true;} templatevoid PrintFormat(T...){}void Event(string,string){} templatestruct Array:std::vector{int Count(){return this->size();}void Clear(){this->clear();}}; struct Queue{int scheduled=0,removed=0;bool active=false;void Schedule(int ms){assert(ms==200);scheduled++;active=true;}void Remove(){removed++;active=false;}}; struct Mode{int target=30,largeTarget=0,ledger=0,phase=Phase::PRISON;bool active=true;int Phase(){return phase;}bool Active(){return active;}};Mode* current=nullptr; struct Definition{}; struct Mining{bool stopped=false;void Stop(){stopped=true;}}; struct Scheduler{ Mode* m_Mode;Definition* m_Definition=nullptr;Queue queue;Mining m_Mining; bool m_BuildGroup=false;void BuildPost(){if(m_Mode->ledger>=250)CancelPost();}void CancelPost(){m_BuildGroup=false;} bool m_Begun=false,m_Ready=false,m_Stopped=false,m_HaveCandidate=false,m_Background=false; int m_SmallTarget=0,m_LargeTarget=0,m_MainRoadWidth=0,m_Target=0,m_Attempts=0,m_Started=0,m_CandidateStarted=0,m_Candidates=0,m_Planned=0; int m_NextSearch=0,m_NextReport=0,m_BackgroundSteps=0,m_BackgroundWork=0,m_MaxBackgroundStep=0,m_Low=0,m_High=0; Arraym_Loaded,m_Positions,m_Groups; int candidateCost=0,candidateCalls=0,planCalls=0,spawnCalls=0;bool accept=false,spawn=true,roster=true;int plan=2; string Status(){return "status";}bool LoadRoster(){m_Loaded.push_back(1);return roster;} bool Candidate(Definition*){candidateCalls++;clockMs+=candidateCost;if(accept){m_Candidates++;m_CandidateStarted=Now();}return accept;} int Plan(Array&,Array&){planCalls++;return plan;} bool SpawnPost(Array&,Array&){spawnCalls++;if(spawn){m_Groups.push_back(1);m_Positions.push_back(1);m_NextSearch=Now()+2000;}return spawn;} '''+s+r''' }; int main(){ Mode mode;current=&mode;Definition definition;Scheduler s;s.m_Mode=&mode; // No candidates in the entire initial window: admission yields, never gives up. assert(s.Prepare(&definition)==Result::PENDING&&s.candidateCalls==8); clockMs=12000;assert(s.Prepare(&definition)==Result::READY&&s.m_Background&&s.queue.active&&s.m_Definition==&definition); assert(s.m_Loaded.Count()==1&&s.m_Positions.Count()==0&&s.queue.scheduled==1); assert(s.Prepare(&definition)==Result::READY&&s.queue.scheduled==1); // Empty, full ledger and inactive/initializing cases cannot do terrain work. players=0;s.BackgroundStep();assert(s.candidateCalls==8); players=1;mode.ledger=250;s.BackgroundStep();assert(s.candidateCalls==8);mode.ledger=0; mode.phase=Phase::PREPARING;s.BackgroundStep();assert(s.candidateCalls==8);mode.phase=Phase::ESCAPED; // Native operations yield between samples if the cooperative time budget expires. s.candidateCost=3;s.BackgroundStep();assert(s.candidateCalls==9&&s.m_MaxBackgroundStep==3); s.candidateCost=0;s.BackgroundStep();assert(s.candidateCalls==17); // Arbitrarily many exhausted attempts do not end background placement. s.m_Attempts=50000;clockMs+=60000;s.BackgroundStep();assert(s.m_Background&&s.m_Attempts==50008); s.accept=true; // Candidate planning/physical creation occur on a separate callback. s.BackgroundStep();assert(s.m_HaveCandidate&&s.planCalls==0); s.plan=1;clockMs+=500;s.BackgroundStep();assert(s.m_HaveCandidate&&s.spawnCalls==0); clockMs+=2000;s.BackgroundStep();assert(!s.m_HaveCandidate&&s.spawnCalls==0);s.plan=2; // Failed creation remains pending. No partial count or allowance is consumed. s.spawn=false;s.BackgroundStep();s.BackgroundStep();assert(s.m_Positions.Count()==0&&s.m_Background);s.spawn=true; // Finish all 30 after prison unlock, at most one physical post per callback. for(int i=0;i<30;i++){ s.BackgroundStep();assert(s.m_Positions.Count()==i); s.BackgroundStep();assert(s.m_Positions.Count()==i+1); int samples=s.candidateCalls;s.BackgroundStep();assert(s.candidateCalls==samples); // two-second spawn cooldown clockMs+=2000; } assert(!s.m_Background&&!s.queue.active&&s.m_Loaded.Count()==0&&!s.m_Definition&&s.m_Ready); int calls=s.spawnCalls;s.m_Groups.clear(); // killed/removed groups never refund the round's posts s.BackgroundStep();assert(s.spawnCalls==calls&&s.Prepare(&definition)==Result::READY&&s.m_Positions.Count()==30); // A newly full ledger must cancel a partial post instead of leaving actors stranded. {Scheduler pending;pending.m_Mode=&mode;pending.Prepare(&definition);clockMs+=12000;pending.Prepare(&definition); pending.m_BuildGroup=true;mode.ledger=250;pending.BackgroundStep();assert(!pending.m_BuildGroup);mode.ledger=0;} // Attempt ceiling also hands off, without waiting for the wall-clock deadline. clockMs=0;Scheduler attempts;attempts.m_Mode=&mode;attempts.Prepare(&definition);attempts.m_Attempts=300; assert(attempts.Prepare(&definition)==Result::READY&&attempts.m_Background); mode.active=false;attempts.BackgroundStep();assert(attempts.m_Stopped&&!attempts.queue.active&&attempts.m_Mining.stopped&&!attempts.m_Definition); mode.active=true;Scheduler ended;ended.m_Mode=&mode;ended.Prepare(&definition);clockMs=12000;ended.Prepare(&definition);current=nullptr; ended.BackgroundStep();assert(ended.m_Stopped&&!ended.queue.active);current=&mode; mode.target=0;Scheduler disabled;disabled.m_Mode=&mode;assert(disabled.Prepare(&definition)==Result::READY&&!disabled.queue.active); mode.target=30;Scheduler missing;missing.m_Mode=&mode;missing.roster=false;assert(missing.Prepare(&definition)==Result::READY&&!missing.queue.active); } ''') # Every background plan and physical transaction still uses the original safety checks. assert 'if (!mesh || !PlayersClear()) return 0;' in body(p,'protected int Plan(') assert 'if (!PlayersClear() || m_Mode.Forces().Count() >= 250 ||' in body(p,'protected bool SpawnPost(') assert 'if (!PlayersClear() || m_Mode.Forces().Count() >= 250) { CancelPost(); return; }' in body(p,'protected void BuildPost(') print('PASS: actual roadblock scheduler yields initial deadline/attempt cap to background, budgets samples, pauses empty/full worlds, completes 30 after unlock, preserves safety and never replenishes losses; teardown cancels work') # Run actual staged physical creation/rollback. Opaque native boundaries are doubled. s='\n'.join(body(p,sig) for sig in ('bool LocalOnly(', 'protected void Rollback(', 'protected bool SpawnPost(', 'protected void CancelPost()', 'protected bool ShareProps()', 'protected void BuildPost()')) s=s.replace('protected ', '').replace('array positions','Array& positions').replace('array covers','Array& covers').replace('ERR_RoadblockProp prop', 'Prop prop') s=s.replace('array spawned', 'Array& spawned').replace('SCR_AIGroup group', 'Node* group').replace('SCR_AIGroup guardGroup', 'Node* guardGroup') s=s.replace('SCR_AIGroup.Cast','Node::Cast').replace('AIWaypoint.Cast','Node::Cast').replace('IEntity actor','Node* actor').replace('IEntity cover','Node* cover') s=s.replace('foreach (IEntity ownedEntity : m_BuildSpawned)','for (Node* ownedEntity : m_BuildSpawned)').replace('foreach (IEntity entity : spawned)', 'for (Node* entity : spawned)').replace('vector point','int point').replace('vector coverPosition','int coverPosition') s=s.replace('foreach (ERR_RoadblockProp plannedProp : covers)', 'for (Prop plannedProp : covers)') s=s.replace('m_Forward.VectorToAngles()', '0').replace('m_PostSerial.ToString()', 'std::to_string(m_PostSerial)') s=s.replace('ERR_ForceRecord responders', 'Record* responders').replace('responders.Id', 'responders->Id') s=s.replace('GetGame().GetWorld().GetWorldTime()', 'Now()').replace('System.GetTickCount()', 'Now()').replace('null','nullptr') for name in ('m_BuildGroup','m_BuildHold','m_BuildLocalGroup','m_BuildLocalHold','guardGroup'):s=s.replace(name+'.',name+'->') s=s.replace('IEntity entity', 'Node* entity').replace('RplComponent component;', 'Replica* component=nullptr;').replace('RplComponent component =', 'Replica* component =') s=s.replace('RplComponent.Cast', 'Replica::Cast').replace('FindComponent(RplComponent)', 'FindComponent()').replace('RplId.Invalid()', '-1') s=s.replace('entity.FindComponent', 'entity->FindComponent').replace('m_BuildSpawned[i].FindComponent', 'm_BuildSpawned[i]->FindComponent').replace('component.', 'component->') for ns in ('ERR_Actors','ERR_Resources','ERR_ForceKind','ERR_RoadblockRules'):s=s.replace(ns+'.',ns+'::') rule=body(p,'static int PropCount(')+body(p,'static int GuardCount(') run(r''' #include #include #include #include #include #include #include using string=std::string; int Now(){return 100;} templatevoid PrintFormat(T...){} templatestruct Array:std::vector{using std::vector::vector;using std::vector::operator=;int Count(){return this->size();}void Clear(){this->clear();}void Copy(Array&other){*this=other;}void Insert(T value){this->push_back(value);}bool Contains(T value){return std::find(this->begin(),this->end(),value)!=this->end();}}; struct Replica{int id=1;bool streaming=true;static Replica* Cast(Replica*r){return r;}int Id(){return id;}void EnableStreaming(bool value){streaming=value;}}; struct Node{Replica replica;int position=0,deleted=0,activated=0;bool missing=false,acceptMember=true;Arraymembers,holds; Replica* FindComponent(){return missing?nullptr:&replica;}static Node* Cast(Node*n){return n;}void SetFaction(int){}void SetSpawnImmediately(bool){}void SetDeleteWhenEmpty(bool){}void SetPermanentLOD(int){} bool AddAIEntityToGroup(Node*n){if(!acceptMember)return false;members.Insert(n);return true;}void SetCompletionRadius(int r){assert(r==25);}void AddWaypoint(Node*n){holds.Insert(n);}void ActivateAI(){activated++;}}; namespace ERR_Actors{int calls=0,failAt=0,deletes=0;bool faction=true;Arrayall; Node* Spawn(string,int position,int=0){calls++;if(calls==failAt)return nullptr;auto*n=new Node;n->position=position;all.Insert(n);return n;} bool SetFaction(Node*,int){return faction;}void DeleteSpawned(Node*n){if(!n)return;assert(!n->deleted);n->deleted++;deletes++;}} namespace ERR_Resources{string DefendWaypoint(){return "hold";}} namespace ERR_ForceKind{int CHECKPOINT=1;} struct Record{int Id=0,strength=0;Node*group=nullptr;}; struct Ledger{int count=0,attempts=0,failAt=0,unregistered=0,next=0;std::maplive;std::setsources; int Count(){return count+live.size();} Record* Register(string id,int,int,int strength,void*,Node*group,int){assert(strength==2&&group&&group->members.Count()==2);attempts++;if(attempts==failAt)return nullptr;assert(sources.insert(id).second);int n=++next;live[n]={n,strength,group};return &live[n];} void Unregister(int id){assert(live.erase(id)==1);unregistered++;}}; struct Mode{Ledger ledger;int owned=0,groups=0,hqGroups=0,localGroups=0;ArrayownedNodes,published;std::functionlocal; Ledger& Forces(){return ledger;}int EnemyFaction(){return 1;}void Own(Node*n){assert(n&&!n->deleted&&!ownedNodes.Contains(n));owned++;ownedNodes.Insert(n);} void RegisterGroup(Node*n){assert(n&&ownedNodes.Contains(n)&&n->members.Count()==2&&n->holds.Count()==1);groups++;published.Insert(n);if(local(n))localGroups++;else hqGroups++;}}; struct Placement{bool clear=true;bool Clear(int,void*){return clear;}}; struct Mining{int registered=0;Node*group=nullptr;Node*hold=nullptr;void Register(Node*g,Node*h,int){registered++;group=g;hold=h;}}; struct Prop{string Prefab="cover";int Position=5,Angles=0;}; namespace ERR_RoadblockRules{'''+rule+r'''} struct Build{ bool m_Large=false;int m_SmallPlaced=0,m_LargePlaced=0,m_PostSerial=0; Mode m_Mode;Placement m_Placement;Mining m_Mining;Node* m_BuildGroup=nullptr;Node* m_BuildHold=nullptr;Node* m_BuildLocalGroup=nullptr;Node* m_BuildLocalHold=nullptr; Arraym_BuildPositions,m_Positions;Arraym_BuildCovers;Arraym_BuildSpawned,m_Groups,m_LocalGroups;Arraym_Roster={"one","two"}; int m_BuildActors=0,m_BuildCoverCount=0,m_Centre=10,m_Right=0,m_NextSearch=0;string COVER="cover";bool playersClear=true,coversClear=true; Build(){m_Mode.local=[this](Node*g){return LocalOnly(g);};} bool PlayersClear(){return playersClear;}bool CoverClear(Prop){return coversClear;} '''+s+r''' }; int main(){for(bool large:{false,true}){ const int guards=ERR_RoadblockRules::GuardCount(large);assert(guards==4); Arraypositions={10,20,30,40};Arraycovers(ERR_RoadblockRules::PropCount(large)); const int pieces=guards+covers.Count()+2; // actors, props and two holds; neither group is in this array const int callbacks=pieces+1; // local group has its own callback; HQ group is created by SpawnPost auto empty=[&](Build&b){assert(!b.m_BuildGroup&&!b.m_BuildLocalGroup&&!b.m_BuildHold&&!b.m_BuildLocalHold&&b.m_BuildSpawned.Count()==0&&b.m_Positions.Count()==0&&b.m_Groups.Count()==0&&b.m_LocalGroups.Count()==0&&b.m_Mode.owned==0&&b.m_Mode.groups==0&&b.m_Mode.ledger.live.empty());}; auto finish=[&](Build&b){for(int i=0;imembers.Count()==2&&local->members.Count()==2);assert(hq->members[0]==shared[0]&&hq->members[1]==shared[2]);assert(local->members[0]==shared[1]&&local->members[1]==shared[3]); assert(hold!=localHold&&hold->position==positions[0]&&localHold->position==positions[1]); for(int i=guards;ireplica.streaming); int commitCalls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==commitCalls); for(int i=guards;ireplica.streaming); assert(!b.m_BuildGroup&&!b.m_BuildLocalGroup&&b.m_Positions.Count()==1&&b.m_Groups.Count()==1&&b.m_LocalGroups.Count()==1&&b.m_Mode.owned==pieces+2&&b.m_Mode.groups==2&&b.m_Mode.hqGroups==1&&b.m_Mode.localGroups==1); assert(b.m_Mode.published[0]==hq&&b.m_Mode.published[1]==local&&b.LocalOnly(local)&&!b.LocalOnly(hq)&&!b.LocalOnly(nullptr)); assert(hq->holds[0]==hold&&local->holds[0]==localHold&&hq->activated==1&&local->activated==1); assert(b.m_Mining.registered==1&&b.m_Mining.group==hq&&b.m_Mining.hold==hold&&b.m_NextSearch==2100); assert(b.m_Mode.ledger.live.size()==2&&b.m_Mode.ledger.sources.count("roadblock-1")&&b.m_Mode.ledger.sources.count("roadblock-1-local")); assert(b.m_SmallPlaced==(large?0:1)&&b.m_LargePlaced==(large?1:0)); } // Unreplicated props cannot consume a quota or publish either guard pair. for(int bad=guards;badmissing=missing;spawned[bad]->replica.id=-1; b.BuildPost();empty(b);assert(ERR_Actors::deletes-deleted==pieces+2); for(int i=guards;ireplica.streaming); } // Player approach at every stage, including before local group creation and final commit. for(int stage=0;stage<=callbacks;stage++){Build b;b.m_Large=large;int deleted=ERR_Actors::deletes;assert(b.SpawnPost(positions,covers));for(int i=0;iacceptMember=false; b.BuildPost();empty(b);ERR_Actors::faction=true; } for(int stage=0;stage<=callbacks;stage++){Build b;b.m_Large=large;b.SpawnPost(positions,covers);for(int i=0;i