--- a/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMountedAmmoProbe.c +++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMountedAmmoProbe.c @@ -35,6 +35,8 @@ protected bool m_FleetReady; protected bool m_SwapEligibility; protected bool m_MineStock; + protected bool m_BMP; + protected ref ERR_ConvoyVehicle m_BMPVehicle; protected bool m_MountedDiagnosticPrinted; protected ref ERR_ConvoyMineStock m_MineBatch; protected ref ERR_ConvoyVehicle m_MineTruck; @@ -58,11 +60,14 @@ void ERR_NativeMountedAmmoProbe(ERR_GameMode mode, float now) { m_Mode = mode; m_Deadline = now + 180000; - string swap, mines, courier; + string swap, mines, courier, bmp; + m_BMP = System.GetCLIParam("errMountedBMP", bmp) && bmp == "1"; m_MineStock = System.GetCLIParam("errMineStock", mines) && mines == "1"; m_SwapEligibility = System.GetCLIParam("errMountedSwapEligibility", swap) && swap == "1"; if (m_MineStock && (m_SwapEligibility || (System.GetCLIParam("errMountedCourier", courier) && courier == "1"))) Check(false, "mine-stock proof requires the normal mounted radio path, without swap or courier flags"); + if (m_BMP && (m_MineStock || m_SwapEligibility || (System.GetCLIParam("errMountedCourier", courier) && courier == "1"))) + Check(false, "BMP proof requires the normal mounted radio path, without mine, swap or courier flags"); if (m_SwapEligibility) m_SwapProbe = new ERR_NativeMountedSwapProbe(); } bool SwapEligibility() { return m_SwapEligibility; } @@ -100,6 +105,13 @@ } return !vehicle.Members.IsEmpty(); } + protected bool BMPReady(ERR_ConvoyVehicle vehicle, ERR_HQMountedAmmoSnapshot sample) + { + return vehicle && vehicle.Entity && vehicle.Entity.GetPrefabData() + && vehicle.Entity.GetPrefabData().GetPrefabName() == ERR_ConvoyResources.VehiclePrefab(4) + && sample && sample.Valid() && sample.Complete && sample.Weapons > 0 && sample.Magazines > 0 + && sample.Rounds + sample.Chambers > 0; + } protected void Capture(ERR_HQ hq, ERR_ConvoyVehicle vehicle, float now) { if (m_Queued || !vehicle || !vehicle.Registered) return; @@ -107,10 +119,12 @@ if (!Original(vehicle, unit) || !unit.PendingStatus || !unit.Uplink) return; ERR_HQStatusReport report = unit.PendingStatus; if (!report.MountedAmmo || report.MountedAmmo.Weapons < 1 || report.MountedAmmo.Magazines < 1) return; + if (m_BMP && !BMPReady(vehicle, report.MountedAmmo)) return; ERR_HQWatchEntry before = hq.Watch().Find(report.SourceId); if (before && before.MessageId >= report.MessageId) return; if (!Check(ERR_HQWatchRules.Valid(report, now) && report.SourceId == unit.StatusSampler.InitialSourceId() && unit.Uplink.MessageId == report.MessageId && unit.Uplink.Cursor < unit.Uplink.Nodes.Count(), "original mounted payload observed before real radio delivery")) return; + if (m_BMP) m_BMPVehicle = vehicle; m_Unit = unit; m_Root = vehicle.Entity; m_Queued = report; m_Payload = report.MountedAmmo.Snapshot(); m_Route = unit.Uplink; m_RoutePayload = m_Route.Payload; Check(m_Payload != report.MountedAmmo && Same(m_Payload, report.MountedAmmo) @@ -208,7 +222,7 @@ { ERR_ConvoyVehicle vehicle = task.Vehicles[slot]; if (!Check(vehicle && Original(vehicle, hq.ERR_MountedFixtureUnit(vehicle.Group)), "root retains original admitted seated crew and ledger")) return false; - bool diagnose = m_MineStock && slot == 1 && !m_MountedDiagnosticPrinted; + bool diagnose = ((m_MineStock && slot == 1) || (m_BMP && slot == 4)) && !m_MountedDiagnosticPrinted; if (diagnose) ERR_HQMountedAmmoSampler.BeginDiagnostic(); ERR_HQMountedAmmoSnapshot sample = ERR_HQMountedAmmoSampler.Inspect(vehicle.Entity); if (diagnose) @@ -225,6 +239,7 @@ if (!Check(sample && sample.Valid(), "native root returns bounded valid snapshot")) return false; PrintFormat("ERR MOUNTED SAMPLE: source=%1 slot=%2 complete=%3 weapons=%4 muzzles=%5 magazines=%6", task.Receipt.SourceId, slot, sample.Complete, sample.Weapons, sample.Muzzles, sample.Magazines); PrintFormat("ERR MOUNTED COUNTS: slot=%1 rounds=%2 capacity=%3 chambers=%4 low=%5", slot, sample.Rounds, sample.Capacity, sample.Chambers, sample.LowMagazines); + if (m_BMP && slot == 4 && !Check(BMPReady(vehicle, sample), "original slot4 BMP has complete valid armed mounted counts")) return false; if (sample.Weapons > 0 && sample.Magazines > 0) armed = true; if (slot == 1) { @@ -277,7 +292,11 @@ { ERR_ConvoyTask task = hq.Convoys().At(convoy); if (!task || (m_ProofTask && task != m_ProofTask)) continue; - for (int slot = 0; slot < task.Vehicles.Count() && slot < 6; slot++) Capture(hq, task.Vehicles[slot], now); + for (int slot = 0; slot < task.Vehicles.Count() && slot < 6; slot++) + { + if (m_BMP && slot != 4) continue; + Capture(hq, task.Vehicles[slot], now); + } } if (!m_FleetReady) m_FleetReady = Fleet(hq); if (m_Failed) return -1; @@ -301,6 +320,10 @@ && delivered.Fit == m_Queued.Fit && m_Route.MessageId == m_Queued.MessageId && m_Route.Payload == m_RoutePayload, "delivered observation age, roster counts and radio payload preserved")) return -1; if (!Check(hq.ERR_MountedFixtureUnit(m_Unit.Group) == m_Unit && ERR_Actors.Alive(m_Root), "original source and root retained through delivery")) return -1; + if (m_BMP && !Check(m_ProofTask && m_ProofTask.Vehicles.Count() == 6 + && m_BMPVehicle && m_ProofTask.Vehicles[4] == m_BMPVehicle && m_BMPVehicle.Entity == m_Root + && Original(m_BMPVehicle, m_Unit) && BMPReady(m_BMPVehicle, m_Payload), + "same original slot4 BMP and complete armed queued snapshot retained through actual radio delivery")) return -1; if (!Mines(m_ProofTask, false)) return -1; return 1; } @@ -309,7 +332,7 @@ if (hq.SupplyConvoys().Target() == 3 && hq.Convoys().Count() <= 3) return true; return Check(false, "unchanged default three-convoy production target"); } - protected void Forget() { m_Unit = null; m_Root = null; m_Queued = null; m_Payload = null; m_Route = null; m_RoutePayload = string.Empty; } + protected void Forget() { m_BMPVehicle = null; m_Unit = null; m_Root = null; m_Queued = null; m_Payload = null; m_Route = null; m_RoutePayload = string.Empty; } void Stop() { m_Stopped = true; if (m_SwapProbe) m_SwapProbe.Stop(); --- a/tools/headless_workbench.py +++ b/tools/headless_workbench.py @@ -18,6 +18,7 @@ p.add_argument('--trace-random-radius', action='store_true', help='Workbench-only read-only zero-radius caller traces; one normal Chat round, capped at sixteen records') p.add_argument('--maintenance-refuel', action='store_true', help='Opt-in normal-route check: original held Southern fuel can transfers finite fuel through its native UAZ action; one arranged tank depletion, no stock creation') p.add_argument('--mounted-swap-eligibility', action='store_true', help='Read-only mounted-ammo variant: one exact finite cargo/mounted magazine CanSwap call; no transfer or radio proof') +p.add_argument('--mounted-bmp', action='store_true', help='Read-only original BMP slot4 complete mounted counts and its natural HQ radio delivery; isolates unrelated opening hunters before unlock') p.add_argument('--mine-stock', action='store_true', help='Read-only mounted-ammo check of exact callback-proved finite PMN4 cargo before and after natural radio delivery') p.add_argument('--mounted-courier', action='store_true', help='Mounted-ammo variant: intentionally deplete one original installed magazine, then require natural HQ radio, finite pickup, native exchange and original-seat return') p.add_argument('--mounted-intercept', action='store_true', help='After convoy driving, two synthetic witness samples exercise real HQ radio/planning and original escort detour/hold/expiry/resume') @@ -71,6 +72,7 @@ if a.smoke == 'supply-convoys' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 900): p.error('supply-convoys requires Chat, one round, and at least900 seconds') if a.smoke == 'alarm-hearing' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 360): p.error('alarm-hearing requires Chat, exactly one round, and timeout at least360 seconds') if a.smoke == 'mounted-ammo' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 360): p.error('mounted-ammo requires Chat, exactly one round, and timeout at least360 seconds') +if a.mounted_bmp and (a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 360 or a.mine_stock or a.mounted_courier or a.mounted_swap_eligibility): p.error('--mounted-bmp requires Chat mounted-ammo, one round, timeout at least360 seconds, without mine/courier/swap flags') if a.smoke == 'convoy' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 600): p.error('convoy requires Chat, exactly one round, and timeout at least600 seconds') if a.smoke == 'infantry-transfer' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 360): p.error('infantry-transfer requires Chat, one round and timeout at least360 seconds') if a.smoke == 'hq-local-handoff' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 360): p.error('hq-local-handoff requires Chat, one round and timeout at least360 seconds') @@ -137,6 +139,7 @@ if a.mounted_intercept: command += ['-errMountedIntercept', '1'] if a.mounted_swap_eligibility: command += ['-errMountedSwapEligibility', '1'] if a.mounted_courier: command += ['-errMountedCourier', '1'] +if a.mounted_bmp: command += ['-errMountedBMP', '1'] if a.mine_stock: command += ['-errMineStock', '1'] if a.maintenance_refuel: command += ['-errMaintenanceRefuel', '1'] if a.trace_random_radius: command += ['-errTraceRandomRadius', '1'] --- a/tests/test_native_mounted_ammo_probe.py +++ b/tests/test_native_mounted_ammo_probe.py @@ -4,8 +4,8 @@ from policy_harness import ROOT, body, run ROOT=Path(os.environ.get('ERR_NATIVE_SWAP_ROOT',ROOT)) p=Path(os.environ.get('ERR_NATIVE_MOUNTED_AMMO_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMountedAmmoProbe.c')) -s=p.read_text();smoke=(ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c').read_text() -runner=(ROOT/'tools/headless_workbench.py').read_text() +s=p.read_text();smoke=Path(os.environ.get('ERR_NATIVE_SMOKE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c')).read_text() +runner=Path(os.environ.get('ERR_NATIVE_MOUNTED_RUNNER',ROOT/'tools/headless_workbench.py')).read_text() assert s.startswith('#ifdef WORKBENCH') and s.rstrip().endswith('#endif') for forbidden in ('Spawn(', 'SpawnEntity', 'TrySpawn', '.Ingest(', '.Advance(', '.Dispatch(', '.BindVehicle(', '.Sample(', '.SetOrigin(', '.SetAmmoCount(', '.Kill(', '.SetHealth(', 'NextStatus =', 'PendingStatus =', 'Uplink =', 'new ERR_HQStatusReport'): @@ -34,7 +34,7 @@ code=code.replace('string.Empty','string{}').replace('protected ','').replace('null','nullptr').replace('ERR_GameMode.Get()', 'ERR_GameMode::Get()').replace('ERR_Actors.Alive(', 'ERR_Actors::Alive(') for t in ('ERR_HQMountedAmmoSnapshot','ERR_HQ','ERR_ConvoyTask','ERR_HQWatchEntry'): code=re.sub(r'\b'+t+r' (?=\w)',t+'* ',code) -for var in ('a','b','hq','m_Mode','task','m_Queued','delivered','m_Unit','m_Route','m_ProofTask','m_SwapProbe'): +for var in ('a','b','hq','m_Mode','task','m_Queued','delivered','m_Unit','m_Route','m_ProofTask','m_SwapProbe','m_BMPVehicle'): code=re.sub(r'\b'+var+r'\.',var+'->',code) for call in ('Convoys()','SupplyConvoys()','Watch()'):code=code.replace(call+'.',call+'->') run(r''' @@ -49,16 +49,16 @@ struct Route{int MessageId=7;string Payload="original";};struct Unit{int Group=1;Report* PendingStatus=nullptr;Route* Uplink=nullptr;}; struct ERR_GameMode{bool active=true;static inline ERR_GameMode* current=nullptr;static ERR_GameMode* Get(){return current;}bool Active(){return active;}}; templatestruct Arr:std::vector{int Count(){return this->size();}void Clear(){this->clear();}}; -struct ERR_ConvoyTask{bool Stopped=false;Arr Vehicles;};struct Pool{int count=3;int Count(){return count;}ERR_ConvoyTask* At(int){return nullptr;}}; +struct Escort{Root* Entity=nullptr;};struct ERR_ConvoyTask{bool Stopped=false;Arr Vehicles;};struct Pool{ERR_ConvoyTask* task=nullptr;int count=3;int Count(){return count;}ERR_ConvoyTask* At(int){return task;}}; struct Supply{int target=3;int Target(){return target;}string Status(){return "fixture production status";}};struct Watch{ERR_HQWatchEntry* entry=nullptr;ERR_HQWatchEntry* Find(string){return entry;}}; struct ERR_HQ{Pool pool;Supply supply;struct Watch watch;Unit* unit=nullptr; Pool* Convoys(){return &pool;}Supply* SupplyConvoys(){return &supply;}struct Watch* Watch(){return &watch;}Unit* ERR_MountedFixtureUnit(int){return unit;}}; templatevoid PrintFormat(T...){} struct SwapProbe{int result=1,calls=0,stops=0;int Tick(ERR_GameMode*,ERR_HQ*,ERR_ConvoyTask*){calls++;return result;}void Stop(){stops++;}}; -struct Probe{bool m_MineStock=false,mineProof=true;int mineChecks=0;void*m_MineBatch=nullptr,*m_MineTruck=nullptr,*m_MineInventory=nullptr;Arrm_Mines,m_PrimaryCargo;bool Mines(ERR_ConvoyTask*,bool){mineChecks++;return !m_MineStock||mineProof;}bool m_SwapEligibility=false;SwapProbe* m_SwapProbe=nullptr;ERR_ConvoyTask* candidate=nullptr;ERR_ConvoyTask* Candidate(ERR_HQ*){return candidate;}bool m_Stopped=false,m_Failed=false,m_FleetReady=false,fleet=true;float m_Deadline=180000,m_NextLog=0; +struct Probe{bool m_BMP=false,original=true;Escort*m_BMPVehicle=nullptr;bool Original(Escort*,Unit*){return original;}bool BMPReady(Escort*,ERR_HQMountedAmmoSnapshot*s){return s&&s->Complete&&s->Weapons>0&&s->Magazines>0&&s->Rounds+s->Chambers>0;}bool m_MineStock=false,mineProof=true;int mineChecks=0;void*m_MineBatch=nullptr,*m_MineTruck=nullptr,*m_MineInventory=nullptr;Arrm_Mines,m_PrimaryCargo;bool Mines(ERR_ConvoyTask*,bool){mineChecks++;return !m_MineStock||mineProof;}bool m_SwapEligibility=false;SwapProbe* m_SwapProbe=nullptr;ERR_ConvoyTask* candidate=nullptr;ERR_ConvoyTask* Candidate(ERR_HQ*){return candidate;}bool m_Stopped=false,m_Failed=false,m_FleetReady=false,fleet=true;float m_Deadline=180000,m_NextLog=0; ERR_ConvoyTask* m_ProofTask=nullptr;ERR_GameMode* m_Mode=nullptr;Report* m_Queued=nullptr;ERR_HQMountedAmmoSnapshot* m_Payload=nullptr;Route* m_Route=nullptr; int captures=0;string m_RoutePayload="original";Unit* m_Unit=nullptr;Root* m_Root=nullptr; - bool Check(bool b,string){if(!b)m_Failed=true;return b;}bool Fleet(ERR_HQ*){return fleet;}void Capture(ERR_HQ*,int,float){captures++;} + bool Check(bool b,string){if(!b)m_Failed=true;return b;}bool Fleet(ERR_HQ*){return fleet;}void Capture(ERR_HQ*,Escort*,float){captures++;} '''+code+r''' }; struct Setup{ERR_GameMode mode;ERR_HQ hq;Probe p;Root root;Unit unit;Route route;Report queued;ERR_HQWatchEntry delivered; @@ -67,6 +67,14 @@ unit.PendingStatus=&queued;unit.Uplink=&route;queued.MountedAmmo=&original;delivered.MountedAmmo=&received;hq.watch.entry=&delivered;hq.unit=&unit;} }; int main(){ + // BMP delivery cannot succeed through a different slot/root, lost ownership or incomplete payload. + for(int fault=0;fault<6;fault++){Setup s;ERR_ConvoyTask task;Escort bmp,other;Root replacement; + task.Vehicles.resize(6);bmp.Entity=&s.root;task.Vehicles[4]=&bmp;s.p.m_BMP=true;s.p.m_BMPVehicle=&bmp;s.p.m_ProofTask=&task;s.hq.pool.task=&task; + if(fault==1)task.Vehicles[4]=&other;if(fault==2)bmp.Entity=&replacement;if(fault==3)s.p.original=false; + if(fault==4){s.payload.Complete=false;s.original.Complete=false;s.received.Complete=false;} + if(fault==5){s.payload.Rounds=0;s.payload.Chambers=0;s.original=s.payload;s.received=s.payload;} + assert(s.p.Tick(&s.hq,3000)==(fault==0?1:-1));assert(s.p.captures==3); // exactly slot4 per observed task, never other slots + } {Setup s;s.p.m_MineStock=true;s.p.mineProof=false;assert(s.p.Tick(&s.hq,3000)==-1&&s.p.mineChecks==1);} {Setup s;s.p.m_MineStock=true;s.p.m_Mines.push_back(1);s.p.m_PrimaryCargo.push_back(2);s.p.m_MineBatch=&s;s.p.Stop();assert(s.p.m_Mines.empty()&&s.p.m_PrimaryCargo.empty()&&!s.p.m_MineBatch);} {Setup s;SwapProbe dry;ERR_ConvoyTask task;s.p.m_SwapEligibility=true;s.p.m_SwapProbe=&dry;s.p.candidate=&task; @@ -143,3 +151,95 @@ } ''') print('PASS: actual one-existing-convoy candidate readiness, sender-root identity, incomplete cargo refusal and fixed three-candidate bound') + +# Execute the exact Capture and BMPReady methods; native identity/roster/watch +# boundaries are explicit doubles, while the snapshot validator has its own full suite. +selected=[] +if 'protected bool BMPReady(' in s: selected.append(body(p,'protected bool BMPReady(')) +selected.append(body(p,'protected void Capture(')) +capture='\n'.join(selected).replace('protected ','').replace('null','nullptr') +for ns in ('ERR_ConvoyResources','ERR_HQWatchRules'): + capture=capture.replace(ns+'.',ns+'::') +for t in ('ERR_ConvoyVehicle','ERR_HQMountedAmmoSnapshot','ERR_HQ','ERR_HQUnit','ERR_HQStatusReport','ERR_HQWatchEntry'): + capture=re.sub(r'\b'+t+r' (?=\w)',t+'* ',capture) +for v in ('vehicle','sample','hq','unit','report','before','m_Route','m_Payload'): + capture=re.sub(r'\b'+v+r'\.',v+'->',capture) +for x in ('Entity','MountedAmmo','StatusSampler','Uplink'): + capture=capture.replace('.'+x+'.','.'+x+'->').replace('->'+x+'.','->'+x+'->') +for x in ('GetPrefabData()','Watch()'): + capture=capture.replace(x+'.',x+'->') +capture=capture.replace('unit->Uplink->Nodes.Count()', 'int(unit->Uplink->Nodes.size())') +run(r''' +#include +#include +#include +struct string:std::string{using std::string::string;string(const std::string&s):std::string(s){}bool Contains(const char*s){return find(s)!=npos;}}; +struct Prefab{string name="bmp";string GetPrefabName(){return name;}}; +struct Entity{Prefab prefab;Prefab*GetPrefabData(){return &prefab;}}; +struct ERR_ConvoyResources{static string VehiclePrefab(int slot){assert(slot==4);return "bmp";}}; +struct ERR_HQMountedAmmoSnapshot{bool Complete=true,valid=true;int Weapons=3,Magazines=3,Rounds=80,Chambers=1; + bool Valid(){return valid;}ERR_HQMountedAmmoSnapshot*Snapshot(){return new ERR_HQMountedAmmoSnapshot(*this);}}; +struct ERR_ConvoyVehicle{struct Entity*Entity=nullptr;bool Registered=true;int Group=1;}; +struct ERR_HQStatusReport{ERR_HQMountedAmmoSnapshot*MountedAmmo=nullptr;int MessageId=7;string SourceId="source";}; +struct ERR_HQWatchEntry{int MessageId=6;}; +struct Sampler{string InitialSourceId(){return "source";}}; +struct Route{int MessageId=7,Cursor=0;std::vectorNodes={1,2};string Payload="mounted ammunition=original";}; +struct ERR_HQUnit{ERR_HQStatusReport*PendingStatus=nullptr;Route*Uplink=nullptr;Sampler*StatusSampler=nullptr;}; +struct Watch{ERR_HQWatchEntry*entry=nullptr;ERR_HQWatchEntry*Find(string){return entry;}}; +struct ERR_HQ{ERR_HQUnit*unit=nullptr;struct Watch watch;ERR_HQUnit*ERR_MountedFixtureUnit(int){return unit;}struct Watch*Watch(){return &watch;}}; +struct ERR_HQWatchRules{static bool Valid(ERR_HQStatusReport*,float){return true;}}; +struct Probe{bool m_BMP=true,original=true,failed=false;ERR_ConvoyVehicle*m_BMPVehicle=nullptr;ERR_HQStatusReport*m_Queued=nullptr;ERR_HQUnit*m_Unit=nullptr;Entity*m_Root=nullptr;ERR_HQMountedAmmoSnapshot*m_Payload=nullptr;Route*m_Route=nullptr;string m_RoutePayload; + bool Original(ERR_ConvoyVehicle*,ERR_HQUnit*){return original;}bool Check(bool x,const char*){if(!x)failed=true;return x;} + bool Same(ERR_HQMountedAmmoSnapshot*a,ERR_HQMountedAmmoSnapshot*b){return a->Complete==b->Complete&&a->Weapons==b->Weapons&&a->Magazines==b->Magazines&&a->Rounds==b->Rounds&&a->Chambers==b->Chambers;} +'''+capture+r''' +}; +struct Setup{Probe p;Entity root;ERR_ConvoyVehicle vehicle;ERR_HQ hq;ERR_HQUnit unit;Sampler sampler;Route route;ERR_HQStatusReport report;ERR_HQMountedAmmoSnapshot sample; + Setup(){vehicle.Entity=&root;hq.unit=&unit;unit.PendingStatus=&report;unit.Uplink=&route;unit.StatusSampler=&sampler;report.MountedAmmo=&sample;}}; +int main(){ + {Setup s;s.root.prefab.name="uaz";s.p.Capture(&s.hq,&s.vehicle,1000);assert(!s.p.m_Queued&&!s.p.m_Payload);} + for(int bad=0;bad<6;bad++){Setup s;if(bad==0)s.sample.Complete=false;if(bad==1)s.sample.valid=false; + if(bad==2)s.sample.Weapons=0;if(bad==3)s.sample.Magazines=0;if(bad==4){s.sample.Rounds=0;s.sample.Chambers=0;}if(bad==5)s.p.original=false; + s.p.Capture(&s.hq,&s.vehicle,1000);assert(!s.p.m_Queued&&!s.p.m_BMPVehicle);} + {Setup s;s.p.Capture(&s.hq,&s.vehicle,1000);assert(s.p.m_Queued==&s.report&&s.p.m_BMPVehicle==&s.vehicle&&s.p.m_Root==&s.root); + assert(s.p.m_Payload!=&s.sample&&s.p.m_Payload->Complete&&s.p.m_Route==&s.route&&!s.p.failed); + s.sample.Rounds=0;assert(s.p.m_Payload->Rounds==80);} + {Setup s;s.p.m_BMP=false;s.root.prefab.name="uaz";s.sample.Complete=false;s.p.Capture(&s.hq,&s.vehicle,1000);assert(s.p.m_Queued==&s.report&&!s.p.m_BMPVehicle);} +} +'''.replace(';if(', ';\n if(')) +node=next(n for n in ast.parse(runner).body if isinstance(n,ast.If) and ast.unparse(n.test).startswith('a.mounted_bmp and')) +condition=compile(ast.Expression(node.test),'','eval') +for field in [None,'smoke','project','rounds','timeout','mine_stock','mounted_courier','mounted_swap_eligibility']: + values=dict(mounted_bmp=True,smoke='mounted-ammo',project='chat',rounds=1,timeout=360,mine_stock=False,mounted_courier=False,mounted_swap_eligibility=False) + if field:values[field]={'smoke':'convoy','project':'core','rounds':2,'timeout':359}.get(field,True) + assert eval(condition,{'a':SimpleNamespace(**values)})==(field is not None) +assert "if a.mounted_bmp: command += ['-errMountedBMP', '1']" in runner +print('PASS: actual BMP capture requires complete armed exact original resource; pinned delivery/slot checks and isolated CLI flag') + +# WORKBENCH isolation changes only unrelated services before the real unlock. +assert start.index('!mixedBMP && StartMode() == 0 && Phase() == ERR_Phase.PRISON') < start.index('bmpHQ.ERR_IsolateMountedBMP()') < start.index('UnlockPrison(') +assert all(x in start for x in ('errMineStock','errMountedSwapEligibility','errMountedCourier')) +hook_path=Path(os.environ.get('ERR_NATIVE_SMOKE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c')) +hook=body(hook_path,'bool ERR_IsolateMountedBMP(').replace('ERR_NativeMountedAmmoProbe.','ERR_NativeMountedAmmoProbe::').replace('System.','System::').replace('ERR_GameMode.','ERR_GameMode::').replace('ERR_Phase.','ERR_Phase::') +for v in ('m_Mode','m_Hunters','m_Towns'):hook=hook.replace(v+'.',v+'->') +run(r''' +#include +#include +using string=std::string; +struct ERR_NativeMountedAmmoProbe{static inline bool enabled=true;static bool Enabled(){return enabled;}}; +struct System{static inline string flag="1";static bool GetCLIParam(string key,string&out){assert(key=="errMountedBMP");out=flag;return !flag.empty();}}; +struct ERR_Phase{static constexpr int PRISON=1;}; +struct ERR_GameMode{int start=0,phase=1;static inline ERR_GameMode*current=nullptr;static ERR_GameMode*Get(){return current;}int StartMode(){return start;}int Phase(){return phase;}}; +struct Service{int deployed=0,stops=0;int Deployed(){return deployed;}void Stop(){stops++;}}; +struct HQ{ERR_GameMode*m_Mode=nullptr;Service*m_Hunters=nullptr,*m_Towns=nullptr; +'''+hook+r''' +}; +int main(){for(int fault=0;fault<10;fault++){ + ERR_GameMode mode,other;ERR_GameMode::current=&mode;Service hunters,towns;HQ h;h.m_Mode=&mode;h.m_Hunters=&hunters;h.m_Towns=&towns; + ERR_NativeMountedAmmoProbe::enabled=true;System::flag="1"; + if(fault==1)ERR_NativeMountedAmmoProbe::enabled=false;if(fault==2)System::flag="";if(fault==3)System::flag="0"; + if(fault==4)ERR_GameMode::current=&other;if(fault==5)mode.start=1;if(fault==6)mode.phase=2; + if(fault==7)hunters.deployed=1;if(fault==8)h.m_Hunters=nullptr;if(fault==9)h.m_Towns=nullptr; + assert(h.ERR_IsolateMountedBMP()==(fault==0));assert(hunters.stops==(fault==0?1:0)&&towns.stops==(fault==0?1:0)); +}} +'''.replace(';if(', ';\n if(')) +print('PASS: actual opt-in pre-unlock isolation refuses wrong mode/phase/flag and existing hunters; no baseline service changes')