142 lines
14 KiB
Python
142 lines
14 KiB
Python
"""Execute real finite-stock/planning/reservation bodies; native inventory/group APIs are doubles."""
|
|
import os,re
|
|
from pathlib import Path
|
|
from policy_harness import ROOT,body,run
|
|
P=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_InfantryResupply.c'))
|
|
I=ROOT/'addons/core/Scripts/Game/ERR/ERR_Inventory.c'
|
|
H=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
|
|
def cpp(s):
|
|
s=s.replace('protected ','').replace('notnull ','').replace('null','nullptr')
|
|
s=s.replace('array<IEntity> items = {};','Array<IEntity> items;').replace('array<IEntity> raw = {};','Array<IEntity> raw;').replace('array<AIAgent> agents = {};','Array<AIAgent> agents;')
|
|
s=s.replace('out array<IEntity> items','Array<IEntity>& items').replace('array<IEntity> items','Array<IEntity>& items')
|
|
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
|
|
for typ in ('InventoryStorageManagerComponent','ERR_ForceRecord'):
|
|
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
|
for name in ('ERR_ForceRules','ERR_InfantryResupplyRules','ERR_ConvoyRules','ERR_ForceKind','ERR_ConvoyResources','ERR_Inventory','ERR_Actors','SCR_FactionManager','CompartmentAccessComponent','Replication','Math'):
|
|
s=s.replace(name+'.',name+'::')
|
|
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('InventoryStorageManagerComponent.Cast','InventoryStorageManagerComponent::Cast').replace('FindComponent(InventoryStorageManagerComponent)','FindComponent()')
|
|
for name in ('m_Mode','m_HQ','m_Convoy','m_Truck','m_Recipient','m_Donor','m_Move','recipient','donor','demand','donorStatus','entry','operation','inventory','item','owner','record','actor','agent','unit','m_MoveWaypoint','m_HoldWaypoint','magazine'):
|
|
s=s.replace(name+'.',name+'->')
|
|
for name in ('Receipt','Entity','Group','Utility','StatusSampler','AmmoState','Combat','m_Perception'):
|
|
s=s.replace(name+'.',name+'->')
|
|
for name in ('Forces()','GetPrefabData()'):s=s.replace(name+'.',name+'->')
|
|
s=s.replace('bool available;','bool available=false;').replace('bool living;','bool living=false;')
|
|
return s
|
|
rules='\n'.join(cpp(body(P,x)) for x in ('static bool Fresh(', 'static bool Need(', 'static bool Pair('))
|
|
methods='\n'.join(cpp(body(P,x)) for x in ('protected bool Contains(', 'protected bool TruckValid(', 'protected bool Quiet(', 'protected bool Roster(', 'protected void Stock(', 'bool TryPlan(', 'protected void ClearWaypoints(', 'protected void Finish(', 'void Stop('))
|
|
reserve=cpp(body(H,'bool ReserveLogistics('))+cpp(body(H,'void ReleaseLogistics('))
|
|
unique=cpp(body(I,'static int AppendUnique('))+cpp(body(I,'static int Items('))
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <array>
|
|
#include <map>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
using string=std::string;
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
|
template<class V>struct Map{std::map<string,V>data;V Get(string k){auto it=data.find(k);return it==data.end()?V{}:it->second;}void Set(string k,V v){data[k]=v;}};
|
|
struct Vec{float x=0,y=0,z=0;static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
|
struct ERR_ForceRules{static bool Finite(float f){return std::isfinite(f);}static bool FiniteVector(Vec v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}};
|
|
struct Math{static float Min(float a,float b){return std::min(a,b);}};
|
|
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
|
|
struct Entity;using IEntity=Entity*;
|
|
struct InventoryStorageManagerComponent{Array<IEntity>items;void GetItems(Array<IEntity>& out){out=items;}static auto Cast(InventoryStorageManagerComponent* p){return p;}};
|
|
struct Prefab{string name="magazine";string GetPrefabName(){return name;}};
|
|
struct Group;using SCR_AIGroup=Group*;
|
|
struct Entity{Prefab prefab;bool hasPrefab=true,alive=true,player=false,fight=true,aboard=false,captured=false,released=false;int faction=1;Group* group=nullptr;InventoryStorageManagerComponent* inventory=nullptr;Prefab* GetPrefabData(){return hasPrefab?&prefab:nullptr;}InventoryStorageManagerComponent* FindComponent(){return inventory;}};
|
|
struct Agent{IEntity actor;IEntity GetControlledEntity(){return actor;}};using AIAgent=Agent*;
|
|
struct Waypoint{};
|
|
struct Group{Array<AIAgent>agents;bool slave=false;int players=0,faction=1;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}void GetAgents(Array<AIAgent>& out){out=agents;}void RemoveWaypoint(Waypoint*){}};
|
|
struct ERR_Actors{static bool Alive(IEntity a){return a&&a->alive;}static bool Player(IEntity a){return a&&a->player;}static SCR_AIGroup Group(IEntity a){return a->group;}static bool CanFight(IEntity a){return a&&a->alive&&a->fight;}static void DeleteSpawned(Waypoint*){}};
|
|
struct SCR_FactionManager{static int SGetFaction(IEntity a){return a->faction;}};
|
|
struct CompartmentAccessComponent{static bool GetVehicleIn(IEntity a){return a->aboard;}};
|
|
struct ERR_Inventory{
|
|
'''+unique+r'''
|
|
};
|
|
struct Ammo{bool Complete=true;int Members=2,SmallArmsMuzzles=2,SmallArmsRounds=60,SmallArmsChambers=0;bool Valid(int n){return Members==n;}};
|
|
struct Entry{bool Closed=false;int Count=2,Fit=2,MessageId=1;float ObservedAt=0;Vec Position;string SourceId;Ammo* AmmoState=nullptr;};using ERR_HQWatchEntry=Entry*;
|
|
struct ERR_InfantryResupplyRules{
|
|
'''+rules+r'''
|
|
};
|
|
namespace ERR_ForceKind{const int CONVOY=3;}
|
|
struct ERR_ForceRecord{bool Physical=true,Eliminated=false;int Kind=3,Id=2;};
|
|
struct Ledger{Group* owned=nullptr;ERR_ForceRecord record;ERR_ForceRecord* FindGroup(Group* g){return g==owned?&record:nullptr;}};
|
|
struct Mode{Ledger ledger;Ledger* Forces(){return &ledger;}int EnemyFaction(){return 1;}};
|
|
struct Perception{Array<int>m_aTargets;};
|
|
struct Utility{Perception* m_Perception=nullptr;};
|
|
struct Sampler{Group* group;Array<IEntity>original;int InitialStrength(){return original.Count();}IEntity InitialMember(int i){return original[i];}bool CanAttribute(int i,IEntity a){return original[i]==a&&!a->released;}bool OwnsRoster(Group* g){return g==group;}};
|
|
struct Unit{struct Group* Group=nullptr;struct Utility* Utility=nullptr;Sampler* StatusSampler=nullptr;string ForceSourceId;bool HomeGuard=false,eligible=true;int LogisticsReservation=0;void* Downlink=nullptr;void* PendingOrder=nullptr;void* PendingConvoyOrder=nullptr;void* Radio=this;void* Assignment=nullptr;void* Waypoint=nullptr;bool Eligible(Mode*){return eligible&&LogisticsReservation==0;}};using ERR_HQUnit=Unit*;
|
|
struct Combat{bool Held=false;};
|
|
struct Truck{int Index=1,SupplyToken=0;bool Registered=true,Released=false,Arrived=false,SupplyHeld=false,crew=true;struct Entity* Entity=nullptr;struct Group* Group=nullptr;struct Combat* Combat=nullptr;bool CrewValid(Mode*){return crew;}};
|
|
struct Receipt{bool Consumed=true;Array<int>Records={1,2,3,4,5,6};};
|
|
struct Cargo{bool Finished=true;};using ERR_ConvoyCargoCallback=Cargo*;
|
|
struct Convoy{bool Stopped=false,Ready=true;struct Receipt* Receipt=nullptr;Array<Truck*>Vehicles;Array<struct Cargo*>Cargo;float Deadline=30000;};
|
|
struct ERR_ConvoyRules{static bool Captured(Entity* e,Group*){return e->captured;}};
|
|
struct ERR_ConvoyResources{static string Magazine(){return "magazine";}};
|
|
struct Route{};struct Leaf{void Stop(){}};
|
|
struct HQ{Mode* m_Mode;Array<ERR_HQUnit>m_Units;Convoy* convoy=nullptr;Route route;bool active=true,routeAvailable=true;int downlinks=0;bool MissionActive(){return active;}Convoy* ConvoyTask(){return convoy;}
|
|
Route* Downlink(Group*,string,string,int){downlinks++;return routeAvailable?&route:nullptr;}
|
|
'''+reserve+r'''
|
|
};
|
|
struct Service{HQ* m_HQ;Mode* m_Mode;Convoy* m_Convoy=nullptr;Truck* m_Truck=nullptr;Unit* m_Recipient=nullptr;Unit* m_Donor=nullptr;Leaf* m_Move=nullptr;Route* m_Order=nullptr;IEntity m_Receiving=nullptr;
|
|
Array<IEntity>m_Stock,m_Spent,m_Members,m_Served;Map<int>m_Consumed;Map<float>m_NextAttempt;int m_State=0,m_Trip=0,m_Token=0,m_ReportId=0,m_TripDelivered=0;float m_Deadline=0;bool m_Stopped=false;
|
|
Waypoint* m_MoveWaypoint=nullptr;Waypoint* m_HoldWaypoint=nullptr;string reason;bool Current(){return !m_Stopped&&Replication::IsServer()&&m_HQ->active;}void Audit(string why){reason=why;}
|
|
'''+methods+r'''
|
|
};
|
|
struct Fixture{Mode mode;HQ hq;Service s;InventoryStorageManagerComponent inventory;Entity vehicle;std::array<Entity,140>items;Entity one,two;Agent a{&one},b{&two};Group recipients,donors;Utility recipientUtility,donorUtility;Sampler sampler;Unit recipient,donor;Combat combat;Truck truck;Receipt receipt;Cargo cargo;Convoy convoy;Ammo ammo;Entry demand,donorStatus;
|
|
Fixture():sampler{&recipients,{&one,&two}}{
|
|
hq.m_Mode=&mode;hq.convoy=&convoy;s.m_HQ=&hq;s.m_Mode=&mode;
|
|
vehicle.inventory=&inventory;vehicle.group=&donors;truck.Entity=&vehicle;truck.Group=&donors;truck.Combat=&combat;
|
|
mode.ledger.owned=&donors;convoy.Receipt=&receipt;convoy.Vehicles={&truck,&truck,&truck,&truck,&truck,&truck};convoy.Cargo={&cargo};
|
|
one.group=&recipients;two.group=&recipients;recipients.agents={&a,&b};recipient.Group=&recipients;recipient.Utility=&recipientUtility;recipient.StatusSampler=&sampler;recipient.ForceSourceId="recipient";
|
|
donor.Group=&donors;donor.Utility=&donorUtility;donor.ForceSourceId="donor";hq.m_Units={&recipient,&donor};
|
|
demand.SourceId="recipient";demand.AmmoState=&ammo;donorStatus.SourceId="donor";donorStatus.AmmoState=&ammo;
|
|
for(int i=0;i<48;i++)inventory.items.Insert(&items[i]);
|
|
}
|
|
bool Plan(float now=0){return s.TryPlan(&recipient,&donor,&demand,&donorStatus,now);}
|
|
void Refresh(float now,int id){demand.ObservedAt=now;donorStatus.ObservedAt=now;demand.MessageId=id;}
|
|
void Unchanged(){assert(s.m_Trip==0&&s.m_Consumed.Get("recipient")==0&&s.m_NextAttempt.Get("recipient")==0&&recipient.LogisticsReservation==0&&truck.SupplyToken==0&&hq.downlinks==0);}
|
|
};
|
|
int main(){
|
|
// Actual inventory aggregation collapses repeated native storage references, not distinct identical-prefab magazines.
|
|
{Fixture f;f.inventory.items.Insert(&f.items[0]);f.inventory.items.Insert(nullptr);f.s.Stock(0);assert(f.s.m_State==1&&!f.s.m_Stopped&&f.s.m_Stock.Count()==48);assert(f.s.m_Stock[0]!=f.s.m_Stock[1]);}
|
|
// Malformed/non-magazine items never become spendable stock; native startup partial cargo stays partial.
|
|
{Fixture f;f.inventory.items={&f.items[0],&f.items[1],&f.items[2],nullptr};f.items[1].hasPrefab=false;f.items[2].prefab.name="rifle";f.s.Stock(0);assert(f.s.m_Stock.Count()==1&&f.s.m_State==1);}
|
|
{Fixture f;f.cargo.Finished=false;f.s.Stock(0);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());f.s.Stock(30000);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
|
{Fixture f;f.cargo.Finished=false;f.s.Stock(0);f.cargo.Finished=true;f.inventory.items={&f.items[0]};f.s.Stock(1000);assert(f.s.m_Stock.Count()==1&&!f.s.m_Stopped);}
|
|
{Fixture f;f.inventory.items.Insert(&f.items[48]);f.s.Stock(0);assert(f.s.m_Stopped&&!f.Plan());f.Unchanged();}
|
|
{Fixture f;for(auto& item:f.items)f.inventory.items.Insert(&item);f.s.Stock(0);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
|
{Fixture f;f.vehicle.inventory=nullptr;f.s.Stock(0);assert(f.s.m_Stopped&&!f.Plan());}
|
|
{Fixture f;f.convoy.Ready=false;f.s.Stock(0);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());}
|
|
{Fixture f;f.receipt.Consumed=false;f.s.Stock(0);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
|
// Removed stock is unavailable; a different magazine added after capture does not replace its identity.
|
|
{Fixture f;f.s.Stock(0);f.inventory.items={&f.items[48]};assert(!f.Plan());f.Unchanged();f.inventory.items.Insert(&f.items[0]);assert(f.Plan());assert(f.s.m_Stock.Count()==48&&!f.s.m_Stock.Contains(&f.items[48]));}
|
|
{Fixture f;f.s.Stock(0);f.s.m_Spent=f.s.m_Stock;assert(!f.Plan());f.Unchanged();}
|
|
// A delivered report is tied to exact original source identities and original still-owned members.
|
|
{Fixture f;f.s.Stock(0);f.demand.SourceId="foreign";assert(!f.Plan());f.Unchanged();}
|
|
{Fixture f;f.s.Stock(0);f.donorStatus.SourceId="foreign";assert(!f.Plan());f.Unchanged();}
|
|
{Fixture f;f.s.Stock(0);f.one.released=true;assert(!f.Plan());f.Unchanged();assert(!f.s.m_Recipient&&!f.s.m_Donor);}
|
|
{Fixture f;f.s.Stock(0);Entity foreign;foreign.group=&f.recipients;Agent extra{&foreign};f.recipients.agents.Insert(&extra);assert(!f.Plan());f.Unchanged();}
|
|
// Real HQ force reservation refuses busy/support-ineligible or silent-radio recipients without consuming the demand.
|
|
for(int refusal=0;refusal<5;refusal++){Fixture f;f.s.Stock(0);void* owned=&f;
|
|
if(refusal==0){f.recipient.Assignment=owned;}if(refusal==1){f.recipient.Downlink=owned;}if(refusal==2){f.recipient.Waypoint=owned;}if(refusal==3){f.recipient.Radio=nullptr;}if(refusal==4){f.recipient.eligible=false;}
|
|
assert(!f.Plan());f.Unchanged();assert(!f.s.m_Recipient&&!f.s.m_Donor);
|
|
f.recipient.Assignment=nullptr;f.recipient.Downlink=nullptr;f.recipient.Waypoint=nullptr;f.recipient.Radio=&f;f.recipient.eligible=true;
|
|
assert(f.Plan());assert(f.s.m_Trip==1&&f.s.m_Consumed.Get("recipient")==1&&f.recipient.LogisticsReservation==100001);}
|
|
{Fixture f;f.s.Stock(0);f.recipient.LogisticsReservation=7;assert(!f.Plan());assert(f.recipient.LogisticsReservation==7&&f.s.m_Trip==0&&f.hq.downlinks==0);}
|
|
// Failed truck downlink is still a consumed planning attempt; cleanup preserves all cargo and other ownership.
|
|
{Fixture f;f.s.Stock(0);f.hq.routeAvailable=false;assert(f.Plan());assert(f.s.m_State==1&&f.s.m_Trip==1&&f.s.m_Consumed.Get("recipient")==1&&f.recipient.LogisticsReservation==0&&f.truck.SupplyToken==0&&f.inventory.items.Count()==48);
|
|
f.hq.routeAvailable=true;f.Refresh(180000,1);assert(!f.Plan(180000)); // same consumed message cannot replay after cooldown
|
|
f.Refresh(179999,2);assert(!f.Plan(179999));f.Refresh(180000,2);assert(f.Plan(180000));assert(f.s.m_Trip==2&&f.s.m_Deadline==300000);}
|
|
// A stale-but-still-admissible demand gets only the remaining evidence horizon, never a new full lifetime.
|
|
{Fixture f;f.s.Stock(0);assert(f.Plan(120000));assert(f.s.m_Deadline==180000);assert(!f.Plan(120000)&&f.hq.downlinks==1);}
|
|
// Independent guarded failures do not leak reservations or acquire replacement force identities.
|
|
for(int refusal=0;refusal<7;refusal++){Fixture f;f.s.Stock(0);
|
|
if(refusal==0){f.recipient.HomeGuard=true;}if(refusal==1){f.truck.Combat->Held=true;}if(refusal==2){f.vehicle.captured=true;}if(refusal==3){f.mode.ledger.record.Eliminated=true;}if(refusal==4){f.donor.PendingConvoyOrder=&f;}if(refusal==5){f.ammo.Complete=false;}if(refusal==6){f.demand.Position.x=201;}
|
|
assert(!f.Plan());f.Unchanged();}
|
|
}
|
|
''')
|
|
print('PASS: actual stock capture, unique finite identities, original recipients, HQ reservations, delivered report replay/cooldown and failed-downlink cleanup')
|