Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
109 lines
8.3 KiB
Python
109 lines
8.3 KiB
Python
"""Execute recovery spawn retries and retirement using production method bodies."""
|
|
from policy_harness import ROOT, body, run
|
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_BodyRecovery.c'
|
|
def adapt(s):
|
|
s=s.replace('Math.Max', 'std::max').replace('protected ', '').replace('ERR_BodyRecoveryJob job','Job* job').replace('AIAgent agent','Agent* agent')
|
|
s=s.replace('ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());','World* world = GetGame()->GetAIWorld();')
|
|
s=s.replace('job.Team.GetOnAgentAdded().Remove(job.MemberAdded);','job.Team.GetOnAgentAdded()->Remove();')
|
|
s=s.replace('new array<ResourceName>()','Slots{0}').replace('array<AIAgent> agents = {};','Array<Agent*> agents;')
|
|
s=s.replace('foreach (','for (').replace('null','nullptr')
|
|
s=s.replace('ERR_HQ.Event','ERR_HQ::Event').replace('string.Format','string::Format')
|
|
for v in ('job','agent','world','m_HQ','m_Mode'):
|
|
s=s.replace(v+'.',v+'->')
|
|
s=s.replace('Team.','Team->').replace('Evidence().','Evidence()->')
|
|
return s
|
|
added=adapt(body(p,'void MemberAdded('))
|
|
retry=adapt(body(p,'protected void RetryEmptySpawn('))
|
|
refund=adapt(body(p,'protected void ReturnUnusedWave('))
|
|
preparation=adapt(body(p,'protected bool RetryPreparation('))
|
|
retire=adapt(body(p,'protected void RetireSpawn('))
|
|
finish=adapt(body(p,'protected void Finish('))
|
|
pins=adapt(body(p,'protected void ReleasePins('))
|
|
diagnostic=adapt(body(p,'protected void Diagnostic(')).split(' int agents, living;')[0]
|
|
diagnostic += ' diagnostics++;\n }'
|
|
reuse=adapt(body(p,'protected bool Reusable(')).replace('ERR_BodyRecoveryJob previous','Job* previous').replace('previous.', 'previous->')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <vector>
|
|
struct string:std::string{using std::string::string;template<class...T>static string Format(string fmt,T...){return fmt;}};
|
|
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}};
|
|
struct Agent{int lod=-1,activations=0;void SetPermanentLOD(int v){lod=v;}void ActivateAI(){activations++;}};
|
|
struct Slots{int size=4;int Count(){return size;}};
|
|
struct Hook{int removals=0;void Remove(){removals++;}};
|
|
struct Group{int count=0,lod=0,requests=0,wanted=0;bool immediately=true;int players=0,faction=1,living=4;int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}Slots m_aUnitPrefabSlots;Hook hook;Array<Agent*> agents;
|
|
int GetAgentsCount(){return count;}void RequestSpawn(int n){requests++;wanted=n;}void SetSpawnImmediately(bool b){immediately=b;}
|
|
Hook* GetOnAgentAdded(){return &hook;}void SetPermanentLOD(int n){lod=n;}void GetAgents(Array<Agent*>& out){out=agents;}};
|
|
struct World{int purges=0;Group* last=nullptr;void PurgeSpawnRequestsForGroup(Group*g){purges++;last=g;}};
|
|
struct Game{World world;bool ai=true;World* GetAIWorld(){return ai?&world:nullptr;}} game;
|
|
bool gameAvailable=true;Game* GetGame(){return gameAvailable?&game:nullptr;}
|
|
struct Job{int Id=1,State=0,SpawnAttempts=1;float NextSpawnAttempt=10000;bool SpawnedMember=false,Delivered=false,Prepared=false,SpawnWave=false,HadTeam=false;int PreparationRetries=0;float RetryAt=0;float NextStatusDiagnostic=0,NextDiagnostic=0;string LastDiagnosticReason;Group* Team=nullptr;Array<string> Missing;int Position=99,Body=7;void* Order=nullptr;void* Report=nullptr;
|
|
'''+added+r'''
|
|
};
|
|
struct Evidence{int delivered=0;void Delivered(int){delivered++;}};
|
|
struct Mode{int EnemyFaction(){return 1;}int ForceLiving(Group*g){return g->living;}struct Evidence evidence;struct Evidence* Evidence(){return &evidence;}};
|
|
struct ERR_HQ{int compromised=0,released=0;static void Event(string,string){}void CompromisedSites(Array<string>,int){compromised++;}void ReleaseRecovery(Group*){released++;}};
|
|
struct Recovery{Mode* m_Mode;ERR_HQ* m_HQ;int removed=0,diagnostics=0,m_Waves=0,m_SpawnRequests=0;void RemoveWaypoint(Job*){removed++;}
|
|
'''+retry+retire+pins+refund+preparation+finish+reuse+diagnostic+r'''
|
|
};
|
|
int main(){
|
|
Mode mode;ERR_HQ hq;Recovery r{&mode,&hq};Group team;Job job;job.Team=&team;
|
|
r.RetryEmptySpawn(&job,9999);assert(team.requests==0&&job.SpawnAttempts==1);
|
|
r.RetryEmptySpawn(&job,10000);assert(team.requests==1&&team.wanted==4&&job.SpawnAttempts==2&&game.world.last==&team);
|
|
r.RetryEmptySpawn(&job,19999);assert(team.requests==1);
|
|
r.RetryEmptySpawn(&job,20000);assert(team.requests==2&&job.SpawnAttempts==3);
|
|
r.RetryEmptySpawn(&job,90000);assert(team.requests==2&&game.world.purges==2);
|
|
// Unchanged waits are rate limited; changed causes are visible immediately.
|
|
Job status;r.Diagnostic(&status,0,"roster");assert(r.diagnostics==1);
|
|
r.Diagnostic(&status,29999,"roster");assert(r.diagnostics==1);
|
|
r.Diagnostic(&status,29999,"radio");assert(r.diagnostics==2);
|
|
status.NextDiagnostic=1000000; // movement logs have a separate throttle
|
|
r.Diagnostic(&status,59999,"radio");assert(r.diagnostics==3);
|
|
// Never replenish a soldier that spawned and disappeared before the next poll.
|
|
Job lost;lost.Team=&team;Agent original;lost.MemberAdded(&original);assert(lost.SpawnedMember&&original.lod==0&&original.activations==1);
|
|
r.RetryEmptySpawn(&lost,90000);assert(team.requests==2);
|
|
Job partial;partial.Team=&team;team.count=1;r.RetryEmptySpawn(&partial,90000);assert(team.requests==2);team.count=0;
|
|
Job callback;callback.Team=&team;callback.MemberAdded(nullptr);r.RetryEmptySpawn(&callback,90000);assert(team.requests==2);
|
|
// Abort cancels pending spawn intent and clears orders, but never reports intel.
|
|
job.Order=&team;job.Report=&team;team.agents.push_back(&original);
|
|
r.Finish(&job,false,"preparation deadline");assert(job.State==4&&!job.Delivered&&!job.Order&&!job.Report);
|
|
assert(team.m_aUnitPrefabSlots.Count()==0&&!team.immediately&&game.world.purges==3&&team.hook.removals==1&&team.lod==-1&&original.lod==-1);
|
|
assert(!mode.evidence.delivered&&!hq.compromised&&hq.released==1);
|
|
// Only actual successful receipt calls the evidence/compromise boundary.
|
|
Job received;received.Team=&team;received.State=3;received.Missing.push_back("radio-1");
|
|
r.Finish(&received,true,"radio delivered");assert(received.Delivered&&mode.evidence.delivered==1&&hq.compromised==1);
|
|
assert(game.world.purges==3); // a completed registered team has no preparation queue to cancel
|
|
Job deleted;r.Finish(&deleted,false,"team deleted");assert(deleted.State==4&&!deleted.Delivered);
|
|
// A failed unregistered preparation may leave survivors, but cannot be reused
|
|
// as a registered team. Completed ready teams may be reused; ownership holds.
|
|
Job waiting,previous;previous.Team=&team;previous.State=4;
|
|
assert(!r.Reusable(&waiting,&previous));previous.Prepared=true;
|
|
assert(r.Reusable(&waiting,&previous)&&!r.Reusable(&previous,&previous));
|
|
previous.State=2;assert(!r.Reusable(&waiting,&previous));previous.State=4;
|
|
team.players=1;assert(!r.Reusable(&waiting,&previous));team.players=0;
|
|
team.faction=2;assert(!r.Reusable(&waiting,&previous));team.faction=1;
|
|
team.living=0;assert(!r.Reusable(&waiting,&previous));team.living=1;
|
|
assert(r.Reusable(&waiting,&previous));previous.Team=nullptr;assert(!r.Reusable(&waiting,&previous));
|
|
// Failed empty attempts do not consume the allowance for actual teams.
|
|
Job empty;empty.Team=&team;empty.SpawnWave=true;r.m_Waves=1;
|
|
r.ReturnUnusedWave(&empty);assert(r.m_Waves==0&&!empty.SpawnWave);
|
|
r.ReturnUnusedWave(&empty);assert(r.m_Waves==0);
|
|
empty.SpawnWave=true;empty.SpawnedMember=true;r.m_Waves=1;
|
|
r.ReturnUnusedWave(&empty);assert(r.m_Waves==1&&empty.SpawnWave);
|
|
empty.SpawnedMember=false;team.count=1;
|
|
r.ReturnUnusedWave(&empty);assert(r.m_Waves==1);team.count=0;
|
|
empty.PreparationRetries=2;assert(!r.RetryPreparation(&empty,100000));
|
|
empty.PreparationRetries=0;r.m_SpawnRequests=32;assert(!r.RetryPreparation(&empty,100000));
|
|
r.m_SpawnRequests=8;empty.SpawnedMember=true;assert(!r.RetryPreparation(&empty,100000));
|
|
empty.SpawnedMember=false;int purges=game.world.purges;
|
|
assert(r.RetryPreparation(&empty,100000));
|
|
assert(!empty.Team&&!empty.HadTeam&&empty.RetryAt==160000&&empty.PreparationRetries==1&&r.m_Waves==0);
|
|
assert(game.world.purges==purges+1);
|
|
// Teardown can retire intent after the game/AI-world accessor has disappeared.
|
|
Job stop;stop.Team=&team;gameAvailable=false;r.RetireSpawn(&stop);assert(!team.immediately);
|
|
gameAvailable=true;game.ai=false;int previousPurges=game.world.purges;r.RetireSpawn(&stop);assert(game.world.purges==previousPurges);
|
|
}
|
|
''')
|
|
print('PASS: bounded same-group empty-spawn retries, no casualty refill, queue retirement, delivery-gated intel, prepared-team reuse, throttled diagnostics and LOD release')
|