Complete roadblocks in background and fix session regressions

Continue safe roadblock placement after admission and prison unlock, build posts incrementally, and clean up incomplete transactions. Add bounded stuck-patrol dismount recovery, defer deployment initialization during terrain takeover, reset deferred site search timing on first player arrival, and fix atlas page serialization and terrain resource lookup.

Validation: 30 native Everon posts with 120 living guards after unlock; seven affected regression suites pass; two final restart smoke rounds complete with 21 assertions and no script/runtime failures. Existing dependency asset errors remain.
This commit is contained in:
2026-10-14 19:48:29 +02:00
parent 01994ed344
commit fda19a5200
18 changed files with 684 additions and 74 deletions
+29
View File
@@ -82,3 +82,32 @@ int main(){
}
''')
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')
# Exercise the production background activation path: waiting for the first
# player must not consume the next mandatory site's placement budget.
scenario=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c'
activation=body(scenario, 'protected void BackgroundStep()').split(' if (m_PendingSite)')[0]+'}\n'
activation=activation.replace('protected ', '').replace('ERR_GameMode mode = ERR_GameMode.Cast(GetOwner());', 'Mode* mode = GetOwner();')
for a,b in [('Replication.IsServer()', 'true'),('ERR_GameMode.Get()', 'GetOwner()'),('mode.Active()', 'mode->Active()'),('GetGame().GetPlayerManager().GetPlayerCount()', 'players'),('ERR_PrepareResult.', 'Result::')]: activation=activation.replace(a,b)
assert 'm_Generated.StartupOnly(false)' not in body(scenario,'ERR_PrepareResult Prepare(')
run(r'''
#include <cassert>
#include <string>
using string=std::string;
namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value;
int now=0,players=0;
struct Mode{bool active=true;bool Active(){return active;}};
struct Generated{int began=0,resumes=0,ticks=0;bool startup=true;void StartupOnly(bool v){assert(!v);startup=v;began=now;resumes++;}ERR_PrepareResult Tick(){assert(!startup);ticks++;return now-began>=30000?Result::FAILED:Result::PENDING;}string Error(){return "expired";}};
struct Scenario{Mode mode;Generated m_Generated;bool m_BackgroundActivated=false,m_PlacementDone=false,stopped=false,failed=false;Mode* GetOwner(){return &mode;}bool BackgroundPending(){return true;}void StopBackground(){stopped=true;}void FailBackground(string){failed=true;}
'''+activation+r'''
};
int main(){Scenario s;
for(now=0;now<3600000;now+=250)s.BackgroundStep();
assert(s.m_Generated.resumes==0&&s.m_Generated.ticks==0&&!s.failed);
players=1;s.BackgroundStep();assert(s.m_Generated.resumes==1&&s.m_Generated.ticks==1&&!s.failed&&s.m_Generated.began==now);
players=0;now+=250;s.BackgroundStep();assert(s.m_Generated.resumes==1&&s.m_Generated.ticks==2&&!s.failed);
now+=30000;s.BackgroundStep();assert(s.failed); // the active search is still bounded
Scenario ended;ended.mode.active=false;ended.BackgroundStep();assert(ended.stopped&&ended.m_Generated.resumes==0);
}
''')
print('PASS: actual background activation waits an hour empty without consuming placement time; disconnect does not reset active budget')
+82
View File
@@ -0,0 +1,82 @@
"""Execute production stuck-route timing and original-crew dismount selection."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
s=body(p,'protected void ObserveMovement(')
s=s.replace('protected ', '').replace('ERR_HunterVehicle unit','Unit* unit').replace('unit.', 'unit->')
for name in ('Entity','Waypoint','Group'):
s=s.replace(name+'.',name+'->')
s=s.replace('Physics physics','Physics* physics').replace('physics.','physics->').replace('GetPhysics().','GetPhysics()->')
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('vector ', 'Vec ').replace('ERR_HunterCombat.Active','Active').replace('ERR_HQ.Event','Event').replace('string.Format','Format')
s=s.replace('AIWaypoint->Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), retryDestination))','Spawn(retryDestination)').replace('unit->Repaths.ToString()', 'std::to_string(unit->Repaths)')
run(r'''
#include <cassert>
#include <string>
#include <cmath>
struct Vec{float x=0;float Length(){return std::abs(x);}static float DistanceXZ(Vec a,Vec b){return std::abs(a.x-b.x);}};
struct Physics{float speed=0;Vec GetVelocity(){return {speed};}};
struct Entity{Vec position;Physics physics;bool hasPhysics=true;Vec GetOrigin(){return position;}Physics* GetPhysics(){return hasPhysics?&physics:nullptr;}};
struct Group{bool contact=false;int orders=0;void AddWaypoint(void*){orders++;}};
struct Waypoint{Vec position;bool arrived=false;Vec GetOrigin(){return position;}bool IsWithinCompletionRadius(Group*){return arrived;}void SetCompletionRadius(int value){assert(value==50);}};
struct Unit{Entity* Entity=nullptr;Group* Group=nullptr;Waypoint* Waypoint=nullptr;float ProgressAt=0,NextMotionLog=0;Vec ProgressPosition,Destination{9999};int Repaths=0,clears=0;bool Released=false;std::string Source="bike";void ClearOrder(){clears++;Waypoint=nullptr;}};
bool Active(Group* g){return g->contact;}void Event(std::string,std::string){}template<class...T>std::string Format(T...){return {};}
Waypoint replacement;int spawned=0;Waypoint* Spawn(Vec dest){spawned++;replacement.position=dest;return &replacement;}
int abandoned=0;void AbandonStuckVehicle(Unit* unit){abandoned++;unit->Released=true;}
'''.replace('Entity* Entity','struct Entity* Entity').replace('Group* Group','struct Group* Group').replace('Waypoint* Waypoint','struct Waypoint* Waypoint')+s+r'''
int main(){
Entity vehicle;Group group;Waypoint waypoint;waypoint.position={1234};Unit unit;unit.Entity=&vehicle;unit.Group=&group;unit.Waypoint=&waypoint;
ObserveMovement(&unit,1000);ObserveMovement(&unit,60999);assert(spawned==0&&abandoned==0);
for(int i=1;i<=3;i++){ObserveMovement(&unit,1000+i*60000);assert(unit.Repaths==i&&spawned==i&&abandoned==0&&replacement.position.x==1234);}
ObserveMovement(&unit,240999);assert(!abandoned);
vehicle.physics.speed=4;ObserveMovement(&unit,241000);assert(!abandoned);
vehicle.physics.speed=0;vehicle.hasPhysics=false;ObserveMovement(&unit,241001);assert(!abandoned);
vehicle.hasPhysics=true;group.contact=true;ObserveMovement(&unit,241002);assert(!abandoned);
group.contact=false;ObserveMovement(&unit,301001);assert(!abandoned);
ObserveMovement(&unit,301002);assert(abandoned==1&&unit.Released&&spawned==3);
// Actual movement restores retry allowance; arrival is never a stuck patrol.
unit.Released=false;vehicle.position={20};ObserveMovement(&unit,400000);assert(unit.Repaths==0&&!unit.Released);
replacement.arrived=true;ObserveMovement(&unit,500000);assert(!unit.Released&&abandoned==1&&spawned==3);
}
''')
print('PASS: actual movement recovery retries delivered orders three times, respects combat/speed/arrival/progress, then abandons the stalled vehicle')
s=body(p,'protected void AbandonStuckVehicle(').replace('protected ', '').replace('ERR_HunterVehicle unit','Unit* unit').replace('unit.', 'unit->')
s=s.replace('SCR_AIGroupUtilityComponent utility = unit->Group.GetGroupUtilityComponent();','Utility* utility = unit->Group->GetGroupUtilityComponent();')
s=s.replace('SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit->Entity.FindComponent(SCR_AIVehicleUsageComponent));','void* usage = unit->Entity;')
s=s.replace('foreach (IEntity member : unit->Members)','for (Actor* member : unit->Members)')
s=s.replace('SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(member));','Agent* agent = member->agent;')
s=s.replace('agent.', 'agent->').replace('m_UtilityComponent.', 'm_UtilityComponent->').replace('utility.', 'utility->').replace('null','nullptr').replace('priority: ', '')
for ns in ('ERR_ConvoyRules','ERR_Actors','ERR_ConvoyMountedCrew','CompartmentAccessComponent','ERR_HQ'):s=s.replace(ns+'.',ns+'::')
run(r'''
#include <cassert>
#include <vector>
#include <string>
struct Utility{int detached=0;void RemoveUsableVehicle(void*){detached++;}};
struct Group{Utility utility;Utility* GetGroupUtilityComponent(){return &utility;}};
struct Action{};
struct AgentUtility{bool m_AIInfo=true;int exits=0;void AddAction(Action* action){exits++;delete action;}};
struct SCR_AIGetOutVehicle:Action{SCR_AIGetOutVehicle(AgentUtility*,void*,void*,int priority){assert(priority==1000);}};
struct Agent{AgentUtility* m_UtilityComponent=nullptr;};
struct Actor{bool fit=true,player=false;Group* group=nullptr;void* vehicle=nullptr;Agent* agent=nullptr;int releases=0;};
struct Unit{bool Released=false,PlayerCaptureObserved=false;struct Group* Group=nullptr;void* Entity=nullptr;std::vector<Actor*> Members;std::string Source="original bike";int releases=0;void Release(void*){releases++;Released=true;}};
void* m_Mode=nullptr;
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,Group*){return captured;}}
namespace ERR_Actors{bool CanFight(Actor*a){return a&&a->fit;}bool Player(Actor*a){return a->player;}struct Group* Group(Actor*a){return a->group;}}
namespace ERR_ConvoyMountedCrew{void Release(Actor*a){a->releases++;}}
namespace CompartmentAccessComponent{void* GetVehicleIn(Actor*a){return a->vehicle;}}
namespace ERR_HQ{void Event(std::string,std::string){}}
'''+s+r'''
int main(){Group group,other;Unit unit;unit.Group=&group;unit.Entity=&unit;
AgentUtility utility;Agent agent{&utility};Actor driver,passenger,player,unfit,transferred,foot;
for(auto*a:{&driver,&passenger,&player,&unfit,&transferred,&foot}){a->group=&group;a->vehicle=&unit;a->agent=&agent;unit.Members.push_back(a);}
player.player=true;unfit.fit=false;transferred.group=&other;foot.vehicle=nullptr;
unit.PlayerCaptureObserved=true;AbandonStuckVehicle(&unit);assert(unit.releases==0);
unit.PlayerCaptureObserved=false;ERR_ConvoyRules::captured=true;AbandonStuckVehicle(&unit);assert(unit.releases==0);
ERR_ConvoyRules::captured=false;AbandonStuckVehicle(&unit);
assert(unit.releases==1&&group.utility.detached==1&&utility.exits==2);
assert(driver.releases==1&&passenger.releases==1&&foot.releases==1);
assert(player.releases==0&&unfit.releases==0&&transferred.releases==0);
AbandonStuckVehicle(&unit);assert(unit.releases==1&&utility.exits==2);
}
''')
assert 'ObserveMovement(unit, now);\n if (unit.Released) continue;' in p.read_text()
print('PASS: actual abandonment releases once, detaches transport, requests native exits only for fit original AI aboard, and leaves players/captured crews alone')
+152
View File
@@ -0,0 +1,152 @@
"""Run the shipped initial/background scheduler with fake native work and clock."""
import re
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c'
s='\n'.join(body(p,signature) for signature in ('protected ERR_PrepareResult Finish(', 'ERR_PrepareResult Prepare(', 'protected void SearchStep(', 'protected void BackgroundStep()', 'void Stop()'))
s=re.sub(r'#ifdef WORKBENCH.*?#endif','',s,flags=re.S)
s=s.replace('protected ', '').replace('ERR_ScenarioDefinition definition', 'Definition* definition')
s=s.replace('array<vector> positions = {}, covers = {};', 'Array<int> positions, covers;')
s=s.replace('GetGame().GetCallqueue().CallLater(BackgroundStep, 500, true)', 'queue.Schedule(500)').replace('GetGame().GetCallqueue().Remove(BackgroundStep)', 'queue.Remove()')
s=s.replace('GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High)', 'Bounds(m_Low, m_High)').replace('GetGame().GetPlayerManager().GetPlayerCount()', 'players')
s=s.replace('m_Mode.Settings().Get("roadblockCount")', 'm_Mode->target').replace('m_Mode.Forces().Count()', 'm_Mode->ledger')
s=s.replace('m_Mode.', 'm_Mode->').replace('System.GetTickCount()', 'Now()').replace('Replication.IsServer()', 'server').replace('ERR_GameMode.Get()', 'current')
s=s.replace('ERR_PrepareResult.', 'Result::').replace('ERR_Phase.', 'Phase::').replace('ERR_RoadblockRules.', 'Rules::').replace('ERR_TerrainSites.Bounds', 'ValidBounds').replace('ERR_HQ.Event','Event').replace('Math.Max','std::max').replace('null','nullptr')
rules='\n'.join(body(p,sig) for sig in ('static int SearchMilliseconds(', 'static int SearchAttempts('))
run(r'''
#include <cassert>
#include <algorithm>
#include <string>
#include <vector>
using string=std::string;
namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value;
namespace Phase{enum{PREPARING,PRISON,ESCAPED,ENDED};}
namespace Rules{
'''+rules+r'''
}
int clockMs=0,players=1;bool server=true;
int Now(){return clockMs;}bool GetGame(){return true;}void Bounds(int&,int&){}bool ValidBounds(int,int){return true;}
template<class...T>void PrintFormat(T...){}void Event(string,string){}
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}void Clear(){this->clear();}};
struct Queue{int scheduled=0,removed=0;bool active=false;void Schedule(int ms){assert(ms==500);scheduled++;active=true;}void Remove(){removed++;active=false;}};
struct Mode{int target=30,ledger=0,phase=Phase::PRISON;bool active=true;int Phase(){return phase;}bool Active(){return active;}};Mode* current=nullptr;
struct Definition{};
struct Mining{bool stopped=false;void Stop(){stopped=true;}};
struct Scheduler{
Mode* m_Mode;Definition* m_Definition=nullptr;Queue queue;Mining m_Mining;
bool m_BuildGroup=false;void BuildPost(){if(m_Mode->ledger>=250)CancelPost();}void CancelPost(){m_BuildGroup=false;}
bool m_Begun=false,m_Ready=false,m_Stopped=false,m_HaveCandidate=false,m_Background=false;
int m_Target=0,m_Attempts=0,m_Started=0,m_CandidateStarted=0,m_Candidates=0,m_Planned=0;
int m_NextSearch=0,m_NextReport=0,m_BackgroundSteps=0,m_BackgroundWork=0,m_MaxBackgroundStep=0,m_Low=0,m_High=0;
Array<int>m_Loaded,m_Positions,m_Groups;
int candidateCost=0,candidateCalls=0,planCalls=0,spawnCalls=0;bool accept=false,spawn=true,roster=true;int plan=2;
string Status(){return "status";}bool LoadRoster(){m_Loaded.push_back(1);return roster;}
bool Candidate(Definition*){candidateCalls++;clockMs+=candidateCost;if(accept){m_Candidates++;m_CandidateStarted=Now();}return accept;}
int Plan(Array<int>&,Array<int>&){planCalls++;return plan;}
bool SpawnPost(Array<int>&,Array<int>&){spawnCalls++;if(spawn){m_Groups.push_back(1);m_Positions.push_back(1);m_NextSearch=Now()+2000;}return spawn;}
'''+s+r'''
};
int main(){
Mode mode;current=&mode;Definition definition;Scheduler s;s.m_Mode=&mode;
// No candidates in the entire initial window: admission yields, never gives up.
assert(s.Prepare(&definition)==Result::PENDING&&s.candidateCalls==8);
clockMs=12000;assert(s.Prepare(&definition)==Result::READY&&s.m_Background&&s.queue.active&&s.m_Definition==&definition);
assert(s.m_Loaded.Count()==1&&s.m_Positions.Count()==0&&s.queue.scheduled==1);
assert(s.Prepare(&definition)==Result::READY&&s.queue.scheduled==1);
// Empty, full ledger and inactive/initializing cases cannot do terrain work.
players=0;s.BackgroundStep();assert(s.candidateCalls==8);
players=1;mode.ledger=250;s.BackgroundStep();assert(s.candidateCalls==8);mode.ledger=0;
mode.phase=Phase::PREPARING;s.BackgroundStep();assert(s.candidateCalls==8);mode.phase=Phase::ESCAPED;
// Native operations yield between samples if the cooperative time budget expires.
s.candidateCost=3;s.BackgroundStep();assert(s.candidateCalls==9&&s.m_MaxBackgroundStep==3);
s.candidateCost=0;s.BackgroundStep();assert(s.candidateCalls==17);
// Arbitrarily many exhausted attempts do not end background placement.
s.m_Attempts=50000;clockMs+=60000;s.BackgroundStep();assert(s.m_Background&&s.m_Attempts==50008);
s.accept=true;
// Candidate planning/physical creation occur on a separate callback.
s.BackgroundStep();assert(s.m_HaveCandidate&&s.planCalls==0);
s.plan=1;clockMs+=500;s.BackgroundStep();assert(s.m_HaveCandidate&&s.spawnCalls==0);
clockMs+=2000;s.BackgroundStep();assert(!s.m_HaveCandidate&&s.spawnCalls==0);s.plan=2;
// Failed creation remains pending. No partial count or allowance is consumed.
s.spawn=false;s.BackgroundStep();s.BackgroundStep();assert(s.m_Positions.Count()==0&&s.m_Background);s.spawn=true;
// Finish all 30 after prison unlock, at most one physical post per callback.
for(int i=0;i<30;i++){
s.BackgroundStep();assert(s.m_Positions.Count()==i);
s.BackgroundStep();assert(s.m_Positions.Count()==i+1);
int samples=s.candidateCalls;s.BackgroundStep();assert(s.candidateCalls==samples); // two-second spawn cooldown
clockMs+=2000;
}
assert(!s.m_Background&&!s.queue.active&&s.m_Loaded.Count()==0&&!s.m_Definition&&s.m_Ready);
int calls=s.spawnCalls;s.m_Groups.clear(); // killed/removed groups never refund the round's posts
s.BackgroundStep();assert(s.spawnCalls==calls&&s.Prepare(&definition)==Result::READY&&s.m_Positions.Count()==30);
// A newly full ledger must cancel a partial post instead of leaving actors stranded.
{Scheduler pending;pending.m_Mode=&mode;pending.Prepare(&definition);clockMs+=12000;pending.Prepare(&definition);
pending.m_BuildGroup=true;mode.ledger=250;pending.BackgroundStep();assert(!pending.m_BuildGroup);mode.ledger=0;}
// Attempt ceiling also hands off, without waiting for the wall-clock deadline.
clockMs=0;Scheduler attempts;attempts.m_Mode=&mode;attempts.Prepare(&definition);attempts.m_Attempts=300;
assert(attempts.Prepare(&definition)==Result::READY&&attempts.m_Background);
mode.active=false;attempts.BackgroundStep();assert(attempts.m_Stopped&&!attempts.queue.active&&attempts.m_Mining.stopped&&!attempts.m_Definition);
mode.active=true;Scheduler ended;ended.m_Mode=&mode;ended.Prepare(&definition);clockMs=12000;ended.Prepare(&definition);current=nullptr;
ended.BackgroundStep();assert(ended.m_Stopped&&!ended.queue.active);current=&mode;
mode.target=0;Scheduler disabled;disabled.m_Mode=&mode;assert(disabled.Prepare(&definition)==Result::READY&&!disabled.queue.active);
mode.target=30;Scheduler missing;missing.m_Mode=&mode;missing.roster=false;assert(missing.Prepare(&definition)==Result::READY&&!missing.queue.active);
}
''')
# Every background plan and physical transaction still uses the original safety checks.
assert 'if (!mesh || !PlayersClear()) return 0;' in body(p,'protected int Plan(')
assert 'if (!PlayersClear() || m_Mode.Forces().Count() >= 250 ||' in body(p,'protected bool SpawnPost(')
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 void BuildPost()'))
s=s.replace('protected ', '').replace('array<vector> positions','Array<int>& positions').replace('array<vector> covers','Array<int>& covers')
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('m_Forward.VectorToAngles()', '0').replace('m_Positions.Count().ToString()', 'std::to_string(m_Positions.Count())')
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 ns in ('ERR_Actors','ERR_Resources','ERR_ForceKind'):s=s.replace(ns+'.',ns+'::')
run(r'''
#include <cassert>
#include <string>
#include <vector>
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);}};
struct Node{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;}}
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);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 Placement{bool clear=true;bool Clear(int,void*){return clear;}};
struct Mining{int registered=0;void Register(Node*,Node*,int){registered++;}};
struct Build{
Mode m_Mode;Placement m_Placement;Mining m_Mining;Node* m_BuildGroup=nullptr;Node* m_BuildHold=nullptr;
Array<int> m_BuildPositions,m_BuildCovers,m_Positions;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 PlayersClear(){return playersClear;}bool CoverClear(int){return coversClear;}
void Rollback(Array<Node*>&entities,Node*group){rolledBack+=entities.Count()+(group?1:0);}
'''+s+r'''
};
int main(){Array<int> positions={1,2,3,4},covers={5,6};
{Build b;int start=ERR_Actors::calls;assert(b.SpawnPost(positions,covers));assert(ERR_Actors::calls==start+1);
for(int piece=0;piece<7;piece++){int calls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==calls+1&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);}
b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==1&&b.m_Groups.Count()==1&&b.m_Mode.owned==8&&b.m_Mode.groups==1&&b.m_Mining.registered==1&&b.m_NextSearch==2100);
}
// Player approaches at any stage: no registered post and every owned piece removed.
for(int stage=0;stage<=7;stage++){Build b;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<8;stage++){Build b;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;
}
{Build b;b.SpawnPost(positions,covers);b.m_Placement.clear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==1);}
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<4;i++)b.BuildPost();b.coversClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==5);}
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<7;i++)b.BuildPost();b.m_Mode.ledger.accept=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==8&&b.m_Positions.Count()==0);}
{Build b;b.SpawnPost(positions,covers);b.BuildPost();b.CancelPost();assert(!b.m_BuildGroup&&b.rolledBack==2&&b.m_Mode.owned==0);}
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 guards/two covers/waypoint, and rolls back player approach, obstructions, spawn/ledger failure and cancellation')
+8 -6
View File
@@ -1,9 +1,10 @@
"""Execute road suitability/spacing and validate admin/settings integration."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c'
code=body(p,'static bool Road(')+'\n'+body(p,'static bool Spaced(')
code=code.replace('array<vector> used','Array<Vec>* used').replace('vector ', 'Vec ').replace('foreach (','for (').replace(' : used)', ' : *used)').replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_ForceRules.FiniteVector','Finite').replace('ERR_ForceRules.Finite','std::isfinite').replace('Math.AbsFloat','std::abs')
code=body(p,'static int SearchMilliseconds(')+'\n'+body(p,'static int SearchAttempts(')+'\n'+body(p,'static bool Road(')+'\n'+body(p,'static bool Spaced(')
code=code.replace('array<vector> used','Array<Vec>* used').replace('vector ', 'Vec ').replace('foreach (','for (').replace(' : used)', ' : *used)').replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_ForceRules.FiniteVector','Finite').replace('ERR_ForceRules.Finite','std::isfinite').replace('Math.AbsFloat','std::abs').replace('Math.Clamp','std::clamp')
run(r'''
#include <algorithm>
#include <cassert>
#include <cmath>
#include <limits>
@@ -13,6 +14,7 @@ bool Finite(Vec v){return std::isfinite(v.x)&&std::isfinite(v.y)&&std::isfinite(
template<class T>using Array=std::vector<T>;
'''+code+r'''
int main(){
assert(SearchMilliseconds()==12000 && SearchAttempts()==300);
float nan=std::numeric_limits<float>::quiet_NaN();
assert(Road(4,40,2)&&Road(16,100,-2));
assert(!Road(3.9,100,0)&&!Road(17,100,0)&&!Road(6,39,0));
@@ -38,10 +40,10 @@ valid=re.search(r'if \(key == "roadblockCount"\) return ([^;]+);',body(settings,
default=re.search(r'if \(key == "roadblockCount"\) return ([^;]+);',body(settings,'static int AddedDefault('))[1]
run('#include <cassert>\nbool valid(int value){return '+valid+';}\nint main(){assert(!valid(-1)&&valid(0)&&valid(4)&&valid(12)&&valid(13)&&valid(30)&&!valid(31));assert('+default+'==4);}')
s=p.read_text()
assert s.index('m_Mode.Forces().Register(')<s.index('m_Mode.RegisterGroup(group)')
assert 'm_Groups.Insert(group)' in s and 'm_Mode.Own(ownedEntity)' in s
assert 'm_Attempts >= 300' in s and '>= 12000' in s and 'm_Ready) return ERR_PrepareResult.READY' in s
assert s.index('m_Mode.Forces().Register(')<s.index('m_Mode.RegisterGroup(m_BuildGroup)')
assert 'm_Groups.Insert(m_BuildGroup)' in s and 'm_Mode.Own(ownedEntity)' in s
assert 'm_Attempts >= ERR_RoadblockRules.SearchAttempts()' in s and '>= ERR_RoadblockRules.SearchMilliseconds()' in s and 'm_Ready) return ERR_PrepareResult.READY' in s
assert 'if (m_Target == 0) return Finish("disabled")' in s
assert 'm_Groups.Count() >= m_Target' in s
assert 'm_Positions.Count() >= m_Target' in s
assert 'roadblockCount' not in body(settings,'static bool Randomizable(')
print('PASS: road width/bridge/length checks, prison and post spacing, setting bounds/default, finite startup and owned HQ admission')
+23
View File
@@ -72,3 +72,26 @@ int main(){
}
''')
print('PASS: shipped sheet geometry covers square, rectangular and small terrains with overlap and shifted origins; regional grids cover all detail pages with round-trip numbering; Chernarus has 12 x 9 fixed detail sheets')
component=ROOT/'addons/core/Scripts/Game/ERR/30_Map/ERRM_SharedMapComponent.c'
wire='\n'.join(body(component,sig) for sig in ('override bool RplSave(', 'override bool RplLoad('))
wire=wire.replace('override ', '').replace('ScriptBitWriter writer', 'ScriptBitWriter& writer').replace('ScriptBitReader reader','ScriptBitReader& reader').replace('super.','Native::')
run(r'''
#include <cassert>
#include <cstdint>
#include <vector>
struct ScriptBitWriter{std::vector<int32_t> data;void WriteInt(int value){data.push_back(value);}};
struct ScriptBitReader{std::vector<int32_t> data;bool ReadInt(int& value){if(data.empty())return false;value=data[0];data.erase(data.begin());return true;}};
struct Native{int saves=0,loads=0;bool RplSave(ScriptBitWriter&){saves++;return true;}bool RplLoad(ScriptBitReader&){loads++;return true;}};
struct Map:Native{int m_ESMPage=-1,m_ESMLastSelectedPage=-1;
'''+wire+r'''
};
int main(){
for(int page:{0,4097,16777214,16777215,16777216,16777227,16777615}){
Map source,target;source.m_ESMPage=page;ScriptBitWriter writer;assert(source.RplSave(writer)&&source.saves==1);
ScriptBitReader reader{writer.data};assert(target.RplLoad(reader)&&target.loads==1&&target.m_ESMPage==page&&target.m_ESMLastSelectedPage==page);
}
Map missing;ScriptBitReader empty;assert(!missing.RplLoad(empty)&&missing.loads==0&&missing.m_ESMLastSelectedPage==-1);
}
''')
print('PASS: actual map replication round-trips cover, overview, detail and regional pages, including the failing server value 16777227; truncated reads fail')
+62
View File
@@ -0,0 +1,62 @@
"""Execute native deployment init with delayed mode arrival and teardown."""
from policy_harness import ROOT, body, run
p = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainControllerInit.c'
for kind in ('Deploy',):
protected = 'protected ' if kind == 'Deploy' else ''
init_signature = 'protected override void OnPostInit(' if kind == 'Deploy' else '\n override void OnPostInit('
code = '\n'.join(body(p, signature) for signature in (
init_signature, f'protected void ERR_Retry{kind}Init(', f'override {protected}void OnDelete('))
for before, after in (
('protected ', ''), ('override ', ''), ('IEntity owner', 'IEntity* owner'),
('owner.', 'owner->'), ('GetGame().', 'GetGame()->'), ('GetCallqueue().', 'GetCallqueue()->'),
('super.', 'Base::'), ('null', 'nullptr'), ('LogLevel.ERROR', '1'),
(f'CallLater(ERR_Retry{kind}Init,', 'CallLater(1,'),
(f'Remove(ERR_Retry{kind}Init)', 'Remove(1)'),
):
code = code.replace(before, after)
fields = f'''
IEntity* m_ERRDeferred{kind}Owner=nullptr;
World* m_ERRDeferred{kind}World=nullptr;
int m_ERR{kind}InitAttempts=0;
bool m_ERR{kind}InitComplete=false;
'''
run(r'''
#include <cassert>
struct World{};
struct IEntity{World* world;World* GetWorld(){return world;}};
struct Queue{int scheduled=0,removed=0;void CallLater(int,int delay,bool repeat){assert(delay==50&&!repeat);scheduled++;}void Remove(int){removed++;}};
struct Game{World* world=nullptr;bool pending=false,mode=false;Queue queue;
bool ERR_ClientTerrainModePending(){return pending;}bool GetGameMode(){return mode;}
World* GetWorld(){return world;}Queue* GetCallqueue(){return &queue;}} game;
Game* current=&game;Game* GetGame(){return current;}
void Print(const char*,int=0){}
struct Base{int initialized=0,deleted=0;void OnPostInit(IEntity*){initialized++;}void OnDelete(IEntity*){deleted++;}};
struct Component:Base{
''' + fields + code + r'''
};
int main(){
World first,next;IEntity owner{&first};game.world=&first;
// Ordinary startup still calls native initialization immediately, only once.
game.mode=true;Component ordinary;ordinary.OnPostInit(&owner);ordinary.OnPostInit(&owner);
assert(ordinary.initialized==1&&game.queue.scheduled==0);
game.pending=true;game.mode=false;Component delayed;
delayed.OnPostInit(&owner);delayed.OnPostInit(&owner);
assert(delayed.initialized==0&&game.queue.scheduled==1);
delayed.RETRY();assert(delayed.initialized==0&&game.queue.scheduled==2);
game.mode=true;game.pending=false;delayed.RETRY();delayed.RETRY();delayed.OnPostInit(&owner);
assert(delayed.initialized==1);
// Deletion cancels pending initialization; even a late callback is harmless.
game.pending=true;game.mode=false;Component deleted;deleted.OnPostInit(&owner);deleted.OnDelete(&owner);
game.mode=true;deleted.RETRY();assert(deleted.initialized==0&&deleted.deleted==1&&game.queue.removed==1);
// World replacement must not initialize a controller from the outgoing round.
game.mode=false;Component stale;stale.OnPostInit(&owner);game.world=&next;game.mode=true;stale.RETRY();
assert(stale.initialized==0);
game.world=&first;game.mode=false;Component timeout;int before=game.queue.scheduled;timeout.OnPostInit(&owner);
for(int i=0;i<250;i++)timeout.RETRY();
assert(timeout.initialized==0&&game.queue.scheduled-before==200);
game.mode=true;timeout.RETRY();assert(timeout.initialized==0);
}
'''.replace('RETRY', f'ERR_Retry{kind}Init'))
assert 'modded class SCR_RespawnComponent' not in p.read_text(), 'Native respawn RPC registration must stay immediate'
print('PASS: deployment init waits for mode, runs once, cancels on deletion/world change and bounds retries; respawn RPC registration stays immediate')
+15 -10
View File
@@ -121,6 +121,8 @@ struct string:std::string {
void Replace(string a,string b){size_t n=0;while((n=find(a,n))!=npos){replace(n,a.size(),b);n+=b.size();}}
string Trim(){auto a=find_first_not_of(" \r\n");if(a==npos)return "";return substr(a,find_last_not_of(" \r\n")-a+1);}
string GetPath(){auto n=find('}');return n==npos?*this:string(substr(n+1));}
bool IsExternal(){return size()>18&&front()=='{'&&at(17)=='}';}
bool IsInternal(){return size()==18&&front()=='{'&&at(17)=='}';}
};
string StripPath(string s){auto n=s.find_last_of('/');return n==s.npos?s:string(s.substr(n+1));}
template<class T>struct Array:std::vector<T>{void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
@@ -128,16 +130,19 @@ struct Container{string parent;bool Get(string& p){p=parent;return !p.IsEmpty();
struct Resource{Container container;bool valid=true;bool IsValid(){return valid;}Resource* GetResource(){return this;}Container* ToBaseContainer(){return &container;}};
std::map<string,Resource> resources;int calls=0;
Resource* Load(string world){calls++;auto it=resources.find(world);return it==resources.end()?nullptr:&it->second;}
string Res(string path){return "{0123456789ABCDEF}"+path;}
struct Names {
'''+resolver+r'''
};
int main(){
resources["Worlds/GM/Conflict.ent"].container.parent="Worlds/Armenhof/Armenhof.ent";
resources["Worlds/Armenhof/Armenhof.ent"];
assert(Names::Resolve("Worlds/GM/Conflict.ent")=="Armenhof");
resources["Worlds/Nogova/Nogova_Test.ent"].container.parent="Worlds/Nogova/Nogova.ent";
resources["Worlds/Nogova/Nogova.ent"];
assert(Names::Resolve("Worlds/Nogova/Nogova_Test.ent")=="Nogova");
resources[Res("Worlds/GM/Conflict.ent")].container.parent=Res("Worlds/Armenhof/Armenhof.ent");
resources[Res("Worlds/Armenhof/Armenhof.ent")];
assert(Names::Resolve(Res("Worlds/GM/Conflict.ent"))=="Armenhof");
resources[Res("Worlds/Nogova/Nogova_Test.ent")].container.parent=Res("Worlds/Nogova/Nogova.ent");
resources[Res("Worlds/Nogova/Nogova.ent")];
assert(Names::Resolve(Res("Worlds/Nogova/Nogova_Test.ent"))=="Nogova");
calls=0;assert(Names::Resolve("worlds/Eden/Eden.ent")=="Everon"&&calls==0);
assert(Names::Resolve("$NogovaReforger:Worlds/Nogova/Nogova.ent")=="Nogova"&&calls==0);
assert(Names::Label("{853E92315D1D9EFE}worlds/Eden/Eden.ent")=="Everon");
assert(Names::Resolve("Worlds/New_Island.ent")=="New Island");
assert(Names::Label("Worlds/GameMaster/GM_Armenhof.ent")=="Armenhof");
@@ -150,10 +155,10 @@ int main(){
assert(Names::Path("{bad}Worlds/Island.ent")=="{bad}Worlds/Island.ent");
assert(Names::Label("$NogovaReforger:Worlds/Nogova/Nogova_Test.ent")=="Nogova");
resources["a.ent"].container.parent="b.ent";resources["b.ent"].container.parent="a.ent";
calls=0;assert(Names::Resolve("a.ent")=="B"&&calls==2);
for(int i=0;i<30;i++)resources[std::to_string(i)+".ent"].container.parent=std::to_string(i+1)+".ent";
calls=0;assert(Names::Resolve("0.ent")=="16"&&calls==16);
resources[Res("a.ent")].container.parent=Res("b.ent");resources[Res("b.ent")].container.parent=Res("a.ent");
calls=0;assert(Names::Resolve(Res("a.ent"))=="B"&&calls==2);
for(int i=0;i<30;i++)resources[Res(std::to_string(i)+".ent")].container.parent=Res(std::to_string(i+1)+".ent");
calls=0;assert(Names::Resolve(Res("0.ent"))=="16"&&calls==16);
}
''')
print('PASS: actual terrain parent traversal, Armenhof/Nogova names, Everon alias, missing-resource fallback, empty names and bounded cycles/depth')