--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_MountedAmmoPickup.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_MountedAmmoPickup.c @@ -188,6 +188,9 @@ bool Unresolved() { if (!m_Submitted) return false; + // A deleted exact item cannot complete this one-item move. Its committed + // reservation stays spent; capture or damage alone does not settle custody. + if (!m_Incoming) return false; if (m_Acknowledgement == 1 && PhysicalProof()) return false; if (m_Acknowledgement < 0 && OriginalProof()) return false; // False native return without a definitive callback is also quarantined. --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_MountedAmmoTransfer.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_MountedAmmoTransfer.c @@ -181,6 +181,9 @@ bool Unresolved() { if (!m_Submitted) return false; + // Both exchange identities must be deleted before loss alone settles this + // two-item operation. Either surviving item may still be in flight. + if (!m_Incoming && !m_Loaded) return false; if (m_Acknowledgement == 1 && SwappedProof()) return false; if (m_Acknowledgement < 0 && OriginalProof()) return false; // Even a false submission result without callback cannot authorize another --- a/tests/test_mounted_ammo_pickup.py +++ b/tests/test_mounted_ammo_pickup.py @@ -133,6 +133,19 @@ {Fixture f;auto t=f.Job();t.Tick(0);t.Stop();f.mode.active=false;f.Move();f.truckInventory.last->OnComplete();assert(!t.Unresolved()&&t.Tick(1)==FAILED&&f.truckInventory.calls==1);} {Fixture f;auto t=f.Job();t.Tick(0);assert(t.Tick(8000)==FAILED&&t.Unresolved()&&f.truckInventory.calls==1);} {Fixture f;auto t=f.Job();t.Tick(10);assert(t.Tick(9)==FAILED&&t.Unresolved()&&f.truckInventory.calls==1);} + // Native deletion clears the weak exact identity: no item can arrive later. + // Retain the spent slot, do not count delivery, and never retry on late callbacks. + {Fixture f;auto t=f.Job();assert(t.Tick(0)==PENDING&&t.Unresolved()); + t.m_Incoming=nullptr;f.reservation.m_Item=nullptr;f.stock[0]=nullptr;f.spent[0]=nullptr; + f.fromSlot.attached=nullptr;f.truckInventory.items.clear(); + assert(t.Tick(1)==FAILED&&!t.Unresolved()&&f.spent.Count()==1&&!f.reservation.ReleaseUnsubmitted()); + f.truckInventory.last->OnComplete();f.truckInventory.last->OnFailed(); + assert(t.Tick(2)==FAILED&&!t.Unresolved()&&f.truckInventory.calls==1&&f.spent.Count()==1);} + // Damage, worker loss, source capture and a moved-but-existing item remain uncertain. + for(int k=0;k<4;k++){Fixture f;auto t=f.Job();assert(t.Tick(0)==PENDING); + if(k==0){f.incoming.alive=false;}if(k==1){t.m_Worker=nullptr;} + if(k==2){f.donor.Released=true;}if(k==3){f.truckInventory.items.clear();f.newItem.slot=nullptr;} + t.Stop();assert(t.Unresolved()&&t.Tick(1)==FAILED&&f.truckInventory.calls==1);} // Original identity, carried capacity and source access losses fail before any submission. for(int k=0;k<43;k++){Fixture f;auto t=f.Job(); switch(k){case 0:f.task.Stopped=true;break;case 1:f.task.Ready=false;break;case 2:f.donor.Released=true;break;case 3:f.target.Released=true;break; --- a/tests/test_mounted_ammo_transfer.py +++ b/tests/test_mounted_ammo_transfer.py @@ -126,6 +126,16 @@ {Fixture f;auto t=f.Job();t.Tick(0);t.Stop();f.mode.active=false;f.Swap();f.carried.last->OnComplete();assert(!t.Unresolved()&&t.Tick(1)==FAILED&&f.carried.calls==1);} {Fixture f;auto t=f.Job();t.Tick(0);assert(t.Tick(8000)==FAILED&&t.Unresolved()&&f.carried.calls==1);} {Fixture f;auto t=f.Job();t.Tick(10);assert(t.Tick(9)==FAILED&&f.carried.calls==1);} + // A swap retains quarantine while either exact exchange identity survives. + for(int side=0;side<2;side++){Fixture f;auto t=f.Job();assert(t.Tick(0)==PENDING&&t.Unresolved()); + if(side==0)t.m_Incoming=nullptr;else t.m_Loaded=nullptr; + t.Stop();assert(t.Unresolved()&&t.Tick(1)==FAILED&&f.carried.calls==1); + f.carried.last->OnComplete();assert(t.Unresolved()); + t.m_Incoming=nullptr;t.m_Loaded=nullptr;f.reservation.m_Item=nullptr;f.stock[0]=nullptr;f.spent[0]=nullptr; + assert(!t.Unresolved()&&!f.reservation.ReleaseUnsubmitted()&&f.spent.Count()==1); + f.carried.last->OnFailed();assert(t.Tick(2)==FAILED&&!t.Unresolved()&&f.carried.calls==1);} + {Fixture f;auto t=f.Job();assert(t.Tick(0)==PENDING);f.incoming.alive=false;f.loaded.alive=false; + t.Stop();assert(t.Unresolved()&&f.carried.calls==1);} // Physical target changes, outgoing capacity/content guards, source inventory bounds. for(int k=0;k<34;k++){Fixture f;auto t=f.Job(); switch(k){case 0:f.task.Stopped=true;break;case 1:f.task.Ready=false;break;case 2:f.donor.Released=true;break;case 3:f.target.Released=true;break; --- a/tests/test_mounted_resupply.py +++ b/tests/test_mounted_resupply.py @@ -125,6 +125,14 @@ // Uncertain pickup survives Stop and keeps both matching claims until callback/proof settles. {Fixture f;f.Dispatch();f.c.Tick(2000);assert(f.c.m_State==4);f.c.m_Pickup->unresolved=true;auto*p=f.c.m_Pickup;f.c.Stop(); assert(f.c.m_State==8&&f.c.Busy()&&f.hq.tokens.size()==2&&f.spent.Count()==1);p->unresolved=false;f.c.Tick(3000);assert(!f.c.Busy()&&f.hq.tokens.empty()&&f.spent.Count()==1);} + // A terminally lost submitted pickup releases holds, retaining spent identity. + // Leaf deletion semantics are exercised in the actual pickup/swap regressions. + {Fixture f;Entity surviving;f.stock.Insert(&surviving);f.Dispatch();f.c.Tick(2000);auto*p=f.c.m_Pickup;assert(p&&f.c.m_State==4); + p->unresolved=true;f.c.workerValid=false;f.c.Tick(3000); + assert(f.c.m_State==8&&f.hq.tokens.size()==2&&f.truck.SupplyHeld&&f.target.SupplyHeld); + p->unresolved=false;f.c.Tick(4000);assert(!f.c.Busy()&&f.hq.tokens.empty()&&!f.truck.SupplyHeld&&!f.target.SupplyHeld&&f.spent.Count()==1); + f.c.m_Incoming=&surviving;f.c.workerValid=true;f.demand.MessageId=2;f.demand.ObservedAt=f.status.ObservedAt=200000; + assert(f.Begin(200000)&&f.spent.Count()==2&&f.c.m_Trip==2);} // Uncertain swap is not released merely because the controller times out or is stopped. {Fixture f;f.Pickup();f.c.m_Swap->result=0;f.c.Tick(3000);f.c.m_Swap->unresolved=true;auto*t=f.c.m_Swap;f.c.Stop(); assert(f.c.m_State==8&&f.c.m_Swap==t&&f.hq.tokens.size()==2);t->unresolved=false;f.c.Tick(4000);assert(!f.c.Busy()&&f.spent.Count()==1);}