--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMine.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMine.c @@ -238,6 +238,11 @@ if (m_State == 2) { if (!m_Removal || m_Removal.Result < 0) { Stop(); return; } + // Observed transfer ends our custody; a later nearby drop cannot restore it. + // Original inventory ownership may still settle after the native callback. + InventoryItemComponent item = InventoryItemComponent.Cast(m_Mine.FindComponent(InventoryItemComponent)); + bool attached = m_Mine.GetParent() || (item && item.GetParentSlot()); + if (attached && !ERR_InfantryAmmoTransfer.StoredUnder(m_Mine, m_Actor)) { Stop(); return; } if (!m_Removal.Result || !Grounded()) return; if (vector.DistanceXZ(m_Actor.GetOrigin(), m_Position) > 3) { Stop(); return; } // A callback proves removal, not continued custody of a distant loose item. --- a/tests/test_engineer_mine.py +++ b/tests/test_engineer_mine.py @@ -117,7 +117,14 @@ Fixture refused;refused.inventory.accepted=false;refused.Prepare();assert(refused.controller.Finished()&&refused.inventory.calls==1);refused.inventory.callback->OnComplete();refused.Drop();refused.controller.Tick(13000);assert(!refused.trigger.calls&&!refused.mine.moves); Fixture failed;failed.Prepare();failed.inventory.callback->OnFailed();failed.inventory.callback->OnComplete();failed.Drop();failed.controller.Tick(12001);assert(failed.controller.Finished()&&failed.inventory.callback->Result==-1&&!failed.mine.moves); // ACK alone cannot establish custody. A stolen item is never positioned/armed. - Fixture stolen;stolen.Prepare();stolen.inventory.callback->OnComplete();stolen.mine.parent=&stolen.actor;stolen.Drop();stolen.controller.Tick(12001);assert(stolen.controller.m_State==2&&!stolen.mine.moves);stolen.controller.Tick(181000);assert(stolen.controller.Finished()); + Fixture stolen;stolen.Prepare();stolen.inventory.callback->OnComplete();Entity thief;stolen.mine.parent=&thief;stolen.Drop();stolen.controller.Tick(12001);assert(stolen.controller.Finished()&&!stolen.mine.moves);stolen.controller.Tick(181000);assert(stolen.controller.Finished()); + // Observed foreign custody is terminal even if the same exact item returns nearby. + // Exercise both an acknowledged removal and an accepted, still-pending removal. + for(int ack=0;ack<2;ack++)for(int slotOnly=0;slotOnly<2;slotOnly++){Fixture f;f.Prepare();f.Drop();Entity holder;if(slotOnly)f.item.slot=&holder;else f.mine.parent=&holder;f.mine.stored=true;f.mine.storageOwner=&holder;if(ack)f.inventory.callback->OnComplete(); + f.controller.Tick(12001);assert(f.controller.Finished()&&!f.mine.moves&&!f.trigger.calls); + f.mine.parent=nullptr;f.Drop();f.inventory.callback->OnComplete();f.controller.Tick(12002);f.actor.position={0,0,0};f.controller.Tick(14000);assert(!f.mine.moves&&!f.trigger.calls&&f.inventory.calls==1);} + // Native ownership still attached to the original actor may settle normally. + Fixture settling;settling.Prepare();settling.inventory.callback->OnComplete();settling.controller.Tick(12001);assert(!settling.controller.Finished()&&!settling.mine.moves);settling.Drop();settling.controller.Tick(12002);assert(settling.controller.m_State==3&&settling.mine.moves==1); // Cancellation retains callback and item tombstone and only completes own action. Fixture stopped;stopped.Prepare();SCR_AIBehaviorBase unrelated;stopped.utility.actions.Insert(&unrelated);auto owned=stopped.controller.m_Action;stopped.controller.Stop();stopped.controller.Stop();stopped.Drop();stopped.inventory.callback->OnComplete();stopped.controller.Tick(13000);assert(owned->completed==1&&!unrelated.completed&&!stopped.mine.moves&&!stopped.trigger.calls&&stopped.attempted.Contains(&stopped.mine)); // Original identity changes abort before irreversible work, including replacement