Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
85 lines
7.6 KiB
Python
85 lines
7.6 KiB
Python
"""Run the actual bounded placement scheduler with terrain and clock adapters."""
|
|
import re
|
|
from policy_harness import ROOT,body,run
|
|
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c'
|
|
s='\n'.join(body(p, signature) for signature in ('static bool StartupSite(', 'void StartupOnly(', 'bool StartupReady(', 'protected bool SkipOptional(', 'protected bool RescueFits(', 'ERR_PrepareResult Tick('))
|
|
s=s.replace('ERR_SiteDefinition site','Site* site').replace('site.','site->').replace('m_Terrain.Site','m_Terrain.SiteAt')
|
|
s=s.replace('System.GetTickCount','Clock::Now').replace('Replication.IsServer','Server').replace('Math.RandomFloat','RandomFloat').replace('ERR_PrepareResult.','Result::')
|
|
s=s.replace('float floor;','float floor=0;').replace('protected ', '').replace('ref ERR_SiteDefinition retained', 'Site* retained').replace('retained.', 'retained->').replace('m_Definition.', 'm_Definition->').replace('ERR_LocationKind.', 'Kind::')
|
|
s=s.replace('foreach (Site* site : m_Definition->Sites)', 'for (Site* site : m_Definition->Sites)')
|
|
s=s.replace('ERR_Actors.', 'ERR_Actors::').replace('ERR_CampTerrain.', 'ERR_CampTerrain::').replace('ERR_RescueLanding.', 'ERR_RescueLanding::')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
using string=std::string;
|
|
struct vector{float a[3];float& operator[](int i){return a[i];}};vector Vector(float x,float y,float z){return {{x,y,z}};}
|
|
vector operator+(vector a,vector b){return Vector(a[0]+b[0],a[1]+b[1],a[2]+b[2]);}
|
|
namespace ERR_Actors{vector Ground(vector p){return p;}}
|
|
namespace ERR_CampTerrain{vector Rotate(vector p,float){return p;}}
|
|
namespace ERR_RescueLanding{bool fits=true;bool Fits(vector){return fits;}}
|
|
namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value;
|
|
namespace Clock {int now=0,step=1;int Now(){int t=now;now+=step;return t;}}
|
|
bool Server(){return true;}float RandomFloat(float a,float b){return (a+b)/2;}template<class...T>void PrintFormat(T...){}
|
|
namespace Kind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
|
|
struct Site{int Kind=Kind::PRISON;bool required=false;string Label="Test site";vector Position=Vector(10,0,20),AircraftOffset=Vector(12,0,0);float Heading=0;bool PlacementResolved=false,RandomClear=true;string Id="site";};
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;int Count(){return this->size();}int Find(T v){auto i=std::find(this->begin(),this->end(),v);return i==this->end()?-1:i-this->begin();}void Remove(int i){this->erase(this->begin()+i);}void InsertAt(T v,int i){this->insert(this->begin()+i,v);}};
|
|
struct Definition{bool TerrainGenerated=false;Array<Site*> Sites;Site* required=nullptr;bool Valid(){return !required||Sites.Find(required)>=0;}};
|
|
struct Terrain{bool clear=true;Site* refused=nullptr;int queries=0;bool SiteAt(Site* site,vector,float,float& height){queries++;height=42;return clear&&site!=refused;}};
|
|
struct Scheduler{
|
|
Definition definition;Definition* m_Definition=&definition;
|
|
bool m_StartupOnly=false;
|
|
bool m_Started=true;ERR_PrepareResult m_Result=Result::PENDING;int m_Index=0,m_Attempts=0,m_Began=0;
|
|
vector m_Low=Vector(0,0,0),m_High=Vector(10000,0,10000);Array<Site*>m_Pending;Terrain m_Terrain;string m_Error;
|
|
bool Separated(Site*,vector,bool){return true;}
|
|
'''+s+r'''
|
|
};
|
|
int main(){
|
|
// Startup pauses before radio/camps, and resuming resets the next-site deadline.
|
|
{Clock::now=0;Scheduler s;Site prison,radio,extraction;radio.Kind=Kind::RADIO;extraction.Kind=Kind::EXTRACTION;extraction.RandomClear=false;
|
|
s.definition.Sites={&prison,&radio,&extraction};s.m_Pending={&prison,&radio};s.StartupOnly(true);
|
|
while(!s.StartupReady()) assert(s.Tick()==Result::PENDING);
|
|
assert(prison.PlacementResolved&&!radio.PlacementResolved);int calls=s.m_Terrain.queries;
|
|
Clock::now=60000;assert(s.Tick()==Result::PENDING&&s.m_Terrain.queries==calls);
|
|
s.StartupOnly(false);assert(s.m_Began>=60000);while(s.Tick()==Result::PENDING){}assert(radio.PlacementResolved);}
|
|
|
|
{Scheduler s;s.m_Started=false;assert(s.Tick()==Result::FAILED);}
|
|
{Scheduler s;assert(s.Tick()==Result::READY);assert(s.m_Terrain.queries==0);}
|
|
{Clock::now=0;Scheduler s;Site a,b;s.m_Pending={&a,&b};while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&a.PlacementResolved&&b.PlacementResolved);assert(a.Position[1]>42&&a.Position[1]<42.1);int calls=s.m_Terrain.queries;assert(s.Tick()==Result::READY&&s.m_Terrain.queries==calls);}
|
|
{Clock::now=0;Scheduler s;Site a;s.m_Pending={&a};s.m_Terrain.clear=false;assert(s.Tick()==Result::PENDING);assert(s.m_Terrain.queries<10&&!a.PlacementResolved);Clock::now=30001;assert(s.Tick()==Result::FAILED&&!s.m_Error.empty()&&!a.PlacementResolved);int calls=s.m_Terrain.queries;assert(s.Tick()==Result::FAILED&&s.m_Terrain.queries==calls);}
|
|
{Clock::now=0;Scheduler s;Site a;s.m_Pending={&a};s.m_Attempts=12000;assert(s.Tick()==Result::READY&&a.Position[0]==10&&a.Position[2]==20&&a.PlacementResolved);}
|
|
{Clock::now=30001;Scheduler s;Site a;s.m_Pending={&a};assert(s.Tick()==Result::READY&&a.Position[0]==10&&a.Position[2]==20);}
|
|
// Each optional type can fail after exhausting candidates without blocking the next site.
|
|
for(int kind : {Kind::OUTPOST,Kind::HOSPITAL,Kind::DEPOT}){
|
|
Clock::now=0;Scheduler s;Site a,b;a.Kind=kind;a.Id="unplaceable";b.Id="next";
|
|
s.m_Pending={&a,&b};s.definition.Sites={&a,&b};s.m_Terrain.refused=&a;s.m_Attempts=12000;
|
|
while(s.Tick()==Result::PENDING){}
|
|
assert(s.m_Result==Result::READY&&s.m_Error.empty()&&!a.PlacementResolved&&b.PlacementResolved);
|
|
assert(s.definition.Sites.size()==1&&s.definition.Sites[0]==&b);
|
|
int calls=s.m_Terrain.queries;assert(s.Tick()==Result::READY&&s.m_Terrain.queries==calls);
|
|
}
|
|
// A deadline skip gets a fresh candidate/time budget for the following optional site.
|
|
{Clock::now=30001;Scheduler s;Site a,b;a.Kind=Kind::OUTPOST;b.Kind=Kind::HOSPITAL;
|
|
s.m_Pending={&a,&b};s.definition.Sites={&a,&b};s.m_Terrain.refused=&a;
|
|
while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&b.Position[0]!=10&&b.PlacementResolved);}
|
|
// Required objectives (including mortars) still fail; no unsafe terrain is accepted.
|
|
for(int kind : {Kind::PRISON,Kind::RADIO,Kind::EXTRACTION,Kind::MORTAR}){
|
|
Clock::now=0;Scheduler s;Site a;a.Kind=kind;s.m_Pending={&a};s.definition.Sites={&a};s.m_Attempts=12000;s.m_Terrain.clear=false;
|
|
assert(s.Tick()==Result::FAILED&&s.definition.Sites[0]==&a&&!a.PlacementResolved);
|
|
}
|
|
// Optional by type can still be indispensable for minimum accessible radio intel.
|
|
{Clock::now=0;Scheduler s;Site before,a,after;a.Kind=Kind::HOSPITAL;
|
|
s.definition.Sites={&before,&a,&after};s.definition.required=&a;s.m_Pending={&a};s.m_Attempts=12000;s.m_Terrain.clear=false;
|
|
assert(s.Tick()==Result::FAILED&&s.definition.Sites.size()==3&&s.definition.Sites[1]==&a&&!a.PlacementResolved);}
|
|
// Extraction candidates without secondary fleet space remain uncommitted and are retried.
|
|
{Clock::now=0;Scheduler s;Site a;a.Kind=Kind::EXTRACTION;s.m_Pending={&a};s.definition.Sites={&a};ERR_RescueLanding::fits=false;
|
|
assert(s.Tick()==Result::PENDING&&!a.PlacementResolved);ERR_RescueLanding::fits=true;
|
|
while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&a.PlacementResolved);}
|
|
// An authored fallback cannot bypass the new fleet-space qualification.
|
|
{Clock::now=0;Scheduler s;Site a;a.Kind=Kind::EXTRACTION;s.m_Pending={&a};s.definition.Sites={&a};s.m_Attempts=12000;ERR_RescueLanding::fits=false;
|
|
assert(s.Tick()==Result::FAILED&&!a.PlacementResolved);ERR_RescueLanding::fits=true;}
|
|
}
|
|
''')
|
|
print('PASS: actual generation scheduler bounds each tick, enforces deadline/candidate ceiling, validates fallback, commits only accepted positions, skips only dispensable sites, preserves required objectives/intel, and resets the next search budget')
|