--- a/tests/test_engineer_mine.py +++ b/tests/test_engineer_mine.py @@ -9,7 +9,9 @@ s=s.replace('protected override ','').replace('protected ','').replace('null','nullptr').replace('ref ','') s=s.replace('array<','Array<').replace(' = {};',';').replace('foreach (','for (') s=s.replace('array attempted','Array* attempted') - for typ in ['SCR_AIGroupUtilityComponent','ERR_GameMode','ERR_HQUnit','ERR_HQTask','SCR_ChimeraAIAgent','SCR_InventoryStorageManagerComponent','SCR_PressureTriggerComponent','InventoryItemComponent','DamageManagerComponent','FactionAffiliationComponent','Faction','Physics']: + s=s.replace('Array priorSites','Array* priorSites').replace('priorSites.','priorSites->').replace(': priorSites)',': *priorSites)') + s=s.replace(', m_PriorSites)',', &m_PriorSites)').replace('Math.Clamp','Math::Clamp') + for typ in ['ERR_ForceRecord','SCR_AIGroupUtilityComponent','ERR_GameMode','ERR_HQUnit','ERR_HQTask','SCR_ChimeraAIAgent','SCR_InventoryStorageManagerComponent','SCR_PressureTriggerComponent','InventoryItemComponent','DamageManagerComponent','FactionAffiliationComponent','Faction','Physics']: s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s) for ns in ['ERR_ForceRules','Replication','ERR_GameMode','ERR_HunterCombat','ERR_Actors','CompartmentAccessComponent','SCR_FactionManager','SCR_ChimeraAIAgent','SCR_AIUtils','ERR_EnemyLoadoutCatalog','ERR_Inventory','ERR_InfantryAmmoTransfer','InventoryItemComponent','DamageManagerComponent','FactionAffiliationComponent','ChimeraCharacter','Vehicle','SCR_InventoryStorageManagerComponent','SCR_PressureTriggerComponent','ERR_EngineerMineRules','ERR_HQ','vector']: s=s.replace(ns+'.',ns+'::') @@ -25,9 +27,9 @@ s=s.replace('ERR_EngineerMine(Array attempted)','ERR_EngineerMine(Array* attempted)') s=s.replace('Faction* faction;','Faction* faction=nullptr;').replace('Array', 'Array').replace('void ERR_EngineerMine(', 'ERR_EngineerMine(') return s -policies='\n'.join(body(P,x) for x in ['static bool Within(','static float PrepareSince(','static bool Prepared(','static bool MayArm(']) +policies='\n'.join(body(P,x) for x in ['static bool CorridorClear(','static bool WithinWorkArea(','static bool Within(','static float PrepareSince(','static bool Prepared(','static bool MayArm(']) callback='\n'.join(body(P,x) for x in ['protected void Finish(','protected override void OnComplete(','protected override void OnAllComplete(','protected override void OnFailed(','protected override void OnAllFailed(']) -methods='\n'.join(body(P,x) for x in ['void ERR_EngineerMine(','bool Finished(','protected bool Refuse(','protected bool Quiet(','protected bool Owned(','protected bool MineValid(','protected bool Carried(','protected bool Grounded(','protected bool Placed(','protected void ClearAction(','protected void Move(','protected void WaitAtMine(','protected bool CheckFriendly(','protected bool SelectActor(','bool Begin(','void Stop(','void Tick(']) +methods='\n'.join(body(P,x) for x in ['void ERR_EngineerMine(','bool Finished(','bool BlocksNextPost(','protected bool Refuse(','protected bool Quiet(','protected bool Owned(','protected bool MineValid(','protected bool Carried(','protected bool Grounded(','protected bool Placed(','protected void ClearAction(','protected void Move(','protected void WaitAtMine(','protected bool CheckFriendly(','protected bool SelectActor(','bool Begin(','void Stop(','void Tick(']) run(r''' #include #include @@ -36,10 +38,11 @@ #include #include struct string:std::string {using std::string::string;using std::string::operator=;bool IsEmpty(){return empty();}}; -struct vector {float x=0,y=0,z=0;float LengthSq(){return x*x+y*y+z*z;} +struct vector {float x=0,y=0,z=0;float operator[](int i){return i==0?x:i==1?y:z;}float LengthSq(){return x*x+y*y+z*z;} static float Distance(vector a,vector b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));} static float DistanceXZ(vector a,vector b){a.y=b.y=0;return Distance(a,b);}}; templatestruct Array:std::vector{using std::vector::vector;using std::vector::operator=;int Count(){return this->size();}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}void Insert(T t){this->push_back(t);}}; +namespace Math{float Clamp(float v,float a,float b){return std::max(a,std::min(v,b));}} struct Entity;using IEntity=Entity*;struct SCR_AIGroup;struct SCR_ChimeraAIAgent;struct SCR_InventoryStorageManagerComponent;struct ERR_EngineerMineRemoval; struct Faction {bool enemy=false;bool IsFactionEnemy(Faction* f){return f&&f->enemy;}}; struct Physics {vector velocity;vector GetVelocity(){return velocity;}}; @@ -101,7 +104,7 @@ '''+cpp(callback)+r'''}; bool SCR_InventoryStorageManagerComponent::TryRemoveItemFromInventory(IEntity,void*,ERR_EngineerMineRemoval* cb){calls++;callback=cb;if(inlineResult>0)cb->OnComplete();if(inlineResult<0)cb->OnFailed();return accepted;} struct ERR_EngineerMine { - ERR_GameMode* m_Mode=nullptr;ERR_HQUnit* m_Unit=nullptr;SCR_AIGroup* m_Group=nullptr;ERR_HQTask* m_Task=nullptr;ERR_HQReport* m_Assignment=nullptr;ERR_ForceRecord* m_Record=nullptr;string m_SourceId;int m_Strength=0;IEntity m_Actor=nullptr;int m_Slot=0;IEntity m_Mine=nullptr;SCR_PressureTriggerComponent* m_Trigger=nullptr;SCR_InventoryStorageManagerComponent* m_Inventory=nullptr;SCR_AIUtilityComponent* m_Utility=nullptr;SCR_AIBehaviorBase* m_Action=nullptr;ERR_EngineerMineRemoval* m_Removal=nullptr;Array* m_Attempted=nullptr;Array m_IntelFolders;vector m_Position,m_Rally;float m_Started=-1,m_LastTick=-1,m_PreparedAt=-1,m_NextClearance=0;int m_State=0,m_Checked=0;bool m_FriendlyNear=false,m_Begun=false,m_Stopped=false; + ERR_GameMode* m_Mode=nullptr;ERR_HQUnit* m_Unit=nullptr;SCR_AIGroup* m_Group=nullptr;ERR_HQTask* m_Task=nullptr;ERR_HQReport* m_Assignment=nullptr;ERR_ForceRecord* m_Record=nullptr;string m_SourceId;int m_Strength=0;IEntity m_Actor=nullptr;int m_Slot=0;IEntity m_Mine=nullptr;SCR_PressureTriggerComponent* m_Trigger=nullptr;SCR_InventoryStorageManagerComponent* m_Inventory=nullptr;SCR_AIUtilityComponent* m_Utility=nullptr;SCR_AIBehaviorBase* m_Action=nullptr;ERR_EngineerMineRemoval* m_Removal=nullptr;Array* m_Attempted=nullptr;Array m_IntelFolders;Array m_PriorSites;vector m_Position,m_Rally;float m_Started=-1,m_LastTick=-1,m_PreparedAt=-1,m_NextClearance=0;int m_State=0,m_Checked=0;bool m_FriendlyNear=false,m_Begun=false,m_Stopped=false; '''+cpp(methods)+r'''}; struct Fixture {Faction faction,hostile{true};SCR_AIGroup group;SCR_AIGroupUtilityComponent groupUtility;SCR_AIUtilityComponent utility;SCR_ChimeraAIAgent agent;SCR_InventoryStorageManagerComponent inventory;Entity actor,mine;Prefab prefab;Physics physics;InventoryItemComponent item;SCR_PressureTriggerComponent trigger;Sampler roster;ERR_HQTask task;ERR_HQReport report;ERR_ForceRecord record;Ledger ledger;ERR_GameMode mode;ERR_HQUnit unit;Array attempted;ERR_EngineerMine controller{&attempted}; Fixture(){ERR_GameMode::current=&mode;Replication::server=true;ERR_HunterCombat::engaged=false;currentGame=&game;game.world.query.clear();game.world.queries=0; @@ -114,6 +117,22 @@ void Withdraw(){Prepare();Drop();inventory.callback->OnComplete();controller.Tick(12001);assert(controller.m_State==3&&mine.moves==1&&utility.additions==3);} }; int main(){ + // History geometry uses bounded detached coordinates, not live mine entities. + Array hazards;hazards.Insert({20,0,20}); + assert(!ERR_EngineerMineRules::CorridorClear({0,0,0},{40,0,0},&hazards));hazards[0].z=20.001f; + assert(ERR_EngineerMineRules::CorridorClear({0,0,0},{40,0,0},&hazards)); + hazards[0]={20,0,0};assert(!ERR_EngineerMineRules::CorridorClear({0,0,0},{0,0,0},&hazards)); + assert(ERR_EngineerMineRules::WithinWorkArea({0,0,0},{60,100,0}));assert(!ERR_EngineerMineRules::WithinWorkArea({0,0,0},{60.001f,0,0})); + for(int invalid=0;invalid<3;invalid++){Fixture f;Array prior;if(invalid==0)prior.Insert({20,0,5});if(invalid==1)prior.resize(33);if(invalid==2)prior.Insert({NAN,0,0});f.actor.position={-20,0,0};assert(!f.controller.Begin(&f.mode,&f.unit,&f.task,{60,0,0},1000,&prior));assert(f.attempted.empty()&&!f.utility.additions&&!f.inventory.calls);} + {Fixture f;Array prior;prior.Insert({100,0,0});assert(f.controller.Begin(&f.mode,&f.unit,&f.task,{30,0,0},1000,&prior));prior[0]={30,0,0};f.controller.Tick(2000);assert(!f.controller.Finished()&&f.controller.m_PriorSites[0].x==100);f.actor.position={60,0,0};f.controller.Tick(3000);assert(!f.controller.Finished());auto action=f.controller.m_Action;f.actor.position={60.001f,0,0};f.controller.Tick(4000);assert(f.controller.Finished()&&action->completed==1&&!f.inventory.calls);} + {Fixture f;Array prior;prior.Insert({50,0,30});assert(f.controller.Begin(&f.mode,&f.unit,&f.task,{30,0,0},1000,&prior));f.actor.position={50,0,11};f.controller.Tick(2000);assert(f.controller.Finished()&&!f.inventory.calls&&!f.trigger.calls);} + // Active job blocks its exact original record. Stopped pending native removal + // remains blocking even if its surviving item is destroyed; callbacks only settle. + {Fixture f;ERR_ForceRecord other;f.Begin();assert(f.controller.BlocksNextPost(&f.record)&&!f.controller.BlocksNextPost(&other)&&!f.controller.BlocksNextPost(nullptr));f.controller.Stop();assert(!f.controller.BlocksNextPost(&f.record));} + for(int completion=0;completion<3;completion++){Fixture f;f.Prepare();f.controller.Stop();auto callback=f.inventory.callback;assert(f.controller.BlocksNextPost(&f.record));f.mine.alive=false;assert(f.controller.BlocksNextPost(&f.record));if(completion==0)callback->OnComplete();if(completion==1)callback->OnFailed();if(completion==2)f.controller.m_Mine=nullptr;assert(!f.controller.BlocksNextPost(&f.record));f.Drop();callback->OnComplete();f.controller.Tick(14000);assert(f.inventory.calls==1&&!f.mine.moves&&!f.trigger.calls&&f.attempted.Count()==1);} + // An exact different physical mine can be used at a distinct post, while the + // first exact entity remains in the shared lifetime tombstone. + {Fixture f;f.Begin();f.controller.Stop();Entity second=f.mine;SCR_PressureTriggerComponent secondTrigger;InventoryItemComponent secondItem;second.trigger=&secondTrigger;second.item=&secondItem;secondItem.slot=&f.inventory;f.inventory.items={&f.mine,&second};ERR_HQTask next;ERR_HQReport nextReport;next.Destination={200,0,0};f.unit.Task=&next;f.unit.Assignment=&nextReport;f.actor.position={200,0,0};f.group.center={200,0,0};Array prior;prior.Insert({30,0,0});ERR_EngineerMine job(&f.attempted);assert(job.Begin(&f.mode,&f.unit,&next,{230,0,0},2000,&prior));assert(job.m_Mine==&second&&f.attempted.Count()==2&&f.attempted[0]==&f.mine);} // Current native defend radius is strict and three dimensional: the broad // 25..60 m mission band does not authorize bypassing the group's own envelope. for(int fault=0;fault<4;fault++){Fixture f;SCR_DefendWaypoint defend;f.group.waypoint=&defend;vector point={30,0,0}; --- a/tests/test_engineer_mine_hq.py +++ b/tests/test_engineer_mine_hq.py @@ -1,80 +1,56 @@ -"""Execute actual HQ engineer admission/maintenance; native movement is separate.""" +"""Actual HQ admission with actual finite post ledger; native jobs are boundary doubles.""" from pathlib import Path import os,re from policy_harness import ROOT,body,run +from test_engineer_mine_posts import BOUNDARY,ledger_cpp p=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c')) s=body(p,'protected void MaintainEngineerMines(') -s=s.replace('protected ','').replace('int active;','int active=0;').replace('vector position;','vector position=0;') -s=s.replace('foreach (ERR_HQUnit unit : m_Units)','for (ERR_HQUnit* unit : m_Units)').replace('foreach (ERR_EngineerMine job : m_EngineerMineJobs)','for (ERR_EngineerMine* job : m_EngineerMineJobs)') -for name in ('Replication','vector','ERR_EngineerMineSite','ERR_GameMode'):s=s.replace(name+'.',name+'::') -for name in ('unit','m_Mode','job'):s=s.replace(name+'.',name+'->') -s=s.replace('->EngineerMine.','->EngineerMine->').replace('->Task.','->Task->').replace('->Group.','->Group->') -s=s.replace('vector::DistanceXZ','DistanceXZ') -run(r''' -#include -#include -#include -using vector=float; -float DistanceXZ(float a,float b){return std::fabs(a-b);} -struct Replication{static bool server;static bool IsServer(){return server;}};bool Replication::server=true; -struct ERR_GameMode{static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}bool active=true;bool Active(){return active;}};ERR_GameMode* ERR_GameMode::current=nullptr; -struct Task{bool RoadIntercept=true;float ExpiresAt=100000,Destination=0;}; -struct Group{float origin=0;float GetOrigin(){return origin;}}; -struct AttemptArray:std::vector{int Count(){return size();}}; +s=s.replace('protected ','').replace('int active;','int active=0;').replace('bool blocked;','bool blocked=false;').replace('vector position;','vector position;') +s=s.replace('array priorSites = {};','Array priorSites;') +for typ in ['ERR_HQUnit','ERR_EngineerMine','ERR_ForceRecord']:s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s) +s=s.replace('foreach (','for (') +for name in ('ERR_ForceRules','Replication','vector','ERR_EngineerMineSite','ERR_GameMode'):s=s.replace(name+'.',name+'::') +for name in ('unit','m_Mode','job','previous','record'):s=re.sub(r'\b'+name+r'\.',name+'->',s) +for name in ['EngineerMine','Task','Group','StatusSampler']:s=s.replace('->'+name+'.','->'+name+'->') +s=s.replace('Forces().','Forces()->') +run(BOUNDARY+ledger_cpp()+r''' +struct Ledger{Array records;ERR_ForceRecord* FindGroup(SCR_AIGroup* group){for(auto r:records)if(r->Group==group)return r;return nullptr;}}; +struct Replication{static inline bool server=true;static bool IsServer(){return server;}}; +struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;static ERR_GameMode* Get(){return current;}bool active=true;Ledger ledger;bool Active(){return active;}Ledger* Forces(){return &ledger;}}; +using AttemptArray=Array; +struct Sampler{bool owns=true;int strength=2;bool OwnsRoster(SCR_AIGroup*){return owns;}int InitialStrength(){return strength;}}; struct ERR_HQUnit; -struct ERR_EngineerMine { - int ticks=0,stops=0;bool finished=false; - AttemptArray& attempts; - ERR_EngineerMine(AttemptArray& a):attempts(a){} - bool Begin(ERR_GameMode*,ERR_HQUnit*,Task*,float,float); - void Tick(float){ticks++;}bool Finished(){return finished;}void Stop(){stops++;finished=true;} -}; -struct ERR_HQUnit{ - ERR_EngineerMine* EngineerMine=nullptr;bool EngineerMineAttempted=false; - struct Task* Task=nullptr;void* Assignment=nullptr;void* PendingTask=nullptr;void* PendingOrder=nullptr;void* Downlink=nullptr; - struct Group* Group=nullptr;bool eligible=true,fighting=false,begin=true; - bool Eligible(ERR_GameMode*){return eligible;} -}; -bool ERR_EngineerMine::Begin(ERR_GameMode*,ERR_HQUnit* unit,struct Task*,float,float){attempts.push_back(1);return unit->begin;} -struct ERR_EngineerMineSite{static bool valid;static int calls;static bool Select(float,float& p){calls++;p=30;return valid;}}; -bool ERR_EngineerMineSite::valid=true;int ERR_EngineerMineSite::calls=0; -struct Jobs:std::vector{int Count(){return size();}void Insert(ERR_EngineerMine* j){push_back(j);}}; -struct HQ{ - Jobs m_EngineerMineJobs; - bool m_Stopping=false;ERR_GameMode* m_Mode=nullptr;AttemptArray m_EngineerMineAttempts;std::vector m_Units; - bool Fighting(ERR_HQUnit* unit){return unit->fighting;} -'''+s+r''' -}; -struct Fixture{ - ERR_GameMode mode;HQ h;Task tasks[4];Group groups[4];ERR_HQUnit units[4];int report; - Fixture(){ERR_GameMode::current=&mode;h.m_Mode=&mode;for(int i=0;i<4;i++){units[i].Task=&tasks[i];units[i].Group=&groups[i];units[i].Assignment=&report;h.m_Units.push_back(&units[i]);}} +struct ERR_EngineerMine{int ticks=0,stops=0;bool finished=false,unresolved=false;ERR_ForceRecord* record=nullptr;Array prior;AttemptArray& attempts; + ERR_EngineerMine(AttemptArray& a):attempts(a){} bool Begin(ERR_GameMode*,ERR_HQUnit*,ERR_HQTask*,vector,float,Array sites={}); + void Tick(float){ticks++;}bool Finished(){return finished;}void Stop(){stops++;finished=true;}bool BlocksNextPost(ERR_ForceRecord* r){return r==record&&(!finished||unresolved);}}; +struct ERR_HQUnit{ERR_EngineerMine* EngineerMine=nullptr;bool EngineerMineAttempted=false;ERR_HQTask* Task=nullptr;ERR_HQReport* Assignment=nullptr;void* PendingTask=nullptr;void* PendingOrder=nullptr;void* Downlink=nullptr;SCR_AIGroup* Group=nullptr;Sampler* StatusSampler=nullptr;string ForceSourceId;bool eligible=true,fighting=false,begin=true;bool Eligible(ERR_GameMode*){return eligible;}}; +bool ERR_EngineerMine::Begin(ERR_GameMode* mode,ERR_HQUnit*unit,ERR_HQTask*,vector,float,Array sites){record=mode->Forces()->FindGroup(unit->Group);prior=sites;if(unit->begin)attempts.Insert(1);return unit->begin;} +struct HQ{Array m_EngineerMineJobs;ERR_EngineerMinePosts m_EngineerMinePosts;bool m_Stopping=false;ERR_GameMode* m_Mode=nullptr;AttemptArray m_EngineerMineAttempts;Array m_Units;bool Fighting(ERR_HQUnit*u){return u->fighting;} +'''+s+r'''}; +struct Fixture{ERR_GameMode mode;HQ h;ERR_HQTask tasks[40];ERR_HQReport reports[40];SCR_AIGroup groups[4];ERR_ForceRecord records[4];Sampler samplers[4];ERR_HQUnit units[4]; + Fixture(){ERR_GameMode::current=&mode;h.m_Mode=&mode;ERR_EngineerMineSite::valid=true;for(int i=0;i<40;i++){tasks[i].Destination={float(i*1000),0,0};tasks[i].QueuedAt=10+i*10;tasks[i].BeliefRevision=i+1;reports[i].ObservedAt=5+i*10;reports[i].MessageId=i+1;}for(int i=0;i<4;i++){records[i].Group=&groups[i];records[i].SourceId=std::to_string(i);units[i].ForceSourceId=records[i].SourceId;units[i].Group=&groups[i];units[i].StatusSampler=&samplers[i];mode.ledger.records.Insert(&records[i]);NewPost(i,i);h.m_Units.Insert(&units[i]);}} + void NewPost(int u,int n){units[u].Task=&tasks[n];units[u].Assignment=&reports[n];groups[u].position=tasks[n].Destination;} + void Tick(){h.MaintainEngineerMines(1000);} ~Fixture(){for(auto j:h.m_EngineerMineJobs)delete j;} }; int main(){ - {Fixture f;f.h.MaintainEngineerMines(1);assert(f.h.m_EngineerMineAttempts.Count()==1);assert(!f.units[1].EngineerMineAttempted); - f.h.MaintainEngineerMines(2);assert(f.units[0].EngineerMine->ticks==1&&f.h.m_EngineerMineAttempts.Count()==2); - f.h.MaintainEngineerMines(3);assert(!f.units[2].EngineerMineAttempted); - f.units[0].EngineerMine->Stop();f.h.MaintainEngineerMines(4);assert(f.units[2].EngineerMineAttempted&&!f.units[3].EngineerMineAttempted);assert(f.h.m_EngineerMineAttempts.Count()==3); - f.units[1].EngineerMine->Stop();f.units[2].EngineerMine->Stop();f.h.MaintainEngineerMines(5);f.units[3].EngineerMine->Stop();f.h.MaintainEngineerMines(6);assert(f.h.m_EngineerMineAttempts.Count()==4);} - {Fixture f;f.h.MaintainEngineerMines(1);auto job=f.units[0].EngineerMine;job->Stop();f.h.m_Units.clear();f.h.MaintainEngineerMines(2);assert(f.h.m_EngineerMineJobs.Count()==1&&job->ticks==1);} - {Fixture f;for(int i=0;i<32;i++){auto job=new ERR_EngineerMine(f.h.m_EngineerMineAttempts);job->Stop();f.h.m_EngineerMineJobs.Insert(job);}int before=ERR_EngineerMineSite::calls;f.h.MaintainEngineerMines(1);assert(!f.units[0].EngineerMineAttempted&&ERR_EngineerMineSite::calls==before);} - {Fixture f;for(auto& u:f.units)u.begin=false;int before=ERR_EngineerMineSite::calls;f.h.MaintainEngineerMines(1);assert(ERR_EngineerMineSite::calls==before+1&&!f.units[1].EngineerMineAttempted);f.h.MaintainEngineerMines(2);assert(ERR_EngineerMineSite::calls==before+2&&f.units[0].EngineerMine->ticks==1);} - {Fixture f;ERR_EngineerMineSite::valid=false;int before=ERR_EngineerMineSite::calls;f.h.MaintainEngineerMines(1);assert(ERR_EngineerMineSite::calls==before+1&&!f.units[1].EngineerMineAttempted);f.h.MaintainEngineerMines(2);assert(ERR_EngineerMineSite::calls==before+2);ERR_EngineerMineSite::valid=true;} - for(int invalid=0;invalid<9;invalid++) {Fixture f;f.h.m_Units.resize(1);auto& u=f.units[0]; - if(invalid==0){u.Assignment=nullptr;}if(invalid==1){u.Task=nullptr;}if(invalid==2){f.tasks[0].RoadIntercept=false;} - if(invalid==3){u.PendingTask=&f.report;}if(invalid==4){u.PendingOrder=&f.report;}if(invalid==5){u.Downlink=&f.report;} - if(invalid==6){u.eligible=false;}if(invalid==7){u.fighting=true;}if(invalid==8){f.groups[0].origin=21;} - int before=ERR_EngineerMineSite::calls;f.h.MaintainEngineerMines(1);assert(!u.EngineerMineAttempted&&!u.EngineerMine&&ERR_EngineerMineSite::calls==before);} - {Fixture f;f.h.m_Units.resize(1);f.tasks[0].ExpiresAt=1;f.h.MaintainEngineerMines(1);assert(!f.units[0].EngineerMineAttempted);} - {Fixture f;f.h.m_Units.resize(1);ERR_EngineerMineSite::valid=false;int before=ERR_EngineerMineSite::calls;f.h.MaintainEngineerMines(1);f.h.MaintainEngineerMines(2);assert(f.units[0].EngineerMineAttempted&&!f.units[0].EngineerMine&&ERR_EngineerMineSite::calls==before+1);ERR_EngineerMineSite::valid=true;} - {Fixture f;f.h.m_Units.resize(1);f.units[0].begin=false;f.h.MaintainEngineerMines(1);f.h.MaintainEngineerMines(2);assert(f.units[0].EngineerMine->stops==1&&f.h.m_EngineerMineAttempts.Count()==1);} - {Fixture f;f.h.MaintainEngineerMines(1);f.h.m_EngineerMineAttempts.resize(256);f.h.MaintainEngineerMines(2);assert(f.units[0].EngineerMine->ticks==1&&!f.units[2].EngineerMineAttempted);} - {Fixture f;Replication::server=false;f.h.MaintainEngineerMines(1);Replication::server=true;f.h.m_Stopping=true;f.h.MaintainEngineerMines(1);f.h.m_Stopping=false;f.mode.active=false;f.h.MaintainEngineerMines(1);f.mode.active=true;ERR_GameMode::current=nullptr;f.h.MaintainEngineerMines(1);assert(f.h.m_EngineerMineAttempts.empty());} + // Genuine second delivered post succeeds; no timer retry or replenishment at old post. + {Fixture f;f.h.m_Units.resize(1);f.Tick();auto first=f.units[0].EngineerMine;first->Stop();f.Tick();assert(f.h.m_EngineerMineJobs.Count()==1);f.NewPost(0,4);f.Tick();assert(f.h.m_EngineerMineJobs.Count()==2&&f.h.m_EngineerMineAttempts.Count()==2);assert(f.units[0].EngineerMine!=first&&f.units[0].EngineerMine->prior.size()==1&&f.units[0].EngineerMine->prior[0].x==30);} + {Fixture f;f.Tick();f.Tick();assert(f.h.m_EngineerMineJobs.Count()==2);int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls);f.units[0].EngineerMine->Stop();f.Tick();assert(f.h.m_EngineerMineJobs.Count()==3);} + // Settled old controller retained even after unit pointer disappears; pending receipt blocks exact original force. + {Fixture f;f.h.m_Units.resize(1);f.Tick();auto first=f.units[0].EngineerMine;first->Stop();first->unresolved=true;f.NewPost(0,4);f.units[0].EngineerMine=nullptr;int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&first->ticks==1);first->unresolved=false;f.Tick();assert(f.h.m_EngineerMineJobs.Count()==2);f.h.m_Units.Clear();f.Tick();assert(first->ticks==3);} + // Four consumed attempts survive current unit replacement. No-item Begin failures are still consumed. + {Fixture f;f.h.m_Units.resize(1);f.units[0].begin=false;for(int i=0;i<5;i++){f.NewPost(0,i*4);int before=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==before+(i<4));}assert(f.h.m_EngineerMinePosts.Count()==4&&f.h.m_EngineerMineJobs.Count()==4&&f.h.m_EngineerMineAttempts.empty());ERR_HQUnit replacement=f.units[0];f.h.m_Units[0]=&replacement;replacement.Task=&f.tasks[24];replacement.Assignment=&f.reports[24];f.groups[0].position=f.tasks[24].Destination;f.Tick();assert(f.h.m_EngineerMinePosts.Count()==4);} + {Fixture f;ERR_EngineerMineSite::valid=false;int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls+1&&f.h.m_EngineerMinePosts.Count()==1&&f.h.m_EngineerMineJobs.empty());f.Tick();assert(ERR_EngineerMineSite::calls==calls+2&&f.h.m_EngineerMinePosts.Count()==2);} + {Fixture f;f.h.m_Units.resize(1);ERR_EngineerMineSite::valid=false;f.Tick();int calls=ERR_EngineerMineSite::calls;ERR_EngineerMineSite::valid=true;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&f.h.m_EngineerMinePosts.Count()==1);} + {Fixture f;for(int i=0;i<32;i++){auto j=new ERR_EngineerMine(f.h.m_EngineerMineAttempts);j->Stop();f.h.m_EngineerMineJobs.Insert(j);}int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls);} + {Fixture f;f.Tick();f.h.m_EngineerMineAttempts.resize(256);int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&f.units[0].EngineerMine->ticks==1);} + for(int fault=0;fault<12;fault++){Fixture f;f.h.m_Units.resize(1);auto&u=f.units[0];if(fault==0)u.Assignment=nullptr;if(fault==1)u.Task=nullptr;if(fault==2)f.tasks[0].RoadIntercept=false;if(fault==3)u.PendingTask=&f;if(fault==4)u.PendingOrder=&f;if(fault==5)u.Downlink=&f;if(fault==6)u.eligible=false;if(fault==7)u.fighting=true;if(fault==8)f.groups[0].position.x=21;if(fault==9)f.samplers[0].owns=false;if(fault==10)f.samplers[0].strength=1;if(fault==11)f.tasks[0].ExpiresAt=1000;int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&f.h.m_EngineerMinePosts.Count()==0&&!u.EngineerMine);} + {Fixture f;Replication::server=false;f.Tick();Replication::server=true;f.h.m_Stopping=true;f.Tick();f.h.m_Stopping=false;f.mode.active=false;f.Tick();f.mode.active=true;ERR_GameMode::current=nullptr;f.Tick();assert(f.h.m_EngineerMinePosts.Count()==0);} } -'''.replace('Task* Task=', 'struct Task* Task=').replace('Group* Group=', 'struct Group* Group=')) -source=p.read_text() +''') assert 'if (EngineerMine) EngineerMine.Stop();' in body(p,'void ReleaseAssignment(') assert 'MaintainEngineerMines(now);' in body(p,'override void Tick(') assert 'm_EngineerMineAttempts.Clear();' in body(p,'override void Stop(') assert 'foreach (ERR_EngineerMine job : m_EngineerMineJobs) job.Stop();' in body(p,'override void Stop(') -print('PASS: actual HQ mine admission, one site query per tick, maximum two active/32 retained jobs, delivered-only original post, once-per-unit refusal latch, 256-item ceiling and maintenance/teardown ownership') +print('PASS: actual HQ and post ledger: second delivered post, one query/tick,2 active/32 jobs,4 consumed lifetime attempts, no-stock/query refusal consumption, pending old job and original identity constraints') --- /dev/null +++ b/tests/test_engineer_mine_posts.py @@ -0,0 +1,53 @@ +"""Actual finite post ledger; engine identity/vector boundaries are doubles.""" +import os,re +from pathlib import Path +from policy_harness import ROOT,run +P=Path(os.environ.get('ERR_ENGINEER_MINE_POSTS_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMinePosts.c')) +BOUNDARY=r''' +#include +#include +#include +#include +#include +struct string:std::string{using std::string::string;using std::string::operator=;string(std::string s):std::string(s){} bool IsEmpty(){return empty();}}; +struct vector{float x=0,y=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}}; +templatestruct Array:std::vector{using std::vector::vector;int Count(){return this->size();}void Insert(T p){this->push_back(p);}void Clear(){this->clear();}}; +struct SCR_AIGroup{vector position;vector GetOrigin(){return position;}}; +struct ERR_ForceRecord{SCR_AIGroup* Group=nullptr;bool Physical=true,Eliminated=false;string SourceId="s";int InitialStrength=2;}; +struct ERR_HQTask{bool RoadIntercept=true,ReleaseOnly=false,investigation=false;string SecuritySite;int BeliefId=1,BeliefRevision=1;vector Destination;float QueuedAt=10,ExpiresAt=100000;bool Investigation(){return investigation;}}; +struct ERR_HQReport{int MessageId=1;float ObservedAt=5;vector Position;}; +namespace ERR_ForceRules{bool Finite(float f){return std::isfinite(f);}bool FiniteVector(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}} +struct ERR_EngineerMineSite{static inline bool valid=true;static inline int calls=0;static bool InBand(vector a,vector b){float d=vector::DistanceXZ(a,b);return d>=25&&d<=60;}static bool Select(vector post,vector&site){calls++;site={post.x+30,post.y,post.z};return valid;}}; +''' +def ledger_cpp(path=P): + s=path.read_text().replace('class ','struct ').replace('protected ','').replace('ref ','').replace('out array sites','Array& sites').replace('array<','Array<').replace(' = {};',';') + for typ in ['ERR_EngineerMinePost','ERR_ForceRecord','SCR_AIGroup','ERR_HQTask','ERR_HQReport']: + s=re.sub(r'\b'+typ+r'(?=\s+\w)',typ+'*',s);s=s.replace('Array<'+typ+'>','Array<'+typ+'*>') + for ns in ['ERR_ForceRules','vector','ERR_EngineerMineSite']:s=s.replace(ns+'.',ns+'::') + for obj in ['record','task','assignment','previous','entry']:s=re.sub(r'\b'+obj+r'\.',obj+'->',s) + s=s.replace('foreach (','for (').replace('int used;','int used=0;') + # Enforce initializes scalar fields to zero; C++ aggregate members must match. + s=re.sub(r'^( (?:bool|int|float|ERR_ForceRecord\*|SCR_AIGroup\*|ERR_HQTask\*|ERR_HQReport\*) \w+);$',r'\1{};',s,flags=re.M) + s=s.replace('\n}\n','\n};\n') + return s +TEST=r''' +struct Fixture{SCR_AIGroup group;ERR_ForceRecord record;ERR_HQTask tasks[40];ERR_HQReport reports[40];ERR_EngineerMinePosts posts; + Fixture(){record.Group=&group;for(int i=0;i<40;i++){tasks[i].Destination={float(i*200),0,0};tasks[i].QueuedAt=10+i*10;tasks[i].BeliefRevision=1+i;reports[i].ObservedAt=5+i*10;reports[i].MessageId=i+1;}} + int Take(int i){return posts.Consume(&record,"s",&tasks[i],&reports[i],1000);}}; +int main(){ + {Fixture f;assert(f.Take(0)==0);assert(f.Take(1)==1);assert(f.Take(2)==2);assert(f.Take(3)==3);assert(f.Take(4)==-1&&f.posts.Count()==4);} + {Fixture f;assert(f.Take(0)==0);assert(f.Take(0)==-1);f.tasks[0].Destination={900,0,0};f.tasks[0].QueuedAt=900;f.reports[0].ObservedAt=800;f.reports[0].MessageId=999;f.tasks[0].BeliefRevision=999; + assert(f.Take(1)==1);assert(f.posts.RecordSite(0,{30,0,0}));assert(!f.posts.RecordSite(0,{31,0,0}));Array sites;f.posts.PriorSites(sites);assert(sites.size()==1&&sites[0].x==30);sites[0].x=900;f.posts.PriorSites(sites);assert(sites[0].x==30);} + for(int fault=0;fault<8;fault++){Fixture f;f.Take(0);if(fault==0)f.tasks[1].Destination={160,0,0};if(fault==1)f.tasks[1].QueuedAt=10;if(fault==2)f.reports[1].ObservedAt=5;if(fault==3)f.reports[1].MessageId=1;if(fault==4)f.tasks[1].BeliefRevision=1;if(fault==5)f.tasks[1].BeliefRevision=0;if(fault==6)f.tasks[1].ExpiresAt=1000;if(fault==7)f.tasks[1].Destination.x=NAN;assert(f.Take(1)==-1&&f.posts.Count()==1);} + {Fixture f;f.Take(0);f.tasks[1].Destination.x=160.001f;assert(f.Take(1)==1);} + {Fixture f;f.Take(0);f.tasks[1].BeliefId=2;f.tasks[1].BeliefRevision=1;assert(f.Take(1)==1);} + {Fixture f;f.Take(0);ERR_ForceRecord alias=f.record;assert(f.posts.Consume(&alias,"s",&f.tasks[1],&f.reports[1],1000)==-1);alias.SourceId="alias";assert(f.posts.Consume(&alias,"alias",&f.tasks[1],&f.reports[1],1000)==-1);f.record.SourceId="changed";assert(f.posts.Consume(&f.record,"changed",&f.tasks[1],&f.reports[1],1000)==-1);} + {Fixture f;f.Take(0);SCR_AIGroup replacement;f.record.Group=&replacement;assert(f.Take(1)==-1);f.record.Group=&f.group;f.record.InitialStrength=3;assert(f.Take(1)==-1);} + {Fixture f;assert(!f.posts.RecordSite(-1,{30,0,0}));assert(f.Take(0)==0);assert(!f.posts.RecordSite(0,{24,0,0}));assert(!f.posts.RecordSite(0,{61,0,0}));assert(!f.posts.RecordSite(0,{30,NAN,0}));Array empty;f.posts.PriorSites(empty);assert(empty.empty());assert(f.posts.RecordSite(0,{25,0,0}));} + {Fixture f;ERR_ForceRecord records[33];SCR_AIGroup groups[33];for(int i=0;i<33;i++){records[i].Group=&groups[i];records[i].SourceId=std::to_string(i);int result=f.posts.Consume(&records[i],records[i].SourceId,&f.tasks[i],&f.reports[i],1000);assert(result==(i<32?i:-1));}assert(f.posts.Count()==32);} + for(int fault=0;fault<10;fault++){Fixture f;if(fault==0)f.record.Physical=false;if(fault==1)f.record.Eliminated=true;if(fault==2)f.record.InitialStrength=33;if(fault==3)f.record.Group=nullptr;if(fault==4)f.tasks[0].ReleaseOnly=true;if(fault==5)f.tasks[0].investigation=true;if(fault==6)f.tasks[0].SecuritySite="site";if(fault==7)f.tasks[0].QueuedAt=1001;if(fault==8)f.reports[0].ObservedAt=11;if(fault==9)f.reports[0].Position.x=NAN;assert(f.Take(0)==-1&&f.posts.Count()==0);} +} +''' +if __name__=='__main__': + run(BOUNDARY+ledger_cpp()+TEST) + print('PASS: actual mine post ledger immutable history, strict chronology/evidence, aliases, >160m separation, detached sites and finite4/32 consumed attempts')