Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
check: /tmp/erri-resupply-tests-0826/scratch/err-policy-tlhvgc3l/check.cpp:447: int main(): Assertion `!f.Plan()' failed.
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-resupply-tests-0826/tests/test_infantry_resupply_planning.py", line 48, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<238 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
|
||||
subprocess.run([str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['/tmp/erri-resupply-tests-0826/scratch/err-policy-tlhvgc3l/check']' died with <Signals.SIGABRT: 6>.
|
||||
@@ -0,0 +1,126 @@
|
||||
--- 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')
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1,71 @@
|
||||
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)
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"status": "pass",
|
||||
"source_sha256": "bf50a7a7d5cd182d29a02ea5de8d58b46979019158250c5ab589ed8ffe49b974",
|
||||
"test_sha256": "20747ffda8754f81f3b9e9912b9f60a300c439f2ea2c473bb900302f61dd691f",
|
||||
"test_baseline_sha256": "9929ee0f5a3272c348ee329355b6ba67fdcb745fc9ce1f377f6c989eae91bf9f",
|
||||
"compatible_source_sha256": "99fa26beb0c0869d0cd694500f3bfc0705c2c1cf674dea90d98fb93a59570ef7",
|
||||
"patch_sha256": "36ed4109cf8be7262ac56823beddb94bfdb78a1253f0162b9209f17b47677377",
|
||||
"results": [
|
||||
{
|
||||
"name": "baseline-red",
|
||||
"exit_code": 1,
|
||||
"compiled": true,
|
||||
"source_sha256": "b01fa948f204d49acbfbdfc5fa641b1d914e0eb91081e92a93816ae52ae88b1e",
|
||||
"log_sha256": "f7829da13dac935abf634116da29d81d398b58f599c760bb33c94a0d5b317331"
|
||||
},
|
||||
{
|
||||
"name": "green",
|
||||
"exit_code": 0,
|
||||
"compiled": true,
|
||||
"source_sha256": "bf50a7a7d5cd182d29a02ea5de8d58b46979019158250c5ab589ed8ffe49b974",
|
||||
"log_sha256": "c12aa5ba9b2141830d3916c3ab0285c06cbb44f606a9f8fbe42787ade0e67b21"
|
||||
}
|
||||
],
|
||||
"coverage": [
|
||||
"Actual TryPlan, new CompatibleStock and retained LauncherStock wrapper plus actual ERR_InfantryAmmoTransfer.Compatible execute together. Native weapon/muzzle/well/item/ownership boundaries are doubled; same native shape as existing transfer regression.",
|
||||
"Wrong well, empty magazine, missing well, invalid zero capacity or overcapacity refuse before shared report consumption/cooldown, original group/truck claims, downlink or hold. Stock matching only launcher cannot satisfy small-arms planning.",
|
||||
"Two original convoy service instances share consumption/cooldown maps: incompatible first truck refuses untouched; compatible later truck accepts the exact same original recipient and delivered report with its own exact token.",
|
||||
"Surviving frozen/unspent membership required; compatible post-freeze insertion cannot qualify. Restoring compatible original unspent stock allows planning.32-member/96-stock caps and last-of96 compatible stock covered.",
|
||||
"Real separate rifle and reloadable launcher fixtures preserve launcher planning, with disposable launcher refusal. Existing entire planning harness retained, including convoy identities, receipt/startup barriers, source guards, token ranges, shared cooldown, claims and uncertain transfer cleanup."
|
||||
],
|
||||
"limits": [
|
||||
"C++ actual-method translation, no native/Enforce/physics proof. StoredUnder is a narrow direct-parent native ownership boundary double for these launcher fixtures.",
|
||||
"No target changes, full suite, native runs or mutation sweep. Source author patch remains separate.",
|
||||
"CanAttribute may permanently latch original-slot loss, as in prior launcher preflight. No claim that roster observation is globally side-effect-free; refusal guarantees are limited to trip/report/cooldown/reservation/order state."
|
||||
],
|
||||
"all_compilers_finished": true
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
from pathlib import Path
|
||||
import subprocess,os,json,hashlib,difflib
|
||||
r=Path('/home/jakub/devel/escapistsrereintroduced');o=Path('/tmp/erri-resupply-tests-0826');a=Path('/tmp/erri-resupply-source-0826');rel='addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c';p=a/rel;test=o/'tests/test_infantry_resupply_planning.py'
|
||||
env=dict(os.environ,PYTHONPATH=str(r/'tests'),TMPDIR=str(o/'scratch'))
|
||||
results=[]
|
||||
for name,source,expected in [('baseline-red',a/'baseline'/rel,False),('green',p,True)]:
|
||||
env['ERR_INFANTRY_RESUPPLY_SOURCE']=str(source)
|
||||
with (o/(name+'.log')).open('w') as f:res=subprocess.run(['python3',str(test)],env=env,stdout=f,stderr=subprocess.STDOUT)
|
||||
log=(o/(name+'.log')).read_text();assert (res.returncode==0)==expected,(name,log)
|
||||
if not expected:assert 'Assertion' in log and 'failed.' in log and "Command '['g++'" not in log,(name,log)
|
||||
results.append({'name':name,'exit_code':res.returncode,'compiled':True,'source_sha256':hashlib.sha256(source.read_bytes()).hexdigest(),'log_sha256':hashlib.sha256((o/(name+'.log')).read_bytes()).hexdigest()})
|
||||
before=(r/'tests/test_infantry_resupply_planning.py').read_text();after=test.read_text();patch=''.join(difflib.unified_diff(before.splitlines(True),after.splitlines(True),fromfile='a/tests/test_infantry_resupply_planning.py',tofile='b/tests/test_infantry_resupply_planning.py'));(o/'fix.patch').write_text(patch)
|
||||
review={'status':'pass','source_sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'test_sha256':hashlib.sha256(test.read_bytes()).hexdigest(),'test_baseline_sha256':hashlib.sha256((r/'tests/test_infantry_resupply_planning.py').read_bytes()).hexdigest(),'compatible_source_sha256':hashlib.sha256((r/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c').read_bytes()).hexdigest(),'patch_sha256':hashlib.sha256((o/'fix.patch').read_bytes()).hexdigest(),'results':results,'coverage':['Actual TryPlan, new CompatibleStock and retained LauncherStock wrapper plus actual ERR_InfantryAmmoTransfer.Compatible execute together. Native weapon/muzzle/well/item/ownership boundaries are doubled; same native shape as existing transfer regression.','Wrong well, empty magazine, missing well, invalid zero capacity or overcapacity refuse before shared report consumption/cooldown, original group/truck claims, downlink or hold. Stock matching only launcher cannot satisfy small-arms planning.','Two original convoy service instances share consumption/cooldown maps: incompatible first truck refuses untouched; compatible later truck accepts the exact same original recipient and delivered report with its own exact token.','Surviving frozen/unspent membership required; compatible post-freeze insertion cannot qualify. Restoring compatible original unspent stock allows planning.32-member/96-stock caps and last-of96 compatible stock covered.','Real separate rifle and reloadable launcher fixtures preserve launcher planning, with disposable launcher refusal. Existing entire planning harness retained, including convoy identities, receipt/startup barriers, source guards, token ranges, shared cooldown, claims and uncertain transfer cleanup.'],'limits':['C++ actual-method translation, no native/Enforce/physics proof. StoredUnder is a narrow direct-parent native ownership boundary double for these launcher fixtures.','No target changes, full suite, native runs or mutation sweep. Source author patch remains separate.'],'all_compilers_finished':True}
|
||||
(o/'review.json').write_text(json.dumps(review,indent=2)+'\n');print(json.dumps(review,indent=2))
|
||||
Reference in New Issue
Block a user