--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSupplyTransfer.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSupplyTransfer.c @@ -81,6 +81,13 @@ ERR_MortarSupplyState Poll(float now) { if (!Replication.IsServer()) return ERR_MortarSupplyState.FAILED; + // The exact deleted shell cannot complete later. Match courier settlement: + // release only uncertainty, never authorize a shot or retry this lost item. + if (!m_Shell) + { + m_Resolved = true; m_Invalidated = true; m_Ready = false; + return ERR_MortarSupplyState.FAILED; + } bool valid = Valid(now); bool sourceContains = Contains(m_Source); bool destinationContains = Contains(m_Destination); --- a/tests/test_mortar_transactions.py +++ b/tests/test_mortar_transactions.py @@ -1,7 +1,9 @@ """Actual Enforce policies and transaction observation body under narrow API fakes.""" from policy_harness import ROOT, body, run +from pathlib import Path +import os r=ROOT/'addons/core/Scripts/Game/ERR/10_AI' -readiness=r/'ERR_MortarReadiness.c'; transfer=r/'ERR_MortarSupplyTransfer.c'; battery=r/'ERR_MortarBattery.c' +readiness=r/'ERR_MortarReadiness.c'; transfer=Path(os.environ.get('ERR_MORTAR_SUPPLY_TRANSFER_SOURCE',r/'ERR_MortarSupplyTransfer.c')); battery=r/'ERR_MortarBattery.c' policy='\n'.join([body(readiness,'static ERR_MortarSupplyState Population('),body(readiness,'static bool AllocationValid('),body(readiness,'static int CycleGate('),body(transfer,'static bool Authority('),body(transfer,'static bool Proven(')]) policy=policy.replace('ERR_MortarSupplyState.','ERR_MortarSupplyState::') run('''#include @@ -44,6 +46,7 @@ poll=poll.replace('ERR_MortarSupplyState.','ERR_MortarSupplyState::').replace('Replication.IsServer()','Replication::IsServer()').replace('InventoryItemComponent item;','InventoryItemComponent* item=nullptr;').replace('if (m_Shell) item = InventoryItemComponent.Cast(m_Shell.FindComponent(InventoryItemComponent));','if (m_Shell) item = m_Shell;').replace('item.GetParentSlot().GetStorage()','item->GetParentSlot()->GetStorage()').replace('item.GetParentSlot()','item->GetParentSlot()') ack=body(transfer,'void Acknowledge(') run('''#include +#include enum class ERR_MortarSupplyState {FAILED=-1,PENDING,READY}; struct Replication { static bool IsServer(){return true;} }; struct Slot { void* storage; void* GetStorage(){return storage;} }; @@ -57,7 +60,10 @@ Slot slot{this}; InventoryItemComponent item{&slot}; InventoryItemComponent* m_Shell=&item; bool Valid(float){return valid&&!m_Invalidated;} bool Contains(int which){return m_Shell && (which==1 ? source : destination);} -'''+ack+poll+''' +'''+ack+body(transfer,'bool Unresolved(')+poll+''' +}; +struct Battery {Transfer* m_Transfer=nullptr; +'''+body(battery,'bool SupplyPending(').replace('m_Transfer.Unresolved()', 'm_Transfer->Unresolved()')+''' }; int main(){ using S=ERR_MortarSupplyState; @@ -76,8 +82,19 @@ assert(late.Poll(8000)==S::FAILED); // no deadline-boundary success bypass Transfer failed;failed.Acknowledge(false);failed.Acknowledge(true); assert(failed.Poll(1)==S::FAILED && failed.m_Acknowledgement==-1 && failed.m_Resolved); - Transfer gone;gone.m_Shell=nullptr;gone.Acknowledge(true); - assert(gone.Poll(1)==S::PENDING && !gone.m_Resolved); // ambiguous until deadline, never another move + // Exact deletion is permanent loss, not an operation that can still arrive. + for(int ack=0;ack<3;ack++)for(bool invalidated:{false,true}){ + Transfer gone;Battery battery;battery.m_Transfer=&gone;gone.m_Invalidated=invalidated;assert(gone.Unresolved()&&battery.SupplyPending());gone.m_Shell=nullptr; + if(ack==1)gone.Acknowledge(true); + if(ack==2)gone.Acknowledge(false); + assert(gone.Poll(1)==S::FAILED&&gone.m_Resolved&&gone.m_Invalidated&&!gone.m_Ready&&!gone.Unresolved()); + assert(!battery.SupplyPending()); + gone.Acknowledge(true);assert(gone.Poll(90000)==S::FAILED&&!gone.Unresolved()&&!gone.m_Ready); + } + // A surviving detached shell, missing inventory or lost authority remains + // uncertain; only exact identity deletion permits unconditional settlement. + Transfer detached;detached.source=false;detached.destination=false;detached.Acknowledge(true); + assert(detached.Poll(1)==S::PENDING&&detached.Unresolved());assert(detached.Poll(8000)==S::FAILED&&detached.Unresolved()); Transfer unauthorized;unauthorized.valid=false; assert(unauthorized.Poll(0)==S::FAILED && !unauthorized.m_Resolved); }