Files
EscapistsReintroduced/tests/test_infantry_resupply_coordinator.py
T
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

135 lines
12 KiB
Python

"""Actual coordinator decisions/transitions with native boundaries doubled; no engine proof."""
import os,re,sys
from pathlib import Path
REPO=Path(os.environ.get('ERR_REPO','/home/jakub/devel/escapistsrereintroduced'))
sys.path.insert(0,str(REPO/'tests'))
from policy_harness import body,run
SOURCE=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',REPO/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
def method(signature):
s=body(SOURCE,signature).replace('protected ','').replace('null','nullptr')
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_ForceRules.','ERR_ForceRules::').replace('ERR_InfantryResupplyRules.','ERR_InfantryResupplyRules::').replace('ERR_Actors.','ERR_Actors::')
s=s.replace(' + "3 0 0"',' + Vec{3,0,0}')
s=re.sub(r'string\.Format\("walk to reported stopped ammo truck at %1", m_Rendezvous\)', '"rendezvous"', s)
for kind in ['ERR_HQWatchEntry','Physics']:
s=re.sub(r'\b'+kind+r' (\w+)',kind+r'* \1',s)
# Enforce native entity/reference member access becomes C++ pointer member access.
for name in ['entry','recipient','donor','stopped','m_HQ','m_Move','m_Recipient','m_Donor','m_Truck','m_Order','physics']:
s=s.replace(name+'.',name+'->')
for name in ['AmmoState','Group','Combat','Entity']:
s=s.replace(name+'.',name+'->')
s=s.replace('Watch().','Watch()->').replace('GetVelocity().','GetVelocity().').replace('GetAngularVelocity().','GetAngularVelocity().')
return s
rules='\n'.join(method(x) for x in ['static bool Fresh','static bool Need','static bool Pair'])
methods='\n'.join(method(x) for x in ['protected void ClearWaypoints','protected void Finish','void Tick(float now)','void Stop()'])
code=r'''
#include <cassert>
#include <cmath>
#include <limits>
#include <string>
#include <vector>
#include <algorithm>
using string=std::string;
struct Vec {float x=0,y=0,z=0; float Length(){return std::sqrt(x*x+y*y+z*z);} static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);} Vec operator+(Vec b){return {x+b.x,y+b.y,z+b.z};} string ToString(){return "position";}};
struct ERR_ForceRules {static bool Finite(float n){return std::isfinite(n);} static bool FiniteVector(Vec v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}};
struct Ammo {bool Complete=true,valid=true;int Members=2,SmallArmsMuzzles=2,SmallArmsRounds=60,SmallArmsChambers=0;bool Valid(int){return valid;}};
struct ERR_HQWatchEntry {bool Closed=false;int Count=2,Fit=2,SourceId=1;float ObservedAt=0;Vec Position;Ammo* AmmoState=nullptr;};
struct ERR_InfantryResupplyRules {
RULES
};
struct Waypoint {int id;};
struct Group {std::vector<Waypoint*> points;void RemoveWaypoint(Waypoint* p){points.erase(std::remove(points.begin(),points.end(),p),points.end());}};
struct ERR_Actors {static inline int deleted=0;static void DeleteSpawned(Waypoint*){deleted++;}};
struct Physics {Vec linear,angular;Vec GetVelocity(){return linear;} Vec GetAngularVelocity(){return angular;}};
struct Entity {Physics* physics=nullptr;Physics* GetPhysics(){return physics;}};
struct Combat {bool Held=false;};
struct Truck {int SupplyToken=7,clears=0;bool SupplyHeld=false;struct Combat* Combat=nullptr;struct Entity* Entity=nullptr;void ClearOrder(){clears++;}};
struct Unit {struct Group* Group=nullptr;int ForceSourceId=1;float NextStatus=-1;};
struct Leaf {int stops=0;bool unresolved=true;void Stop(){stops++;}bool Unresolved(){return unresolved;}};
struct Route {int result=0,advances=0;int Advance(float,int){advances++;return result;}};
struct Watch {ERR_HQWatchEntry* entry=nullptr;ERR_HQWatchEntry* Find(int){return entry;}};
struct HQ {int token=7,releases=0,downlinks=0;struct Watch watch;Route* next=nullptr;int Journal(){return 0;}int LogisticsToken(Group*){return token;}void ReleaseLogistics(Group*,int owned){releases++;if(token==owned)token=0;}struct Watch* Watch(){return &watch;} Route* Downlink(Group*,string,string,int){downlinks++;return next;}};
struct Service {
HQ* m_HQ;Truck* m_Truck;Unit* m_Recipient;Unit* m_Donor;Leaf* m_Move=nullptr;Route* m_Order=nullptr;Entity* m_Receiving=nullptr;
Waypoint* m_MoveWaypoint=nullptr;Waypoint* m_HoldWaypoint=nullptr;Vec m_Rendezvous;int m_State=2,m_Token=7,m_ReportId=11;
float m_NextTick=0,m_Deadline=200000,m_HeldAt=-1;bool m_Stopped=false,valid=true,quiet=true,roster=true,waypoints=true;int audits=0,transfers=0,waypointCalls=0,stocks=0;
bool Current(){return !m_Stopped;}bool TruckValid(){return valid;}bool Quiet(Unit*){return quiet;}bool Roster(){return roster;}void Audit(string){audits++;}void Stock(float){stocks++;}bool Waypoints(){waypointCalls++;return waypoints;}void Transfer(float){transfers++;}
METHODS
};
struct Fixture {Physics physics;Entity entity{&physics};Combat combat;Truck truck;Group group,donorGroup;Unit recipient{&group},donor{&donorGroup};HQ hq;Route order,next;Service s;
Fixture(){truck.Combat=&combat;truck.Entity=&entity;s.m_HQ=&hq;s.m_Truck=&truck;s.m_Recipient=&recipient;s.m_Donor=&donor;s.m_Order=&order;hq.next=&next;}
};
int main(){
Ammo ammo;ERR_HQWatchEntry demand,donor;demand.AmmoState=&ammo;donor.SourceId=2;donor.Position={200,500,0};
assert(ERR_InfantryResupplyRules::Need(&demand,120000));assert(ERR_InfantryResupplyRules::Pair(&demand,&donor,120000));
assert(!ERR_InfantryResupplyRules::Fresh(&demand,120001));assert(!ERR_InfantryResupplyRules::Fresh(nullptr,0));
demand.ObservedAt=1;assert(!ERR_InfantryResupplyRules::Fresh(&demand,0));demand.ObservedAt=0;
ammo.SmallArmsChambers=1;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.SmallArmsChambers=0;
ammo.Complete=false;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.Complete=true;
ammo.Members=1;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.Members=2;
ammo.valid=false;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.valid=true;
donor.SourceId=1;assert(!ERR_InfantryResupplyRules::Pair(&demand,&donor,0));donor.SourceId=2;donor.Position.x=201;assert(!ERR_InfantryResupplyRules::Pair(&demand,&donor,0));
demand.Position.x=std::numeric_limits<float>::quiet_NaN();assert(!ERR_InfantryResupplyRules::Need(&demand,0));demand.Position.x=0;
{Fixture f;f.s.Tick(0);assert(f.s.m_State==2&&!f.truck.SupplyHeld&&f.truck.clears==0&&f.s.waypointCalls==0);f.order.result=1;f.s.Tick(1000);assert(f.s.m_State==3&&f.truck.SupplyHeld&&f.truck.clears==1&&f.s.m_Order==nullptr);f.physics.linear.x=1;f.s.Tick(2000);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);f.physics.linear.x=0;f.s.Tick(3000);assert(f.s.m_HeldAt==3000&&f.donor.NextStatus==3000);ERR_HQWatchEntry report;report.Position={20,0,5};report.ObservedAt=2999;f.hq.watch.entry=&report;f.s.Tick(4000);assert(f.hq.downlinks==0);report.ObservedAt=3000;f.s.Tick(5000);assert(f.s.m_State==4&&f.hq.downlinks==1&&f.s.m_Rendezvous.x==23);f.s.Tick(6000);assert(f.s.m_State==4&&f.s.waypointCalls==0&&f.s.transfers==0);f.next.result=1;f.s.Tick(7000);assert(f.s.m_State==5&&f.s.waypointCalls==1&&f.s.transfers==1);}
{Fixture f;f.order.result=-1;f.s.Tick(0);assert(f.s.m_State==1&&f.hq.token==0&&f.truck.SupplyToken==0&&!f.truck.SupplyHeld&&f.truck.clears==0);}
{Fixture f;f.s.m_State=4;f.order.result=-1;f.s.Tick(0);assert(f.s.waypointCalls==0&&f.s.transfers==0&&f.hq.token==0);}
{Fixture f;f.hq.token=99;f.truck.SupplyToken=99;f.truck.SupplyHeld=true;f.s.Tick(0);assert(f.s.m_State==1&&f.hq.token==99&&f.truck.SupplyToken==99&&f.truck.SupplyHeld);}
{Fixture f;Waypoint own1{1},own2{2},other{3};f.group.points={&own1,&other,&own2};f.s.m_MoveWaypoint=&own1;f.s.m_HoldWaypoint=&own2;Leaf leaf;f.s.m_Move=&leaf;f.truck.SupplyHeld=true;f.s.Stop();assert(f.group.points.size()==1&&f.group.points[0]==&other&&leaf.stops==1&&f.s.m_Move==&leaf&&f.hq.token==0&&!f.truck.SupplyHeld&&f.s.m_Stopped);int audits=f.s.audits;f.s.Stop();f.s.Tick(5000);assert(f.s.audits==audits&&leaf.stops==1);}
{Fixture f;Leaf leaf;f.s.m_Move=&leaf;f.s.Finish("timeout");f.s.Tick(0);assert(f.s.m_Move==&leaf);leaf.unresolved=false;f.s.Tick(1000);assert(f.s.m_Move==nullptr);}
{Fixture f;f.s.m_State=3;f.physics.linear.x=std::numeric_limits<float>::quiet_NaN();f.s.Tick(0);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);}
{Fixture f;f.s.m_State=3;f.physics.angular.x=std::numeric_limits<float>::quiet_NaN();f.s.Tick(0);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);}
{Fixture f;f.s.m_Deadline=0;f.s.Tick(0);assert(f.s.m_State==1&&f.order.advances==0&&f.hq.token==0);}
// Waiting for physical transfer never extends the original bounded rendezvous deadline.
{Fixture f;f.s.m_State=5;f.s.m_Deadline=1000;f.s.Tick(0);assert(f.s.transfers==1&&f.s.m_State==5);
f.s.Tick(1000);assert(f.s.transfers==1&&f.s.m_State==1&&f.hq.token==0&&!f.truck.SupplyHeld);}
{Fixture f;f.s.quiet=false;f.order.result=1;f.s.Tick(0);assert(f.s.m_State==1&&f.order.advances==0&&!f.truck.SupplyHeld);}
{Fixture f;f.s.m_State=4;f.order.result=1;f.s.waypoints=false;f.s.Tick(0);assert(f.s.m_State==1&&f.s.waypointCalls==1&&f.s.transfers==0&&f.hq.token==0);}
{Fixture f;f.s.m_State=3;f.s.m_HeldAt=100;ERR_HQWatchEntry report;report.ObservedAt=100;f.hq.watch.entry=&report;f.hq.next=nullptr;f.s.Tick(1000);assert(f.s.m_State==1&&f.hq.downlinks==1&&f.s.waypointCalls==0&&f.hq.token==0);}
}
'''.replace('RULES',rules).replace('METHODS',methods)
run(code)
print('infantry resupply actual rules, radio transitions, owned cleanup and unresolved-operation tests passed')
# Recheck permanent initial-roster eligibility during an active trip.
src=SOURCE
s=body(src,'protected bool Roster()').replace('protected ','')
s=s.replace('array<AIAgent> agents = {};','Array<Agent*> agents;')
s=s.replace('foreach (AIAgent agent : agents)','for (Agent* agent : agents)').replace('foreach (IEntity actor : m_Members)','for (Actor* actor : m_Members)')
for x in ['m_Recipient','agent']:s=s.replace(x+'.',x+'->')
s=s.replace('StatusSampler.','StatusSampler->').replace('Group.','Group->')
for x in ['ERR_Actors','SCR_FactionManager','CompartmentAccessComponent']:s=s.replace(x+'.',x+'::')
s=s.replace('m_Mode.','m_Mode->').replace('bool living;', 'bool living = false;')
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
template<class T> struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}int Count(){return this->size();}bool Contains(T a){return std::find(this->begin(),this->end(),a)!=this->end();}};
struct Group;
struct Actor{bool alive=true,player=false,fight=true;Group* group=nullptr;int faction=1;};
struct Agent{Actor* actor;Actor* GetControlledEntity(){return actor;}};
struct Group{Array<Agent*> agents;void GetAgents(Array<Agent*>& out){out=agents;}};
struct Sampler{Group* group;bool released=false;int strength=1;Array<Actor*> original;int InitialStrength(){return strength;}Actor* InitialMember(int slot){return slot>=0&&slot<(int)original.size()?original[slot]:nullptr;}bool OwnsRoster(Group* g){return g==group;}bool CanAttribute(int,Actor*){return !released;}};
struct Unit{struct Group* Group;Sampler* StatusSampler;};
struct Mode{int EnemyFaction(){return 1;}};
struct ERR_Actors{static bool Alive(Actor* a){return a->alive;}static bool Player(Actor* a){return a->player;}static struct Group* Group(Actor* a){return a->group;}static bool CanFight(Actor* a){return a->fight;}};
struct SCR_FactionManager{static int SGetFaction(Actor* a){return a->faction;}};
struct CompartmentAccessComponent{static bool GetVehicleIn(Actor*){return false;}};
struct Service{Unit* m_Recipient;Mode* m_Mode;Array<Actor*> m_Members;bool Quiet(Unit*){return true;}
METHOD
};
int main(){Group group;Actor soldier;soldier.group=&group;Agent agent{&soldier};group.agents={&agent};Sampler sampler{&group,false,1,{&soldier}};Unit unit{&group,&sampler};Mode mode;Service service{&unit,&mode,{&soldier}};assert(service.Roster());sampler.released=true;assert(!sampler.CanAttribute(0,&soldier));
// Permanent sampler release survives returning to the physical group.
assert(!service.Roster());sampler.released=false;assert(service.Roster());
sampler.strength=33;assert(!service.Roster());sampler.strength=0;assert(!service.Roster());sampler.strength=1;
Actor foreign;foreign.group=&group;sampler.original={&foreign};assert(!service.Roster());
// Initial roster order, not current GetAgents order, determines the slot.
sampler.strength=2;sampler.original={&foreign,&soldier};assert(service.Roster());
soldier.player=true;assert(!service.Roster());soldier.player=false;
soldier.group=nullptr;assert(!service.Roster());soldier.group=&group;
soldier.alive=false;assert(!service.Roster());soldier.alive=true;
Agent extra{&foreign};group.agents.push_back(&extra);assert(!service.Roster());
}
'''.replace('METHOD',s))
print('PASS: actual resupply Roster preserves permanent release, original slot mapping and bounded ownership')