Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
144 lines
14 KiB
Python
144 lines
14 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_MineMove','m_Mounted','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 PollTransfers','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 Mounted{bool busy=false;int ticks=0,stops=0;bool Busy(){return busy;}void Tick(float){ticks++;}void Stop(){stops++;}};
|
|
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;Leaf*m_MineMove=nullptr;bool m_MineTrip=false,m_LauncherTrip=false;int m_MineRecipientSlot=-1;Mounted*m_Mounted=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++;}int mineTransfers=0;void TransferMine(float){mineTransfers++;}
|
|
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=ℴhq.next=&next;}
|
|
};
|
|
int main(){
|
|
{Fixture f;Leaf leaf;f.s.m_Move=&leaf;f.s.m_LauncherTrip=true;f.truck.SupplyHeld=true;f.s.Stop();assert(f.s.m_State==7&&f.hq.token==7&&f.truck.SupplyHeld);
|
|
f.s.Tick(200000);assert(f.s.m_State==7&&f.hq.token==7&&f.s.transfers==0);leaf.unresolved=false;f.s.Tick(201000);assert(f.s.m_State==1&&f.hq.token==0&&!f.truck.SupplyHeld&&!f.s.m_Move);}
|
|
|
|
{Fixture f;Leaf leaf;f.s.m_MineMove=&leaf;f.s.m_MineTrip=true;f.truck.SupplyHeld=true;f.s.Stop();assert(f.s.m_State==7&&f.hq.token==7&&f.truck.SupplyHeld&&f.s.m_Recipient);
|
|
f.s.Tick(200000);assert(f.hq.token==7&&f.s.m_State==7&&f.s.transfers==0&&f.s.mineTransfers==0);leaf.unresolved=false;f.s.Tick(201000);assert(f.hq.token==0&&!f.truck.SupplyHeld&&f.s.m_State==1&&!f.s.m_MineMove);}
|
|
{Fixture f;f.s.m_State=5;f.s.m_MineTrip=true;f.s.Tick(0);assert(f.s.mineTransfers==1&&f.s.transfers==0);}
|
|
|
|
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==7&&f.truck.SupplyHeld&&f.s.m_State==7&&f.s.m_Stopped);int audits=f.s.audits;f.s.Stop();f.s.Tick(5000);assert(f.s.audits==audits&&leaf.stops==1);leaf.unresolved=false;f.s.Tick(6000);assert(f.hq.token==0&&!f.truck.SupplyHeld&&f.s.m_State==1&&!f.s.m_Move);}
|
|
{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')
|