Split checkpoint guards between VirtualHQ and local defence; record Workshop 0.0.151
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-10 13:33:00 +01:00
parent 4b737c825f
commit 9889bd6404
23 changed files with 1557 additions and 78 deletions
+15 -1
View File
@@ -91,7 +91,7 @@ struct Recon{SCR_AIGroup owned=nullptr;bool Owns(SCR_AIGroup group){return group
struct BodyService:GroupService{void RegisterGroup(SCR_AIGroup g,Sampler& s){assert(s.Bound()&&s.group==g);GroupService::RegisterGroup(g);}};
struct Reporting{int initializations=0;void Initialize(int id,float now){assert(id>0&&now==10);initializations++;}};
struct Unit{Reporting AmmoReporting;string HomeId,ForceSourceId;bool ReservedForSupport=false;bool HomeGuard=false;float NextStatus=0;Sampler StatusSampler;int Id=0;SCR_AIGroup Group=nullptr;struct Utility* Utility=nullptr;float NextReport=0;};using ERR_HQUnit=Unit*;
struct Roadblocks{SCR_AIGroup owned=nullptr;bool Contains(SCR_AIGroup g){return g&&g==owned;}};
struct Roadblocks{SCR_AIGroup owned=nullptr,local=nullptr;bool Contains(SCR_AIGroup g){return g&&g==owned;}bool LocalOnly(SCR_AIGroup g){return g&&g==local;}};
struct HQ{void BindSpecialSkills(SCR_AIGroup){}void AuditGroup(ERR_HQUnit){}Mode* m_Mode;GroupService m_Equipment;BodyService m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;Mortars* m_Mortars=nullptr;Scouts* m_Scouts=nullptr;Recon* m_Recon=nullptr;Recon* m_PlaneRecon=nullptr;Airfield* m_Airfield=nullptr;Scouts* m_Hunters=nullptr;Scouts* m_Towns=nullptr;Roadblocks* m_Roadblocks=nullptr;
'''+hqbind+hqregister+r'''
};
@@ -153,6 +153,20 @@ int main(){
assert(hq.m_Equipment.ids.back()==1&&hq.m_Equipment.fits.back());
int checkpointUnits=hq.m_Units.Count(),checkpointFits=hq.m_Equipment.ids.Count();
hq.RegisterGroup(&checkpoint);assert(hq.m_Units.Count()==checkpointUnits&&hq.m_Equipment.ids.Count()==checkpointFits);
// The parallel local pair keeps equipment/skills but never enters HQ,
// receives a radio allocation, reserves an HQ ID, or binds an HQ body roster.
Group localCheckpoint;posts.local=&localCheckpoint;
int beforeLocalUnits=hq.m_Units.Count(),beforeLocalId=hq.m_NextId,beforeLocalBodies=hq.m_Bodies.registrations;
int beforeLocalEquipment=hq.m_Equipment.registrations;
for(int repeat=0;repeat<5;repeat++){
hq.RegisterGroup(&localCheckpoint);
assert(hq.m_Units.Count()==beforeLocalUnits&&hq.m_NextId==beforeLocalId);
assert(hq.m_Bodies.registrations==beforeLocalBodies);
assert(hq.m_Equipment.ids.back()==0&&!hq.m_Equipment.fits.back());
}
assert(hq.m_Equipment.registrations==beforeLocalEquipment+1);
assert(!posts.Contains(&localCheckpoint)&&posts.LocalOnly(&localCheckpoint));
assert(posts.Contains(&checkpoint)&&!posts.LocalOnly(&checkpoint));
Group ordinaryAgain;hq.RegisterGroup(&ordinaryAgain);assert(hq.m_Equipment.ids.back()==8); // ordinary fitting remains distributed
Group town;Scouts towns;towns.owned=&town;hq.m_Towns=&towns;hq.RegisterGroup(&town);
assert(hq.m_Equipment.ids.back()==1&&hq.m_Equipment.fits.back()&&!hq.m_Units.back()->ReservedForSupport&&!hq.m_Units.back()->HomeGuard);
+79 -35
View File
@@ -98,15 +98,17 @@ assert 'if (!PlayersClear() || m_Mode.Forces().Count() >= 250 ||' in body(p,'pro
assert 'if (!PlayersClear() || m_Mode.Forces().Count() >= 250) { CancelPost(); return; }' in body(p,'protected void BuildPost(')
print('PASS: actual roadblock scheduler yields initial deadline/attempt cap to background, budgets samples, pauses empty/full worlds, completes 30 after unlock, preserves safety and never replenishes losses; teardown cancels work')
# Run actual staged physical creation and rollback, with native entities doubled.
s='\n'.join(body(p,sig) for sig in ('protected bool SpawnPost(', 'protected void CancelPost()', 'protected bool ShareProps()', 'protected void BuildPost()'))
# Run actual staged physical creation/rollback. Opaque native boundaries are doubled.
s='\n'.join(body(p,sig) for sig in ('bool LocalOnly(', 'protected void Rollback(', 'protected bool SpawnPost(', 'protected void CancelPost()', 'protected bool ShareProps()', 'protected void BuildPost()'))
s=s.replace('protected ', '').replace('array<vector> positions','Array<int>& positions').replace('array<ref ERR_RoadblockProp> covers','Array<Prop>& covers').replace('ERR_RoadblockProp prop', 'Prop prop')
s=s.replace('array<IEntity> spawned', 'Array<Node*>& spawned').replace('SCR_AIGroup group', 'Node* group').replace('SCR_AIGroup guardGroup', 'Node* guardGroup')
s=s.replace('SCR_AIGroup.Cast','Node::Cast').replace('AIWaypoint.Cast','Node::Cast').replace('IEntity actor','Node* actor').replace('IEntity cover','Node* cover')
s=s.replace('foreach (IEntity ownedEntity : m_BuildSpawned)','for (Node* ownedEntity : m_BuildSpawned)').replace('vector point','int point').replace('vector coverPosition','int coverPosition')
s=s.replace('foreach (IEntity ownedEntity : m_BuildSpawned)','for (Node* ownedEntity : m_BuildSpawned)').replace('foreach (IEntity entity : spawned)', 'for (Node* entity : spawned)').replace('vector point','int point').replace('vector coverPosition','int coverPosition')
s=s.replace('foreach (ERR_RoadblockProp plannedProp : covers)', 'for (Prop plannedProp : covers)')
s=s.replace('m_Forward.VectorToAngles()', '0').replace('m_Positions.Count().ToString()', 'std::to_string(m_Positions.Count())')
s=s.replace('m_Forward.VectorToAngles()', '0').replace('m_PostSerial.ToString()', 'std::to_string(m_PostSerial)')
s=s.replace('ERR_ForceRecord responders', 'Record* responders').replace('responders.Id', 'responders->Id')
s=s.replace('GetGame().GetWorld().GetWorldTime()', 'Now()').replace('System.GetTickCount()', 'Now()').replace('null','nullptr')
s=s.replace('m_BuildGroup.', 'm_BuildGroup->').replace('m_BuildHold.', 'm_BuildHold->')
for name in ('m_BuildGroup','m_BuildHold','m_BuildLocalGroup','m_BuildLocalHold','guardGroup'):s=s.replace(name+'.',name+'->')
s=s.replace('IEntity entity', 'Node* entity').replace('RplComponent component;', 'Replica* component=nullptr;').replace('RplComponent component =', 'Replica* component =')
s=s.replace('RplComponent.Cast', 'Replica::Cast').replace('FindComponent(RplComponent)', 'FindComponent()').replace('RplId.Invalid()', '-1')
s=s.replace('entity.FindComponent', 'entity->FindComponent').replace('m_BuildSpawned[i].FindComponent', 'm_BuildSpawned[i]->FindComponent').replace('component.', 'component->')
@@ -116,58 +118,100 @@ run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
#include <set>
#include <functional>
using string=std::string;
int Now(){return 100;}
template<class...T>void PrintFormat(T...){}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}void Clear(){this->clear();}void Copy(Array<T>&other){*this=other;}void Insert(T value){this->push_back(value);}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}void Clear(){this->clear();}void Copy(Array<T>&other){*this=other;}void Insert(T value){this->push_back(value);}bool Contains(T value){return std::find(this->begin(),this->end(),value)!=this->end();}};
struct Replica{int id=1;bool streaming=true;static Replica* Cast(Replica*r){return r;}int Id(){return id;}void EnableStreaming(bool value){streaming=value;}};
struct Node{Replica replica;bool missing=false;Replica* FindComponent(){return missing?nullptr:&replica;}static Node* Cast(Node*n){return n;}void SetFaction(int){}void SetSpawnImmediately(bool){}void SetDeleteWhenEmpty(bool){}void SetPermanentLOD(int){}bool AddAIEntityToGroup(Node*){return true;}void SetCompletionRadius(int r){assert(r==25);}void AddWaypoint(Node*){}void ActivateAI(){}};
namespace ERR_Actors{int calls=0,failAt=0;Array<Node*> all;Node* Spawn(string,int,int=0){calls++;if(calls==failAt)return nullptr;auto*n=new Node;all.Insert(n);return n;}bool SetFaction(Node*,int){return true;}}
struct Node{Replica replica;int position=0,deleted=0,activated=0;bool missing=false,acceptMember=true;Array<Node*>members,holds;
Replica* FindComponent(){return missing?nullptr:&replica;}static Node* Cast(Node*n){return n;}void SetFaction(int){}void SetSpawnImmediately(bool){}void SetDeleteWhenEmpty(bool){}void SetPermanentLOD(int){}
bool AddAIEntityToGroup(Node*n){if(!acceptMember)return false;members.Insert(n);return true;}void SetCompletionRadius(int r){assert(r==25);}void AddWaypoint(Node*n){holds.Insert(n);}void ActivateAI(){activated++;}};
namespace ERR_Actors{int calls=0,failAt=0,deletes=0;bool faction=true;Array<Node*>all;
Node* Spawn(string,int position,int=0){calls++;if(calls==failAt)return nullptr;auto*n=new Node;n->position=position;all.Insert(n);return n;}
bool SetFaction(Node*,int){return faction;}void DeleteSpawned(Node*n){if(!n)return;assert(!n->deleted);n->deleted++;deletes++;}}
namespace ERR_Resources{string DefendWaypoint(){return "hold";}}
namespace ERR_ForceKind{int CHECKPOINT=1;}
struct Ledger{int count=0,registered=0;bool accept=true;int Count(){return count;}bool Register(string,int,int,int strength,void*,Node*,int){assert(strength==4||strength==8);if(!accept)return false;registered++;return true;}};
struct Mode{Ledger ledger;int owned=0,groups=0;Ledger& Forces(){return ledger;}int EnemyFaction(){return 1;}void Own(Node*n){assert(n);owned++;}void RegisterGroup(Node*){groups++;}};
struct Record{int Id=0,strength=0;Node*group=nullptr;};
struct Ledger{int count=0,attempts=0,failAt=0,unregistered=0,next=0;std::map<int,Record>live;std::set<string>sources;
int Count(){return count+live.size();}
Record* Register(string id,int,int,int strength,void*,Node*group,int){assert(strength==2&&group&&group->members.Count()==2);attempts++;if(attempts==failAt)return nullptr;assert(sources.insert(id).second);int n=++next;live[n]={n,strength,group};return &live[n];}
void Unregister(int id){assert(live.erase(id)==1);unregistered++;}};
struct Mode{Ledger ledger;int owned=0,groups=0,hqGroups=0,localGroups=0;Array<Node*>ownedNodes,published;std::function<bool(Node*)>local;
Ledger& Forces(){return ledger;}int EnemyFaction(){return 1;}void Own(Node*n){assert(n&&!n->deleted&&!ownedNodes.Contains(n));owned++;ownedNodes.Insert(n);}
void RegisterGroup(Node*n){assert(n&&ownedNodes.Contains(n)&&n->members.Count()==2&&n->holds.Count()==1);groups++;published.Insert(n);if(local(n))localGroups++;else hqGroups++;}};
struct Placement{bool clear=true;bool Clear(int,void*){return clear;}};
struct Mining{int registered=0;void Register(Node*,Node*,int){registered++;}};
struct Mining{int registered=0;Node*group=nullptr;Node*hold=nullptr;void Register(Node*g,Node*h,int){registered++;group=g;hold=h;}};
struct Prop{string Prefab="cover";int Position=5,Angles=0;};
namespace ERR_RoadblockRules{'''+rule+r'''}
struct Build{
bool m_Large=false;int m_SmallPlaced=0,m_LargePlaced=0;
Mode m_Mode;Placement m_Placement;Mining m_Mining;Node* m_BuildGroup=nullptr;Node* m_BuildHold=nullptr;
Array<int> m_BuildPositions,m_Positions;Array<Prop>m_BuildCovers;Array<Node*>m_BuildSpawned,m_Groups;Array<string>m_Roster={"one","two"};
int m_BuildActors=0,m_BuildCoverCount=0,m_Centre=10,m_Right=0,m_NextSearch=0,rolledBack=0;string COVER="cover";bool playersClear=true,coversClear=true;
bool m_Large=false;int m_SmallPlaced=0,m_LargePlaced=0,m_PostSerial=0;
Mode m_Mode;Placement m_Placement;Mining m_Mining;Node* m_BuildGroup=nullptr;Node* m_BuildHold=nullptr;Node* m_BuildLocalGroup=nullptr;Node* m_BuildLocalHold=nullptr;
Array<int>m_BuildPositions,m_Positions;Array<Prop>m_BuildCovers;Array<Node*>m_BuildSpawned,m_Groups,m_LocalGroups;Array<string>m_Roster={"one","two"};
int m_BuildActors=0,m_BuildCoverCount=0,m_Centre=10,m_Right=0,m_NextSearch=0;string COVER="cover";bool playersClear=true,coversClear=true;
Build(){m_Mode.local=[this](Node*g){return LocalOnly(g);};}
bool PlayersClear(){return playersClear;}bool CoverClear(Prop){return coversClear;}
void Rollback(Array<Node*>&entities,Node*group){rolledBack+=entities.Count()+(group?1:0);}
'''+s+r'''
};
int main(){for(bool large:{false,true}){
const int guards=ERR_RoadblockRules::GuardCount(large);Array<int> positions(guards,1);Array<Prop>covers(ERR_RoadblockRules::PropCount(large));const int pieces=guards+covers.Count()+1;
const int guards=ERR_RoadblockRules::GuardCount(large);assert(guards==4);
Array<int>positions={10,20,30,40};Array<Prop>covers(ERR_RoadblockRules::PropCount(large));
const int pieces=guards+covers.Count()+2; // actors, props and two holds; neither group is in this array
const int callbacks=pieces+1; // local group has its own callback; HQ group is created by SpawnPost
auto empty=[&](Build&b){assert(!b.m_BuildGroup&&!b.m_BuildLocalGroup&&!b.m_BuildHold&&!b.m_BuildLocalHold&&b.m_BuildSpawned.Count()==0&&b.m_Positions.Count()==0&&b.m_Groups.Count()==0&&b.m_LocalGroups.Count()==0&&b.m_Mode.owned==0&&b.m_Mode.groups==0&&b.m_Mode.ledger.live.empty());};
auto finish=[&](Build&b){for(int i=0;i<callbacks;i++){int calls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==calls+1);}b.BuildPost();};
{Build b;b.m_Large=large;int start=ERR_Actors::calls;assert(b.SpawnPost(positions,covers));assert(ERR_Actors::calls==start+1);
for(int piece=0;piece<pieces;piece++){int calls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==calls+1&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);}
auto shared=b.m_BuildSpawned;for(int i=guards;i<guards+covers.Count();i++)assert(shared[i]->replica.streaming);
b.BuildPost();for(int i=guards;i<guards+covers.Count();i++)assert(!shared[i]->replica.streaming);assert(!b.m_BuildGroup&&b.m_Positions.Count()==1&&b.m_Groups.Count()==1&&b.m_Mode.owned==pieces+1&&b.m_Mode.groups==1&&b.m_Mining.registered==1&&b.m_NextSearch==2100);
Node*hq=b.m_BuildGroup;for(int piece=0;piece<callbacks;piece++){int calls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==calls+1&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);}
Node*local=b.m_BuildLocalGroup;Node*hold=b.m_BuildHold;Node*localHold=b.m_BuildLocalHold;auto shared=b.m_BuildSpawned;
assert(local&&local!=hq&&shared.Count()==pieces&&!shared.Contains(hq)&&!shared.Contains(local));
assert(hq->members.Count()==2&&local->members.Count()==2);assert(hq->members[0]==shared[0]&&hq->members[1]==shared[2]);assert(local->members[0]==shared[1]&&local->members[1]==shared[3]);
assert(hold!=localHold&&hold->position==positions[0]&&localHold->position==positions[1]);
for(int i=guards;i<guards+covers.Count();i++)assert(shared[i]->replica.streaming);
int commitCalls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==commitCalls);
for(int i=guards;i<guards+covers.Count();i++)assert(!shared[i]->replica.streaming);
assert(!b.m_BuildGroup&&!b.m_BuildLocalGroup&&b.m_Positions.Count()==1&&b.m_Groups.Count()==1&&b.m_LocalGroups.Count()==1&&b.m_Mode.owned==pieces+2&&b.m_Mode.groups==2&&b.m_Mode.hqGroups==1&&b.m_Mode.localGroups==1);
assert(b.m_Mode.published[0]==hq&&b.m_Mode.published[1]==local&&b.LocalOnly(local)&&!b.LocalOnly(hq)&&!b.LocalOnly(nullptr));
assert(hq->holds[0]==hold&&local->holds[0]==localHold&&hq->activated==1&&local->activated==1);
assert(b.m_Mining.registered==1&&b.m_Mining.group==hq&&b.m_Mining.hold==hold&&b.m_NextSearch==2100);
assert(b.m_Mode.ledger.live.size()==2&&b.m_Mode.ledger.sources.count("roadblock-1")&&b.m_Mode.ledger.sources.count("roadblock-1-local"));
assert(b.m_SmallPlaced==(large?0:1)&&b.m_LargePlaced==(large?1:0));
}
// A server-only or unregistered prop must never count as a complete roadblock.
// Unreplicated props cannot consume a quota or publish either guard pair.
for(int bad=guards;bad<guards+covers.Count();bad++)for(bool missing:{false,true}){
Build b;b.m_Large=large;b.SpawnPost(positions,covers);for(int i=0;i<pieces;i++)b.BuildPost();
Build b;b.m_Large=large;int deleted=ERR_Actors::deletes;b.SpawnPost(positions,covers);for(int i=0;i<callbacks;i++)b.BuildPost();
auto spawned=b.m_BuildSpawned;spawned[bad]->missing=missing;spawned[bad]->replica.id=-1;
b.BuildPost();assert(b.m_Positions.Count()==0&&!b.m_BuildGroup&&b.m_Mode.owned==0&&b.rolledBack==pieces+1);
for(int i=guards;i<guards+covers.Count();i++)assert(spawned[i]->replica.streaming); // reject before changing any streaming rule
b.BuildPost();empty(b);assert(ERR_Actors::deletes-deleted==pieces+2);
for(int i=guards;i<guards+covers.Count();i++)assert(spawned[i]->replica.streaming);
}
// Player approaches at any stage: no registered post and every owned piece removed.
for(int stage=0;stage<=pieces;stage++){Build b;b.m_Large=large;assert(b.SpawnPost(positions,covers));for(int i=0;i<stage;i++)b.BuildPost();b.playersClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0&&b.rolledBack==stage+1);}
// Any native entity creation failure rolls back rather than consuming a post.
for(int stage=0;stage<pieces+1;stage++){Build b;b.m_Large=large;ERR_Actors::failAt=ERR_Actors::calls+stage+1;bool begun=b.SpawnPost(positions,covers);
if(stage==0)assert(!begun);else{for(int i=0;i<stage;i++)b.BuildPost();assert(b.rolledBack==stage);}
assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);ERR_Actors::failAt=0;
// Player approach at every stage, including before local group creation and final commit.
for(int stage=0;stage<=callbacks;stage++){Build b;b.m_Large=large;int deleted=ERR_Actors::deletes;assert(b.SpawnPost(positions,covers));for(int i=0;i<stage;i++)b.BuildPost();b.playersClear=false;b.BuildPost();empty(b);assert(ERR_Actors::deletes-deleted==stage+1);}
// Every native creation failure cleans up both groups and all existing pieces exactly once.
for(int stage=0;stage<callbacks+1;stage++){Build b;b.m_Large=large;int deleted=ERR_Actors::deletes;ERR_Actors::failAt=ERR_Actors::calls+stage+1;bool begun=b.SpawnPost(positions,covers);
if(stage==0)assert(!begun);else for(int i=0;i<stage;i++)b.BuildPost();
empty(b);assert(ERR_Actors::deletes-deleted==stage);ERR_Actors::failAt=0;
}
{Build b;b.m_Large=large;b.SpawnPost(positions,covers);b.m_Placement.clear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==1);}
{Build b;b.m_Large=large;b.SpawnPost(positions,covers);for(int i=0;i<guards;i++)b.BuildPost();b.coversClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==guards+1);}
{Build b;b.m_Large=large;b.SpawnPost(positions,covers);for(int i=0;i<pieces;i++)b.BuildPost();b.m_Mode.ledger.accept=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==pieces+1&&b.m_Positions.Count()==0);}
{Build b;b.m_Large=large;b.SpawnPost(positions,covers);b.BuildPost();b.CancelPost();assert(!b.m_BuildGroup&&b.rolledBack==2&&b.m_Mode.owned==0);}
// Either ledger registration can fail; failed first IDs stay retired, next attempt is unique.
for(int failure:{1,2}){Build b;b.m_Large=large;int deleted=ERR_Actors::deletes;b.m_Mode.ledger.failAt=failure;b.SpawnPost(positions,covers);finish(b);empty(b);
assert(ERR_Actors::deletes-deleted==pieces+2&&b.m_Mode.ledger.unregistered==(failure==2?1:0)&&b.m_PostSerial==1);
b.m_Mode.ledger.failAt=0;b.SpawnPost(positions,covers);finish(b);assert(b.m_Positions.Count()==1&&b.m_PostSerial==2&&b.m_Mode.ledger.live.size()==2&&b.m_Mode.ledger.sources.count("roadblock-2")&&b.m_Mode.ledger.sources.count("roadblock-2-local"));
}
{Build b;b.m_Large=large;b.SpawnPost(positions,covers);b.BuildPost();b.m_Placement.clear=false;b.BuildPost();empty(b);}
{Build b;b.m_Large=large;b.SpawnPost(positions,covers);for(int i=0;i<guards+1;i++)b.BuildPost();b.coversClear=false;b.BuildPost();empty(b);}
for(bool factionFailure:{false,true})for(int actorSlot=0;actorSlot<4;actorSlot++){
Build b;b.m_Large=large;b.SpawnPost(positions,covers);b.BuildPost();for(int i=0;i<actorSlot;i++)b.BuildPost();
if(factionFailure)ERR_Actors::faction=false;else (actorSlot%2?b.m_BuildLocalGroup:b.m_BuildGroup)->acceptMember=false;
b.BuildPost();empty(b);ERR_Actors::faction=true;
}
for(int stage=0;stage<=callbacks;stage++){Build b;b.m_Large=large;b.SpawnPost(positions,covers);for(int i=0;i<stage;i++)b.BuildPost();int deleted=ERR_Actors::deletes;b.CancelPost();empty(b);assert(ERR_Actors::deletes-deleted==stage+1);b.CancelPost();assert(ERR_Actors::deletes-deleted==stage+1);}
// Existing scheduler cap remains 250; a full ledger cancels any partial build.
for(int stage=0;stage<=callbacks;stage++){Build b;b.m_Large=large;b.SpawnPost(positions,covers);for(int i=0;i<stage;i++)b.BuildPost();b.m_Mode.ledger.count=250;b.BuildPost();empty(b);}
{Build b;b.m_Large=large;b.m_Mode.ledger.count=250;int calls=ERR_Actors::calls;assert(!b.SpawnPost(positions,covers)&&ERR_Actors::calls==calls);}
{Build b;b.m_Large=large;b.m_Mode.ledger.count=249;assert(b.SpawnPost(positions,covers));finish(b);assert(b.m_Mode.ledger.Count()==251);}
}
for(auto*n:ERR_Actors::all)delete n;
}
''')
print('PASS: actual staged post creation spawns at most one entity per callback, commits only all four/eight guards, all planned props and waypoint, and rolls back player approach, obstructions, spawn/replication/ledger failure and cancellation; all committed props bypass distance streaming')
print('PASS: both actual staged checkpoint sizes commit four guards in two distinct strength-two groups/holds; only HQ pair enters the modeled HQ boundary and mining; one entity per callback; second-ledger failure retires first ID; all-stage approach/full-ledger/cancellation and every spawn/replication/membership failure clean up without quota consumption')
+15 -2
View File
@@ -2,7 +2,7 @@
from policy_harness import ROOT, body, run
import re
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c'
s='\n'.join(body(p,x) for x in ('protected void Layout(', 'protected void AddProp(', 'protected vector GuardPosition('))
s='\n'.join(body(p,x) for x in ('static int GuardCount(', 'protected void Layout(', 'protected void AddProp(', 'protected vector GuardPosition('))
s=s.replace('protected ', '').replace('out ', '').replace('array<ref ERR_RoadblockProp> covers', 'Array<Prop*>& covers')
s=s.replace('ResourceName ', 'string ').replace('vector ', 'Vec ').replace('ERR_Actors.Ground','Ground').replace('new ERR_RoadblockProp','new Prop')
shoulder = re.search(r'm_Shoulder = ([^;]+);', body(p, 'protected bool Candidate('))[1]
@@ -44,12 +44,25 @@ int main(){
if(p->Prefab=="barrier")barriers.push_back(along);
if(p->Prefab=="sign")assert(dot(p->Forward,l.m_Forward)*along>0); // both approaches face outward
// Guard starting volumes cannot intersect any prop in the planned post.
for(int side:{-1,1})for(int row=0;row<(large?4:2);row++){
for(int side:{-1,1})for(int row=0;row<2;row++){
Vec guard=l.GuardPosition(side,row);
Vec rel=guard-p->Position;
assert(std::abs(dot(rel,p->Right))>p->HalfWidth+.6 || std::abs(dot(rel,p->Forward))>p->HalfDepth+.6);
}
}
// Four distinct slots for both sizes: two on each shoulder. Large posts
// retain their wider approach footprint with guards at -9m and +9m.
assert(l.GuardCount(large)==4);
std::vector<Vec> guards;
for(int side:{-1,1})for(int row=0;row<2;row++){
Vec guard=l.GuardPosition(side,row);Vec rel=guard-l.m_Centre;
assert(std::abs(dot(rel,l.m_Right)-side*(l.m_Shoulder+1.5))<1e-6);
double expectedAlong=large?(row*18-9):side*(row*2-2.5);
assert(std::abs(dot(rel,l.m_Forward)-expectedAlong)<1e-6);
for(Vec other:guards)assert(dot(guard-other,guard-other)>1.);
guards.push_back(guard);
}
assert(guards.size()==4);
assert(counts["cover"]==(large?8:4)&&counts["barrier"]==(large?4:2)&&counts["sign"]==2&&counts.size()==3);
assert(barriers.size()==(large?4:2));
if(!large)assert(std::abs(barriers[0]-barriers[1])>11.9);