Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
+353
@@ -0,0 +1,353 @@
|
||||
"""Execute the actual finite native magazine transaction; engine behavior remains separate."""
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
from policy_harness import ROOT,run,body
|
||||
from test_finite_magazine_reservation import translated_reservation
|
||||
p=Path(os.environ.get('ERR_INFANTRY_TRANSFER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c'))
|
||||
s=p.read_text()
|
||||
for forbidden in ('TrySpawn','DeleteEntity','SetOrigin','SetTransform','TryInsert','TryRemove','SetAmmoCount'):
|
||||
assert forbidden not in s,forbidden
|
||||
callback=s[s.index('class ERR_InfantryAmmoCallback'):s.index('// One exact physical')]
|
||||
code=s[s.index('class ERR_InfantryAmmoTransfer\n'):]
|
||||
code=re.sub(r'//[^\n]*','',code).replace('protected ','').replace('class ','struct ')
|
||||
for src,dst in {
|
||||
'm_Donor.FindComponent(VehicleHelicopterSimulation)':'m_Donor->helicopter',
|
||||
'InventoryItemComponent.Cast(loaded.GetOwner().FindComponent(InventoryItemComponent))':'loaded->GetOwner()->item',
|
||||
'BaseMagazineComponent.Cast(exactMagazine.FindComponent(BaseMagazineComponent))':'exactMagazine->magazine',
|
||||
'BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent))':'actor->weapons',
|
||||
'BaseWeaponComponent.Cast(rifle.FindComponent(BaseWeaponComponent))':'rifle->weapon',
|
||||
'InventoryItemComponent.Cast(entity.FindComponent(InventoryItemComponent))':'entity->item',
|
||||
'InventoryItemComponent.Cast(m_Magazine.FindComponent(InventoryItemComponent))':'m_Magazine->item',
|
||||
'InventoryItemComponent.Cast(exact.FindComponent(InventoryItemComponent))':'exact->item',
|
||||
'InventoryItemComponent.Cast(candidate.FindComponent(InventoryItemComponent))':'candidate->item',
|
||||
'BaseMagazineComponent.Cast(m_Magazine.FindComponent(BaseMagazineComponent))':'m_Magazine->magazine',
|
||||
'BaseMagazineComponent.Cast(candidate.FindComponent(BaseMagazineComponent))':'candidate->magazine',
|
||||
'SCR_AIVehicleUsageComponent.Cast(m_Donor.FindComponent(SCR_AIVehicleUsageComponent))':'m_Donor->usage',
|
||||
'InventoryStorageManagerComponent.Cast(m_Donor.FindComponent(InventoryStorageManagerComponent))':'m_Donor->inventory',
|
||||
'InventoryStorageManagerComponent.Cast(m_Recipient.FindComponent(InventoryStorageManagerComponent))':'m_Recipient->inventory',
|
||||
}.items():code=code.replace(src,dst)
|
||||
code=code.replace('SCR_MuzzleInMagComponent.Cast(muzzle)', '(muzzle->inMag ? muzzle : null)')
|
||||
code=code.replace('ref ERR_InfantryAmmoCallback','ERR_InfantryAmmoCallback*').replace('array<','Array<').replace('foreach (','for (').replace('null','nullptr')
|
||||
code=re.sub(r'\bERR_GameMode (?=\w)','ERR_GameMode* ',code)
|
||||
code=code.replace('void ERR_InfantryAmmoTransfer(','ERR_InfantryAmmoTransfer(').replace('EWeaponType kind','int kind').replace('EMuzzleType muzzleKind','int muzzleKind')
|
||||
for name in ('ERR_InfantryAmmoTransferState','ERR_InfantryAmmoAccess','ERR_GameMode','ERR_EnemyEquipmentRules','ERR_ForceRules','Replication','SCR_Global','ERR_Actors','SCR_FactionManager','ERR_ConvoyRules','CompartmentAccessComponent','EWeaponType','EMuzzleType','EStoragePurpose','vector'):
|
||||
code=code.replace(name+'.',name+'::')
|
||||
for name in ('m_Mode','m_Donor','m_DonorGroup','m_Recipient','m_RecipientGroup','m_Source','m_Destination','m_Storage','m_OriginalSlot','m_Callback','magazine','reloadable','loaded','loadedItem','loadedSlot','manager','weapon','muzzle','entity','item','storage','inventory','incoming','empty','source','destination','m_ReturnCallback','donorRecord','recipientRecord','utility','usage','physics'):
|
||||
code=re.sub(r'\b'+name+r'\.',name+'->',code)
|
||||
code=code.replace('GetMagazineWell().','GetMagazineWell()->').replace('GetParentSlot().','GetParentSlot()->').replace('Forces().','Forces()->').replace('m_VehicleMgr.','m_VehicleMgr->').replace('GetPilotCompartmentSlot().','GetPilotCompartmentSlot()->')
|
||||
code=re.sub(r'\b(bool|int|Vehicle|IEntity|SCR_AIGroup|ERR_ForceRecord|InventoryStorageSlot|InventoryStorageManagerComponent|BaseInventoryStorageComponent|ERR_GameMode\*|ERR_InfantryAmmoCallback\*) (\w+);',r'\1 \2{};',code)
|
||||
code=code.replace('\n}\n','\n};\n')
|
||||
callback=callback.replace('class ERR_InfantryAmmoCallback : ScriptedInventoryOperationCallback','').strip()[1:-1]
|
||||
callbacks=[]
|
||||
for name in ('OnComplete','OnFailed','OnAllComplete','OnAllFailed'):
|
||||
m=re.search(r'protected override void '+name+r'\(\) (\{[^\n]+\})',callback)
|
||||
callbacks.append('void ERR_InfantryAmmoCallback::'+name+'() '+m[1].replace('Transfer.','Transfer->'))
|
||||
# Integrate the actual coordinator transfer selector with this actual leaf.
|
||||
coordinator_path=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
||||
transfer=body(coordinator_path,'protected void Transfer(float now)').replace('protected ','').replace('null','nullptr').replace('foreach (','for (')
|
||||
transfer=transfer.replace('ERR_HQAmmoSnapshot local','ERR_HQAmmoSnapshot* local')
|
||||
for name in ('ERR_Actors','ERR_HQAmmoSampler','ERR_InfantryAmmoTransfer','vector'):
|
||||
transfer=transfer.replace(name+'.',name+'::')
|
||||
transfer=transfer.replace('ERR_FiniteMagazineReservation reservation', 'ERR_FiniteMagazineReservation* reservation').replace('new ERR_FiniteMagazineReservation(m_Stock, m_Spent,', 'new ERR_FiniteMagazineReservation(&m_Stock, &m_Spent,')
|
||||
for name in ('m_Move','actor','m_Truck','m_Recipient','local','reservation'):
|
||||
transfer=transfer.replace(name+'.',name+'->')
|
||||
transfer=transfer.replace('Entity.GetOrigin()', 'Entity->GetOrigin()')
|
||||
pre=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <functional>
|
||||
using string=std::string;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T t){this->push_back(t);}void RemoveItem(T t){this->erase(std::remove(this->begin(),this->end(),t),this->end());}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
|
||||
struct vector{float x=0,y=0,z=0;float Length(){return std::sqrt(x*x+y*y+z*z);}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}static float Distance(vector a,vector b){return vector{a.x-b.x,a.y-b.y,a.z-b.z}.Length();}};
|
||||
struct Entity;struct Group;struct Inventory;struct Storage;struct Item;struct Slot;struct Magazine;struct Weapon;struct Muzzle;struct Manager;struct Utility;struct Usage;struct Physics_;struct Record;
|
||||
using IEntity=Entity*;using Vehicle=Entity*;using SCR_AIGroup=Group*;using ERR_ForceRecord=Record*;
|
||||
using InventoryStorageSlot=Slot*;using InventoryStorageManagerComponent=Inventory*;using BaseInventoryStorageComponent=Storage*;using InventoryItemComponent=Item*;using BaseMagazineComponent=Magazine*;
|
||||
using BaseWeaponManagerComponent=Manager*;using BaseWeaponComponent=Weapon*;using BaseMuzzleComponent=Muzzle*;using SCR_MuzzleInMagComponent=Muzzle*;
|
||||
using SCR_AIGroupUtilityComponent=Utility*;using SCR_AIVehicleUsageComponent=Usage*;using Physics=Physics_*;
|
||||
enum ERR_InfantryAmmoTransferState{FAILED=-1,PENDING,SUCCEEDED};
|
||||
struct ERR_InfantryAmmoTransfer;
|
||||
struct ScriptedInventoryOperationCallback{virtual void OnComplete(){}virtual void OnFailed(){}virtual void OnAllComplete(){}virtual void OnAllFailed(){}virtual ~ScriptedInventoryOperationCallback()=default;};
|
||||
struct ERR_InfantryAmmoCallback:ScriptedInventoryOperationCallback{ERR_InfantryAmmoTransfer*Transfer=nullptr;bool Returning=false;void OnComplete()override;void OnFailed()override;void OnAllComplete()override;void OnAllFailed()override;};
|
||||
struct EWeaponType{static const int WT_RIFLE=1,WT_SNIPERRIFLE=2,WT_MACHINEGUN=3,WT_HANDGUN=4,WT_GRENADELAUNCHER=5,WT_ROCKETLAUNCHER=6;};
|
||||
struct EMuzzleType{static const int MT_BaseMuzzle=0,MT_UGLMuzzle=1,MT_RPGMuzzle=2;};struct EStoragePurpose{static const int PURPOSE_DEPOSIT=1;};
|
||||
struct Well{int type=1,parent=0;int Type(){return type;}bool IsInherited(int t){return type==t||parent==t;}};
|
||||
struct Magazine{IEntity owner=nullptr;IEntity GetOwner(){return owner;}Well*well=nullptr;int rounds=30,capacity=30;bool used=false;bool IsUsed(){return used;}Well*GetMagazineWell(){return well;}int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}};
|
||||
struct Muzzle{bool inMag=false,canReload=true;bool CanBeReloaded(){return canReload;}bool disposable=false;bool IsDisposable(){return disposable;}Magazine*loaded=nullptr;Magazine*GetMagazine(){return loaded;}Well*well=nullptr;int kind=0;Well*GetMagazineWell(){return well;}int GetMuzzleType(){return kind;}};
|
||||
struct Weapon{IEntity owner=nullptr;Array<Muzzle*>muzzles;int kind=1;IEntity GetOwner(){return owner;}int GetWeaponType(){return kind;}void GetMuzzlesList(Array<Muzzle*>&a){a=muzzles;}};
|
||||
struct Manager{Array<IEntity>weapons;void GetWeaponsList(Array<IEntity>&a){a=weapons;}};
|
||||
struct Storage{IEntity owner=nullptr;IEntity GetOwner(){return owner;}};
|
||||
struct Slot{Storage*storage=nullptr;IEntity occupant=nullptr;Muzzle*container=nullptr;IEntity attached=nullptr;Muzzle*GetParentContainer(){return container;}IEntity GetAttachedEntity(){return attached;}Storage*GetStorage(){return storage;}IEntity GetOccupant(){return occupant;}};
|
||||
struct Item{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};
|
||||
struct Physics_{vector velocity,angular;vector GetVelocity(){return velocity;}vector GetAngularVelocity(){return angular;}};
|
||||
struct Usage{Slot*pilot=nullptr;Slot*GetPilotCompartmentSlot(){return pilot;}};
|
||||
struct VehicleManager{IEntity vehicle=nullptr;IEntity FindVehicle(IEntity v){return vehicle==v?v:nullptr;}};
|
||||
struct Utility{VehicleManager*m_VehicleMgr=nullptr;bool military=true;bool IsMilitary(){return military;}};
|
||||
struct Group{int faction=1,players=0;bool slave=false;Utility*utility=nullptr;int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}bool IsSlave(){return slave;}Utility*GetGroupUtilityComponent(){return utility;}};
|
||||
struct Entity{bool helicopter=false;bool alive=true,awake=true,player=false,captured=false;int faction=1;Group*group=nullptr;IEntity vehicle=nullptr,parent=nullptr;
|
||||
Inventory*inventory=nullptr;Magazine*magazine=nullptr;Manager*weapons=nullptr;Weapon*weapon=nullptr;Item*item=nullptr;Usage*usage=nullptr;Physics_*physics=nullptr;vector position;
|
||||
IEntity GetParent(){return parent;}Physics_*GetPhysics(){return physics;}vector GetOrigin(){return position;}};
|
||||
struct Record{SCR_AIGroup Group=nullptr;bool Physical=true,Eliminated=false;};
|
||||
struct Ledger{Array<Record*>records;Record*FindGroup(SCR_AIGroup g){for(auto*r:records)if(r->Group==g)return r;return nullptr;}};
|
||||
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;Ledger*ledger=nullptr;static ERR_GameMode*Get(){return current;}bool Active(){return active;}Ledger*Forces(){return ledger;}int EnemyFaction(){return 1;}};
|
||||
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
||||
struct SCR_Global{inline static bool editor=false;static bool IsEditMode(){return editor;}};
|
||||
struct ERR_Actors{static bool CanFight(IEntity a){return a&&a->alive&&a->awake;}static bool Alive(IEntity a){return a&&a->alive;}};
|
||||
struct SCR_FactionManager{static int SGetFaction(IEntity a){return a?a->faction:-1;}};
|
||||
struct CompartmentAccessComponent{static IEntity GetVehicleIn(IEntity a){return a?a->vehicle:nullptr;}};
|
||||
struct ERR_ConvoyRules{static bool Captured(Vehicle v,SCR_AIGroup g){return v->captured||g->players>0;}static bool PartOf(IEntity a,IEntity root){return a==root;}};
|
||||
struct ERR_EnemyEquipmentRules{static bool Eligible(ERR_GameMode*m,SCR_AIGroup g,IEntity a){return m&&g&&a&&a->alive&&a->awake&&!a->player&&a->group==g&&g->players==0&&g->faction==m->EnemyFaction()&&g->utility&&g->utility->military;}};
|
||||
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}};
|
||||
struct ERR_InfantryAmmoAccess{static inline bool clear=true;static inline int checks=0;static bool Clear(Vehicle donor,IEntity recipient){assert(donor&&recipient);checks++;return clear;}};
|
||||
struct Inventory{IEntity owner=nullptr;Array<IEntity>items;Array<Storage*>storages;Storage*selected=nullptr;bool allow=true,accept=true;int calls=0,callback=0;ScriptedInventoryOperationCallback*last=nullptr;std::function<void()> effect;
|
||||
IEntity GetOwner(){return owner;}void GetItems(Array<IEntity>&a,int purpose){assert(purpose==1);a=items;}void GetStorages(Array<Storage*>&a,int purpose){assert(purpose==1);a=storages;}
|
||||
Storage*FindStorageForItem(IEntity,int purpose){assert(purpose==1);return selected;}
|
||||
bool CanMoveItemToStorage(IEntity,Storage*,int slot){assert(slot==-1);return allow;}
|
||||
bool TryMoveItemToStorage(IEntity,Storage*,int slot,ScriptedInventoryOperationCallback*cb){assert(slot==-1);calls++;last=cb;if(effect)effect();if(callback==1){cb->OnComplete();}if(callback==2){cb->OnFailed();}return accept;}};
|
||||
'''
|
||||
main=r'''
|
||||
struct Fixture{ERR_GameMode mode;Ledger ledger;Group donorGroup,recipientGroup,otherGroup;Record donorRecord,recipientRecord;Entity truck,recipient,driver,spareDriver,magEntity,rifle,foreign;
|
||||
VehicleManager vehicleManager;Utility donorUtility,recipientUtility;Physics_ physics;Usage usage;Slot pilot,fromSlot,toSlot;Storage sourceStorage,targetStorage;Inventory source,destination;Item item;Magazine magazine;Manager weapons;Weapon weapon;Muzzle muzzle;Well well;
|
||||
Fixture(){ERR_GameMode::current=&mode;mode.ledger=&ledger;donorRecord.Group=&donorGroup;recipientRecord.Group=&recipientGroup;ledger.records={&donorRecord,&recipientRecord};
|
||||
donorGroup.utility=&donorUtility;recipientGroup.utility=&recipientUtility;donorUtility.m_VehicleMgr=&vehicleManager;vehicleManager.vehicle=&truck;
|
||||
truck.physics=&physics;truck.usage=&usage;usage.pilot=&pilot;pilot.occupant=&driver;driver.group=&donorGroup;driver.vehicle=&truck;spareDriver.group=&donorGroup;spareDriver.vehicle=&truck;
|
||||
recipient.group=&recipientGroup;recipient.position.x=4;truck.inventory=&source;source.owner=&truck;recipient.inventory=&destination;destination.owner=&recipient;
|
||||
sourceStorage.owner=&truck;targetStorage.owner=&recipient;source.storages={&sourceStorage};destination.storages={&targetStorage};destination.selected=&targetStorage;
|
||||
source.items={&magEntity};magEntity.item=&item;item.slot=&fromSlot;fromSlot.storage=&sourceStorage;fromSlot.attached=&magEntity;toSlot.storage=&targetStorage;magEntity.magazine=&magazine;magazine.well=&well;
|
||||
recipient.weapons=&weapons;weapons.weapons={&rifle};rifle.weapon=&weapon;weapon.owner=&rifle;weapon.muzzles={&muzzle};muzzle.well=&well;}
|
||||
ERR_InfantryAmmoTransfer Job(){return ERR_InfantryAmmoTransfer(&mode,&truck,&donorGroup,&recipient,&recipientGroup,&magEntity,true);}
|
||||
void Move(){source.items.RemoveItem(&magEntity);destination.items.Insert(&magEntity);item.slot=&toSlot;}
|
||||
};
|
||||
struct EmptyFixture:Fixture{
|
||||
Entity empty,secondEmpty;Magazine emptyMag;Item emptyItem,secondItem;Slot emptyFrom,emptyTo;
|
||||
EmptyFixture(){emptyMag.well=&well;emptyMag.rounds=0;empty.magazine=&emptyMag;empty.item=&emptyItem;
|
||||
emptyFrom.storage=&targetStorage;emptyTo.storage=&sourceStorage;emptyItem.slot=&emptyFrom;
|
||||
secondEmpty.magazine=&emptyMag;secondEmpty.item=&secondItem;secondItem.slot=&emptyFrom;
|
||||
destination.items={&empty,&secondEmpty};destination.selected=nullptr;source.selected=&sourceStorage;}
|
||||
void Return(bool capacity=true){destination.items.RemoveItem(&empty);source.items.Insert(&empty);emptyItem.slot=&emptyTo;if(capacity)destination.selected=&targetStorage;}
|
||||
};
|
||||
struct ERR_HQAmmoSnapshot{bool Complete=true,LauncherComplete=true;int Members=1,LauncherMuzzles=1,LauncherMagazines=1,LauncherRounds=0,LauncherChambers=0;bool Valid(int n){return n==Members;}int SmallArmsMuzzles=1,SmallArmsRounds=0,SmallArmsChambers=0;};
|
||||
struct ERR_HQAmmoSampler{static void Inspect(IEntity actor,SCR_AIGroup group,ERR_HQAmmoSnapshot* stock){assert(actor&&actor->group==group&&stock);}};
|
||||
struct SupplyTruck{IEntity Entity;SCR_AIGroup Group;};
|
||||
struct SupplyRecipient{SCR_AIGroup Group;};
|
||||
struct Coordinator{bool m_LauncherTrip=false;ERR_GameMode* m_Mode;SupplyTruck* m_Truck;SupplyRecipient* m_Recipient;
|
||||
ERR_InfantryAmmoTransfer* m_Move=nullptr;Array<IEntity>m_Members,m_Stock,m_Spent,m_Served;IEntity m_Receiving=nullptr;
|
||||
int m_Delivered=0,m_TripDelivered=0,finishes=0,audits=0;
|
||||
void Finish(string){finishes++;if(m_Move)m_Move->Stop();}
|
||||
void Audit(string){audits++;}
|
||||
bool Contains(IEntity owner,IEntity item){return owner&&owner->inventory&&owner->inventory->items.Count()<=128&&owner->inventory->items.Contains(item);}
|
||||
'''+transfer+r'''
|
||||
};
|
||||
struct SupplyFixture:Fixture{SupplyTruck supplyTruck;SupplyRecipient supplyRecipient;Coordinator c;
|
||||
SupplyFixture():supplyTruck{&truck,&donorGroup},supplyRecipient{&recipientGroup}{
|
||||
c.m_Mode=&mode;c.m_Truck=&supplyTruck;c.m_Recipient=&supplyRecipient;c.m_Members={&recipient};c.m_Stock={&magEntity};}
|
||||
};
|
||||
struct LauncherFixture:SupplyFixture{Entity loaded;Magazine empty;Item loadedItem;Slot loadedSlot;
|
||||
LauncherFixture(){c.m_LauncherTrip=true;weapon.kind=EWeaponType::WT_GRENADELAUNCHER;muzzle.kind=EMuzzleType::MT_UGLMuzzle;
|
||||
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());
|
||||
if(bad!=0)f.source.last->OnFailed();
|
||||
if(bad==1)f.item.slot=nullptr;
|
||||
if(bad==2)f.fromSlot.attached=nullptr;
|
||||
if(bad==3)f.sourceStorage.owner=&f.foreign;
|
||||
if(bad==4)f.destination.items.Insert(&f.magEntity);
|
||||
if(bad==5)f.truck.inventory=&f.destination;
|
||||
if(bad==6)f.source.items.RemoveItem(&f.magEntity);
|
||||
assert(f.c.m_Move->Unresolved());f.c.m_Move->Stop();assert(f.c.m_Move->Unresolved());}
|
||||
|
||||
{LauncherFixture f;f.muzzle.loaded=nullptr;assert(ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity,true));f.c.Transfer(0);assert(f.source.calls==1&&f.c.m_Move->Submitted());}
|
||||
|
||||
{LauncherFixture f;assert(!ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));assert(ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity,true));
|
||||
f.c.Transfer(0);assert(f.c.m_Move&&f.c.m_Move->Submitted()&&f.c.m_Spent.Count()==1&&f.source.calls==1);
|
||||
f.Move();f.source.last->OnComplete();f.c.Transfer(1000);f.c.Transfer(2000);assert(f.c.m_Delivered==1&&f.source.calls==1&&f.c.m_Served.Count()==1);}
|
||||
for(int bad=0;bad<8;bad++){LauncherFixture f;if(bad==0)f.magazine.used=true;if(bad==1)f.muzzle.well=nullptr;if(bad==2)f.muzzle.disposable=true;if(bad==3)f.empty.well=nullptr;if(bad==4)f.loadedSlot.attached=nullptr;if(bad==5)f.rifle.parent=nullptr;if(bad==6)f.empty.capacity=0;if(bad==7){f.muzzle.inMag=true;f.muzzle.canReload=false;}
|
||||
f.c.Transfer(0);assert(!f.c.m_Move&&f.source.calls==0&&f.c.m_Spent.IsEmpty());}
|
||||
{LauncherFixture f;f.source.accept=false;f.c.Transfer(0);assert(f.c.m_Move&&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.Move();f.source.last->OnComplete();assert(!f.c.m_Move->Unresolved());}
|
||||
{LauncherFixture f;auto t=ERR_InfantryAmmoTransfer(&f.mode,&f.truck,&f.donorGroup,&f.recipient,&f.recipientGroup,&f.magEntity,false,true);
|
||||
f.source.effect=[&](){t.Stop();f.Move();};f.source.callback=1;assert(t.Tick(0)==FAILED&&!t.Unresolved()&&f.source.calls==1);assert(t.Tick(1)==FAILED);}
|
||||
|
||||
// A capacity recovery uses one exact unused empty, separately proving both native operations.
|
||||
{Fixture f;Entity empty;Magazine emptyMag;Item emptyItem;Slot emptyFrom{&f.targetStorage},emptyTo{&f.sourceStorage};
|
||||
emptyMag.well=&f.well;emptyMag.rounds=0;empty.magazine=&emptyMag;empty.item=&emptyItem;emptyItem.slot=&emptyFrom;
|
||||
f.destination.items={&empty};f.destination.selected=nullptr;f.source.selected=&f.sourceStorage;
|
||||
auto t=f.Job();assert(t.Tick(0)==PENDING&&t.Submitted()&&t.Unresolved()&&f.destination.calls==1&&f.source.calls==0);
|
||||
auto returning=f.destination.last;returning->OnComplete();assert(t.Tick(1)==PENDING&&f.source.calls==0&&t.Unresolved());
|
||||
f.destination.items.clear();f.source.items.Insert(&empty);emptyItem.slot=&emptyTo;f.destination.selected=&f.targetStorage;
|
||||
assert(t.Tick(2)==PENDING&&!t.Unresolved()&&f.source.calls==0);assert(t.Tick(3)==PENDING&&f.source.calls==1&&t.Unresolved());
|
||||
returning->OnAllComplete();returning->OnFailed();assert(t.Tick(4)==PENDING&&t.Unresolved());
|
||||
f.Move();assert(t.Tick(5)==PENDING);f.source.last->OnComplete();assert(t.Tick(6)==SUCCEEDED&&!t.Unresolved());
|
||||
assert(f.destination.calls==1&&f.source.calls==1);}
|
||||
// Ground coordinator reserves the incoming identity as soon as reverse native movement is submitted.
|
||||
// A failed/ambiguous return never releases it to try a second empty or another incoming magazine.
|
||||
for(int outcome=0;outcome<3;outcome++){EmptyFixture f;SupplyTruck truck{&f.truck,&f.donorGroup};SupplyRecipient recipient{&f.recipientGroup};Coordinator c;
|
||||
c.m_Mode=&f.mode;c.m_Truck=&truck;c.m_Recipient=&recipient;c.m_Members={&f.recipient};c.m_Stock={&f.magEntity};
|
||||
if(outcome==0){f.destination.accept=false;}
|
||||
c.Transfer(0);assert(c.m_Move&&c.m_Move->Submitted()&&c.m_Spent.Count()==1&&c.m_Receiving==&f.recipient&&c.m_Delivered==0);
|
||||
if(outcome==1){f.Return(false);f.destination.last->OnComplete();c.Transfer(1000);}
|
||||
c.Transfer(outcome==2?8000:2000);assert(c.finishes==1&&c.m_Spent.Contains(&f.magEntity)&&c.m_Delivered==0&&f.destination.calls==1&&f.source.calls==0);
|
||||
assert(c.m_Move->Unresolved()==(outcome==2));}
|
||||
{EmptyFixture f;SupplyTruck truck{&f.truck,&f.donorGroup};SupplyRecipient recipient{&f.recipientGroup};Coordinator c;
|
||||
c.m_Mode=&f.mode;c.m_Truck=&truck;c.m_Recipient=&recipient;c.m_Members={&f.recipient};c.m_Stock={&f.magEntity};
|
||||
c.Transfer(0);f.Return();f.destination.last->OnComplete();c.Transfer(1000);assert(c.m_Delivered==0&&f.source.calls==0);
|
||||
c.Transfer(2000);assert(c.m_Delivered==0&&f.source.calls==1);f.Move();f.source.last->OnComplete();c.Transfer(3000);c.Transfer(4000);
|
||||
assert(c.m_Delivered==1&&c.m_Served.Count()==1&&c.m_Spent.Count()==1&&f.source.calls==1&&f.destination.calls==1&&f.source.items.Contains(&f.empty));}
|
||||
// Default callers (including air delivery) retain the original one-way behavior.
|
||||
{EmptyFixture f;ERR_InfantryAmmoTransfer t(&f.mode,&f.truck,&f.donorGroup,&f.recipient,&f.recipientGroup,&f.magEntity);
|
||||
assert(t.Tick(0)==FAILED&&!t.Submitted()&&f.destination.calls==0&&f.source.calls==0);}
|
||||
// No return when capacity already exists, even with carried empties.
|
||||
{EmptyFixture f;f.destination.selected=&f.targetStorage;auto t=f.Job();assert(t.Tick(0)==PENDING&&f.source.calls==1&&f.destination.calls==0);}
|
||||
// Unused, zero-round, exact-well, exclusive recipient storage ownership and donor space are required.
|
||||
for(int invalid=0;invalid<12;invalid++){EmptyFixture f;f.destination.items={&f.empty};Well other;other.type=2;
|
||||
if(invalid==0)f.emptyMag.used=true;if(invalid==1)f.emptyMag.rounds=1;if(invalid==2)f.emptyMag.rounds=-1;
|
||||
if(invalid==3)f.emptyMag.capacity=0;if(invalid==4)f.emptyMag.well=&other;if(invalid==5)f.emptyMag.well=nullptr;
|
||||
if(invalid==6)f.emptyItem.slot=&f.emptyTo;if(invalid==7)f.source.items.Insert(&f.empty);
|
||||
if(invalid==8)f.source.selected=nullptr;if(invalid==9)f.destination.allow=false;
|
||||
if(invalid==10){for(int i=0;i<129;i++)f.destination.items.Insert(&f.empty);}if(invalid==11)ERR_InfantryAmmoAccess::clear=false;
|
||||
auto t=f.Job();assert(t.Tick(0)==FAILED&&!t.Submitted()&&!t.Unresolved()&&f.destination.calls==0&&f.source.calls==0);
|
||||
ERR_InfantryAmmoAccess::clear=true;}
|
||||
// Return proof needs exact destination slot as well as callback; no second candidate is ever moved.
|
||||
{EmptyFixture f;auto t=f.Job();t.Tick(0);f.Return();f.emptyItem.slot=&f.emptyFrom;f.destination.last->OnComplete();
|
||||
assert(t.Tick(1)==PENDING&&t.Unresolved()&&f.source.calls==0&&f.destination.calls==1);
|
||||
f.emptyItem.slot=&f.emptyTo;assert(t.Tick(2)==PENDING&&!t.Unresolved());assert(t.Tick(3)==PENDING&&f.source.calls==1&&f.destination.calls==1);}
|
||||
// Inline success callbacks override a false API result for the reverse operation, but require physical proof.
|
||||
{EmptyFixture f;f.destination.accept=false;f.destination.callback=1;f.destination.effect=[&](){f.Return();};auto t=f.Job();
|
||||
assert(t.Tick(0)==PENDING&&t.Submitted()&&!t.Unresolved());assert(t.Tick(1)==PENDING&&f.source.calls==0);
|
||||
f.source.accept=false;f.source.callback=1;f.source.effect=[&](){f.Move();};assert(t.Tick(2)==SUCCEEDED&&f.source.calls==1&&f.destination.calls==1);}
|
||||
// Every attempted reverse move burns the incoming identity, including synchronous rejection.
|
||||
for(int failure=0;failure<3;failure++){EmptyFixture f;if(failure==0)f.destination.accept=false;if(failure==1)f.destination.callback=2;
|
||||
auto t=f.Job();assert(t.Tick(0)==PENDING&&t.Submitted()&&f.destination.calls==1&&f.source.calls==0);
|
||||
assert(t.Tick(failure==2?8000:1)==FAILED);assert(t.Unresolved()==(failure==2));
|
||||
for(int i=0;i<3;i++){t.Tick(9000+i);}assert(f.destination.calls==1&&f.source.calls==0);}
|
||||
// Stop before, during, or after confirmed return can never launch the incoming move.
|
||||
for(int stopAt=0;stopAt<3;stopAt++){EmptyFixture f;auto t=f.Job();if(stopAt==0){t.Stop();assert(t.Tick(0)==FAILED&&!t.Submitted());continue;}
|
||||
t.Tick(0);if(stopAt==2){f.Return();f.destination.last->OnComplete();assert(t.Tick(1)==PENDING&&!t.Unresolved());}
|
||||
t.Stop();assert(t.Tick(2)==FAILED&&t.Submitted()&&f.source.calls==0);if(stopAt==1){assert(t.Unresolved());f.Return();f.destination.last->OnAllComplete();}
|
||||
assert(!t.Unresolved()&&t.Tick(3)==FAILED&&f.source.calls==0&&f.destination.calls==1);}
|
||||
// Confirmed return cannot trigger a second empty or bypass revalidated incoming custody/capacity/access.
|
||||
for(int changed=0;changed<6;changed++){EmptyFixture f;auto t=f.Job();t.Tick(0);f.Return(changed!=0);f.destination.last->OnComplete();assert(t.Tick(1)==PENDING);
|
||||
if(changed==1)f.source.items.RemoveItem(&f.magEntity);if(changed==2)f.destination.items.Insert(&f.magEntity);
|
||||
if(changed==3)f.truck.captured=true;if(changed==4)f.recipient.player=true;if(changed==5)ERR_InfantryAmmoAccess::clear=false;
|
||||
assert(t.Tick(2)==FAILED&&t.Submitted()&&!t.Unresolved()&&f.source.calls==0&&f.destination.calls==1);
|
||||
assert(t.Tick(3)==FAILED&&f.destination.calls==1);ERR_InfantryAmmoAccess::clear=true;}
|
||||
// Original owner/driver/ledger loss while a return is accepted quarantines that exact operation.
|
||||
for(int changed=0;changed<4;changed++){EmptyFixture f;auto t=f.Job();t.Tick(0);
|
||||
if(changed==0)f.truck.captured=true;if(changed==1)f.recipient.player=true;if(changed==2)f.pilot.occupant=&f.spareDriver;if(changed==3)f.donorRecord.Eliminated=true;
|
||||
assert(t.Tick(1)==FAILED&&t.Unresolved());f.destination.last->OnFailed();assert(!t.Unresolved()&&f.source.calls==0&&f.destination.calls==1);}
|
||||
// Both native phases are bounded independently, with no renewal while waiting for callbacks.
|
||||
{EmptyFixture f;auto t=f.Job();t.Tick(0);f.Return();f.destination.last->OnComplete();assert(t.Tick(7999)==PENDING);
|
||||
assert(t.Tick(8000)==PENDING&&f.source.calls==1);assert(t.Tick(15999)==FAILED&&t.Unresolved());}
|
||||
// XZ approach can precede valid vertical reach, stopped state, capacity or clear access.
|
||||
// None of those no-submission outcomes consumes cargo or ends the trip; the same exact item can later submit once.
|
||||
for(int obstruction=0;obstruction<5;obstruction++){SupplyFixture f;
|
||||
if(obstruction==0){f.recipient.position.y=3;}if(obstruction==1){f.physics.velocity.x=1;}
|
||||
if(obstruction==2){f.destination.selected=nullptr;}if(obstruction==3){f.source.allow=false;}
|
||||
if(obstruction==4){ERR_InfantryAmmoAccess::clear=false;}
|
||||
for(int tick=0;tick<3;tick++){f.c.Transfer(tick*1000);assert(f.source.calls==0&&f.c.m_Spent.IsEmpty()&&!f.c.m_Move&&!f.c.m_Receiving&&f.c.finishes==0);}
|
||||
f.recipient.position.y=0;f.physics.velocity.x=0;f.destination.selected=&f.targetStorage;f.source.allow=true;ERR_InfantryAmmoAccess::clear=true;
|
||||
f.c.Transfer(3000);assert(f.source.calls==1&&f.c.m_Spent.Count()==1&&f.c.m_Move&&f.c.m_Move->Submitted());
|
||||
f.Move();f.source.last->OnComplete();f.c.Transfer(4000);assert(f.c.m_Delivered==1&&f.c.m_Served.Contains(&f.recipient)&&f.source.calls==1&&f.c.m_Spent.Count()==1);
|
||||
}
|
||||
// A capacity-blocked first member cannot prevent another original member from receiving this magazine.
|
||||
{SupplyFixture f;Fixture second;ERR_GameMode::current=&f.mode;
|
||||
second.recipient.group=&f.recipientGroup;second.recipientRecord.Group=&f.recipientGroup;
|
||||
f.c.m_Members.Insert(&second.recipient);f.destination.selected=nullptr;
|
||||
f.c.Transfer(0);assert(f.source.calls==1&&f.c.m_Receiving==&second.recipient&&f.c.m_Spent.Count()==1&&f.c.finishes==0);
|
||||
f.source.items.clear();second.destination.items={&f.magEntity};f.item.slot=&second.toSlot;f.source.last->OnComplete();
|
||||
f.c.Transfer(1000);assert(f.c.m_Delivered==1&&f.c.m_Served.Contains(&second.recipient)&&!f.c.m_Served.Contains(&f.recipient));}
|
||||
// A native API may complete both physical movement and callback inline. Account once on the next slow tick.
|
||||
{SupplyFixture f;f.source.callback=1;f.source.effect=[&](){f.Move();};f.c.Transfer(0);
|
||||
assert(f.source.calls==1&&f.c.m_Move&&f.c.m_Move->Submitted()&&f.c.m_Delivered==0&&f.c.m_Spent.Count()==1);
|
||||
f.c.Transfer(1000);f.c.Transfer(2000);assert(f.c.m_Delivered==1&&f.c.m_Served.Count()==1&&f.source.calls==1&&f.c.finishes==0);}
|
||||
// Crossing the native submission boundary burns the exact identity even if native refusal is synchronous.
|
||||
for(int refusal=0;refusal<2;refusal++){SupplyFixture f;if(refusal==0){f.source.accept=false;}else{f.source.callback=2;}
|
||||
f.c.Transfer(0);assert(f.source.calls==1&&f.c.m_Spent.Count()==1&&f.c.m_Move->Submitted());
|
||||
f.c.Transfer(1000);assert(f.c.finishes==1&&f.c.m_Spent.Count()==1&&f.source.calls==1);
|
||||
}
|
||||
// Accepted but unresolved loss stays retained/spent; changing geometry cannot cause a second native move.
|
||||
{SupplyFixture f;f.c.Transfer(0);f.recipient.position.x=8;f.c.Transfer(1000);
|
||||
assert(f.c.finishes==1&&f.c.m_Move&&f.c.m_Move->Unresolved()&&f.c.m_Spent.Count()==1&&f.source.calls==1);}
|
||||
// Several equivalent magazines do not multiply a blocked actor's expensive preflight work on one slow tick.
|
||||
{SupplyFixture f;Entity another=f.magEntity;f.source.items.Insert(&another);f.c.m_Stock.Insert(&another);
|
||||
ERR_InfantryAmmoAccess::clear=false;ERR_InfantryAmmoAccess::checks=0;f.c.Transfer(0);
|
||||
assert(ERR_InfantryAmmoAccess::checks==1&&f.source.calls==0&&f.c.m_Spent.IsEmpty());ERR_InfantryAmmoAccess::clear=true;}
|
||||
// A blocked native access check happens before submission and is terminal.
|
||||
{Fixture f;auto t=f.Job();ERR_InfantryAmmoAccess::clear=false;ERR_InfantryAmmoAccess::checks=0;
|
||||
assert(t.Tick(0)==FAILED&&!t.Submitted()&&!t.Unresolved()&&f.source.calls==0&&ERR_InfantryAmmoAccess::checks==1);
|
||||
ERR_InfantryAmmoAccess::clear=true;assert(t.Tick(1)==FAILED&&f.source.calls==0&&ERR_InfantryAmmoAccess::checks==1);}
|
||||
// Once native move is accepted, later geometry cannot cause a second operation or pretend cancellation.
|
||||
{Fixture f;auto t=f.Job();ERR_InfantryAmmoAccess::checks=0;assert(t.Tick(0)==PENDING&&ERR_InfantryAmmoAccess::checks==1);
|
||||
ERR_InfantryAmmoAccess::clear=false;assert(t.Tick(1)==PENDING&&t.Unresolved()&&f.source.calls==1&&ERR_InfantryAmmoAccess::checks==1);
|
||||
f.Move();f.source.last->OnComplete();assert(t.Tick(2)==SUCCEEDED);ERR_InfantryAmmoAccess::clear=true;}
|
||||
// Read-only selection follows the native inherited-well direction, excluding launcher muzzles and weapons.
|
||||
{Fixture f;assert(ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));Well derived;derived.type=2;derived.parent=1;f.magazine.well=&derived;assert(ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));
|
||||
f.magazine.well=&f.well;f.muzzle.well=&derived;assert(!ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));
|
||||
f.muzzle.well=&f.well;f.muzzle.kind=EMuzzleType::MT_UGLMuzzle;assert(!ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));
|
||||
f.muzzle.kind=0;f.weapon.kind=EWeaponType::WT_ROCKETLAUNCHER;assert(!ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));}
|
||||
// Exactly one source move; neither a callback alone nor physical movement alone is a successful transaction.
|
||||
{Fixture f;auto t=f.Job();assert(t.Tick(100)==PENDING&&f.source.calls==1&&t.Unresolved());f.source.last->OnComplete();assert(t.Tick(101)==PENDING&&t.Unresolved());
|
||||
f.Move();assert(t.Tick(102)==SUCCEEDED&&!t.Unresolved());t.Stop();assert(t.Tick(103)==SUCCEEDED&&f.source.calls==1);}
|
||||
{Fixture f;auto t=f.Job();t.Tick(100);f.Move();assert(t.Tick(101)==PENDING&&t.Unresolved());f.source.last->OnAllComplete();assert(t.Tick(102)==SUCCEEDED);}
|
||||
// Exact destination slot and known donor absence are mandatory even after success callback.
|
||||
{Fixture f;auto t=f.Job();t.Tick(100);f.Move();f.item.slot=&f.fromSlot;f.source.last->OnComplete();assert(t.Tick(101)==PENDING&&t.Unresolved());
|
||||
f.item.slot=&f.toSlot;for(int i=0;i<513;i++)f.source.items.Insert(&f.foreign);assert(t.Tick(102)==PENDING&&t.Unresolved());}
|
||||
// Late callbacks resolve retained reservations after Stop without Tick or further operations; the stopped task stays failed.
|
||||
{Fixture f;auto t=f.Job();t.Tick(0);t.Stop();assert(t.Unresolved());f.Move();f.source.last->OnAllComplete();assert(!t.Unresolved());assert(t.Tick(2)==FAILED&&f.source.calls==1);}
|
||||
{Fixture f;auto t=f.Job();t.Tick(0);t.Stop();f.source.last->OnAllFailed();assert(!t.Unresolved());assert(t.Tick(1)==FAILED&&f.source.calls==1);}
|
||||
{Fixture f;auto t=f.Job();t.Tick(0);f.source.last->OnComplete();f.source.last->OnFailed();assert(t.Unresolved());f.Move();assert(!t.Unresolved()&&t.Tick(1)==SUCCEEDED);}
|
||||
// Failure/rejection and the 8s deadline never submit a second move.
|
||||
for(int failure=0;failure<3;failure++){Fixture f;if(failure==0)f.source.accept=false;if(failure==1)f.source.callback=2;auto t=f.Job();t.Tick(100);
|
||||
if(failure==2){assert(t.Tick(8100)==FAILED&&t.Unresolved());}else assert(t.Tick(101)==FAILED&&!t.Unresolved());
|
||||
for(int i=0;i<10;i++)t.Tick(8200+i);assert(f.source.calls==1);}
|
||||
// Invalid physical state before submission spends no native action; the terminal object cannot revive.
|
||||
for(int invalid=0;invalid<25;invalid++){Fixture f;auto t=f.Job();
|
||||
if(invalid==0)Replication::server=false;if(invalid==1)SCR_Global::editor=true;if(invalid==2)f.mode.active=false;
|
||||
if(invalid==3)f.truck.alive=false;if(invalid==4)f.recipient.alive=false;if(invalid==5)f.recipient.awake=false;if(invalid==6)f.recipient.player=true;
|
||||
if(invalid==7)f.recipient.vehicle=&f.truck;if(invalid==8)f.truck.captured=true;if(invalid==9)f.donorGroup.players=1;
|
||||
if(invalid==10)f.recipient.group=&f.otherGroup;if(invalid==11)f.donorRecord.Physical=false;if(invalid==12)f.recipientRecord.Eliminated=true;
|
||||
if(invalid==13)f.vehicleManager.vehicle=&f.foreign;if(invalid==14)f.driver.alive=false;if(invalid==15)f.driver.vehicle=nullptr;
|
||||
if(invalid==16)f.physics.velocity.x=.251f;if(invalid==17)f.physics.angular.x=.101f;if(invalid==18)f.recipient.position.x=4.001f;
|
||||
if(invalid==19)f.truck.faction=2;if(invalid==20)f.driver.faction=2;if(invalid==21)f.recipientGroup.slave=true;
|
||||
if(invalid==22)f.magazine.rounds=0;if(invalid==23)f.magazine.rounds=31;if(invalid==24)f.physics.velocity.x=std::numeric_limits<float>::quiet_NaN();
|
||||
assert(t.Tick(100)==FAILED&&f.source.calls==0&&!t.Unresolved());Replication::server=true;SCR_Global::editor=false;assert(t.Tick(200)==FAILED&&f.source.calls==0);}
|
||||
// Native stock/storage identity and capacity guards reject shared, foreign, oversized and inaccessible inventory.
|
||||
for(int invalid=0;invalid<9;invalid++){Fixture f;auto t=f.Job();if(invalid==0)f.source.items.clear();if(invalid==1)f.destination.items={&f.magEntity};
|
||||
if(invalid==2)f.fromSlot.storage=&f.targetStorage;if(invalid==3)f.sourceStorage.owner=&f.foreign;if(invalid==4)f.destination.selected=nullptr;
|
||||
if(invalid==5)f.targetStorage.owner=&f.foreign;if(invalid==6)f.source.allow=false;if(invalid==7)f.destination.storages.clear();if(invalid==8){for(int i=0;i<65;i++)f.destination.storages.Insert(&f.targetStorage);}
|
||||
assert(t.Tick(0)==FAILED&&f.source.calls==0);}
|
||||
// Conditions and original native driver/ledger identities remain strict while an accepted move is pending.
|
||||
for(int changed=0;changed<4;changed++){Fixture f;auto t=f.Job();t.Tick(100);Record replacement=f.recipientRecord;
|
||||
if(changed==0)f.pilot.occupant=&f.spareDriver;if(changed==1)f.ledger.records[1]=&replacement;if(changed==2)f.physics.velocity.x=1;if(changed==3)f.recipient.position.x=8;
|
||||
assert(t.Tick(101)==FAILED&&t.Unresolved());f.source.last->OnFailed();assert(!t.Unresolved()&&f.source.calls==1);}
|
||||
{Fixture f;auto t=f.Job();t.Tick(100);assert(t.Tick(99)==FAILED&&t.Unresolved());}
|
||||
// Clothing storage ownership follows native inventory slots and bounds cycles.
|
||||
{Fixture f;Entity vest;Item vestItem;Slot vestSlot;Storage body;body.owner=&f.recipient;vestSlot.storage=&body;vestItem.slot=&vestSlot;vest.item=&vestItem;f.targetStorage.owner=&vest;
|
||||
auto t=f.Job();assert(t.Tick(0)==PENDING);f.Move();f.source.last->OnComplete();assert(t.Tick(1)==SUCCEEDED);
|
||||
vestSlot.storage=&f.targetStorage;assert(!ERR_InfantryAmmoTransfer::StoredUnder(&vest,&f.recipient));}
|
||||
}
|
||||
'''
|
||||
# Brace tiny fixture statements to retain -Werror without suppressing useful compiler diagnostics.
|
||||
main=re.sub(r'if\((invalid|changed|failure)==(\d+)\)([^;{}]+);',r'if(\1==\2){\3;}',main)
|
||||
main=main.replace('for(int i=0;i<10;i++)t.Tick(8200+i);','for(int i=0;i<10;i++){t.Tick(8200+i);}')
|
||||
run(pre+translated_reservation()+code+'\n'.join(callbacks)+main)
|
||||
print('PASS: actual bounded empty-return and finite delivery; phase-isolated callbacks, exact ownership proof, stop quarantine and original actor/vehicle boundaries')
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
"""Actual coordinator decisions/transitions with native boundaries doubled; no engine proof."""
|
||||
import os,re,sys
|
||||
from pathlib import Path
|
||||
REPO=Path(os.environ.get('ERR_REPO','/home/jakub/devel/escapistsrereintroduced'))
|
||||
sys.path.insert(0,str(REPO/'tests'))
|
||||
from policy_harness import body,run
|
||||
SOURCE=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',REPO/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
||||
def method(signature):
|
||||
s=body(SOURCE,signature).replace('protected ','').replace('null','nullptr')
|
||||
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_ForceRules.','ERR_ForceRules::').replace('ERR_InfantryResupplyRules.','ERR_InfantryResupplyRules::').replace('ERR_Actors.','ERR_Actors::')
|
||||
s=s.replace(' + "3 0 0"',' + Vec{3,0,0}')
|
||||
s=re.sub(r'string\.Format\("walk to reported stopped ammo truck at %1", m_Rendezvous\)', '"rendezvous"', s)
|
||||
for kind in ['ERR_HQWatchEntry','Physics']:
|
||||
s=re.sub(r'\b'+kind+r' (\w+)',kind+r'* \1',s)
|
||||
# Enforce native entity/reference member access becomes C++ pointer member access.
|
||||
for name in ['entry','recipient','donor','stopped','m_HQ','m_Move','m_MineMove','m_Mounted','m_Recipient','m_Donor','m_Truck','m_Order','physics']:
|
||||
s=s.replace(name+'.',name+'->')
|
||||
for name in ['AmmoState','Group','Combat','Entity']:
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Watch().','Watch()->').replace('GetVelocity().','GetVelocity().').replace('GetAngularVelocity().','GetAngularVelocity().')
|
||||
return s
|
||||
rules='\n'.join(method(x) for x in ['static bool Fresh','static bool Need','static bool Pair'])
|
||||
methods='\n'.join(method(x) for x in ['protected void PollTransfers','protected void ClearWaypoints','protected void Finish','void Tick(float now)','void Stop()'])
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
using string=std::string;
|
||||
struct Vec {float x=0,y=0,z=0; float Length(){return std::sqrt(x*x+y*y+z*z);} static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);} Vec operator+(Vec b){return {x+b.x,y+b.y,z+b.z};} string ToString(){return "position";}};
|
||||
struct ERR_ForceRules {static bool Finite(float n){return std::isfinite(n);} static bool FiniteVector(Vec v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}};
|
||||
struct Ammo {bool Complete=true,valid=true;int Members=2,SmallArmsMuzzles=2,SmallArmsRounds=60,SmallArmsChambers=0;bool Valid(int){return valid;}};
|
||||
struct ERR_HQWatchEntry {bool Closed=false;int Count=2,Fit=2,SourceId=1;float ObservedAt=0;Vec Position;Ammo* AmmoState=nullptr;};
|
||||
struct ERR_InfantryResupplyRules {
|
||||
RULES
|
||||
};
|
||||
struct Waypoint {int id;};
|
||||
struct Group {std::vector<Waypoint*> points;void RemoveWaypoint(Waypoint* p){points.erase(std::remove(points.begin(),points.end(),p),points.end());}};
|
||||
struct ERR_Actors {static inline int deleted=0;static void DeleteSpawned(Waypoint*){deleted++;}};
|
||||
struct Physics {Vec linear,angular;Vec GetVelocity(){return linear;} Vec GetAngularVelocity(){return angular;}};
|
||||
struct Entity {Physics* physics=nullptr;Physics* GetPhysics(){return physics;}};
|
||||
struct Combat {bool Held=false;};
|
||||
struct Truck {int SupplyToken=7,clears=0;bool SupplyHeld=false;struct Combat* Combat=nullptr;struct Entity* Entity=nullptr;void ClearOrder(){clears++;}};
|
||||
struct Unit {struct Group* Group=nullptr;int ForceSourceId=1;float NextStatus=-1;};
|
||||
struct Leaf {int stops=0;bool unresolved=true;void Stop(){stops++;}bool Unresolved(){return unresolved;}};
|
||||
struct Mounted{bool busy=false;int ticks=0,stops=0;bool Busy(){return busy;}void Tick(float){ticks++;}void Stop(){stops++;}};
|
||||
struct Route {int result=0,advances=0;int Advance(float,int){advances++;return result;}};
|
||||
struct Watch {ERR_HQWatchEntry* entry=nullptr;ERR_HQWatchEntry* Find(int){return entry;}};
|
||||
struct HQ {int token=7,releases=0,downlinks=0;struct Watch watch;Route* next=nullptr;int Journal(){return 0;}int LogisticsToken(Group*){return token;}void ReleaseLogistics(Group*,int owned){releases++;if(token==owned)token=0;}struct Watch* Watch(){return &watch;} Route* Downlink(Group*,string,string,int){downlinks++;return next;}};
|
||||
struct Service {
|
||||
HQ* m_HQ;Truck* m_Truck;Unit* m_Recipient;Unit* m_Donor;Leaf* m_Move=nullptr;Leaf*m_MineMove=nullptr;bool m_MineTrip=false,m_LauncherTrip=false;int m_MineRecipientSlot=-1;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;Entity* m_Receiving=nullptr;
|
||||
Waypoint* m_MoveWaypoint=nullptr;Waypoint* m_HoldWaypoint=nullptr;Vec m_Rendezvous;int m_State=2,m_Token=7,m_ReportId=11;
|
||||
float m_NextTick=0,m_Deadline=200000,m_HeldAt=-1;bool m_Stopped=false,valid=true,quiet=true,roster=true,waypoints=true;int audits=0,transfers=0,waypointCalls=0,stocks=0;
|
||||
bool Current(){return !m_Stopped;}bool TruckValid(){return valid;}bool Quiet(Unit*){return quiet;}bool Roster(){return roster;}void Audit(string){audits++;}void Stock(float){stocks++;}bool Waypoints(){waypointCalls++;return waypoints;}void Transfer(float){transfers++;}int mineTransfers=0;void TransferMine(float){mineTransfers++;}
|
||||
METHODS
|
||||
};
|
||||
struct Fixture {Physics physics;Entity entity{&physics};Combat combat;Truck truck;Group group,donorGroup;Unit recipient{&group},donor{&donorGroup};HQ hq;Route order,next;Service s;
|
||||
Fixture(){truck.Combat=&combat;truck.Entity=&entity;s.m_HQ=&hq;s.m_Truck=&truck;s.m_Recipient=&recipient;s.m_Donor=&donor;s.m_Order=ℴhq.next=&next;}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;Leaf leaf;f.s.m_Move=&leaf;f.s.m_LauncherTrip=true;f.truck.SupplyHeld=true;f.s.Stop();assert(f.s.m_State==7&&f.hq.token==7&&f.truck.SupplyHeld);
|
||||
f.s.Tick(200000);assert(f.s.m_State==7&&f.hq.token==7&&f.s.transfers==0);leaf.unresolved=false;f.s.Tick(201000);assert(f.s.m_State==1&&f.hq.token==0&&!f.truck.SupplyHeld&&!f.s.m_Move);}
|
||||
|
||||
{Fixture f;Leaf leaf;f.s.m_MineMove=&leaf;f.s.m_MineTrip=true;f.truck.SupplyHeld=true;f.s.Stop();assert(f.s.m_State==7&&f.hq.token==7&&f.truck.SupplyHeld&&f.s.m_Recipient);
|
||||
f.s.Tick(200000);assert(f.hq.token==7&&f.s.m_State==7&&f.s.transfers==0&&f.s.mineTransfers==0);leaf.unresolved=false;f.s.Tick(201000);assert(f.hq.token==0&&!f.truck.SupplyHeld&&f.s.m_State==1&&!f.s.m_MineMove);}
|
||||
{Fixture f;f.s.m_State=5;f.s.m_MineTrip=true;f.s.Tick(0);assert(f.s.mineTransfers==1&&f.s.transfers==0);}
|
||||
|
||||
Ammo ammo;ERR_HQWatchEntry demand,donor;demand.AmmoState=&ammo;donor.SourceId=2;donor.Position={200,500,0};
|
||||
assert(ERR_InfantryResupplyRules::Need(&demand,120000));assert(ERR_InfantryResupplyRules::Pair(&demand,&donor,120000));
|
||||
assert(!ERR_InfantryResupplyRules::Fresh(&demand,120001));assert(!ERR_InfantryResupplyRules::Fresh(nullptr,0));
|
||||
demand.ObservedAt=1;assert(!ERR_InfantryResupplyRules::Fresh(&demand,0));demand.ObservedAt=0;
|
||||
ammo.SmallArmsChambers=1;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.SmallArmsChambers=0;
|
||||
ammo.Complete=false;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.Complete=true;
|
||||
ammo.Members=1;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.Members=2;
|
||||
ammo.valid=false;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.valid=true;
|
||||
donor.SourceId=1;assert(!ERR_InfantryResupplyRules::Pair(&demand,&donor,0));donor.SourceId=2;donor.Position.x=201;assert(!ERR_InfantryResupplyRules::Pair(&demand,&donor,0));
|
||||
demand.Position.x=std::numeric_limits<float>::quiet_NaN();assert(!ERR_InfantryResupplyRules::Need(&demand,0));demand.Position.x=0;
|
||||
{Fixture f;f.s.Tick(0);assert(f.s.m_State==2&&!f.truck.SupplyHeld&&f.truck.clears==0&&f.s.waypointCalls==0);f.order.result=1;f.s.Tick(1000);assert(f.s.m_State==3&&f.truck.SupplyHeld&&f.truck.clears==1&&f.s.m_Order==nullptr);f.physics.linear.x=1;f.s.Tick(2000);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);f.physics.linear.x=0;f.s.Tick(3000);assert(f.s.m_HeldAt==3000&&f.donor.NextStatus==3000);ERR_HQWatchEntry report;report.Position={20,0,5};report.ObservedAt=2999;f.hq.watch.entry=&report;f.s.Tick(4000);assert(f.hq.downlinks==0);report.ObservedAt=3000;f.s.Tick(5000);assert(f.s.m_State==4&&f.hq.downlinks==1&&f.s.m_Rendezvous.x==23);f.s.Tick(6000);assert(f.s.m_State==4&&f.s.waypointCalls==0&&f.s.transfers==0);f.next.result=1;f.s.Tick(7000);assert(f.s.m_State==5&&f.s.waypointCalls==1&&f.s.transfers==1);}
|
||||
{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;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<float>::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<float>::quiet_NaN();f.s.Tick(0);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);}
|
||||
{Fixture f;f.s.m_Deadline=0;f.s.Tick(0);assert(f.s.m_State==1&&f.order.advances==0&&f.hq.token==0);}
|
||||
// Waiting for physical transfer never extends the original bounded rendezvous deadline.
|
||||
{Fixture f;f.s.m_State=5;f.s.m_Deadline=1000;f.s.Tick(0);assert(f.s.transfers==1&&f.s.m_State==5);
|
||||
f.s.Tick(1000);assert(f.s.transfers==1&&f.s.m_State==1&&f.hq.token==0&&!f.truck.SupplyHeld);}
|
||||
|
||||
{Fixture f;f.s.quiet=false;f.order.result=1;f.s.Tick(0);assert(f.s.m_State==1&&f.order.advances==0&&!f.truck.SupplyHeld);}
|
||||
{Fixture f;f.s.m_State=4;f.order.result=1;f.s.waypoints=false;f.s.Tick(0);assert(f.s.m_State==1&&f.s.waypointCalls==1&&f.s.transfers==0&&f.hq.token==0);}
|
||||
{Fixture f;f.s.m_State=3;f.s.m_HeldAt=100;ERR_HQWatchEntry report;report.ObservedAt=100;f.hq.watch.entry=&report;f.hq.next=nullptr;f.s.Tick(1000);assert(f.s.m_State==1&&f.hq.downlinks==1&&f.s.waypointCalls==0&&f.hq.token==0);}
|
||||
|
||||
}
|
||||
'''.replace('RULES',rules).replace('METHODS',methods)
|
||||
run(code)
|
||||
print('infantry resupply actual rules, radio transitions, owned cleanup and unresolved-operation tests passed')
|
||||
|
||||
# Recheck permanent initial-roster eligibility during an active trip.
|
||||
src=SOURCE
|
||||
s=body(src,'protected bool Roster()').replace('protected ','')
|
||||
s=s.replace('array<AIAgent> agents = {};','Array<Agent*> agents;')
|
||||
s=s.replace('foreach (AIAgent agent : agents)','for (Agent* agent : agents)').replace('foreach (IEntity actor : m_Members)','for (Actor* actor : m_Members)')
|
||||
for x in ['m_Recipient','agent']:s=s.replace(x+'.',x+'->')
|
||||
s=s.replace('StatusSampler.','StatusSampler->').replace('Group.','Group->')
|
||||
for x in ['ERR_Actors','SCR_FactionManager','CompartmentAccessComponent']:s=s.replace(x+'.',x+'::')
|
||||
s=s.replace('m_Mode.','m_Mode->').replace('bool living;', 'bool living = false;')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
template<class T> struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}int Count(){return this->size();}bool Contains(T a){return std::find(this->begin(),this->end(),a)!=this->end();}};
|
||||
struct Group;
|
||||
struct Actor{bool alive=true,player=false,fight=true;Group* group=nullptr;int faction=1;};
|
||||
struct Agent{Actor* actor;Actor* GetControlledEntity(){return actor;}};
|
||||
struct Group{Array<Agent*> agents;void GetAgents(Array<Agent*>& out){out=agents;}};
|
||||
struct Sampler{Group* group;bool released=false;int strength=1;Array<Actor*> original;int InitialStrength(){return strength;}Actor* InitialMember(int slot){return slot>=0&&slot<(int)original.size()?original[slot]:nullptr;}bool OwnsRoster(Group* g){return g==group;}bool CanAttribute(int,Actor*){return !released;}};
|
||||
struct Unit{struct Group* Group;Sampler* StatusSampler;};
|
||||
struct Mode{int EnemyFaction(){return 1;}};
|
||||
struct ERR_Actors{static bool Alive(Actor* a){return a->alive;}static bool Player(Actor* a){return a->player;}static struct Group* Group(Actor* a){return a->group;}static bool CanFight(Actor* a){return a->fight;}};
|
||||
struct SCR_FactionManager{static int SGetFaction(Actor* a){return a->faction;}};
|
||||
struct CompartmentAccessComponent{static bool GetVehicleIn(Actor*){return false;}};
|
||||
struct Service{Unit* m_Recipient;Mode* m_Mode;Array<Actor*> m_Members;bool Quiet(Unit*){return true;}
|
||||
METHOD
|
||||
};
|
||||
int main(){
|
||||
Group group;Actor soldier;soldier.group=&group;Agent agent{&soldier};group.agents={&agent};Sampler sampler{&group,false,1,{&soldier}};Unit unit{&group,&sampler};Mode mode;Service service{&unit,&mode,{&soldier}};assert(service.Roster());sampler.released=true;assert(!sampler.CanAttribute(0,&soldier));
|
||||
// Permanent sampler release survives returning to the physical group.
|
||||
assert(!service.Roster());sampler.released=false;assert(service.Roster());
|
||||
sampler.strength=33;assert(!service.Roster());sampler.strength=0;assert(!service.Roster());sampler.strength=1;
|
||||
Actor foreign;foreign.group=&group;sampler.original={&foreign};assert(!service.Roster());
|
||||
// Initial roster order, not current GetAgents order, determines the slot.
|
||||
sampler.strength=2;sampler.original={&foreign,&soldier};assert(service.Roster());
|
||||
soldier.player=true;assert(!service.Roster());soldier.player=false;
|
||||
soldier.group=nullptr;assert(!service.Roster());soldier.group=&group;
|
||||
soldier.alive=false;assert(!service.Roster());soldier.alive=true;
|
||||
Agent extra{&foreign};group.agents.push_back(&extra);assert(!service.Roster());
|
||||
}
|
||||
'''.replace('METHOD',s))
|
||||
print('PASS: actual resupply Roster preserves permanent release, original slot mapping and bounded ownership')
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
"""Execute real finite-stock/planning/reservation bodies; native inventory/group APIs are doubles."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
P=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
||||
A=Path(os.environ.get('ERR_INFANTRY_TRANSFER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c'))
|
||||
I=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Inventory.c'
|
||||
H=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('notnull ','').replace('null','nullptr')
|
||||
s=s.replace('map<string, int>', 'Map<int>*').replace('map<string, float>', 'Map<float>*')
|
||||
s=s.replace('array<IEntity> items = {};','Array<IEntity> items;').replace('array<IEntity> raw = {};','Array<IEntity> raw;').replace('array<AIAgent> agents = {};','Array<AIAgent> agents;')
|
||||
s=s.replace('out array<IEntity> items','Array<IEntity>& items').replace('array<IEntity> items','Array<IEntity>& items')
|
||||
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
|
||||
for typ in ('InventoryStorageManagerComponent','ERR_ForceRecord','ERR_SupplyConvoyTask'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
for name in ('ERR_ForceRules','ERR_InfantryResupplyRules','ERR_InfantryMineTransfer','ERR_InfantryAmmoTransfer','ERR_ConvoyRules','ERR_ForceKind','ERR_ConvoyResources','ERR_Inventory','ERR_Actors','SCR_FactionManager','CompartmentAccessComponent','Replication','Math'):
|
||||
s=s.replace(name+'.',name+'::')
|
||||
s=s.replace('ERR_SupplyConvoyTask.Cast','ERR_SupplyConvoyTask::Cast').replace('FindComponent(BaseMagazineComponent)','HasMagazine()')
|
||||
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('InventoryStorageManagerComponent.Cast','InventoryStorageManagerComponent::Cast').replace('FindComponent(InventoryStorageManagerComponent)','FindComponent()')
|
||||
for name in ('m_Mode','m_HQ','m_Convoy','m_Truck','m_Recipient','m_Donor','m_Move','m_MineMove','m_MineBatch','m_Mounted','recipient','donor','demand','donorStatus','entry','operation','inventory','item','owner','record','actor','agent','unit','m_MoveWaypoint','m_HoldWaypoint','magazine','supply','m_Consumed','m_NextAttempt'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
for name in ('Receipt','Entity','Group','Utility','StatusSampler','AmmoState','Combat','m_Perception'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
for name in ('Forces()','GetPrefabData()','Convoys()'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('ERR_SupplyConvoyTask* supply', 'Convoy* supply')
|
||||
s=s.replace('bool designated;','bool designated=false;')
|
||||
s=s.replace('int checks;', 'int checks=0;')
|
||||
s=s.replace('bool available;','bool available=false;').replace('bool living;','bool living=false;')
|
||||
return s
|
||||
rules='\n'.join(cpp(body(P,x)) for x in ('static bool Fresh(', 'static bool Need(', 'static bool Pair(', 'static bool MinePair(', 'static bool LauncherNeed(', 'static bool LauncherPair('))
|
||||
stock_method=('protected bool CompatibleStock(',) if 'protected bool CompatibleStock(' in P.read_text() else ()
|
||||
methods='\n'.join(cpp(body(P,x)) for x in stock_method+('bool Bind(', 'ERR_ConvoyTask Task(', 'protected bool ConvoyCurrent(', 'protected bool Contains(', 'protected bool TruckValid(', 'protected bool Quiet(', 'protected bool Roster(', 'protected void Stock(', 'protected bool CaptureMines(', 'protected bool LauncherStock(', 'bool TryPlanLauncher(', 'bool TryPlanMines(', 'bool TryPlan(', 'protected void ClearWaypoints(', 'protected void Finish(', 'void Stop('))
|
||||
reserve=cpp(body(H,'bool ReserveLogistics('))+cpp(body(H,'void ReleaseLogistics('))
|
||||
unique=cpp(body(I,'static int AppendUnique('))+cpp(body(I,'static int Items('))
|
||||
compatible=body(A,'static bool Compatible(')
|
||||
for a,b in {
|
||||
'BaseMagazineComponent.Cast(exactMagazine.FindComponent(BaseMagazineComponent))':'exactMagazine->ammoComp',
|
||||
'BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent))':'actor->weapons',
|
||||
'BaseWeaponComponent.Cast(rifle.FindComponent(BaseWeaponComponent))':'rifle->weapon',
|
||||
'InventoryItemComponent.Cast(loaded.GetOwner().FindComponent(InventoryItemComponent))':'loaded->GetOwner()->item',
|
||||
'SCR_MuzzleInMagComponent.Cast(muzzle)':'(muzzle->inMag ? muzzle : nullptr)',
|
||||
}.items():compatible=compatible.replace(a,b)
|
||||
compatible=compatible.replace('array<','Array<').replace('foreach (','for (').replace('EWeaponType kind','int kind').replace('EMuzzleType muzzleKind','int muzzleKind')
|
||||
for a in ['EWeaponType','EMuzzleType']:compatible=compatible.replace(a+'.',a+'::')
|
||||
for a in ['magazine','manager','weapon','muzzle','reloadable','loaded','loadedItem','loadedSlot']:compatible=compatible.replace(a+'.',a+'->')
|
||||
compatible=compatible.replace('GetMagazineWell().','GetMagazineWell()->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
using string=std::string;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
template<class V>struct Map{std::map<string,V>data;V Get(string k){auto it=data.find(k);return it==data.end()?V{}:it->second;}void Set(string k,V v){data[k]=v;}};
|
||||
struct Vec{float x=0,y=0,z=0;static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||
struct ERR_ForceRules{static bool Finite(float f){return std::isfinite(f);}static bool FiniteVector(Vec v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}};
|
||||
struct Math{static float Min(float a,float b){return std::min(a,b);}};
|
||||
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
|
||||
struct Entity;using IEntity=Entity*;
|
||||
struct Magazine;struct Manager;struct Weapon;struct Muzzle;struct Item;struct Slot;
|
||||
using BaseMagazineComponent=Magazine*;using BaseWeaponManagerComponent=Manager*;using BaseWeaponComponent=Weapon*;using BaseMuzzleComponent=Muzzle*;using SCR_MuzzleInMagComponent=Muzzle*;using InventoryItemComponent=Item*;using InventoryStorageSlot=Slot*;
|
||||
struct EWeaponType{static const int WT_RIFLE=1,WT_SNIPERRIFLE=2,WT_MACHINEGUN=3,WT_HANDGUN=4,WT_GRENADELAUNCHER=5,WT_ROCKETLAUNCHER=6;};
|
||||
struct EMuzzleType{static const int MT_BaseMuzzle=0,MT_UGLMuzzle=1,MT_RPGMuzzle=2;};
|
||||
struct Well{int type=1,parent=0;int Type(){return type;}bool IsInherited(int t){return type==t||parent==t;}};
|
||||
struct Magazine{IEntity owner=nullptr;Well*well=nullptr;int rounds=30,capacity=30;bool used=false;IEntity GetOwner(){return owner;}Well*GetMagazineWell(){return well;}int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}bool IsUsed(){return used;}};
|
||||
struct Muzzle{bool inMag=true,canReload=true,disposable=false;Magazine*loaded=nullptr;Well*well=nullptr;int kind=0;bool CanBeReloaded(){return canReload;}bool IsDisposable(){return disposable;}Magazine*GetMagazine(){return loaded;}Well*GetMagazineWell(){return well;}int GetMuzzleType(){return kind;}};
|
||||
struct Weapon{IEntity owner=nullptr;Array<Muzzle*>muzzles;int kind=1;IEntity GetOwner(){return owner;}int GetWeaponType(){return kind;}void GetMuzzlesList(Array<Muzzle*>&a){a=muzzles;}};
|
||||
struct Manager{Array<IEntity>weapons;void GetWeaponsList(Array<IEntity>&a){a=weapons;}};
|
||||
struct Slot{Muzzle*container=nullptr;IEntity attached=nullptr;Muzzle*GetParentContainer(){return container;}IEntity GetAttachedEntity(){return attached;}};
|
||||
struct Item{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};
|
||||
struct InventoryStorageManagerComponent{Array<IEntity>items;void GetItems(Array<IEntity>& out){out=items;}static auto Cast(InventoryStorageManagerComponent* p){return p;}};
|
||||
struct Prefab{string name="magazine";string GetPrefabName(){return name;}};
|
||||
struct Group;using SCR_AIGroup=Group*;
|
||||
struct Entity{Magazine*ammoComp=nullptr;Manager*weapons=nullptr;Weapon*weapon=nullptr;Item*item=nullptr;IEntity parent=nullptr;Prefab prefab;bool readyMine=false,magazine=true,hasPrefab=true,alive=true,player=false,fight=true,aboard=false,captured=false,released=false;int faction=1;Group* group=nullptr;InventoryStorageManagerComponent* inventory=nullptr;bool HasMagazine(){return magazine;}Prefab* GetPrefabData(){return hasPrefab?&prefab:nullptr;}InventoryStorageManagerComponent* FindComponent(){return inventory;}};
|
||||
struct Agent{IEntity actor;IEntity GetControlledEntity(){return actor;}};using AIAgent=Agent*;
|
||||
struct Waypoint{};
|
||||
struct Group{Array<AIAgent>agents;bool slave=false;int players=0,faction=1;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}void GetAgents(Array<AIAgent>& out){out=agents;}void RemoveWaypoint(Waypoint*){}};
|
||||
struct ERR_Actors{static bool Alive(IEntity a){return a&&a->alive;}static bool Player(IEntity a){return a&&a->player;}static SCR_AIGroup Group(IEntity a){return a->group;}static bool CanFight(IEntity a){return a&&a->alive&&a->fight;}static void DeleteSpawned(Waypoint*){}};
|
||||
struct SCR_FactionManager{static int SGetFaction(IEntity a){return a->faction;}};
|
||||
struct CompartmentAccessComponent{static bool GetVehicleIn(IEntity a){return a->aboard;}};
|
||||
struct ERR_Inventory{
|
||||
'''+unique+r'''
|
||||
};
|
||||
struct Ammo{bool LauncherComplete=true;int LauncherMuzzles=1,LauncherMagazines=1,LauncherRounds=0,LauncherChambers=0;bool MineComplete=true;int MineMembers=2,ReadyMines=1;bool Complete=true;int Members=2,SmallArmsMuzzles=2,SmallArmsRounds=60,SmallArmsChambers=0;bool Valid(int n){return Members==n;}};
|
||||
struct Entry{bool Closed=false;int Count=2,Fit=2,MessageId=1;float ObservedAt=0;Vec Position;string SourceId;Ammo* AmmoState=nullptr;};using ERR_HQWatchEntry=Entry*;
|
||||
struct ERR_InfantryResupplyRules{
|
||||
'''+rules+r'''
|
||||
};
|
||||
namespace ERR_ForceKind{const int CONVOY=3;}
|
||||
struct ERR_ForceRecord{bool Physical=true,Eliminated=false;int Kind=3,Id=2;};
|
||||
struct Ledger{Group* owned=nullptr;ERR_ForceRecord record;ERR_ForceRecord* FindGroup(Group* g){return g==owned?&record:nullptr;}};
|
||||
struct Mode{Ledger ledger;Ledger* Forces(){return &ledger;}int EnemyFaction(){return 1;}};
|
||||
struct Perception{Array<int>m_aTargets;};
|
||||
struct Utility{Perception* m_Perception=nullptr;};
|
||||
struct Sampler{Group* group;Array<IEntity>original;int InitialStrength(){return original.Count();}IEntity InitialMember(int i){return original[i];}bool CanAttribute(int i,IEntity a){return original[i]==a&&!a->released;}bool OwnsRoster(Group* g){return g==group;}};
|
||||
struct Unit{struct Group* Group=nullptr;struct Utility* Utility=nullptr;Sampler* StatusSampler=nullptr;string ForceSourceId;bool HomeGuard=false,eligible=true;int LogisticsReservation=0;void* Downlink=nullptr;void* PendingOrder=nullptr;void* PendingConvoyOrder=nullptr;void* Radio=this;void* Assignment=nullptr;void* Waypoint=nullptr;bool Eligible(Mode*){return eligible&&LogisticsReservation==0;}};using ERR_HQUnit=Unit*;
|
||||
struct Combat{bool Held=false;};
|
||||
struct Truck{int Index=1,SupplyToken=0;bool Registered=true,Released=false,Arrived=false,SupplyHeld=false,crew=true;struct Entity* Entity=nullptr;struct Group* Group=nullptr;struct Combat* Combat=nullptr;bool CrewValid(Mode*){return crew;}};
|
||||
struct Receipt{bool Consumed=true;Array<int>Records={1,2,3,4,5,6};};
|
||||
struct Cargo{bool Finished=true;};using ERR_ConvoyCargoCallback=Cargo*;
|
||||
struct MineBatch{bool complete=true;Array<IEntity>items;bool Complete(){return complete;}int Count(){return items.Count();}IEntity At(int i){return items[i];}};
|
||||
struct Convoy{MineBatch*MineStock=nullptr;bool supply=false,MagazinesIssued=true,CargoIssued=true,Stopped=false,Ready=true;struct Receipt* Receipt=nullptr;Array<Truck*>Vehicles;Array<struct Cargo*>Cargo;float Deadline=30000;};
|
||||
using ERR_ConvoyTask=Convoy*;
|
||||
struct ConvoyService{Array<Convoy*>tasks;int Count(){return tasks.Count();}Convoy* At(int index){return index>=0&&index<Count()?tasks[index]:nullptr;}};
|
||||
struct ERR_SupplyConvoyTask:Convoy{static Convoy* Cast(Convoy* task){return task&&task->supply?task:nullptr;}};
|
||||
struct ERR_ConvoyRules{static bool Captured(Entity* e,Group*){return e->captured;}};
|
||||
struct ERR_ConvoyResources{static string Magazine(){return "magazine";}};
|
||||
struct ERR_InfantryMineTransfer{static bool ReadyMine(IEntity x){return x&&x->readyMine;}};
|
||||
struct Route{};struct Leaf{bool unresolved=false;void Stop(){}bool Unresolved(){return unresolved;}};struct Mounted{bool busy=false;bool Busy(){return busy;}void Stop(){}};
|
||||
struct HQ{Mode* m_Mode;Array<ERR_HQUnit>m_Units;Convoy* convoy=nullptr;ConvoyService convoys;ConvoyService* Convoys(){return &convoys;}Route route;bool active=true,routeAvailable=true;int downlinks=0;bool MissionActive(){return active;}Convoy* ConvoyTask(){return convoy;}
|
||||
bool designated=false;bool MineSupplyReady(Group*,IEntity,bool replacement=false){return !replacement||designated;}
|
||||
Route* Downlink(Group*,string,string,int){downlinks++;return routeAvailable?&route:nullptr;}
|
||||
'''+reserve+r'''
|
||||
};
|
||||
struct ERR_InfantryAmmoTransfer{static bool StoredUnder(IEntity item,IEntity actor){return item&&item->parent==actor;}
|
||||
'''+compatible+r'''
|
||||
};
|
||||
struct Service{HQ* m_HQ;Mode* m_Mode;Convoy* m_Convoy=nullptr;Truck* m_Truck=nullptr;Unit* m_Recipient=nullptr;Unit* m_Donor=nullptr;Leaf* m_Move=nullptr;Leaf*m_MineMove=nullptr;MineBatch*m_MineBatch=nullptr;bool m_LauncherTrip=false,m_MineTrip=false,m_MineStockCaptured=false,m_EmptyMineReplacement=false;int m_MineRecipientSlot=-1;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;IEntity m_Receiving=nullptr;
|
||||
Array<IEntity>m_Stock,m_MineStock,m_Spent,m_Members,m_Served;Map<int>*m_Consumed=new Map<int>();Map<float>*m_NextAttempt=new Map<float>();int m_ConvoySlot=-1;float m_NextTick=0;int m_State=0,m_Trip=0,m_Token=0,m_ReportId=0,m_TripDelivered=0;float m_Deadline=0,m_StockDeadline=-1;bool m_Stopped=false;
|
||||
Waypoint* m_MoveWaypoint=nullptr;Waypoint* m_HoldWaypoint=nullptr;string reason;bool Current(){return !m_Stopped&&Replication::IsServer()&&m_HQ->active;}void Audit(string why){reason=why;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
struct Fixture{Mode mode;HQ hq;Service s;InventoryStorageManagerComponent inventory;Entity vehicle;std::array<Entity,140>items;std::array<Magazine,140>magazines;Entity rifle,launcher;Manager weapons;Weapon rifleWeapon,launcherWeapon;Muzzle rifleMuzzle,launcherMuzzle;Well well,wrongWell{2,0};Entity one,two;Agent a{&one},b{&two};Group recipients,donors;Utility recipientUtility,donorUtility;Sampler sampler;Unit recipient,donor;Combat combat;Truck truck;Receipt receipt;Cargo cargo;Convoy convoy;Ammo ammo;Entry demand,donorStatus;
|
||||
Fixture():sampler{&recipients,{&one,&two}}{
|
||||
hq.m_Mode=&mode;hq.convoy=&convoy;hq.convoys.tasks={&convoy};s.m_HQ=&hq;s.m_Mode=&mode;
|
||||
one.weapons=&weapons;two.weapons=&weapons;weapons.weapons={&rifle,&launcher};rifle.weapon=&rifleWeapon;rifleWeapon.owner=&rifle;rifleWeapon.muzzles={&rifleMuzzle};rifleMuzzle.well=&well;
|
||||
launcher.weapon=&launcherWeapon;launcherWeapon.owner=&launcher;launcherWeapon.kind=EWeaponType::WT_ROCKETLAUNCHER;launcherWeapon.muzzles={&launcherMuzzle};launcherMuzzle.well=&well;launcherMuzzle.kind=EMuzzleType::MT_RPGMuzzle;launcher.parent=&one;
|
||||
for(int i=0;i<140;i++){items[i].ammoComp=&magazines[i];magazines[i].owner=&items[i];magazines[i].well=&well;}
|
||||
vehicle.inventory=&inventory;vehicle.group=&donors;truck.Entity=&vehicle;truck.Group=&donors;truck.Combat=&combat;
|
||||
mode.ledger.owned=&donors;convoy.Receipt=&receipt;convoy.Vehicles={&truck,&truck,&truck,&truck,&truck,&truck};convoy.Cargo={&cargo};
|
||||
one.group=&recipients;two.group=&recipients;recipients.agents={&a,&b};recipient.Group=&recipients;recipient.Utility=&recipientUtility;recipient.StatusSampler=&sampler;recipient.ForceSourceId="recipient";
|
||||
donor.Group=&donors;donor.Utility=&donorUtility;donor.ForceSourceId="donor";hq.m_Units={&recipient,&donor};
|
||||
demand.SourceId="recipient";demand.AmmoState=&ammo;donorStatus.SourceId="donor";donorStatus.AmmoState=&ammo;
|
||||
for(int i=0;i<48;i++)inventory.items.Insert(&items[i]);
|
||||
}
|
||||
bool Plan(float now=0){return s.TryPlan(&recipient,&donor,&demand,&donorStatus,now);}
|
||||
void Refresh(float now,int id){demand.ObservedAt=now;donorStatus.ObservedAt=now;demand.MessageId=id;}
|
||||
void Unchanged(){assert(s.m_Trip==0&&s.m_Consumed->Get("recipient")==0&&s.m_NextAttempt->Get("recipient")==0&&recipient.LogisticsReservation==0&&truck.SupplyToken==0&&hq.downlinks==0&&!truck.SupplyHeld&&!convoy.Stopped&&!s.m_Recipient&&!s.m_Donor);}
|
||||
};
|
||||
int main(){
|
||||
// Actual Compatible must reject wrong-well, empty or malformed stock before
|
||||
// any report/cooldown consumption, reservation, downlink or convoy hold.
|
||||
for(int bad=0;bad<5;bad++){Fixture f;for(auto&mag:f.magazines){if(bad==0)mag.well=&f.wrongWell;if(bad==1)mag.rounds=0;if(bad==2)mag.capacity=0;if(bad==3)mag.rounds=31;if(bad==4)mag.well=nullptr;}f.s.Stock(0);assert(!f.Plan());f.Unchanged();}
|
||||
{Fixture f;f.s.Stock(0);f.rifleMuzzle.well=&f.wrongWell;assert(!f.Plan());f.Unchanged();f.rifleMuzzle.well=&f.well;assert(f.Plan());}
|
||||
// Same delivered recipient report remains available to a later compatible
|
||||
// truck sharing the real per-source consumption/cooldown maps.
|
||||
{Fixture first,second;Map<int> consumed;Map<float> cooldown;second.hq.convoys.tasks={&first.convoy,&second.convoy};second.hq.convoy=&first.convoy;second.hq.m_Units={&first.recipient,&second.donor};
|
||||
assert(first.s.Bind(&first.convoy,0,&consumed,&cooldown));assert(second.s.Bind(&second.convoy,1,&consumed,&cooldown));
|
||||
for(auto&mag:first.magazines){mag.well=&first.wrongWell;}first.s.Stock(0);second.s.Stock(0);assert(!first.Plan());first.Unchanged();
|
||||
assert(second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,0));assert(consumed.Get("recipient")==1&&cooldown.Get("recipient")==180000&&first.recipient.LogisticsReservation==100129&&second.hq.downlinks==1&&first.hq.downlinks==0);}
|
||||
// A usable compatible item must still be a surviving unspent frozen identity.
|
||||
{Fixture f;for(auto&mag:f.magazines)mag.rounds=0;f.magazines[48].rounds=30;f.s.Stock(0);f.inventory.items.Insert(&f.items[48]);assert(!f.Plan());f.Unchanged();f.magazines[0].rounds=30;f.s.m_Spent.Insert(&f.items[0]);assert(!f.Plan());f.Unchanged();f.s.m_Spent.Clear();assert(f.Plan());}
|
||||
// Hard 32 original carriers and 96 captured magazines remain fail-closed.
|
||||
{Fixture f;f.s.Stock(0);f.sampler.original.resize(33,&f.one);assert(!f.Plan());f.Unchanged();}
|
||||
{Fixture f;f.s.Stock(0);for(int i=48;i<97;i++){f.s.m_Stock.Insert(&f.items[i]);f.inventory.items.Insert(&f.items[i]);}assert(!f.Plan());f.Unchanged();}
|
||||
{Fixture f;for(int i=48;i<96;i++)f.inventory.items.Insert(&f.items[i]);for(int i=0;i<95;i++)f.magazines[i].well=&f.wrongWell;f.s.Stock(0);assert(f.Plan());}
|
||||
// Small-arms demand cannot borrow launcher compatibility, but the dedicated
|
||||
// launcher path still supports its real empty reloadable muzzle and rejects disposables.
|
||||
{Fixture f;f.weapons.weapons={&f.launcher};f.s.Stock(0);assert(!f.Plan());f.Unchanged();assert(f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));}
|
||||
{Fixture f;f.weapons.weapons={&f.launcher};f.launcherMuzzle.disposable=true;f.s.Stock(0);assert(!f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));f.Unchanged();}
|
||||
{Fixture f;for(int i=48;i<96;i++)f.inventory.items.Insert(&f.items[i]);f.s.Stock(0);
|
||||
for(int i=0;i<95;i++)f.magazines[i].well=&f.wrongWell;
|
||||
assert(f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));
|
||||
assert(f.s.m_LauncherTrip);}
|
||||
|
||||
{Fixture f;f.s.Stock(0);for(auto&mag:f.magazines)mag.well=&f.wrongWell;assert(!f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0)&&f.truck.SupplyToken==0);}
|
||||
|
||||
{Fixture f;f.s.Stock(0);f.ammo.SmallArmsRounds=999;f.ammo.LauncherMagazines=0;assert(!f.Plan());assert(f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));assert(f.s.m_LauncherTrip&&!f.s.m_MineTrip);}
|
||||
for(int bad=0;bad<7;bad++){Fixture f;f.s.Stock(0);f.ammo.SmallArmsRounds=999;if(bad==0)f.ammo.LauncherComplete=false;if(bad==1)f.ammo.LauncherMuzzles=0;if(bad==2)f.ammo.LauncherRounds=1;if(bad==3)f.ammo.LauncherChambers=1;if(bad==4)f.ammo.Members=1;if(bad==5)f.demand.ObservedAt=-1;if(bad==6)f.demand.ObservedAt=1;
|
||||
assert(!f.s.TryPlanLauncher(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));assert(f.s.m_State==1&&f.truck.SupplyToken==0);}
|
||||
|
||||
// Additional mounted stock callbacks belong to the same initial freeze barrier.
|
||||
{Fixture f;Cargo mounted;mounted.Finished=false;f.convoy.supply=true;f.convoy.Cargo.Insert(&mounted);
|
||||
f.s.Stock(0);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());
|
||||
mounted.Finished=true;f.inventory.items.Insert(&f.items[48]);f.s.Stock(1000);
|
||||
assert(f.s.m_State==1&&f.s.m_Stock.Count()==49&&f.s.m_Stock.Contains(&f.items[48]));}
|
||||
// Bind accepts only an exact current slot and non-null shared demand histories.
|
||||
{Fixture f;Map<int> consumed;Map<float> cooldown;Convoy other;
|
||||
assert(!f.s.Bind(nullptr,0,&consumed,&cooldown));assert(!f.s.Bind(&f.convoy,-1,&consumed,&cooldown));
|
||||
assert(!f.s.Bind(&f.convoy,20,&consumed,&cooldown));assert(!f.s.Bind(&f.convoy,1,&consumed,&cooldown));
|
||||
assert(!f.s.Bind(&other,0,&consumed,&cooldown));assert(!f.s.Bind(&f.convoy,0,nullptr,&cooldown));assert(!f.s.Bind(&f.convoy,0,&consumed,nullptr));
|
||||
assert(f.s.Bind(&f.convoy,0,&consumed,&cooldown));assert(f.s.Task()==&f.convoy);assert(!f.s.Bind(&f.convoy,0,&consumed,&cooldown));}
|
||||
// Any prior startup/stock/trip/stop refuses late binding.
|
||||
for(int began=0;began<6;began++){Fixture f;Map<int> consumed;Map<float> cooldown;
|
||||
if(began==0){f.s.Stock(0);}if(began==1){f.s.m_NextTick=1000;}if(began==2){f.s.m_Trip=1;}
|
||||
if(began==3){f.s.m_StockDeadline=90000;}if(began==4){f.s.m_Stopped=true;}if(began==5){f.s.m_State=1;}
|
||||
assert(!f.s.Bind(&f.convoy,0,&consumed,&cooldown));}
|
||||
// Second convoy remains valid independently of canonical first, but never follows replacement/reordering.
|
||||
for(int mutation=0;mutation<3;mutation++){Fixture f;Convoy first,replacement;Map<int> consumed;Map<float> cooldown;
|
||||
f.hq.convoy=&first;f.hq.convoys.tasks={&first,&f.convoy};assert(f.s.Bind(&f.convoy,1,&consumed,&cooldown));
|
||||
f.s.Stock(0);assert(f.s.TruckValid()&&f.s.Task()==&f.convoy&&f.s.m_Stock.Count()==48);
|
||||
if(mutation==0)f.hq.convoys.tasks[1]=&replacement;
|
||||
if(mutation==1)std::swap(f.hq.convoys.tasks[0],f.hq.convoys.tasks[1]);
|
||||
if(mutation==2)f.hq.convoys.tasks.resize(1);
|
||||
assert(!f.s.TruckValid()&&!f.Plan()&&f.s.Task()==&f.convoy);}
|
||||
// Canonical legacy startup also pins its first identity before readiness.
|
||||
{Fixture f;Convoy replacement;f.convoy.Ready=false;f.s.Stock(0);assert(f.s.Task()==&f.convoy);
|
||||
f.hq.convoy=&replacement;f.s.Stock(1000);assert(f.s.m_Stopped&&f.s.Task()==&f.convoy);}
|
||||
// Every bound slot owns a disjoint 128-trip token range, including its last allowed trip.
|
||||
for(int slot=0;slot<20;slot++){Fixture f;Map<int> consumed;Map<float> cooldown;
|
||||
f.hq.convoys.tasks.resize(slot+1,nullptr);f.hq.convoys.tasks[slot]=&f.convoy;assert(f.s.Bind(&f.convoy,slot,&consumed,&cooldown));
|
||||
f.s.Stock(0);assert(f.Plan());assert(f.s.m_Token==100000+slot*128+1);f.s.Finish("test released");
|
||||
f.s.m_Trip=127;f.Refresh(180000,2);assert(f.Plan(180000));assert(f.s.m_Token==100000+(slot+1)*128);
|
||||
f.s.Finish("test released");f.Refresh(360000,3);assert(!f.Plan(360000));}
|
||||
// Separate convoys share report consumption and failed-attempt cooldowns for the same recipient source.
|
||||
{Fixture first,second;Map<int> consumed;Map<float> cooldown;
|
||||
second.hq.convoys.tasks={&first.convoy,&second.convoy};second.hq.convoy=&first.convoy;
|
||||
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);first.hq.routeAvailable=false;assert(first.Plan());
|
||||
assert(consumed.Get("recipient")==1&&cooldown.Get("recipient")==180000);
|
||||
second.Refresh(180000,1);assert(!second.Plan(180000));second.Refresh(179999,2);assert(!second.Plan(179999));
|
||||
second.Refresh(180000,2);assert(second.Plan(180000));assert(first.s.m_Consumed->Get("recipient")==2&&first.s.m_NextAttempt->Get("recipient")==360000);}
|
||||
// Actual inventory aggregation collapses repeated native storage references, not distinct identical-prefab magazines.
|
||||
{Fixture f;f.inventory.items.Insert(&f.items[0]);f.inventory.items.Insert(nullptr);f.s.Stock(0);assert(f.s.m_State==1&&!f.s.m_Stopped&&f.s.m_Stock.Count()==48);assert(f.s.m_Stock[0]!=f.s.m_Stock[1]);}
|
||||
// Malformed/non-magazine items never become spendable stock; native startup partial cargo stays partial.
|
||||
{Fixture f;f.inventory.items={&f.items[0],&f.items[1],&f.items[2],nullptr};f.items[1].magazine=false;f.items[2].magazine=false;f.items[2].prefab.name="rifle";f.s.Stock(0);assert(f.s.m_Stock.Count()==1&&f.s.m_State==1);}
|
||||
{Fixture f;f.cargo.Finished=false;f.s.Stock(0);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());f.s.Stock(89999);assert(!f.s.m_Stopped&&f.s.m_Stock.IsEmpty());f.s.Stock(90000);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
{Fixture f;f.cargo.Finished=false;f.s.Stock(0);f.cargo.Finished=true;f.inventory.items={&f.items[0]};f.s.Stock(1000);assert(f.s.m_Stock.Count()==1&&!f.s.m_Stopped);}
|
||||
{Fixture f;for(int i=48;i<97;i++)f.inventory.items.Insert(&f.items[i]);f.s.Stock(0);assert(f.s.m_Stopped&&!f.Plan());f.Unchanged();}
|
||||
{Fixture f;for(auto& item:f.items)f.inventory.items.Insert(&item);f.s.Stock(0);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
{Fixture f;f.vehicle.inventory=nullptr;f.s.Stock(0);assert(f.s.m_Stopped&&!f.Plan());}
|
||||
{Fixture f;f.convoy.Ready=false;f.s.Stock(0);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());}
|
||||
{Fixture f;f.receipt.Consumed=false;f.s.Stock(0);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
// Supply weapons settle after crew deadline; matching ammo must be issued and settled before snapshot.
|
||||
{Fixture f;f.convoy.supply=true;f.convoy.MagazinesIssued=false;f.s.Stock(25000);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());
|
||||
f.s.Stock(31000);assert(!f.s.m_Stopped&&f.s.m_State==0);f.convoy.MagazinesIssued=true;f.cargo.Finished=false;
|
||||
f.s.Stock(32000);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());f.cargo.Finished=true;
|
||||
f.items[0].prefab.name="NATO_556_STANAG";f.s.Stock(33000);assert(f.s.m_State==1&&!f.s.m_Stopped&&f.s.m_Stock.Contains(&f.items[0]));}
|
||||
// Earliest admission: producer may finish weapons only at original Deadline+30000.
|
||||
{Fixture f;f.convoy.supply=true;f.convoy.MagazinesIssued=false;f.cargo.Finished=false;f.s.Stock(0);
|
||||
f.s.Stock(59999);assert(!f.s.m_Stopped&&f.s.m_Stock.IsEmpty());
|
||||
f.convoy.MagazinesIssued=true;f.s.Stock(60000);assert(!f.s.m_Stopped&&f.s.m_State==0);
|
||||
f.cargo.Finished=true;f.items[0].prefab.name="NATO_556_STANAG";f.s.Stock(89000);
|
||||
assert(!f.s.m_Stopped&&f.s.m_State==1&&f.s.m_Stock.Contains(&f.items[0]));}
|
||||
// Missing first-phase submission and missing second phase are bounded by one immutable deadline.
|
||||
for(int phase=0;phase<2;phase++){Fixture f;f.convoy.supply=true;f.convoy.CargoIssued=phase!=0;f.convoy.MagazinesIssued=false;
|
||||
f.s.Stock(25000);f.s.Stock(114999);assert(!f.s.m_Stopped&&f.s.m_StockDeadline==115000&&f.s.m_Stock.IsEmpty());
|
||||
f.s.Stock(115000);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
// Faction catalogs may yield 96 distinct magazine identities; weapons are excluded.
|
||||
{Fixture f;for(int i=48;i<96;i++)f.inventory.items.Insert(&f.items[i]);f.items[0].prefab.name="NATO_556_STANAG";
|
||||
f.items[96].magazine=false;f.inventory.items.Insert(&f.items[96]);f.s.Stock(0);
|
||||
assert(!f.s.m_Stopped&&f.s.m_State==1&&f.s.m_Stock.Count()==96&&f.s.m_Stock.Contains(&f.items[0])&&!f.s.m_Stock.Contains(&f.items[96]));}
|
||||
// Detached known-zero demand also requires settled original designation; unknown never authorizes.
|
||||
{Fixture f;MineBatch batch;f.convoy.supply=true;f.convoy.MineStock=&batch;f.items[48].readyMine=true;batch.items={&f.items[48]};f.inventory.items.Insert(&f.items[48]);f.s.Stock(0);
|
||||
for(int n: {0,2,3}){f.ammo.ReadyMines=n;assert(!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));}
|
||||
f.ammo.ReadyMines=0;f.hq.designated=true;f.one.released=f.two.released=true;assert(!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));f.one.released=f.two.released=false;assert(f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));f.s.Finish("reset");f.s.m_Consumed->data.clear();f.s.m_NextAttempt->data.clear();f.hq.designated=false;
|
||||
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);}
|
||||
// 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");
|
||||
assert(first.s.m_State==7&&first.recipient.LogisticsReservation==100001&&first.truck.SupplyToken==100001);
|
||||
second.s.Stock(0);second.s.m_HQ=&first.hq;first.hq.convoy=&second.convoy;first.hq.convoys.tasks={&second.convoy};second.s.m_Consumed=first.s.m_Consumed;second.s.m_NextAttempt=first.s.m_NextAttempt;
|
||||
first.Refresh(180001,2);second.donorStatus.ObservedAt=180001;
|
||||
assert(!second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,180001));assert(first.recipient.LogisticsReservation==100001);
|
||||
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));}
|
||||
// Removed stock is unavailable; a different magazine added after capture does not replace its identity.
|
||||
{Fixture f;f.s.Stock(0);f.inventory.items={&f.items[48]};assert(!f.Plan());f.Unchanged();f.inventory.items.Insert(&f.items[0]);assert(f.Plan());assert(f.s.m_Stock.Count()==48&&!f.s.m_Stock.Contains(&f.items[48]));}
|
||||
{Fixture f;f.s.Stock(0);f.s.m_Spent=f.s.m_Stock;assert(!f.Plan());f.Unchanged();}
|
||||
// A delivered report is tied to exact original source identities and original still-owned members.
|
||||
{Fixture f;f.s.Stock(0);f.demand.SourceId="foreign";assert(!f.Plan());f.Unchanged();}
|
||||
{Fixture f;f.s.Stock(0);f.donorStatus.SourceId="foreign";assert(!f.Plan());f.Unchanged();}
|
||||
{Fixture f;f.s.Stock(0);f.one.released=true;assert(!f.Plan());f.Unchanged();assert(!f.s.m_Recipient&&!f.s.m_Donor);}
|
||||
{Fixture f;f.s.Stock(0);Entity foreign;foreign.group=&f.recipients;Agent extra{&foreign};f.recipients.agents.Insert(&extra);assert(!f.Plan());f.Unchanged();}
|
||||
// Real HQ force reservation refuses busy/support-ineligible or silent-radio recipients without consuming the demand.
|
||||
for(int refusal=0;refusal<5;refusal++){Fixture f;f.s.Stock(0);void* owned=&f;
|
||||
if(refusal==0){f.recipient.Assignment=owned;}if(refusal==1){f.recipient.Downlink=owned;}if(refusal==2){f.recipient.Waypoint=owned;}if(refusal==3){f.recipient.Radio=nullptr;}if(refusal==4){f.recipient.eligible=false;}
|
||||
assert(!f.Plan());f.Unchanged();assert(!f.s.m_Recipient&&!f.s.m_Donor);
|
||||
f.recipient.Assignment=nullptr;f.recipient.Downlink=nullptr;f.recipient.Waypoint=nullptr;f.recipient.Radio=&f;f.recipient.eligible=true;
|
||||
assert(f.Plan());assert(f.s.m_Trip==1&&f.s.m_Consumed->Get("recipient")==1&&f.recipient.LogisticsReservation==100001);}
|
||||
{Fixture f;f.s.Stock(0);f.recipient.LogisticsReservation=7;assert(!f.Plan());assert(f.recipient.LogisticsReservation==7&&f.s.m_Trip==0&&f.hq.downlinks==0);}
|
||||
// Failed truck downlink is still a consumed planning attempt; cleanup preserves all cargo and other ownership.
|
||||
{Fixture f;f.s.Stock(0);f.hq.routeAvailable=false;assert(f.Plan());assert(f.s.m_State==1&&f.s.m_Trip==1&&f.s.m_Consumed->Get("recipient")==1&&f.recipient.LogisticsReservation==0&&f.truck.SupplyToken==0&&f.inventory.items.Count()==48);
|
||||
f.hq.routeAvailable=true;f.Refresh(180000,1);assert(!f.Plan(180000)); // same consumed message cannot replay after cooldown
|
||||
f.Refresh(179999,2);assert(!f.Plan(179999));f.Refresh(180000,2);assert(f.Plan(180000));assert(f.s.m_Trip==2&&f.s.m_Deadline==300000);}
|
||||
// A stale-but-still-admissible demand gets only the remaining evidence horizon, never a new full lifetime.
|
||||
{Fixture f;f.s.Stock(0);assert(f.Plan(120000));assert(f.s.m_Deadline==180000);assert(!f.Plan(120000)&&f.hq.downlinks==1);}
|
||||
// Independent guarded failures do not leak reservations or acquire replacement force identities.
|
||||
for(int refusal=0;refusal<7;refusal++){Fixture f;f.s.Stock(0);
|
||||
if(refusal==0){f.recipient.HomeGuard=true;}if(refusal==1){f.truck.Combat->Held=true;}if(refusal==2){f.vehicle.captured=true;}if(refusal==3){f.mode.ledger.record.Eliminated=true;}if(refusal==4){f.donor.PendingConvoyOrder=&f;}if(refusal==5){f.ammo.Complete=false;}if(refusal==6){f.demand.Position.x=201;}
|
||||
assert(!f.Plan());f.Unchanged();}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual Compatible and bounded stock preflight before report/claim consumption, later compatible truck, launcher controls, convoy binding, finite identities, disjoint tokens, shared report replay/cooldown, original recipients and HQ cleanup')
|
||||
Reference in New Issue
Block a user