Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
127 lines
15 KiB
Diff
127 lines
15 KiB
Diff
--- a/tests/test_infantry_resupply_planning.py
|
|
+++ b/tests/test_infantry_resupply_planning.py
|
|
@@ -3,6 +3,7 @@
|
|
from pathlib import Path
|
|
from policy_harness import ROOT,body,run
|
|
P=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
|
+A=Path(os.environ.get('ERR_INFANTRY_TRANSFER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c'))
|
|
I=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Inventory.c'
|
|
H=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
|
def cpp(s):
|
|
@@ -28,9 +29,22 @@
|
|
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(', 'static bool MinePair(', 'static bool LauncherNeed(', 'static bool LauncherPair('))
|
|
-methods='\n'.join(cpp(body(P,x)) for x in ('bool Bind(', 'ERR_ConvoyTask Task(', 'protected bool ConvoyCurrent(', 'protected bool Contains(', 'protected bool TruckValid(', 'protected bool Quiet(', 'protected bool Roster(', 'protected void Stock(', 'protected bool CaptureMines(', 'protected bool LauncherStock(', 'bool TryPlanLauncher(', 'bool TryPlanMines(', 'bool TryPlan(', 'protected void ClearWaypoints(', 'protected void Finish(', 'void Stop('))
|
|
+stock_method=('protected bool CompatibleStock(',) if 'protected bool CompatibleStock(' in P.read_text() else ()
|
|
+methods='\n'.join(cpp(body(P,x)) for x in stock_method+('bool Bind(', 'ERR_ConvoyTask Task(', 'protected bool ConvoyCurrent(', 'protected bool Contains(', 'protected bool TruckValid(', 'protected bool Quiet(', 'protected bool Roster(', 'protected void Stock(', 'protected bool CaptureMines(', 'protected bool LauncherStock(', 'bool TryPlanLauncher(', 'bool TryPlanMines(', '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('))
|
|
+compatible=body(A,'static bool Compatible(')
|
|
+for a,b in {
|
|
+ 'BaseMagazineComponent.Cast(exactMagazine.FindComponent(BaseMagazineComponent))':'exactMagazine->ammoComp',
|
|
+ 'BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent))':'actor->weapons',
|
|
+ 'BaseWeaponComponent.Cast(rifle.FindComponent(BaseWeaponComponent))':'rifle->weapon',
|
|
+ 'InventoryItemComponent.Cast(loaded.GetOwner().FindComponent(InventoryItemComponent))':'loaded->GetOwner()->item',
|
|
+ 'SCR_MuzzleInMagComponent.Cast(muzzle)':'(muzzle->inMag ? muzzle : nullptr)',
|
|
+}.items():compatible=compatible.replace(a,b)
|
|
+compatible=compatible.replace('array<','Array<').replace('foreach (','for (').replace('EWeaponType kind','int kind').replace('EMuzzleType muzzleKind','int muzzleKind')
|
|
+for a in ['EWeaponType','EMuzzleType']:compatible=compatible.replace(a+'.',a+'::')
|
|
+for a in ['magazine','manager','weapon','muzzle','reloadable','loaded','loadedItem','loadedSlot']:compatible=compatible.replace(a+'.',a+'->')
|
|
+compatible=compatible.replace('GetMagazineWell().','GetMagazineWell()->')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
@@ -47,10 +61,21 @@
|
|
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 Magazine;struct Manager;struct Weapon;struct Muzzle;struct Item;struct Slot;
|
|
+using BaseMagazineComponent=Magazine*;using BaseWeaponManagerComponent=Manager*;using BaseWeaponComponent=Weapon*;using BaseMuzzleComponent=Muzzle*;using SCR_MuzzleInMagComponent=Muzzle*;using InventoryItemComponent=Item*;using InventoryStorageSlot=Slot*;
|
|
+struct EWeaponType{static const int WT_RIFLE=1,WT_SNIPERRIFLE=2,WT_MACHINEGUN=3,WT_HANDGUN=4,WT_GRENADELAUNCHER=5,WT_ROCKETLAUNCHER=6;};
|
|
+struct EMuzzleType{static const int MT_BaseMuzzle=0,MT_UGLMuzzle=1,MT_RPGMuzzle=2;};
|
|
+struct Well{int type=1,parent=0;int Type(){return type;}bool IsInherited(int t){return type==t||parent==t;}};
|
|
+struct Magazine{IEntity owner=nullptr;Well*well=nullptr;int rounds=30,capacity=30;bool used=false;IEntity GetOwner(){return owner;}Well*GetMagazineWell(){return well;}int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}bool IsUsed(){return used;}};
|
|
+struct Muzzle{bool inMag=true,canReload=true,disposable=false;Magazine*loaded=nullptr;Well*well=nullptr;int kind=0;bool CanBeReloaded(){return canReload;}bool IsDisposable(){return disposable;}Magazine*GetMagazine(){return loaded;}Well*GetMagazineWell(){return well;}int GetMuzzleType(){return kind;}};
|
|
+struct Weapon{IEntity owner=nullptr;Array<Muzzle*>muzzles;int kind=1;IEntity GetOwner(){return owner;}int GetWeaponType(){return kind;}void GetMuzzlesList(Array<Muzzle*>&a){a=muzzles;}};
|
|
+struct Manager{Array<IEntity>weapons;void GetWeaponsList(Array<IEntity>&a){a=weapons;}};
|
|
+struct Slot{Muzzle*container=nullptr;IEntity attached=nullptr;Muzzle*GetParentContainer(){return container;}IEntity GetAttachedEntity(){return attached;}};
|
|
+struct Item{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};
|
|
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 readyMine=false,magazine=true,hasPrefab=true,alive=true,player=false,fight=true,aboard=false,captured=false,released=false;int faction=1;Group* group=nullptr;InventoryStorageManagerComponent* inventory=nullptr;bool HasMagazine(){return magazine;}Prefab* GetPrefabData(){return hasPrefab?&prefab:nullptr;}InventoryStorageManagerComponent* FindComponent(){return inventory;}};
|
|
+struct Entity{Magazine*ammoComp=nullptr;Manager*weapons=nullptr;Weapon*weapon=nullptr;Item*item=nullptr;IEntity parent=nullptr;Prefab prefab;bool readyMine=false,magazine=true,hasPrefab=true,alive=true,player=false,fight=true,aboard=false,captured=false,released=false;int faction=1;Group* group=nullptr;InventoryStorageManagerComponent* inventory=nullptr;bool HasMagazine(){return magazine;}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*){}};
|
|
@@ -91,15 +116,20 @@
|
|
Route* Downlink(Group*,string,string,int){downlinks++;return routeAvailable?&route:nullptr;}
|
|
'''+reserve+r'''
|
|
};
|
|
-struct ERR_InfantryAmmoTransfer{static inline IEntity only=nullptr;static inline int checks=0;static inline bool compatible=true;static bool Compatible(IEntity actor,IEntity item,bool launcher){checks++;return actor&&item&&launcher&&compatible&&(!only||item==only);}};
|
|
+struct ERR_InfantryAmmoTransfer{static bool StoredUnder(IEntity item,IEntity actor){return item&&item->parent==actor;}
|
|
+'''+compatible+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;Leaf*m_MineMove=nullptr;MineBatch*m_MineBatch=nullptr;bool m_LauncherTrip=false,m_MineTrip=false,m_MineStockCaptured=false,m_EmptyMineReplacement=false;int m_MineRecipientSlot=-1;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;IEntity m_Receiving=nullptr;
|
|
Array<IEntity>m_Stock,m_MineStock,m_Spent,m_Members,m_Served;Map<int>*m_Consumed=new Map<int>();Map<float>*m_NextAttempt=new Map<float>();int m_ConvoySlot=-1;float m_NextTick=0;int m_State=0,m_Trip=0,m_Token=0,m_ReportId=0,m_TripDelivered=0;float m_Deadline=0,m_StockDeadline=-1;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;
|
|
+struct Fixture{Mode mode;HQ hq;Service s;InventoryStorageManagerComponent inventory;Entity vehicle;std::array<Entity,140>items;std::array<Magazine,140>magazines;Entity rifle,launcher;Manager weapons;Weapon rifleWeapon,launcherWeapon;Muzzle rifleMuzzle,launcherMuzzle;Well well,wrongWell{2,0};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;hq.convoys.tasks={&convoy};s.m_HQ=&hq;s.m_Mode=&mode;
|
|
+ one.weapons=&weapons;two.weapons=&weapons;weapons.weapons={&rifle,&launcher};rifle.weapon=&rifleWeapon;rifleWeapon.owner=&rifle;rifleWeapon.muzzles={&rifleMuzzle};rifleMuzzle.well=&well;
|
|
+ launcher.weapon=&launcherWeapon;launcherWeapon.owner=&launcher;launcherWeapon.kind=EWeaponType::WT_ROCKETLAUNCHER;launcherWeapon.muzzles={&launcherMuzzle};launcherMuzzle.well=&well;launcherMuzzle.kind=EMuzzleType::MT_RPGMuzzle;launcher.parent=&one;
|
|
+ for(int i=0;i<140;i++){items[i].ammoComp=&magazines[i];magazines[i].owner=&items[i];magazines[i].well=&well;}
|
|
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";
|
|
@@ -109,15 +139,35 @@
|
|
}
|
|
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);}
|
|
+ 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&&!truck.SupplyHeld&&!convoy.Stopped&&!s.m_Recipient&&!s.m_Donor);}
|
|
};
|
|
int main(){
|
|
+ // Actual Compatible must reject wrong-well, empty or malformed stock before
|
|
+ // any report/cooldown consumption, reservation, downlink or convoy hold.
|
|
+ for(int bad=0;bad<5;bad++){Fixture f;for(auto&mag:f.magazines){if(bad==0)mag.well=&f.wrongWell;if(bad==1)mag.rounds=0;if(bad==2)mag.capacity=0;if(bad==3)mag.rounds=31;if(bad==4)mag.well=nullptr;}f.s.Stock(0);assert(!f.Plan());f.Unchanged();}
|
|
+ {Fixture f;f.s.Stock(0);f.rifleMuzzle.well=&f.wrongWell;assert(!f.Plan());f.Unchanged();f.rifleMuzzle.well=&f.well;assert(f.Plan());}
|
|
+ // Same delivered recipient report remains available to a later compatible
|
|
+ // truck sharing the real per-source consumption/cooldown maps.
|
|
+ {Fixture first,second;Map<int> consumed;Map<float> cooldown;second.hq.convoys.tasks={&first.convoy,&second.convoy};second.hq.convoy=&first.convoy;second.hq.m_Units={&first.recipient,&second.donor};
|
|
+ assert(first.s.Bind(&first.convoy,0,&consumed,&cooldown));assert(second.s.Bind(&second.convoy,1,&consumed,&cooldown));
|
|
+ for(auto&mag:first.magazines){mag.well=&first.wrongWell;}first.s.Stock(0);second.s.Stock(0);assert(!first.Plan());first.Unchanged();
|
|
+ assert(second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,0));assert(consumed.Get("recipient")==1&&cooldown.Get("recipient")==180000&&first.recipient.LogisticsReservation==100129&&second.hq.downlinks==1&&first.hq.downlinks==0);}
|
|
+ // A usable compatible item must still be a surviving unspent frozen identity.
|
|
+ {Fixture f;for(auto&mag:f.magazines)mag.rounds=0;f.magazines[48].rounds=30;f.s.Stock(0);f.inventory.items.Insert(&f.items[48]);assert(!f.Plan());f.Unchanged();f.magazines[0].rounds=30;f.s.m_Spent.Insert(&f.items[0]);assert(!f.Plan());f.Unchanged();f.s.m_Spent.Clear();assert(f.Plan());}
|
|
+ // Hard 32 original carriers and 96 captured magazines remain fail-closed.
|
|
+ {Fixture f;f.s.Stock(0);f.sampler.original.resize(33,&f.one);assert(!f.Plan());f.Unchanged();}
|
|
+ {Fixture f;f.s.Stock(0);for(int i=48;i<97;i++){f.s.m_Stock.Insert(&f.items[i]);f.inventory.items.Insert(&f.items[i]);}assert(!f.Plan());f.Unchanged();}
|
|
+ {Fixture f;for(int i=48;i<96;i++)f.inventory.items.Insert(&f.items[i]);for(int i=0;i<95;i++)f.magazines[i].well=&f.wrongWell;f.s.Stock(0);assert(f.Plan());}
|
|
+ // Small-arms demand cannot borrow launcher compatibility, but the dedicated
|
|
+ // launcher path still supports its real empty reloadable muzzle and rejects disposables.
|
|
+ {Fixture f;f.weapons.weapons={&f.launcher};f.s.Stock(0);assert(!f.Plan());f.Unchanged();assert(f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));}
|
|
+ {Fixture f;f.weapons.weapons={&f.launcher};f.launcherMuzzle.disposable=true;f.s.Stock(0);assert(!f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));f.Unchanged();}
|
|
{Fixture f;for(int i=48;i<96;i++)f.inventory.items.Insert(&f.items[i]);f.s.Stock(0);
|
|
- ERR_InfantryAmmoTransfer::only=&f.items[95];ERR_InfantryAmmoTransfer::checks=0;
|
|
+ for(int i=0;i<95;i++)f.magazines[i].well=&f.wrongWell;
|
|
assert(f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));
|
|
- assert(ERR_InfantryAmmoTransfer::checks==191);ERR_InfantryAmmoTransfer::only=nullptr;}
|
|
+ assert(f.s.m_LauncherTrip);}
|
|
|
|
- {Fixture f;f.s.Stock(0);ERR_InfantryAmmoTransfer::compatible=false;assert(!f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0)&&f.truck.SupplyToken==0);ERR_InfantryAmmoTransfer::compatible=true;}
|
|
+ {Fixture f;f.s.Stock(0);for(auto&mag:f.magazines)mag.well=&f.wrongWell;assert(!f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0)&&f.truck.SupplyToken==0);}
|
|
|
|
{Fixture f;f.s.Stock(0);f.ammo.SmallArmsRounds=999;f.ammo.LauncherMagazines=0;assert(!f.Plan());assert(f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));assert(f.s.m_LauncherTrip&&!f.s.m_MineTrip);}
|
|
for(int bad=0;bad<7;bad++){Fixture f;f.s.Stock(0);f.ammo.SmallArmsRounds=999;if(bad==0)f.ammo.LauncherComplete=false;if(bad==1)f.ammo.LauncherMuzzles=0;if(bad==2)f.ammo.LauncherRounds=1;if(bad==3)f.ammo.LauncherChambers=1;if(bad==4)f.ammo.Members=1;if(bad==5)f.demand.ObservedAt=-1;if(bad==6)f.demand.ObservedAt=1;
|
|
@@ -237,4 +287,4 @@
|
|
assert(!f.Plan());f.Unchanged();}
|
|
}
|
|
''')
|
|
-print('PASS: actual convoy binding, finite identities, disjoint tokens, shared report replay/cooldown, original recipients and HQ cleanup')
|
|
+print('PASS: actual Compatible and bounded stock preflight before report/claim consumption, later compatible truck, launcher controls, convoy binding, finite identities, disjoint tokens, shared report replay/cooldown, original recipients and HQ cleanup')
|