Files
EscapistsReintroduced/tests/test_supply_convoy_survey.py
T
jakub 2390f77554
continuous-integration/drone/push Build is passing
Smooth convoy startup and record Workshop 0.0.153
2026-11-10 15:34:24 +01:00

97 lines
9.8 KiB
Python

"""Execute production asynchronous survey orchestration; native road geometry has its own differential harness."""
import re
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoySystem.c'
s='\n'.join(body(p,x) for x in ('protected bool FindBaseStep(', 'bool StartOne(', 'protected void FinishSearch(', 'protected void SearchStep(', 'protected void SearchPiece(', 'void Stop('))
s=s.replace('protected ','').replace('(m_StartingCount + 1).ToString()', 'std::to_string(m_StartingCount + 1)')
s=s.replace('GetGame().', 'GetGame()->').replace('GetCallqueue().CallLater(SearchStep, 20, true)', 'queue.Schedule(20)').replace('GetCallqueue().Remove(SearchStep)', 'queue.Remove()')
for name in ('GetWorldEntity()','GetPlayerManager()','GetWorld()'):s=s.replace(name+'.',name+'->')
s=s.replace('GetGame()->GetWorld()->GetWorldTime()', 'clockMs').replace('System.GetTickCount()', 'clockMs')
s=s.replace('array<vector>', 'Array<vector>').replace('m_Mode.Sites().', 'm_Mode->Sites()->')
for typ in ('ERR_SiteRuntime','ERR_Performance','ERR_ForceRoute','ERR_ConvoyTask','ERR_ConvoyReceipt'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
s=s.replace('ERR_ConvoyRoadSurvey()', 'ERR_ConvoyRoadSurvey()')
for name in ('m_Mode','m_Service','m_Pool','m_Survey','site','task','receipt','perf'):
s=s.replace(name+'.',name+'->')
s=s.replace('->Definition.', '->Definition->').replace('->Group.', '->Group->')
for name in ('Replication','ERR_GameMode','Math','ERR_LocationKind','ERR_SupplyConvoyRules','ERR_Actors','ERR_HQ','ERR_PerfMetric'):
s=s.replace(name+'.',name+'::')
s=s.replace('string.Format','string::Format').replace('string.Empty','string{}').replace('null','nullptr')
s=s.replace('bool complete;', 'bool complete=false;').replace('bool failed = true','bool failed = true')
run(r'''
#include <cassert>
#include <vector>
#include <string>
#include <algorithm>
#include <cmath>
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}template<class...T>static string Format(T...){return "event";}};
struct vector{float v[3]={};float& operator[](int i){return v[i];}};
vector Vector(float x,float y,float z){return {{x,y,z}};}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Clear(){this->clear();}void Insert(T v){this->push_back(v);}void Copy(Array<T>&v){*this=v;}};
int clockMs=0;
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace Math{float Min(float a,float b){return std::min(a,b);}float Max(float a,float b){return std::max(a,b);}float RandomFloat(float a,float){return a;}}
namespace ERR_Actors{vector Ground(vector v){return v;}}
namespace ERR_HQ{int events=0;void Event(string,string){events++;}}
namespace ERR_PerfMetric{int CONVOY_SURVEY=1;}
struct ERR_Performance{int marks=0;void Mark(int,int){marks++;}};
namespace ERR_LocationKind{enum Value{OUTPOST,DEPOT,RADIO,OTHER};}using ERR_LocationKind_t=ERR_LocationKind::Value;
struct Definition{ERR_LocationKind_t Kind=ERR_LocationKind::OUTPOST;vector Position;};
struct Group{int faction=1;int GetFaction(){return faction;}};
struct ERR_SiteRuntime{struct Definition* Definition=nullptr;struct Group* Group=nullptr;};
struct Sites{Array<ERR_SiteRuntime*>sites;int Count(){return sites.Count();}ERR_SiteRuntime* At(int i){return sites.at(i);}};
struct ERR_ForceRoute{};struct ERR_ConvoyReceipt{bool Consumed=false;};struct ERR_ConvoyTask{bool Stopped=false;};
struct Pool{bool available=true,reserve=true;int reserves=0;ERR_ConvoyReceipt receipt;bool BodyBudgetAvailable(string){return available;}ERR_ConvoyReceipt* Reserve(string){reserves++;return reserve?&receipt:nullptr;}};
struct Service{int count=0,dispatches=0,bases=0;bool success=true;ERR_ConvoyTask task;int Count(){return count;}void SetBases(Array<vector>&b){bases=b.Count();}ERR_ConvoyTask* Dispatch(ERR_ConvoyReceipt*,Array<vector>&,Array<vector>&,ERR_ForceRoute*,int){dispatches++;if(!success)return nullptr;count++;return &task;}};
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;struct Sites sites;ERR_Performance perf;static ERR_GameMode* Get(){return current;}bool Active(){return active;}struct Sites* Sites(){return &sites;}int EnemyFaction(){return 1;}ERR_Performance* Performance(){return &perf;}};
struct Queue{bool active=false;int scheduled=0,removed=0;void Schedule(int n){assert(n==20);active=true;scheduled++;}void Remove(){active=false;removed++;}};
struct World{void GetWorldBounds(vector&l,vector&h){l=Vector(0,0,0);h=Vector(10000,0,10000);}};
struct Players{int count=1;int GetPlayerCount(){return count;}};
struct Game{Queue queue;World world;Players players;bool haveWorld=true;World* GetWorldEntity(){return haveWorld?&world:nullptr;}Players* GetPlayerManager(){return &players;}}game;
Game* GetGame(){return &game;}
namespace ERR_SupplyConvoyRules{bool separated=true;bool Separated(vector,Array<vector>&,float){return separated;}}
struct ERR_ConvoyRoadSurvey{static inline int calls=0,begins=0,stepsNeeded=19,cost=0;static inline bool found=true;int progress=0;
void Begin(vector,Array<vector>&,float){begins++;progress=0;}bool Step(){calls++;clockMs+=cost;return ++progress>=stepsNeeded;}
bool Result(Array<vector>&p,Array<vector>&a,ERR_ForceRoute*&r){if(!found)return false;p.Insert(Vector(10,0,10));a.Insert(Vector(0,0,0));r=new ERR_ForceRoute;return true;}};
struct SurveySystem{
ERR_GameMode* m_Mode;Pool* m_Pool;Service* m_Service;
Array<vector>m_Bases,m_Starts;bool m_Stopped=false,m_Searching=false,m_FindingBases=false;
int m_Target=3,m_Attempts=1,m_BaseCursor=0,m_SeedAttempt=0,m_StartingCount=0;
string m_Source,m_Reason,m_LastFailure;vector m_Low,m_High;float m_Separation=0,m_Next=0;
ERR_ConvoyRoadSurvey* m_Survey=nullptr;int approaches=0,notes=0;
bool Approach(vector center,vector&position){approaches++;position=center;return true;}
string RefusalReason(ERR_ConvoyReceipt*){return "budget refusal";}void NoteFailure(){notes++;}string Status(){return m_Reason;}
'''+s.replace('ERR_LocationKind kind','ERR_LocationKind_t kind')+r'''
};
struct Fixture{ERR_GameMode mode;Pool pool;Service service;SurveySystem s{&mode,&pool,&service};
Fixture(){ERR_GameMode::current=&mode;game=Game{};Replication::server=true;ERR_ConvoyRoadSurvey::found=true;ERR_ConvoyRoadSurvey::cost=0;ERR_ConvoyRoadSurvey::stepsNeeded=19;ERR_SupplyConvoyRules::separated=true;s.m_Bases={Vector(0,0,0),Vector(1000,0,0)};}
void Finish(){for(int i=0;s.m_Searching&&i<300;i++)s.SearchStep();assert(!s.m_Searching);}
};
int main(){
{Fixture f;assert(f.s.StartOne());assert(f.s.m_Searching&&game.queue.active&&f.pool.reserves==0&&f.service.count==0);assert(!f.s.StartOne()&&game.queue.scheduled==1);
game.players.count=0;f.s.SearchStep();assert(!f.s.m_Survey&&f.pool.reserves==0);game.players.count=1;
f.s.SearchStep();assert(f.s.m_SeedAttempt==1&&f.s.m_Survey);int calls=ERR_ConvoyRoadSurvey::calls;f.s.SearchStep();assert(ERR_ConvoyRoadSurvey::calls==calls+8&&f.pool.reserves==0);
f.Finish();assert(f.pool.reserves==1&&f.service.dispatches==1&&f.service.count==1&&f.s.m_Starts.Count()==1&&!game.queue.active);
f.s.SearchStep();assert(f.service.dispatches==1);}
// The cooperative budget yields before eight pieces when the native timer advances.
{Fixture f;ERR_ConvoyRoadSurvey::cost=1;f.s.StartOne();f.s.SearchStep();int before=ERR_ConvoyRoadSurvey::calls;f.s.SearchStep();assert(ERR_ConvoyRoadSurvey::calls==before+2);f.s.Stop();}
// Exhaust every original eight-seed attempt without reserving or spawning.
{Fixture f;ERR_ConvoyRoadSurvey::found=false;f.s.StartOne();f.Finish();assert(f.s.m_SeedAttempt==8&&f.pool.reserves==0&&f.service.count==0&&f.s.notes==1);}
// Current capacity is checked again after yield; no stale plan spends another source's slot.
for(int reason=0;reason<4;reason++){Fixture f;f.s.StartOne();f.s.SearchStep();if(reason==0)f.pool.available=false;if(reason==1)f.service.count=1;if(reason==2)f.mode.active=false;if(reason==3)ERR_GameMode::current=nullptr;
f.s.SearchStep();assert(!f.s.m_Searching&&!game.queue.active&&f.pool.reserves==0&&f.service.dispatches==0);}
// A reservation or final native preflight refusal does not record a physical start.
for(int failure=0;failure<3;failure++){Fixture f;if(failure==0)f.pool.reserve=false;if(failure==1)f.pool.receipt.Consumed=true;if(failure==2)f.service.success=false;f.s.StartOne();f.Finish();assert(f.service.count==0&&f.s.m_Starts.Count()==0&&f.s.notes==1);}
{Fixture f;f.service.task.Stopped=true;f.s.StartOne();f.Finish();assert(f.service.count==1&&f.s.m_Starts.Count()==1);}
// No invalid caller queues work or consumes budget.
for(int failure=0;failure<5;failure++){Fixture f;if(failure==0)Replication::server=false;if(failure==1)f.mode.active=false;if(failure==2)f.pool.available=false;if(failure==3)game.haveWorld=false;if(failure==4)f.service.count=3;
assert(!f.s.StartOne()&&!game.queue.active&&f.pool.reserves==0);}
// Base search is one site per callback; no full-site sweep in the HQ tick.
{Fixture f;f.s.m_Bases.Clear();Definition a,b,skip;skip.Kind=ERR_LocationKind::OTHER;a.Position=Vector(100,0,100);b.Position=Vector(1000,0,1000);ERR_SiteRuntime sa{&a},sb{&b},sc{&skip};f.mode.sites.sites={&sa,&sc,&sb};
f.s.StartOne();assert(f.s.approaches==0);f.s.SearchStep();assert(f.s.approaches==1&&f.s.m_Bases.Count()==1);f.s.SearchStep();assert(f.s.approaches==1);f.s.SearchStep();assert(f.s.approaches==2&&f.s.m_Bases.Count()==2);f.s.SearchStep();assert(!f.s.m_FindingBases&&f.service.bases==2);f.Finish();}
{Fixture f;f.s.m_Bases.Clear();f.s.StartOne();f.Finish();assert(f.s.notes==1&&f.pool.reserves==0);}
{Fixture f;f.s.StartOne();f.s.SearchStep();f.s.Stop();f.s.SearchStep();assert(!f.s.m_Survey&&!game.queue.active&&f.pool.reserves==0);}
}
''')
print('PASS: actual queued supply survey yields by piece/time, pauses empty worlds, scans one base per callback, keeps eight seeds, rechecks capacity and stops without native admission on failure')