--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c @@ -106,10 +106,10 @@ protected void PollTransfers(float now) { if (m_Mounted) m_Mounted.Tick(now); - if (m_Move && !m_Move.Unresolved() && (m_State <= 1 || (m_State == 7 && m_LauncherTrip))) + if (m_Move && !m_Move.Unresolved() && (m_State <= 1 || m_State == 7)) { m_Move = null; - if (m_State == 7) Finish("launcher custody settled; original claims released without retry or refund"); + if (m_State == 7) Finish("magazine custody settled; original claims released without retry or refund"); } if (m_MineMove && !m_MineMove.Unresolved() && (m_State <= 1 || m_State == 7)) { @@ -425,8 +425,8 @@ ClearWaypoints(); m_Order = null; // A late native move must not race another truck's newer report. Retain both - // exact claims until this one submitted identity proves custody or deletion. - if ((m_MineMove && m_MineMove.Unresolved()) || (m_LauncherTrip && m_Move && m_Move.Unresolved())) { m_State = 7; return; } + // exact claims until the existing transfer leaf reports that its operation has settled. + if ((m_MineMove && m_MineMove.Unresolved()) || (m_Move && m_Move.Unresolved())) { m_State = 7; return; } if (m_Recipient) m_HQ.ReleaseLogistics(m_Recipient.Group, m_Token); if (m_Truck && m_Truck.SupplyToken == m_Token) { m_Truck.SupplyToken = 0; m_Truck.SupplyHeld = false; } m_Recipient = null; m_Donor = null; m_Order = null; m_Receiving = null; m_State = 1; m_MineTrip = false; m_LauncherTrip = false; m_MineRecipientSlot = -1; --- a/tests/test_infantry_resupply_planning.py +++ b/tests/test_infantry_resupply_planning.py @@ -251,6 +251,20 @@ f.ammo.ReadyMines=1;f.ammo.MineComplete=false;assert(!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));f.ammo.MineComplete=true; batch.complete=false;assert(!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));batch.complete=true; assert(f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));assert(f.s.m_MineTrip&&f.s.m_MineStock.Count()==1);} + // An accepted unresolved rifle move keeps both original claims across a new + // report after the shared180s cooldown. Distinct convoy slots own distinct tokens. + {Fixture first,second;Leaf leaf;leaf.unresolved=true;Map consumed;Map cooldown; + first.hq.convoys.tasks={&first.convoy,&second.convoy};first.hq.m_Units={&first.recipient,&first.donor,&second.donor};second.s.m_HQ=&first.hq; + assert(first.s.Bind(&first.convoy,0,&consumed,&cooldown));assert(second.s.Bind(&second.convoy,1,&consumed,&cooldown)); + first.s.Stock(0);second.s.Stock(0);assert(first.Plan());first.s.m_Move=&leaf;first.s.Finish("accepted native rifle move remains unresolved"); + assert(first.s.m_Move==&leaf&&leaf.Unresolved()); + first.Refresh(180001,2);second.donorStatus.ObservedAt=180001; + assert(!second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,180001)); + assert(first.s.m_State==7&&first.recipient.LogisticsReservation==100001&&first.truck.SupplyToken==100001); + assert(consumed.Get("recipient")==1&&cooldown.Get("recipient")==180000&&second.s.m_Trip==0); + leaf.unresolved=false;first.s.Finish("settled");assert(first.recipient.LogisticsReservation==0); + assert(second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,180001)); + assert(first.recipient.LogisticsReservation==100129&&second.truck.SupplyToken==100129&&first.truck.SupplyToken==0);} // A submitted unknown mine retains the real HQ claim across a newer delivered // report and expired shared cooldown; a second truck cannot race the same actor. {Fixture first,second;Leaf leaf;leaf.unresolved=true;first.s.Stock(0);assert(first.Plan());first.s.m_MineMove=&leaf;first.s.m_MineTrip=true;first.s.Finish("unknown native move"); --- a/tests/test_infantry_resupply_coordinator.py +++ b/tests/test_infantry_resupply_coordinator.py @@ -81,7 +81,7 @@ {Fixture f;f.order.result=-1;f.s.Tick(0);assert(f.s.m_State==1&&f.hq.token==0&&f.truck.SupplyToken==0&&!f.truck.SupplyHeld&&f.truck.clears==0);} {Fixture f;f.s.m_State=4;f.order.result=-1;f.s.Tick(0);assert(f.s.waypointCalls==0&&f.s.transfers==0&&f.hq.token==0);} {Fixture f;f.hq.token=99;f.truck.SupplyToken=99;f.truck.SupplyHeld=true;f.s.Tick(0);assert(f.s.m_State==1&&f.hq.token==99&&f.truck.SupplyToken==99&&f.truck.SupplyHeld);} - {Fixture f;Waypoint own1{1},own2{2},other{3};f.group.points={&own1,&other,&own2};f.s.m_MoveWaypoint=&own1;f.s.m_HoldWaypoint=&own2;Leaf leaf;f.s.m_Move=&leaf;f.truck.SupplyHeld=true;f.s.Stop();assert(f.group.points.size()==1&&f.group.points[0]==&other&&leaf.stops==1&&f.s.m_Move==&leaf&&f.hq.token==0&&!f.truck.SupplyHeld&&f.s.m_Stopped);int audits=f.s.audits;f.s.Stop();f.s.Tick(5000);assert(f.s.audits==audits&&leaf.stops==1);} + {Fixture f;Waypoint own1{1},own2{2},other{3};f.group.points={&own1,&other,&own2};f.s.m_MoveWaypoint=&own1;f.s.m_HoldWaypoint=&own2;Leaf leaf;f.s.m_Move=&leaf;f.truck.SupplyHeld=true;f.s.Stop();assert(f.group.points.size()==1&&f.group.points[0]==&other&&leaf.stops==1&&f.s.m_Move==&leaf&&f.hq.token==7&&f.truck.SupplyHeld&&f.s.m_State==7&&f.s.m_Stopped);int audits=f.s.audits;f.s.Stop();f.s.Tick(5000);assert(f.s.audits==audits&&leaf.stops==1);leaf.unresolved=false;f.s.Tick(6000);assert(f.hq.token==0&&!f.truck.SupplyHeld&&f.s.m_State==1&&!f.s.m_Move);} {Fixture f;Leaf leaf;f.s.m_Move=&leaf;f.s.Finish("timeout");f.s.Tick(0);assert(f.s.m_Move==&leaf);leaf.unresolved=false;f.s.Tick(1000);assert(f.s.m_Move==nullptr);} {Fixture f;f.s.m_State=3;f.physics.linear.x=std::numeric_limits::quiet_NaN();f.s.Tick(0);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);} {Fixture f;f.s.m_State=3;f.physics.angular.x=std::numeric_limits::quiet_NaN();f.s.Tick(0);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);} --- a/tests/test_infantry_ammo_transfer.py +++ b/tests/test_infantry_ammo_transfer.py @@ -150,6 +150,24 @@ 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(){ + + // The shared force claim is necessary: independent rifle leaves can otherwise + // submit the same still-carried empty to two different trucks. Native acceptance + // is delayed here; this proves submission interference, not an engine outcome. + {EmptyFixture first;Fixture second;ERR_GameMode::current=&first.mode; + first.ledger.records.Insert(&second.donorRecord);second.source.selected=&second.sourceStorage; + auto old=first.Job();assert(old.Tick(0)==PENDING&&old.Unresolved()); + assert(old.m_ReturnMagazine==&first.empty&&old.m_ReturnStorage==&first.sourceStorage); + old.Stop();assert(old.Unresolved()&&old.Tick(180001)==FAILED); + ERR_InfantryAmmoTransfer next(&first.mode,&second.truck,&second.donorGroup,&first.recipient,&first.recipientGroup,&second.magEntity,true); + assert(next.Tick(180001)==PENDING&&next.Unresolved()); + assert(first.destination.calls==2&&next.m_ReturnMagazine==old.m_ReturnMagazine); + assert(next.m_ReturnStorage==&second.sourceStorage&&next.m_ReturnStorage!=old.m_ReturnStorage); + assert(first.destination.items.Contains(&first.empty)&&first.source.calls==0&&second.source.calls==0); + // No duplicate object is invented: one physical completion can settle only its + // original donor proof; the competing outstanding submission remains unknown. + first.Return();old.m_ReturnCallback->OnComplete();assert(!old.Unresolved()&&next.Unresolved()); + assert(old.Tick(180002)==FAILED&&first.source.calls==0&&second.source.calls==0);} // 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();