Files
EscapistsReintroduced/tests/test_air_logistics_orders.py
jakub a951a96edc Fix faction ammunition resupply and settings recovery
Wait for both convoy cargo phases and use actual magazine stock. Resolve empty primary weapon ammunition from muzzle metadata and recover cleanly from failed initial settings saves.

Update native fixtures, scoped cleanup guards, packaging evidence and validation documentation. Make reviewed fixture tests checkout-relative. All repository checks pass against the isolated staged tree; native resupply completion and multiplayer qualification remain outstanding.
2026-09-30 14:34:46 +02:00

112 lines
7.3 KiB
Python

"""Execute production air-resupply reservation and native-operation quarantine rules."""
from policy_harness import ROOT, body, run
reserve = body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AirfieldSupport.c', 'bool ReserveAirAmmo(')
reserve = reserve.replace('ERR_HQUnit unit', 'ERR_HQUnit* unit').replace('unit.', 'unit->').replace('Replication.', 'Replication::')
pending = body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AirAmmoMission.c', 'bool PendingNative(')
pending = pending.replace('foreach (', 'for (').replace('ERR_ConvoyCargoCallback loading', 'ERR_ConvoyCargoCallback* loading').replace('ERR_InfantryAmmoTransfer operation', 'ERR_InfantryAmmoTransfer* operation').replace('loading.', 'loading->').replace('operation.', 'operation->')
run(r'''
#include <cassert>
#include <vector>
struct Replication {inline static bool server=true;static bool IsServer(){return server;}};
struct ERR_HQUnit {
bool eligible=true,HomeGuard=false,Radio=true,PendingOrder=false,Downlink=false,fighting=false;
int LogisticsReservation=0,Assignment=42;
bool Eligible(int){return eligible&&LogisticsReservation==0;}
};
struct HQ {
int m_Mode=0;bool active=true;
bool MissionActive(){return active;}
bool Fighting(ERR_HQUnit* unit){return unit->fighting;}
'''+reserve+r'''
};
struct ERR_ConvoyCargoCallback {bool Finished=false;};
struct ERR_InfantryAmmoTransfer {bool unresolved=false;bool Unresolved(){return unresolved;}};
struct Mission {
std::vector<ERR_ConvoyCargoCallback*> m_Loads;
std::vector<ERR_InfantryAmmoTransfer*> m_Operations;
'''+pending+r'''
};
int main(){
HQ hq;ERR_HQUnit unit;
// A quiet unit may be reserved while retaining its existing delivered search.
assert(hq.ReserveAirAmmo(&unit,200001));assert(unit.LogisticsReservation==200001&&unit.Assignment==42);
assert(!hq.ReserveAirAmmo(&unit,200002));
for(int invalid=0;invalid<9;invalid++){
unit={};hq.active=true;Replication::server=true;
if(invalid==0)Replication::server=false;
if(invalid==1)hq.active=false;
if(invalid==2)unit.eligible=false;
if(invalid==3)unit.HomeGuard=true;
if(invalid==4)unit.Radio=false;
if(invalid==5)unit.PendingOrder=true;
if(invalid==6)unit.Downlink=true;
if(invalid==7)unit.fighting=true;
int token=invalid==8?0:200001;
assert(!hq.ReserveAirAmmo(&unit,token)&&unit.Assignment==42&&unit.LogisticsReservation==0);
}
assert(!hq.ReserveAirAmmo(nullptr,200001));
Mission mission;ERR_ConvoyCargoCallback loading;ERR_InfantryAmmoTransfer move;
mission.m_Loads={&loading};mission.m_Operations={&move};
assert(mission.PendingNative());loading.Finished=true;assert(!mission.PendingNative());
move.unresolved=true;assert(mission.PendingNative());move.unresolved=false;assert(!mission.PendingNative());
}
''')
print('PASS: air logistics reservation preserves delivered search until radio handoff, excludes guards/combat/in-flight orders, and quarantines unresolved native loads/moves')
# A rifle with its last magazine removed must still identify supply cargo.
# Execute the real selector with native weapon/muzzle/catalog calls doubled.
import os
from pathlib import Path
selector_source=Path(os.environ.get('ERR_AIR_AMMO_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AirAmmoMission.c'))
selector=body(selector_source,'protected ResourceName Magazine(')
selector=selector.replace('protected ','').replace('ResourceName','std::string').replace('IEntity actor','Actor* actor').replace('IEntity owner = null;','Item* owner=nullptr;')
selector=selector.replace('BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent));','Manager* manager=actor->manager;')
selector=selector.replace('array<BaseWeaponComponent>','std::vector<Weapon*>').replace('array<BaseMuzzleComponent>','std::vector<Muzzle*>')
selector=selector.replace('foreach (','for (').replace('BaseWeaponComponent weapon','Weapon* weapon').replace('BaseMuzzleComponent muzzle','Muzzle* muzzle').replace('BaseMagazineComponent magazine','LoadedMagazine* magazine')
selector=selector.replace('EWeaponType.','EWeaponType::').replace('EMuzzleType.','EMuzzleType::').replace('.IsEmpty()','.empty()')
for name in ('manager','weapon','muzzle','magazine','owner'):selector=selector.replace(name+'.',name+'->')
selector=selector.replace('GetOwner().','GetOwner()->').replace('GetPrefabData().','GetPrefabData()->')
run(r'''
#include <cassert>
#include <string>
#include <vector>
enum class EWeaponType{WT_RIFLE,WT_MACHINEGUN,WT_SNIPERRIFLE,WT_LAUNCHER};
enum class EMuzzleType{MT_BaseMuzzle,MT_UGLMuzzle};
struct Prefab {std::string name="loaded-custom-mag";std::string GetPrefabName(){return name;}};
struct Item {Prefab prefab;bool valid=true;Prefab* GetPrefabData(){return valid?&prefab:nullptr;}};
struct LoadedMagazine {Item* owner=nullptr;Item* GetOwner(){return owner;}};
struct Muzzle {LoadedMagazine* loaded=nullptr;std::string fallback="default-rifle-mag";EMuzzleType type=EMuzzleType::MT_BaseMuzzle;bool well=true;
LoadedMagazine* GetMagazine(){return loaded;}bool GetMagazineWell(){return well;}EMuzzleType GetMuzzleType(){return type;}std::string GetDefaultMagazineOrProjectileName(){return fallback;}};
struct Weapon {EWeaponType type=EWeaponType::WT_RIFLE;std::vector<Muzzle*> muzzles;EWeaponType GetWeaponType(){return type;}void GetMuzzlesList(std::vector<Muzzle*>& out){out=muzzles;}};
struct Manager {std::vector<Weapon*> weapons;void GetWeapons(std::vector<Weapon*>&out){out=weapons;}};
struct Actor {Manager* manager=nullptr;};
struct Mission {
SELECTOR
};
int main(){
Mission mission;Muzzle rifle;Weapon weapon;weapon.muzzles={&rifle};Manager manager;manager.weapons={&weapon};Actor actor{&manager};
// This failed before the fix: no inserted magazine after the final reload.
assert(mission.Magazine(&actor)=="default-rifle-mag");
Item item;LoadedMagazine loaded{&item};rifle.loaded=&loaded;
assert(mission.Magazine(&actor)=="loaded-custom-mag");
item.prefab.name="";assert(mission.Magazine(&actor)=="default-rifle-mag");
item.valid=false;assert(mission.Magazine(&actor)=="default-rifle-mag");
loaded.owner=nullptr;assert(mission.Magazine(&actor)=="default-rifle-mag");
// An earlier auxiliary launcher must not choose grenade cargo for a rifle.
Item grenade;grenade.prefab.name="grenade";LoadedMagazine grenadeMag{&grenade};
Muzzle auxiliary;auxiliary.type=EMuzzleType::MT_UGLMuzzle;auxiliary.loaded=&grenadeMag;
weapon.muzzles={nullptr,&auxiliary,&rifle};assert(mission.Magazine(&actor)=="default-rifle-mag");
rifle.well=false;assert(mission.Magazine(&actor).empty());rifle.well=true;
rifle.loaded=nullptr;rifle.fallback="";assert(mission.Magazine(&actor).empty());
Muzzle alternate;alternate.fallback="second-rifle-mag";weapon.muzzles.push_back(&alternate);
assert(mission.Magazine(&actor)=="second-rifle-mag");
weapon.type=EWeaponType::WT_MACHINEGUN;assert(mission.Magazine(&actor)=="second-rifle-mag");
weapon.type=EWeaponType::WT_SNIPERRIFLE;assert(mission.Magazine(&actor)=="second-rifle-mag");
weapon.type=EWeaponType::WT_LAUNCHER;assert(mission.Magazine(&actor).empty());
manager.weapons={nullptr};assert(mission.Magazine(&actor).empty());
actor.manager=nullptr;assert(mission.Magazine(&actor).empty());assert(mission.Magazine(nullptr).empty());
}
'''.replace('SELECTOR',selector))
print('PASS: actual ammunition cargo selector handles empty rifle/machinegun/sniper magazines, preserves loaded variants, excludes auxiliary launchers and tolerates missing native metadata')