Smooth convoy startup and record Workshop 0.0.153
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -3,7 +3,7 @@ from policy_harness import ROOT, body, run
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
P=Path(os.environ.get('ERR_SUPPLY_SYSTEM_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoySystem.c'))
|
||||
s='\n'.join(body(P,sig) for sig in ('bool StartOne(', 'protected string RefusalReason(',
|
||||
s='\n'.join(body(P,sig) for sig in ('bool StartOne(', 'protected void SearchPiece(', 'protected void FinishSearch(', 'protected string RefusalReason(',
|
||||
'protected string BudgetStatus(', 'protected void NoteFailure(', 'void Tick(', 'string Status('))
|
||||
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('null','nullptr')
|
||||
for name in ('Replication','ERR_GameMode','Math','ERR_Actors','ERR_ConvoyRoadPlan','ERR_SupplyConvoyRules','ERR_HQ','ERR_ForceRules','string'):
|
||||
@@ -11,10 +11,13 @@ for name in ('Replication','ERR_GameMode','Math','ERR_Actors','ERR_ConvoyRoadPla
|
||||
s=s.replace('string::Empty','string{}').replace('array<vector>','Array<vector>')
|
||||
for typ in ('ERR_ForceRoute','ERR_ConvoyReceipt','ERR_ConvoyTask'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
for name in ('m_Mode','m_Pool','m_Service','receipt','task'):
|
||||
for name in ('m_Mode','m_Pool','m_Service','m_Survey','receipt','task'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetWorldEntity().','GetWorldEntity()->').replace('GetWorld().','GetWorld()->')
|
||||
s=s.replace('(m_Service->Count() + 1).ToString()', 'std::to_string(m_Service->Count() + 1)')
|
||||
s=s.replace('(m_StartingCount + 1).ToString()', 'std::to_string(m_StartingCount + 1)')
|
||||
s=s.replace('GetCallqueue().CallLater(SearchStep, 20, true)', 'queue.Schedule()').replace('GetCallqueue().Remove(SearchStep)', 'queue.Remove()')
|
||||
s=s.replace('System.GetTickCount()', '0').replace('bool complete;', 'bool complete=false;')
|
||||
s=s.replace('ERR_ForceRoute* route;', 'ERR_ForceRoute* route=nullptr;')
|
||||
B=Path(os.environ.get('ERR_CONVOY_BUDGET',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBudget.c'))
|
||||
# Execute the real reservation/replay/accounting/consume methods alongside StartOne.
|
||||
pool=B.read_text();pool=re.sub(r'//[^\n]*','',pool).replace('protected ','').replace('ref ','').replace('class ','struct ')
|
||||
@@ -39,10 +42,11 @@ struct string:std::string{using std::string::string;string(std::string s):std::s
|
||||
struct vector{float x=0,y=0,z=0;float operator[](int i)const{return i==0?x:i==1?y:z;}};
|
||||
std::ostream& operator<<(std::ostream& out,vector p){return out<<p.x<<","<<p.y<<","<<p.z;}
|
||||
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();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T x){this->push_back(x);}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T x){this->push_back(x);}void Clear(){this->clear();}};
|
||||
struct Counts{int world=0,bounds=0,bases=0,ground=0,random=0,road=0,reserve=0,dispatch=0;} calls;
|
||||
struct World{void GetWorldBounds(vector& low,vector& high){calls.bounds++;low={0,0,0};high={10000,100,10000};}};
|
||||
struct Game{World world;bool available=true;float now=0;World* GetWorldEntity(){calls.world++;return available?&world:nullptr;}Game* GetWorld(){return this;}float GetWorldTime(){return now;}} game;
|
||||
struct Queue{bool active=false;void Schedule(){assert(!active);active=true;}void Remove(){active=false;}};
|
||||
struct Game{Queue queue;World world;bool available=true;float now=0;World* GetWorldEntity(){calls.world++;return available?&world:nullptr;}Game* GetWorld(){return this;}float GetWorldTime(){return now;}} game;
|
||||
Game* GetGame(){return &game;}
|
||||
enum class ERR_ForceKind{GARRISON,CONVOY};
|
||||
struct ERR_ForceRecord{int Id=0,InitialStrength=0,Strength=0;bool Physical=true,Eliminated=false;void* Group=this;ERR_ForceKind Kind=ERR_ForceKind::CONVOY;string SourceId;};
|
||||
@@ -60,19 +64,25 @@ using Pool=ERR_ConvoyPool;
|
||||
struct ERR_ConvoyTask{bool Stopped=false;};
|
||||
struct Service{Pool* pool=nullptr;int count=0;bool fail=false;ERR_ConvoyTask task;int Count(){return count;}
|
||||
ERR_ConvoyTask* Dispatch(ERR_ConvoyReceipt* r,Array<vector>,Array<vector>,ERR_ForceRoute*,float){calls.dispatch++;if(fail||!pool->Consume(r))return nullptr;count++;return &task;}};
|
||||
namespace ERR_ConvoyRoadPlan{bool clear=true;ERR_ForceRoute found;
|
||||
bool Build(vector,Array<vector>& positions,Array<vector>& angles,ERR_ForceRoute*& route,Array<vector>,float){calls.road++;if(!clear)return false;positions={{100,0,100}};angles={{0,0,0}};route=&found;return true;}}
|
||||
namespace ERR_ConvoyRoadPlan{bool clear=true;ERR_ForceRoute found;}
|
||||
struct ERR_ConvoyRoadSurvey{int progress=0;void Begin(vector,Array<vector>,float){calls.road++;}bool Step(){return ++progress==9;}
|
||||
bool Result(Array<vector>& positions,Array<vector>& angles,ERR_ForceRoute*& route){if(!ERR_ConvoyRoadPlan::clear)return false;positions={{100,0,100}};angles={{0,0,0}};route=&ERR_ConvoyRoadPlan::found;return true;}};
|
||||
namespace ERR_SupplyConvoyRules{bool Separated(vector,Array<vector>,float){return true;}}
|
||||
struct Startup{Mode* m_Mode;Pool* m_Pool;Service* m_Service;Array<vector> m_Bases,m_Starts;int m_Target=3,m_Attempts=0;bool m_Stopped=false,m_Automatic=true;
|
||||
float m_Began=-1,m_Next=0;string m_Reason,m_LastFailure;void FindBases(){calls.bases++;m_Bases={{0,0,0},{1000,0,0}};}
|
||||
float m_Began=-1,m_Next=0;string m_Reason,m_LastFailure;
|
||||
bool m_Searching=false,m_FindingBases=false;int m_BaseCursor=0,m_SeedAttempt=0,m_StartingCount=0;
|
||||
string m_Source;vector m_Low,m_High;float m_Separation=0;ERR_ConvoyRoadSurvey* m_Survey=nullptr;
|
||||
bool FindBaseStep(){calls.bases++;m_Bases={{0,0,0},{1000,0,0}};return true;}
|
||||
'''+s+r'''
|
||||
void Drain(){for(int i=0;i<100&&m_Searching;i++){int before=calls.reserve;SearchPiece();if(m_Searching)assert(calls.reserve==before);}assert(!m_Searching&&!game.queue.active);}
|
||||
bool StartAndDrain(){int before=m_Service->Count();if(!StartOne())return false;assert(calls.dispatch==0||m_Service->Count()==before);Drain();return m_Service->Count()>before;}
|
||||
};
|
||||
void Reset(){calls={};ERR_HQ::events.clear();ERR_ConvoyRoadPlan::clear=true;game.available=true;}
|
||||
void Reset(){calls={};ERR_HQ::events.clear();ERR_ConvoyRoadPlan::clear=true;game.available=true;game.queue.active=false;}
|
||||
void NoSurvey(){assert(calls.bases==0&&calls.world==0&&calls.bounds==0&&calls.ground==0&&calls.random==0&&calls.road==0&&calls.reserve==0&&calls.dispatch==0);}
|
||||
struct Fixture{Mode mode;ERR_ForceRecord baseline;Pool pool;Service service;Startup startup;
|
||||
Fixture(int bodies):pool(&mode,3,true),startup{&mode,&pool,&service}{baseline.Strength=bodies;mode.ledger.records.Insert(&baseline);ERR_GameMode::current=&mode;service.pool=&pool;Reset();}};
|
||||
int main(){
|
||||
{Fixture f(126);auto& blocked=f.startup;assert(!blocked.StartOne());NoSurvey();
|
||||
{Fixture f(126);auto& blocked=f.startup;assert(!blocked.StartAndDrain());NoSurvey();
|
||||
assert(blocked.m_Reason=="insufficient shared body budget for another original convoy");
|
||||
assert(blocked.m_Starts.empty()&&blocked.m_Attempts==0&&f.service.count==0&&f.pool.m_Receipts.empty());
|
||||
blocked.Tick(0,true);blocked.Tick(30000,true);blocked.Tick(34999,true);blocked.Tick(35000,true);
|
||||
@@ -81,35 +91,39 @@ int main(){
|
||||
for(int i=2;i<150;i++)blocked.Tick(30000+5000*i,true);
|
||||
NoSurvey();assert(blocked.m_Attempts==120&&ERR_HQ::events.size()==2);
|
||||
assert(ERR_HQ::events.back().find("attempts exhausted")!=string::npos);}
|
||||
{Fixture f(125);assert(f.startup.StartOne());assert(calls.bases==1&&calls.bounds==1&&calls.road==1&&calls.reserve==1&&calls.dispatch==1);
|
||||
{Fixture f(125);assert(f.startup.StartAndDrain());assert(calls.bases==1&&calls.bounds==1&&calls.road==1&&calls.reserve==1&&calls.dispatch==1);
|
||||
assert(f.service.count==1&&f.startup.m_Starts.Count()==1&&f.pool.m_Receipts[0]->Consumed&&f.pool.CommittedBodies()==135);
|
||||
// A lost task counter must never turn the same consumed source into a replacement.
|
||||
f.service.count=0;assert(!f.startup.StartOne());assert(calls.dispatch==1&&f.startup.m_Reason.find("already consumed")!=string::npos);}
|
||||
{Fixture f(100);ERR_ConvoyRoadPlan::clear=false;assert(!f.startup.StartOne());assert(calls.road==8&&calls.reserve==0&&calls.dispatch==0);
|
||||
f.service.count=0;assert(!f.startup.StartAndDrain());assert(calls.dispatch==1&&f.startup.m_Reason.find("already consumed")!=string::npos);}
|
||||
{Fixture f(100);ERR_ConvoyRoadPlan::clear=false;assert(!f.startup.StartAndDrain());assert(calls.road==8&&calls.reserve==0&&calls.dispatch==0);
|
||||
assert(f.startup.m_Reason=="no clear separated road formation in this bounded survey");}
|
||||
{Fixture f(126);f.startup.Tick(0,true);f.startup.Tick(30000,true);NoSurvey();f.baseline.Strength=125;
|
||||
f.startup.Tick(35000,true);assert(f.service.count==1&&calls.dispatch==1&&f.startup.m_Attempts==2&&f.startup.m_LastFailure.empty());}
|
||||
f.startup.Tick(35000,true);assert(f.startup.m_Searching&&calls.dispatch==0);f.startup.Drain();assert(f.service.count==1&&calls.dispatch==1&&f.startup.m_Attempts==2&&f.startup.m_LastFailure.empty());}
|
||||
// Critical real-pool replay: failed road preflight retains the unconsumed receipt.
|
||||
{Fixture f(123);f.service.fail=true;assert(!f.startup.StartOne());
|
||||
{Fixture f(123);f.service.fail=true;assert(!f.startup.StartAndDrain());
|
||||
assert(f.service.count==0&&f.pool.m_Receipts.Count()==1&&f.pool.CommittedBodies()==133);
|
||||
auto original=f.pool.m_Receipts[0];assert(!original->Consumed&&original->OutstandingBodies==10);
|
||||
assert(f.pool.BodyBudgetAvailable("supply-convoy-1")&&!f.pool.BodyBudgetAvailable("supply-convoy-2"));
|
||||
assert(f.pool.Reserve("supply-convoy-1")==original&&!f.pool.Reserve("supply-convoy-2"));
|
||||
f.service.fail=false;Reset();assert(f.startup.StartOne());
|
||||
f.service.fail=false;Reset();assert(f.startup.StartAndDrain());
|
||||
assert(calls.road==1&&calls.reserve==1&&calls.dispatch==1&&f.pool.m_Receipts.Count()==1);
|
||||
assert(f.pool.m_Receipts[0]==original&&original->Consumed&&f.service.count==1&&f.pool.CommittedBodies()==133);}
|
||||
{Fixture f(123);f.service.fail=true;assert(!f.startup.StartOne());auto original=f.pool.m_Receipts[0];
|
||||
{Fixture f(123);f.service.fail=true;assert(!f.startup.StartAndDrain());auto original=f.pool.m_Receipts[0];
|
||||
// Precheck intentionally cannot grant admission: actual Consume rechecks budget growth.
|
||||
f.baseline.Strength=126;f.service.fail=false;assert(!f.startup.StartOne()&&!original->Consumed&&f.service.count==0);
|
||||
f.baseline.Strength=125;assert(f.startup.StartOne()&&original->Consumed&&f.pool.CommittedBodies()==135);}
|
||||
f.baseline.Strength=126;f.service.fail=false;assert(!f.startup.StartAndDrain()&&!original->Consumed&&f.service.count==0);
|
||||
f.baseline.Strength=125;assert(f.startup.StartAndDrain()&&original->Consumed&&f.pool.CommittedBodies()==135);}
|
||||
// Real budget grows while an admitted survey is still pending: no reservation.
|
||||
{Fixture f(125);assert(f.startup.StartOne());assert(f.startup.m_Searching&&calls.road==0&&calls.reserve==0&&calls.dispatch==0);
|
||||
f.startup.SearchPiece();f.startup.SearchPiece();assert(calls.road==1&&calls.reserve==0);f.baseline.Strength=126;f.startup.Drain();
|
||||
assert(f.service.count==0&&calls.reserve==0&&calls.dispatch==0&&f.pool.m_Receipts.empty());}
|
||||
{Fixture f(0);assert(!f.pool.BodyBudgetAvailable("")&&!f.pool.BodyBudgetAvailable(string(65,'x')));
|
||||
Mode other;ERR_GameMode::current=&other;assert(!f.pool.BodyBudgetAvailable("supply-convoy-1"));
|
||||
ERR_GameMode::current=&f.mode;Replication::server=false;assert(!f.pool.BodyBudgetAvailable("supply-convoy-1"));Replication::server=true;
|
||||
f.mode.active=false;assert(!f.pool.BodyBudgetAvailable("supply-convoy-1"));}
|
||||
{Fixture f(0);f.startup.m_Pool=nullptr;assert(!f.startup.StartOne());NoSurvey();assert(f.startup.m_Reason=="convoy allocator unavailable");}
|
||||
{Fixture f(0);f.startup.m_Target=0;assert(!f.startup.StartOne());NoSurvey();f.startup.m_Target=3;
|
||||
f.startup.m_Stopped=true;assert(!f.startup.StartOne());NoSurvey();}
|
||||
{Fixture f(0);f.startup.m_Pool=nullptr;assert(!f.startup.StartAndDrain());NoSurvey();assert(f.startup.m_Reason=="convoy allocator unavailable");}
|
||||
{Fixture f(0);f.startup.m_Target=0;assert(!f.startup.StartAndDrain());NoSurvey();f.startup.m_Target=3;
|
||||
f.startup.m_Stopped=true;assert(!f.startup.StartAndDrain());NoSurvey();}
|
||||
|
||||
}
|
||||
''')
|
||||
print('PASS: actual StartOne plus actual pool preserve unconsumed receipt retry, reject consumed replay, skip impossible new-budget surveys, retain equality, retry caps and journal dedup; no FPS claim')
|
||||
print('PASS: actual queued StartOne/SearchPiece/FinishSearch plus actual pool preserve unconsumed receipt retry, reject consumed replay, skip impossible new-budget surveys, retain equality, retry caps and journal dedup; no FPS claim')
|
||||
|
||||
Reference in New Issue
Block a user