Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
252 lines
32 KiB
Diff
252 lines
32 KiB
Diff
--- 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<IEntity> attempted','Array<IEntity>* 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<vector> priorSites','Array<vector>* 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<IEntity> attempted)','ERR_EngineerMine(Array<IEntity>* attempted)')
|
|
s=s.replace('Faction* faction;','Faction* faction=nullptr;').replace('Array<AIActionBase>', 'Array<AIActionBase*>').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 <cassert>
|
|
#include <cmath>
|
|
@@ -36,10 +38,11 @@
|
|
#include <algorithm>
|
|
#include <functional>
|
|
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);}};
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::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<IEntity>* m_Attempted=nullptr;Array<IEntity> 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<IEntity>* m_Attempted=nullptr;Array<IEntity> m_IntelFolders;Array<vector> 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<IEntity> 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<vector> 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<vector> 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<vector> 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<vector> 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<vector> 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 <cassert>
|
|
-#include <vector>
|
|
-#include <cmath>
|
|
-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>{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<vector> priorSites = {};','Array<vector> 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<ERR_ForceRecord*> 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<int>;
|
|
+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<ERR_EngineerMine*>{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<ERR_HQUnit*> 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<vector> prior;AttemptArray& attempts;
|
|
+ ERR_EngineerMine(AttemptArray& a):attempts(a){} bool Begin(ERR_GameMode*,ERR_HQUnit*,ERR_HQTask*,vector,float,Array<vector> 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<vector> sites){record=mode->Forces()->FindGroup(unit->Group);prior=sites;if(unit->begin)attempts.Insert(1);return unit->begin;}
|
|
+struct HQ{Array<ERR_EngineerMine*> m_EngineerMineJobs;ERR_EngineerMinePosts m_EngineerMinePosts;bool m_Stopping=false;ERR_GameMode* m_Mode=nullptr;AttemptArray m_EngineerMineAttempts;Array<ERR_HQUnit*> 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 <cassert>
|
|
+#include <cmath>
|
|
+#include <string>
|
|
+#include <vector>
|
|
+#include <algorithm>
|
|
+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);}};
|
|
+template<class T>struct Array:std::vector<T>{using std::vector<T>::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<vector> sites','Array<vector>& 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<vector> 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<vector> 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')
|