Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
72 lines
8.6 KiB
Python
72 lines
8.6 KiB
Python
from pathlib import Path
|
|
p=Path('/tmp/erri-resupply-tests-0826');s=(p/'baseline/tests/test_infantry_resupply_planning.py').read_text()
|
|
s=s.replace("I=ROOT/", "A=Path(os.environ.get('ERR_INFANTRY_TRANSFER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c'))\nI=ROOT/",1)
|
|
s=s.replace("methods='\\n'.join(cpp(body(P,x)) for x in (", "stock_method=('protected bool CompatibleStock(',) if 'protected bool CompatibleStock(' in P.read_text() else ()\nmethods='\\n'.join(cpp(body(P,x)) for x in stock_method+(",1)
|
|
# Actual compatibility method, not a boolean compatibility stand-in.
|
|
needle="run(r'''\n"
|
|
addition='''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()->')
|
|
'''
|
|
assert needle in s;s=s.replace(needle,addition+needle,1)
|
|
s=s.replace('struct Entity;using IEntity=Entity*;', '''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;}};''')
|
|
s=s.replace('struct Entity{Prefab prefab;', 'struct Entity{Magazine*ammoComp=nullptr;Manager*weapons=nullptr;Weapon*weapon=nullptr;Item*item=nullptr;IEntity parent=nullptr;Prefab prefab;')
|
|
old='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);}};'
|
|
new="struct ERR_InfantryAmmoTransfer{static bool StoredUnder(IEntity item,IEntity actor){return item&&item->parent==actor;}\n'''+compatible+r'''\n};"
|
|
assert old in s;s=s.replace(old,new)
|
|
s=s.replace('std::array<Entity,140>items;Entity one,two;', '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;')
|
|
s=s.replace('vehicle.inventory=&inventory;', '''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;''',1)
|
|
s=s.replace('hq.downlinks==0);}', 'hq.downlinks==0&&!truck.SupplyHeld&&!convoy.Stopped&&!s.m_Recipient&&!s.m_Donor);}',1)
|
|
s=s.replace('ERR_InfantryAmmoTransfer::only=&f.items[95];ERR_InfantryAmmoTransfer::checks=0;', 'for(int i=0;i<95;i++)f.magazines[i].well=&f.wrongWell;')
|
|
s=s.replace('assert(ERR_InfantryAmmoTransfer::checks==191);ERR_InfantryAmmoTransfer::only=nullptr;', 'assert(f.s.m_LauncherTrip);')
|
|
s=s.replace('ERR_InfantryAmmoTransfer::compatible=false;', 'for(auto&mag:f.magazines)mag.well=&f.wrongWell;').replace('ERR_InfantryAmmoTransfer::compatible=true;', '')
|
|
needle='int main(){\n'
|
|
cases='''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();}
|
|
'''
|
|
assert needle in s;s=s.replace(needle,cases,1)
|
|
s=s.replace('PASS: actual convoy binding,', 'PASS: actual Compatible and bounded stock preflight before report/claim consumption, later compatible truck, launcher controls, convoy binding,')
|
|
(p/'tests/test_infantry_resupply_planning.py').write_text(s)
|