--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c @@ -300,8 +300,8 @@ if (!m_Accepted) return Fail("native magazine move rejected; exact item will not be retried"); } // Native callbacks may synchronously cancel this leaf or revoke ownership. - if (m_LauncherMagazine && m_State != ERR_InfantryAmmoTransferState.PENDING) return m_State; - if (m_LauncherMagazine && !Context()) return Fail("launcher custody authority changed during native submission"); + if (m_State != ERR_InfantryAmmoTransferState.PENDING) return m_State; + if (!Context()) return Fail("magazine custody authority changed during native submission"); if (PhysicalProof()) { m_State = ERR_InfantryAmmoTransferState.SUCCEEDED; --- a/tests/test_infantry_ammo_transfer.py +++ b/tests/test_infantry_ammo_transfer.py @@ -150,6 +150,20 @@ rifle.parent=&recipient;loaded.parent=&rifle;loaded.magazine=∅empty.owner=&loaded;empty.well=&well;empty.rounds=0;muzzle.loaded=∅loaded.item=&loadedItem;loadedItem.slot=&loadedSlot;loadedSlot.container=&muzzle;loadedSlot.attached=&loaded;} }; int main(){ + // Rifle moves must obey the same irreversible cancellation boundary as launchers. + // Native inventory processing may synchronously invoke mission/ownership events. + for(int loss=0;loss<4;loss++){Fixture f;auto t=f.Job(); + f.source.effect=[&](){if(loss==0)t.Stop();if(loss==1)f.truck.captured=true;if(loss==2)f.mode.active=false;if(loss==3)f.recipient.group=&f.otherGroup;f.Move();}; + f.source.callback=1;assert(t.Tick(0)==FAILED&&t.Submitted()&&!t.Unresolved()&&f.source.calls==1); + assert(t.Tick(1)==FAILED&&f.source.calls==1);} + // Callback without custody after cancellation retains uncertainty until the + // original physical identity arrives, without resurrecting success or retry. + {Fixture f;auto t=f.Job();f.source.effect=[&](){t.Stop();};f.source.callback=1; + assert(t.Tick(0)==FAILED&&t.Submitted()&&t.Unresolved());f.Move();assert(!t.Unresolved());assert(t.Tick(1)==FAILED&&f.source.calls==1);} + // Coordinator must never book a cancelled native rifle transfer as delivery. + {SupplyFixture f;f.source.effect=[&](){f.c.m_Move->Stop();f.Move();};f.source.callback=1; + f.c.Transfer(0);assert(f.c.m_Spent.Count()==1&&f.c.m_Delivered==0);f.c.Transfer(1000); + assert(f.c.finishes==1&&f.c.m_Delivered==0&&f.c.m_Spent.Count()==1&&f.source.calls==1);} // Failed callback settles only exact original unchanged custody; unknown never does. {LauncherFixture f;f.source.callback=2;f.c.Transfer(0);assert(f.c.m_Move->Submitted()&&!f.c.m_Move->Unresolved()&&f.c.m_Spent.Count()==1);f.c.Transfer(1000);assert(f.source.calls==1&&f.c.m_Spent.Count()==1);} for(int bad=0;bad<7;bad++){LauncherFixture f;f.c.Transfer(0);assert(f.c.m_Move->Unresolved());