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:
+343
@@ -0,0 +1,343 @@
|
||||
#ifdef WORKBENCH
|
||||
// Read-only fixture access to the actual production sender; never initiates a report.
|
||||
modded class ERR_HQ
|
||||
{
|
||||
ERR_HQUnit ERR_MountedFixtureUnit(SCR_AIGroup group)
|
||||
{
|
||||
if (!ERR_NativeMountedAmmoProbe.Enabled() || !group) return null;
|
||||
for (int i = 0; i < m_Units.Count() && i < 256; i++)
|
||||
if (m_Units[i].Group == group) return m_Units[i];
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// WORKBENCH-only read access to the exact successful startup callback identity.
|
||||
modded class ERR_ConvoyMineStock
|
||||
{
|
||||
bool ERR_FixtureMine(ERR_SupplyConvoyTask task, ERR_ConvoyVehicle original, IEntity item)
|
||||
{
|
||||
if (!m_Closed || task != m_Task || original != m_Original || !original || original.Entity != m_Truck) return false;
|
||||
if (!m_Truck || !item || !m_Items.Contains(item) || m_Cargo.Count() > 2 || !m_Inventory || m_Inventory.GetOwner() != m_Truck) return false;
|
||||
if (m_Truck.FindComponent(InventoryStorageManagerComponent) != m_Inventory) return false;
|
||||
foreach (ERR_ConvoyMineCargo operation : m_Cargo)
|
||||
if (operation && operation.Finished && operation.Succeeded && operation.Item == item) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativeMountedAmmoProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected float m_Deadline;
|
||||
protected float m_NextLog;
|
||||
protected bool m_Failed;
|
||||
protected bool m_Stopped;
|
||||
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;
|
||||
protected InventoryStorageManagerComponent m_MineInventory;
|
||||
protected ref array<IEntity> m_Mines = {};
|
||||
protected ref array<ref ERR_ConvoyCargoCallback> m_PrimaryCargo = {};
|
||||
protected ref ERR_NativeMountedSwapProbe m_SwapProbe;
|
||||
protected ref ERR_SupplyConvoyTask m_ProofTask;
|
||||
protected ERR_HQUnit m_Unit;
|
||||
protected IEntity m_Root;
|
||||
protected ref ERR_HQStatusReport m_Queued;
|
||||
protected ref ERR_HQMountedAmmoSnapshot m_Payload;
|
||||
protected ref ERR_HQRoute m_Route;
|
||||
protected string m_RoutePayload;
|
||||
static bool Enabled()
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1"
|
||||
&& System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "mounted-ammo";
|
||||
}
|
||||
void ERR_NativeMountedAmmoProbe(ERR_GameMode mode, float now)
|
||||
{
|
||||
m_Mode = mode; m_Deadline = now + 180000;
|
||||
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; }
|
||||
static string CompletionText(bool swapEligibility)
|
||||
{
|
||||
if (swapEligibility) return "ERR SMOKE COMPLETE: native mounted swap eligibility only; no transfer or radio proof; explicit fixture reset";
|
||||
return "ERR SMOKE COMPLETE: production fleet mounted ammunition, cargo exclusion and actual radio delivery; explicit fixture reset";
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: mounted ammo " + detail); return true; }
|
||||
m_Failed = true; Print("ERR SMOKE FAIL: mounted ammo " + detail, LogLevel.ERROR); return false;
|
||||
}
|
||||
protected bool Same(ERR_HQMountedAmmoSnapshot a, ERR_HQMountedAmmoSnapshot b)
|
||||
{
|
||||
return a && b && a.Complete == b.Complete && a.Weapons == b.Weapons && a.Muzzles == b.Muzzles
|
||||
&& a.Magazines == b.Magazines && a.Rounds == b.Rounds && a.Capacity == b.Capacity
|
||||
&& a.Chambers == b.Chambers && a.LowMagazines == b.LowMagazines;
|
||||
}
|
||||
protected bool Original(ERR_ConvoyVehicle vehicle, ERR_HQUnit unit)
|
||||
{
|
||||
if (!vehicle || !unit || !vehicle.Registered || vehicle.Released || !vehicle.Entity || !vehicle.Group
|
||||
|| vehicle.Members.Count() > 32 || unit.Group != vehicle.Group || !unit.StatusSampler || !unit.StatusSampler.OwnsRoster(vehicle.Group)) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(vehicle.Group);
|
||||
if (!record || record.Kind != ERR_ForceKind.CONVOY || !record.Physical || record.Eliminated
|
||||
|| record.SourceId != unit.StatusSampler.InitialSourceId() || record.InitialStrength != vehicle.Members.Count()
|
||||
|| unit.StatusSampler.InitialStrength() != vehicle.Members.Count()) return false;
|
||||
foreach (IEntity member : vehicle.Members)
|
||||
{
|
||||
bool original;
|
||||
for (int slot = 0; slot < unit.StatusSampler.InitialStrength(); slot++)
|
||||
if (unit.StatusSampler.InitialMember(slot) == member) original = true;
|
||||
if (!original || !ERR_Actors.Alive(member) || ERR_Actors.Group(member) != vehicle.Group
|
||||
|| !ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(member), vehicle.Entity)) return false;
|
||||
}
|
||||
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;
|
||||
ERR_HQUnit unit = hq.ERR_MountedFixtureUnit(vehicle.Group);
|
||||
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)
|
||||
&& m_RoutePayload.Contains("mounted ammunition="), "queued counts copied without mutating sender or radio payload");
|
||||
}
|
||||
protected ERR_SupplyConvoyTask Candidate(ERR_HQ hq)
|
||||
{
|
||||
for (int i = 0; i < hq.Convoys().Count() && i < 3; i++)
|
||||
{
|
||||
ERR_SupplyConvoyTask task = ERR_SupplyConvoyTask.Cast(hq.Convoys().At(i));
|
||||
if (m_MineStock && m_ProofTask && task != m_ProofTask) continue;
|
||||
if (!task || task.Stopped || !task.Ready || !task.MagazinesIssued || !task.Receipt || task.Vehicles.Count() != 6 || task.Cargo.Count() > 128) continue;
|
||||
bool finished = true;
|
||||
foreach (ERR_ConvoyCargoCallback pending : task.Cargo) if (!pending || !pending.Finished) finished = false;
|
||||
if (!finished) continue;
|
||||
// If an initial report was already observed, prove that same convoy's roots.
|
||||
if (m_Queued)
|
||||
{
|
||||
bool ownsRoot;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles) if (vehicle && vehicle.Entity == m_Root) ownsRoot = true;
|
||||
if (!ownsRoot) continue;
|
||||
}
|
||||
if (m_MineStock)
|
||||
{
|
||||
// Pin the first ordinary eligible convoy; absent optional cargo must not
|
||||
// select a different fleet or change the original fixed proof deadline.
|
||||
if (!m_ProofTask) m_ProofTask = task;
|
||||
if (!m_MineBatch) m_MineBatch = task.MineStock;
|
||||
if (m_MineBatch && task.MineStock != m_MineBatch)
|
||||
{ Check(false, "original pending optional mine batch replaced"); return null; }
|
||||
if (!m_MineBatch || !m_MineBatch.Complete()) return null;
|
||||
}
|
||||
return task;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
protected bool MineCustody(IEntity item, IEntity truck, array<IEntity> items)
|
||||
{
|
||||
if (!ERR_InfantryMineTransfer.ReadyMine(item) || !items.Contains(item)) return false;
|
||||
if (!item.FindComponent(DamageManagerComponent)) return false;
|
||||
InventoryItemComponent carried = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent));
|
||||
if (!carried) return false;
|
||||
InventoryStorageSlot slot = carried.GetParentSlot();
|
||||
if (!slot || slot.GetAttachedEntity() != item || !slot.GetStorage()) return false;
|
||||
if (slot.GetStorage().GetSlot(slot.GetID()) != slot) return false;
|
||||
return ERR_InfantryAmmoTransfer.StoredUnder(item, truck);
|
||||
}
|
||||
protected bool Mines(ERR_SupplyConvoyTask task, bool capture)
|
||||
{
|
||||
if (!m_MineStock) return true;
|
||||
if (!Check(task && !task.Stopped && task.Ready && task.MagazinesIssued && task.Vehicles.Count() == 6,
|
||||
"mine proof retains ready original production convoy")) return false;
|
||||
ERR_ConvoyVehicle truck = task.Vehicles[1];
|
||||
ERR_ConvoyMineStock batch = task.MineStock;
|
||||
if (!Check(truck && truck.Entity && truck.Registered && !truck.Released && batch && batch.Complete(),
|
||||
"original truck optional mine batch closed")) return false;
|
||||
int count = batch.Count();
|
||||
if (!Check(count >= 1 && count <= 2, "one or two exact initial PMN4 identities available; no stock fabricated")) return false;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(truck.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!Check(inventory && inventory.GetOwner() == truck.Entity && task.Cargo.Count() <= 128,
|
||||
"original mine truck inventory and primary callback bounds")) return false;
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
|
||||
if (!Check(items.Count() <= 128, "mine physical inventory enumeration bounded")) return false;
|
||||
if (capture)
|
||||
{
|
||||
if (!Check(!m_MineBatch || m_MineBatch == batch, "same original mine batch at capture")) return false;
|
||||
m_MineBatch = batch; m_MineTruck = truck; m_MineInventory = inventory;
|
||||
for (int primary = 0; primary < task.Cargo.Count(); primary++) m_PrimaryCargo.Insert(task.Cargo[primary]);
|
||||
}
|
||||
else if (!Check(batch == m_MineBatch && truck == m_MineTruck && inventory == m_MineInventory
|
||||
&& count == m_Mines.Count() && task.Cargo.Count() == m_PrimaryCargo.Count(), "original mine batch, truck, count and primary callbacks unchanged through radio delivery")) return false;
|
||||
for (int cargo = 0; cargo < task.Cargo.Count(); cargo++)
|
||||
if (!Check(task.Cargo[cargo] && task.Cargo[cargo].Finished && task.Cargo[cargo] == m_PrimaryCargo[cargo],
|
||||
"higher-priority primary cargo remains settled and unchanged")) return false;
|
||||
array<IEntity> unique = {};
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
IEntity item = batch.At(index);
|
||||
if (!Check(item && !unique.Contains(item) && batch.ERR_FixtureMine(task, truck, item)
|
||||
&& MineCustody(item, truck.Entity, items), "unique original callback-proved inactive PMN4 retains reciprocal truck storage custody")) return false;
|
||||
unique.Insert(item);
|
||||
if (capture) m_Mines.Insert(item);
|
||||
else if (!Check(m_Mines[index] == item, "exact initial PMN4 snapshot identity unchanged")) return false;
|
||||
}
|
||||
PrintFormat("ERR MINE STOCK PROOF: capture=%1 originals=%2 primary=%3; read-only, no transfer proof", capture, count, task.Cargo.Count());
|
||||
return true;
|
||||
}
|
||||
protected bool Fleet(ERR_HQ hq)
|
||||
{
|
||||
ERR_SupplyConvoyTask task = Candidate(hq);
|
||||
if (!task) return false;
|
||||
if (!Check(task.Vehicles.Count() == 6, "six original production roots in selected convoy")) return false;
|
||||
bool armed;
|
||||
for (int slot = 0; slot < 6; slot++)
|
||||
{
|
||||
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_BMP && slot == 4)) && !m_MountedDiagnosticPrinted;
|
||||
if (diagnose) ERR_HQMountedAmmoSampler.BeginDiagnostic();
|
||||
ERR_HQMountedAmmoSnapshot sample = ERR_HQMountedAmmoSampler.Inspect(vehicle.Entity);
|
||||
if (diagnose)
|
||||
{
|
||||
ref ERR_HQMountedAmmoDiagnostic diagnostic = ERR_HQMountedAmmoSampler.EndDiagnostic();
|
||||
m_MountedDiagnosticPrinted = true;
|
||||
if (sample && !sample.Complete && diagnostic)
|
||||
{
|
||||
PrintFormat("ERR MOUNTED COLLECT: reasons=%1 nodes=%2 allowed=%3 managers=%4 detail=%5", diagnostic.Reasons, diagnostic.Nodes, diagnostic.Allowed, diagnostic.Managers, diagnostic.Detail);
|
||||
PrintFormat("ERR MOUNTED BUDGET: structural=%1 cargo=%2", diagnostic.Structural, diagnostic.Cargo);
|
||||
PrintFormat("ERR MOUNTED FIRST REJECT: prefab=%1 storage-purpose=%2", diagnostic.FirstPrefab, diagnostic.FirstPurpose);
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(vehicle.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!Check(inventory != null, "original ammo truck storage present")) return false;
|
||||
array<IEntity> cargo = {}; inventory.GetItems(cargo, EStoragePurpose.PURPOSE_DEPOSIT);
|
||||
if (!Check(cargo.Count() <= 128, "cargo proof inspection is bounded")) return false;
|
||||
int cargoWeapons;
|
||||
foreach (IEntity item : cargo)
|
||||
if (item && BaseWeaponComponent.Cast(item.FindComponent(BaseWeaponComponent))) cargoWeapons++;
|
||||
if (!Check(cargoWeapons >= 8 && sample.Complete && sample.Weapons == 0 && sample.Magazines == 0
|
||||
&& sample.Rounds == 0 && sample.Capacity == 0 && sample.Chambers == 0, "eight physical cargo weapons excluded from unarmed truck snapshot")) return false;
|
||||
}
|
||||
}
|
||||
if (!Check(armed, "actual installed weapon magazines sampled in production convoy")) return false;
|
||||
if (!Mines(task, true)) return false;
|
||||
m_ProofTask = task;
|
||||
PrintFormat("ERR MOUNTED SCOPE: checked=1 original convoy, deployed=%1 configured=%2; remaining fleet reliability not qualified", hq.Convoys().Count(), hq.SupplyConvoys().Target());
|
||||
return true;
|
||||
}
|
||||
int Tick(ERR_HQ hq, float now)
|
||||
{
|
||||
if (m_Stopped || m_Failed) return -1;
|
||||
if (!m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return -1;
|
||||
if (now > m_Deadline)
|
||||
{
|
||||
if (m_MineStock) Check(false, "fixed 180-second mine-stock and radio proof deadline; original stock or delivery unavailable");
|
||||
else Check(false, "fixed 180-second proof deadline");
|
||||
return -1;
|
||||
}
|
||||
if (!hq || !hq.Convoys() || !hq.SupplyConvoys()) return 0;
|
||||
if (!CheckTarget(hq)) return -1;
|
||||
if (m_ProofTask && m_ProofTask.Stopped) { Check(false, "selected original convoy retained through proof"); return -1; }
|
||||
if (now >= m_NextLog)
|
||||
{
|
||||
PrintFormat("ERR MOUNTED WAIT: fleet=%1 ready=%2 queued=%3 remaining=%4", hq.Convoys().Count(), m_FleetReady, m_Queued != null, m_Deadline - now);
|
||||
PrintFormat("ERR MOUNTED PRODUCTION: %1", hq.SupplyConvoys().Status());
|
||||
m_NextLog = now + 10000;
|
||||
}
|
||||
if (m_SwapEligibility)
|
||||
{
|
||||
if (!m_ProofTask) m_ProofTask = Candidate(hq);
|
||||
if (!m_ProofTask) return 0;
|
||||
int eligibility = m_SwapProbe.Tick(m_Mode, hq, m_ProofTask);
|
||||
if (eligibility < 0) { Check(false, "swap eligibility unavailable or native refusal; no radio-delivery qualification"); return -1; }
|
||||
return eligibility;
|
||||
}
|
||||
// Observe queues during fleet preparation too, so fast initial deliveries are not missed.
|
||||
for (int convoy = 0; convoy < hq.Convoys().Count() && convoy < 3; convoy++)
|
||||
{
|
||||
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++)
|
||||
{
|
||||
if (m_BMP && slot != 4) continue;
|
||||
Capture(hq, task.Vehicles[slot], now);
|
||||
}
|
||||
}
|
||||
if (!m_FleetReady) m_FleetReady = Fleet(hq);
|
||||
if (m_Failed) return -1;
|
||||
if (!m_FleetReady || !m_Queued) return 0;
|
||||
ERR_HQWatchEntry delivered = hq.Watch().Find(m_Queued.SourceId);
|
||||
if (!delivered || delivered.MessageId < m_Queued.MessageId)
|
||||
{
|
||||
if (m_Unit.PendingStatus != m_Queued && m_Unit.Uplink != m_Route)
|
||||
{
|
||||
Check(true, "interrupted observed transmission created no matching HQ knowledge; wait for another natural report");
|
||||
Forget();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (!Check(delivered.MessageId == m_Queued.MessageId && delivered.MountedAmmo
|
||||
&& delivered.MountedAmmo != m_Queued.MountedAmmo && delivered.MountedAmmo != m_Payload
|
||||
&& Same(delivered.MountedAmmo, m_Payload) && Same(m_Queued.MountedAmmo, m_Payload), "actual HQ Watch holds separate copy of the exact queued counts")) return -1;
|
||||
if (!Check(delivered.ObservedAt == m_Queued.ObservedAt && delivered.LastDeliveredAt >= delivered.ObservedAt
|
||||
&& delivered.LastDeliveredAt <= now && delivered.LastDeliveredAt - delivered.ObservedAt <= 120000
|
||||
&& delivered.Strength == m_Queued.Strength && delivered.Count == m_Queued.Count
|
||||
&& 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;
|
||||
}
|
||||
protected bool CheckTarget(ERR_HQ hq)
|
||||
{
|
||||
if (hq.SupplyConvoys().Target() == 3 && hq.Convoys().Count() <= 3) return true;
|
||||
return Check(false, "unchanged default three-convoy production target");
|
||||
}
|
||||
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();
|
||||
m_ProofTask = null; m_MineBatch = null; m_MineTruck = null; m_MineInventory = null;
|
||||
m_Mines.Clear(); m_PrimaryCargo.Clear(); Forget();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+1252
File diff suppressed because it is too large
Load Diff
+245
@@ -0,0 +1,245 @@
|
||||
"""Execute native mounted probe proof/termination methods; inspect read-only setup boundaries."""
|
||||
import re, ast, os
|
||||
from pathlib import Path
|
||||
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=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'):
|
||||
assert forbidden not in s,forbidden
|
||||
assert 'checked=1 original convoy, deployed=%1 configured=%2' in s
|
||||
assert 'if (!task || (m_ProofTask && task != m_ProofTask)) continue' in s
|
||||
assert 'now + 180000' in s and 'now > m_Deadline' in s
|
||||
assert s.index('Capture(hq, task.Vehicles[slot], now)') < s.index('if (!m_FleetReady) m_FleetReady = Fleet(hq)')
|
||||
for evidence in ('cargoWeapons >= 8 && sample.Complete && sample.Weapons == 0', 'sample.Magazines == 0', 'sample.Capacity == 0',
|
||||
'cargo.Count() <= 128', 'sample.Weapons > 0 && sample.Magazines > 0', 'Original(vehicle,',
|
||||
'record.SourceId != unit.StatusSampler.InitialSourceId()', 'unit.StatusSampler.InitialMember(slot)',
|
||||
'vehicle.Members.Count() > 32', 'm_Units.Count() && i < 256'):
|
||||
assert evidence in s,evidence
|
||||
start=smoke.split('if (m_ERRTestMountedAmmo)\n\t\t\t{',1)[1].split('\n\t\t\tif (m_ERRTestConvoy)',1)[0]
|
||||
assert start.index('if (!ERR_Check(UnlockPrison(') < start.index('new ERR_NativeMountedAmmoProbe(this, now)')
|
||||
assert start.index('mountedObserverDamage.EnableDamageHandling(false)') < start.index('UnlockPrison(')
|
||||
assert 'm_ERRSmokeStep = 3000' in start and 'm_ERRMountedAmmo.Stop()' in smoke
|
||||
assert 'm_ERRSmokeStep == 3001' in smoke and 'm_ERRMountedAmmo.Tick(ERR_HQ.Cast(Command()), now)' in smoke
|
||||
# Execute only the selector condition AST, never the process-launching runner.
|
||||
node=next(n for n in ast.parse(runner).body if isinstance(n,ast.If) and "mounted-ammo" in ast.unparse(n.test) and "mounted_swap_eligibility" not in ast.unparse(n.test) and "mounted_courier" not in ast.unparse(n.test))
|
||||
condition=compile(ast.Expression(node.test),'<selector>','eval')
|
||||
from types import SimpleNamespace
|
||||
for project,rounds,timeout,bad in [('chat',1,360,False),('chat',1,359,True),('core',1,360,True),('chat',2,360,True)]:
|
||||
assert eval(condition,{'a':SimpleNamespace(smoke='mounted-ammo',project=project,rounds=rounds,timeout=timeout)})==bad
|
||||
code='\n'.join(body(p,x) for x in ('protected bool Same(', 'int Tick(', 'protected bool CheckTarget(', 'protected void Forget(', 'void Stop('))
|
||||
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','m_BMPVehicle'):
|
||||
code=re.sub(r'\b'+var+r'\.',var+'->',code)
|
||||
for call in ('Convoys()','SupplyConvoys()','Watch()'):code=code.replace(call+'.',call+'->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string=std::string;
|
||||
struct ERR_HQMountedAmmoSnapshot{bool Complete=true;int Weapons=1,Muzzles=1,Magazines=1,Rounds=80,Capacity=100,Chambers=1,LowMagazines=0;};
|
||||
struct Report{string SourceId="source";int MessageId=7,Strength=3,Count=3,Fit=3;float ObservedAt=1000;ERR_HQMountedAmmoSnapshot* MountedAmmo=nullptr;};
|
||||
struct ERR_HQWatchEntry:Report{float LastDeliveredAt=2000;};
|
||||
struct Root{bool alive=true;};struct ERR_Actors{static bool Alive(Root* r){return r&&r->alive;}};
|
||||
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;}};
|
||||
template<class T>struct Arr:std::vector<T>{int Count(){return this->size();}void Clear(){this->clear();}};
|
||||
struct Escort{Root* Entity=nullptr;};struct ERR_ConvoyTask{bool Stopped=false;Arr<Escort*> 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;}};
|
||||
template<class... T>void 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_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;Arr<int>m_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*,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;
|
||||
ERR_HQMountedAmmoSnapshot payload,original,received;
|
||||
Setup(){ERR_GameMode::current=&mode;p.m_Mode=&mode;p.m_Queued=&queued;p.m_Payload=&payload;p.m_Route=&route;p.m_Unit=&unit;p.m_Root=&root;
|
||||
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;
|
||||
s.hq.watch.entry=nullptr;s.p.m_Queued=nullptr;assert(s.p.Tick(&s.hq,3000)==1&&dry.calls==1&&s.p.captures==0);
|
||||
assert(!s.p.m_FleetReady&&s.p.m_ProofTask==&task);s.p.Stop();assert(dry.stops==1);}
|
||||
{Setup s;SwapProbe dry;s.p.m_SwapEligibility=true;s.p.m_SwapProbe=&dry;
|
||||
assert(s.p.Tick(&s.hq,3000)==0&&dry.calls==0&&s.p.captures==0);assert(s.p.Tick(&s.hq,180001)==-1&&dry.calls==0);}
|
||||
{Setup s;assert(s.p.Tick(&s.hq,3000)==1&&!s.p.m_Failed);}
|
||||
for(int field=0;field<8;field++){Setup s;if(field==0)s.received.Complete=false;if(field==1)s.received.Weapons++;
|
||||
if(field==2)s.received.Muzzles++;if(field==3)s.received.Magazines++;if(field==4)s.received.Rounds++;
|
||||
if(field==5)s.received.Capacity++;if(field==6)s.received.Chambers++;if(field==7)s.received.LowMagazines++;
|
||||
assert(s.p.Tick(&s.hq,3000)==-1&&s.p.m_Failed);}
|
||||
for(int fault=0;fault<12;fault++){Setup s;
|
||||
if(fault==0)s.delivered.MountedAmmo=&s.original;if(fault==1)s.delivered.MountedAmmo=&s.payload;
|
||||
if(fault==2)s.delivered.ObservedAt=999;if(fault==3)s.delivered.LastDeliveredAt=999;
|
||||
if(fault==4)s.delivered.LastDeliveredAt=3001;if(fault==5)s.delivered.LastDeliveredAt=122000;
|
||||
if(fault==6)s.delivered.Count--;if(fault==7)s.delivered.Fit--;if(fault==8)s.route.Payload="changed";
|
||||
if(fault==9)s.route.MessageId++;if(fault==10)s.root.alive=false;if(fault==11)s.hq.unit=nullptr;
|
||||
assert(s.p.Tick(&s.hq,3000)==-1);}
|
||||
{Setup s;s.delivered.MessageId=6;assert(s.p.Tick(&s.hq,3000)==0);s.delivered.MessageId=8;assert(s.p.Tick(&s.hq,3000)==-1);}
|
||||
{Setup s;s.p.fleet=false;assert(s.p.Tick(&s.hq,3000)==0);assert(s.p.Tick(&s.hq,180001)==-1);assert(s.p.m_Deadline==180000);}
|
||||
{Setup s;s.hq.watch.entry=nullptr;s.unit.PendingStatus=nullptr;s.unit.Uplink=nullptr;assert(s.p.Tick(&s.hq,3000)==0);
|
||||
assert(!s.p.m_Queued&&!s.p.m_Stopped&&s.p.m_Deadline==180000);}
|
||||
{Setup s;s.p.m_Queued=nullptr;assert(s.p.Tick(&s.hq,3000)==0);}
|
||||
{Setup s;s.hq.supply.target=2;assert(s.p.Tick(&s.hq,3000)==-1);}
|
||||
{Setup s;s.hq.pool.count=4;assert(s.p.Tick(&s.hq,3000)==-1);}
|
||||
{Setup s;s.mode.active=false;assert(s.p.Tick(&s.hq,3000)==-1);}
|
||||
{Setup s;s.p.Stop();s.p.Stop();assert(!s.p.m_Queued&&!s.p.m_Payload&&!s.p.m_Route&&!s.p.m_Root&&!s.p.m_Unit);assert(s.p.Tick(&s.hq,3000)==-1);}
|
||||
}
|
||||
'''.replace(';if(', ';\n if('))
|
||||
print('PASS: actual bounded native mounted proof/detached payload/deadline/cleanup methods, production-only setup and isolated runner selector')
|
||||
|
||||
candidate=body(p,'protected ERR_SupplyConvoyTask Candidate(').replace('protected ','').replace('null','nullptr')
|
||||
candidate=candidate.replace('ERR_SupplyConvoyTask.Cast(hq.Convoys().At(i))','hq->Convoys()->At(i)')
|
||||
candidate=candidate.replace('ERR_HQ hq','ERR_HQ* hq').replace('ERR_SupplyConvoyTask ', 'ERR_SupplyConvoyTask* ')
|
||||
candidate=candidate.replace('foreach (','for (').replace('hq.Convoys().','hq->Convoys()->')
|
||||
for var in ('task','pending','vehicle'):candidate=candidate.replace(var+'.',var+'->')
|
||||
candidate=candidate.replace('bool ownsRoot;', 'bool ownsRoot=false;').replace('m_MineBatch.','m_MineBatch->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
template<class T>struct Arr:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Clear(){this->clear();}};
|
||||
struct Vehicle{int Entity=0;};using ERR_ConvoyVehicle=Vehicle*;
|
||||
struct Cargo{bool Finished=true;};using ERR_ConvoyCargoCallback=Cargo*;
|
||||
struct MineBatch{bool Complete(){return true;}};
|
||||
struct ERR_SupplyConvoyTask{MineBatch*MineStock=nullptr;bool Stopped=false,Ready=true,MagazinesIssued=true,Receipt=true;Arr<Vehicle*> Vehicles;Arr<struct Cargo*> Cargo;
|
||||
ERR_SupplyConvoyTask(){Vehicles.resize(6);}};
|
||||
struct Pool{Arr<ERR_SupplyConvoyTask*> tasks;int reads=0;int Count(){return tasks.Count();}ERR_SupplyConvoyTask* At(int i){reads++;return tasks[i];}};
|
||||
struct ERR_HQ{Pool pool;Pool* Convoys(){return &pool;}};
|
||||
struct Probe{bool m_MineStock=false;ERR_SupplyConvoyTask*m_ProofTask=nullptr;MineBatch*m_MineBatch=nullptr;bool Check(bool yes,const char*){return yes;}bool m_Queued=false;int m_Root=42;
|
||||
'''+candidate+r'''
|
||||
};
|
||||
int main(){
|
||||
ERR_HQ hq;Probe p;ERR_SupplyConvoyTask first,second,third,fourth;Vehicle owned;owned.Entity=42;
|
||||
assert(p.Candidate(&hq)==nullptr);
|
||||
hq.pool.tasks={&first};assert(p.Candidate(&hq)==&first); // one actual convoy sufficient
|
||||
hq.pool.tasks={&first,&second};first.Ready=false;assert(p.Candidate(&hq)==&second);
|
||||
first.Ready=true;p.m_Queued=true;second.Vehicles[3]=&owned;assert(p.Candidate(&hq)==&second); // exact captured sender's task
|
||||
second.Vehicles[3]=nullptr;assert(p.Candidate(&hq)==nullptr);
|
||||
p.m_Queued=false;hq.pool.tasks={&first};
|
||||
for(int failure=0;failure<7;failure++){
|
||||
ERR_SupplyConvoyTask t;Cargo pending;hq.pool.tasks={&t};
|
||||
if(failure==0)t.Stopped=true;
|
||||
if(failure==1)t.MagazinesIssued=false;
|
||||
if(failure==2)t.Receipt=false;
|
||||
if(failure==3)t.Vehicles.resize(5);
|
||||
if(failure==4){pending.Finished=false;t.Cargo={&pending};}
|
||||
if(failure==5)t.Cargo={nullptr};
|
||||
if(failure==6)t.Cargo.resize(129,&pending);
|
||||
assert(p.Candidate(&hq)==nullptr);
|
||||
}
|
||||
first.Ready=false;second.Ready=false;third.Ready=false;hq.pool.tasks={&first,&second,&third,&fourth};hq.pool.reads=0;
|
||||
assert(p.Candidate(&hq)==nullptr&&hq.pool.reads==3); // no fourth candidate traversal
|
||||
}
|
||||
''')
|
||||
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 <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
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::vector<int>Nodes={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),'<bmp-selector>','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 <cassert>
|
||||
#include <string>
|
||||
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')
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run bounded native Workbench smoke tests inside a windowless Gamescope display.
|
||||
Requires Linux, gamescope, Proton, and a disposable COPY of a working Tools compat prefix.
|
||||
No desktop rules or existing Wine processes are changed. This is not a dedicated server.
|
||||
"""
|
||||
import argparse, fcntl, json, os, shutil, signal, subprocess, time
|
||||
from pathlib import Path
|
||||
from native_provenance import snapshot, persist, bindings, seal_run
|
||||
from headless_game import assert_prefix_idle, cleanup_owned_run
|
||||
from external_addons import policy as external_policy, prepare_profile, verify_profile
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
|
||||
p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile')
|
||||
p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam')
|
||||
p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile')
|
||||
p.add_argument('--smoke', choices=['crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory')
|
||||
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')
|
||||
p.add_argument('--convoy-cover', action='store_true', help='After convoy driving, arrange the existing prisoner to verify native sighting/radio HOLD and RESUME')
|
||||
p.add_argument('--field-terrain-audit', action='store_true', help='After a fixed aircraft-field survey refusal, log one read-only 1681-node coarse terrain grid; never selects or qualifies a corridor')
|
||||
p.add_argument('--field-service-cancel', action='store_true', help='Explicit aircraft-field variant: Stop the original retained service during measured cruise; requires physical cancellation landing')
|
||||
p.add_argument('--route-service-cancel', action='store_true', help='Explicit aircraft-route variant: stop the retained service after actual airborne translation and require controlled cancellation landing')
|
||||
p.add_argument('--station-teardown-diagnostics', action='store_true', help='Opt-in bounded native deletion diagnostics; preserves all validation errors')
|
||||
p.add_argument('--flight-autohover', action='store_true', help='Opt-in owned native helicopter autohover experiment; requires Core powered-flight and actor-local action inputs')
|
||||
p.add_argument('--flight-action-inputs', action='store_true', help='Experimental exact-AI ActionManager inputs; Core powered-flight fixture only')
|
||||
p.add_argument('--flight-input-diagnostics', action='store_true', help='Opt-in read-only powered-flight input/application and native rotor signal observations')
|
||||
p.add_argument('--recon-takeover-only', action='store_true', help='Air-recon fixture: only native faction handover/release and explicit aircraft deletion')
|
||||
p.add_argument('--body-recovery-only', action='store_true', help='Air-logistics fixture: only physical corpse inspection and returned document intelligence')
|
||||
p.add_argument('--rounds', type=int, default=2)
|
||||
p.add_argument('--timeout', type=int, default=360)
|
||||
p.add_argument('--project', choices=['core', 'chat'], default='chat', help='Diagnostic fixture family (both load the combined mod)')
|
||||
a = p.parse_args()
|
||||
if a.smoke == 'crash-start' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 240): p.error('crash-start requires Chat, one round and timeout at least 240 seconds')
|
||||
if a.smoke == 'crash-geometry' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('crash-geometry requires Chat, one round and timeout at least180 seconds')
|
||||
if a.trace_random_radius and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--trace-random-radius requires Chat normal-escape-route, one round and timeout at least 420 seconds')
|
||||
if a.smoke == 'objective-map' and (a.project != 'core' or a.rounds != 1): p.error('objective-map requires --project core --rounds 1')
|
||||
if a.smoke == 'unconscious-camera' and a.project != 'core': p.error('unconscious-camera requires --project core to keep the general smoke fixture inactive')
|
||||
if a.mine_stock:
|
||||
if a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 360:
|
||||
p.error('--mine-stock requires --smoke mounted-ammo --project chat --rounds 1 and timeout at least 360 seconds')
|
||||
if a.mounted_courier or a.mounted_swap_eligibility:
|
||||
p.error('--mine-stock cannot combine with --mounted-courier or --mounted-swap-eligibility')
|
||||
if a.maintenance_refuel and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--maintenance-refuel requires Chat normal-escape-route, one round and timeout at least 420 seconds')
|
||||
if a.body_recovery_only and (a.smoke != 'air-logistics' or a.project != 'core' or a.rounds != 1): p.error('--body-recovery-only requires Core air-logistics and one round')
|
||||
if a.recon_takeover_only and (a.smoke != 'air-recon' or a.project != 'core' or a.rounds != 1): p.error('--recon-takeover-only requires Core air-recon and one round')
|
||||
steam, compat, profile = a.steam.resolve(), a.compat.resolve(), a.profile.resolve()
|
||||
if not 1 <= a.rounds <= 10 or not 60 <= a.timeout <= 1800:
|
||||
p.error('Use 1..10 rounds and a 60..1800 second timeout')
|
||||
if a.field_terrain_audit and a.smoke != 'aircraft-field': p.error('--field-terrain-audit requires --smoke aircraft-field')
|
||||
if a.field_terrain_audit and a.field_service_cancel: p.error('--field-terrain-audit and --field-service-cancel are mutually exclusive')
|
||||
if a.field_service_cancel and a.smoke != 'aircraft-field': p.error('--field-service-cancel requires --smoke aircraft-field')
|
||||
if a.route_service_cancel and a.smoke != 'aircraft-route': p.error('--route-service-cancel requires --smoke aircraft-route')
|
||||
if a.smoke == 'mortar-courier' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 500):
|
||||
p.error('mortar-courier requires --project chat --rounds 1 --timeout at least 500')
|
||||
if a.smoke in {'mortar-refresh', 'mortar-salvo', 'mortar-expiry', 'mortar-transfer', 'mortar-fire'} and (a.project != 'chat' or a.rounds != 1 or a.timeout < 400):
|
||||
p.error('mortar-refresh/mortar-salvo/mortar-expiry/mortar-transfer/mortar-fire requires --project chat --rounds 1 --timeout at least 400')
|
||||
if a.smoke in {'aircraft-field', 'aircraft-route', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate'} and (a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('aircraft-field/aircraft-route/aircraft-control/aircraft-cancel/aircraft-translate requires Chat, one round, and at least 420 seconds')
|
||||
if a.mounted_swap_eligibility and a.smoke != 'mounted-ammo': p.error('--mounted-swap-eligibility requires --smoke mounted-ammo')
|
||||
if a.mounted_courier and (a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 780): p.error('--mounted-courier requires --smoke mounted-ammo --project chat --rounds 1 and timeout at least780 seconds')
|
||||
if a.mounted_courier and a.mounted_swap_eligibility: p.error('--mounted-courier and --mounted-swap-eligibility are mutually exclusive')
|
||||
if a.mounted_intercept and (a.smoke != 'convoy' or a.project != 'chat' or a.rounds != 1 or a.timeout < 900): p.error('--mounted-intercept requires Chat --smoke convoy, one round and timeout at least900 seconds')
|
||||
if a.mounted_intercept and a.convoy_cover: p.error('--mounted-intercept and --convoy-cover are mutually exclusive')
|
||||
if a.convoy_cover and a.smoke != 'convoy': p.error('--convoy-cover requires --smoke convoy')
|
||||
if a.convoy_cover and a.timeout < 900: p.error('--convoy-cover requires timeout at least900 seconds')
|
||||
if a.smoke == 'supply-travel' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 480): p.error('supply-travel requires Chat, one round, and at least480 seconds')
|
||||
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')
|
||||
if a.smoke in {'hq-memory', 'hq-awareness', 'hq-body'} and (a.project != 'chat' or a.rounds != 1): p.error('hq-memory/hq-awareness/hq-body requires Chat and exactly one round')
|
||||
if a.smoke == 'map-ownership' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('map-ownership requires Chat, one round, and at least 180 seconds')
|
||||
if a.smoke == 'powered-translate' and (a.project != 'core' or not a.flight_action_inputs or not a.flight_autohover): p.error('powered-translate requires Core with flight-action-inputs and flight-autohover')
|
||||
if a.flight_autohover and (a.project != 'core' or a.smoke not in {'powered-flight', 'powered-translate'} or not a.flight_action_inputs): p.error('flight-autohover requires Core powered-flight with flight-action-inputs')
|
||||
if a.flight_action_inputs and (a.project != 'core' or a.smoke not in {'powered-flight', 'powered-translate'}): p.error('flight-action-inputs requires the Core powered-flight fixture')
|
||||
if a.smoke in {'powered-flight', 'powered-translate'} and (a.project != 'core' or a.rounds != 1 or a.timeout < 420): p.error('powered-flight requires Core, one round, and at least 420 seconds')
|
||||
if a.smoke == 'station-teardown' and (a.project != 'core' or a.rounds != 1): p.error('station-teardown requires --project core --rounds 1')
|
||||
if a.smoke in {'native-end-active', 'native-end-preparing'}:
|
||||
if a.project != 'core' or a.rounds != 2 or a.timeout < 120: p.error('native-end fixtures require Core, exactly two rounds and at least 120 seconds')
|
||||
fixture_names = ['erri-native-end-round.json', 'erri-native-end-round.json.tmp', 'erri-native-end-round.json.bak']
|
||||
if any((profile/name).exists() or (profile/name).is_symlink() for name in fixture_names): p.error('native-end fixture paths must not preexist; preserve prior evidence before retrying')
|
||||
if a.smoke == 'environment-restart':
|
||||
if a.project != 'core' or a.rounds != 2: p.error('environment-restart requires --project core --rounds 2')
|
||||
fixture_names = ['erri-environment-restart.json', 'erri-environment-restart.json.bak', 'erri-environment-restart.json.tmp', 'erri-environment-restart-round.json']
|
||||
if any((profile/name).exists() or (profile/name).is_symlink() for name in fixture_names): p.error('environment-restart fixture paths must not preexist; preserve or move prior evidence before retrying')
|
||||
if compat.is_relative_to(steam) or not (compat/'pfx/system.reg').is_file():
|
||||
p.error('--compat must be a disposable cloned prefix outside the Steam installation')
|
||||
if a.external_addons and profile == Path('/tmp/erri-workbench-profile'): p.error('Use a separate external-dependency profile; preserve the vanilla test profile')
|
||||
if not a.external_addons: p.error('The combined mod requires --external-addons pointing to installed authorized RHS/WCS/BMP dependencies')
|
||||
prefix_lock = (compat/'.erri-workbench.lock').open('a')
|
||||
try: fcntl.flock(prefix_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError: p.error('Another test runner owns this disposable Wine prefix')
|
||||
# The lock must be held before the preflight; never repair a busy prefix here.
|
||||
try: assert_prefix_idle(compat)
|
||||
except (OSError,ValueError) as error: p.error(str(error))
|
||||
if profile.is_relative_to(ROOT) or profile.is_relative_to(steam):
|
||||
p.error('--profile must be outside source and Steam directories')
|
||||
if not shutil.which('gamescope') or not shutil.which('pactl'): p.error('gamescope and pactl are required')
|
||||
wb = steam/'steamapps/common/Arma Reforger Tools/Workbench'
|
||||
proton = steam/'steamapps/common/Proton - Experimental/proton'
|
||||
vanilla = steam/'steamapps/common/Arma Reforger/addons/data'
|
||||
if not proton.is_file() or not (wb/'ArmaReforgerWorkbenchSteamDiag.exe').is_file():
|
||||
p.error('Stable Reforger Tools and Proton Experimental must already be installed')
|
||||
profile.mkdir(parents=True, exist_ok=True)
|
||||
addons = profile/'addons'
|
||||
addons.mkdir(exist_ok=True)
|
||||
link = addons/'ArmaReforger'
|
||||
if not link.exists(): link.symlink_to(vanilla, target_is_directory=True)
|
||||
external_inputs = None
|
||||
external_cache = profile/'external-source-cache.json'
|
||||
allowed_profile_addons = {'ArmaReforger', 'saves'}
|
||||
if a.external_addons:
|
||||
try:
|
||||
external_rules = external_policy(ROOT)
|
||||
external_inputs = prepare_profile(addons, a.external_addons, external_rules, external_cache)
|
||||
allowed_profile_addons.update('ERRI_'+guid for guid in external_inputs)
|
||||
except (OSError, ValueError) as error: p.error(str(error))
|
||||
if link.resolve() != vanilla.resolve() or any(x.name not in allowed_profile_addons for x in addons.iterdir()) or any((addons/'saves').rglob('*.gproj')):
|
||||
p.error('Test profile addons must contain only vanilla and the explicit audited external closure')
|
||||
profile_arg = 'Z:'+str(profile)
|
||||
# Refuse concurrency instead of terminating anybody else's editor.
|
||||
for proc in Path('/proc').iterdir():
|
||||
if not proc.name.isdigit(): continue
|
||||
try:
|
||||
args = (proc/'cmdline').read_bytes().split(b'\0')
|
||||
if profile_arg.encode() in args and any(x.endswith(b'ArmaReforgerWorkbenchSteamDiag.exe') for x in args):
|
||||
p.error('Another Workbench already uses this test profile')
|
||||
except (FileNotFoundError, PermissionError, ProcessLookupError): pass
|
||||
command = ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720', '-w', '1280', '-h', '720', '-r', '30', '--', str(proton), 'run', str(wb/'ArmaReforgerWorkbenchSteamDiag.exe'), '-gproj', 'Z:'+str(ROOT/'addons/core/addon.gproj'), '-addonsDir', 'Z:'+str(ROOT/'addons'), '-profile', profile_arg, '-logconsole', '-wbModule=WorldEditor', '-load', 'Worlds/ERR_Everon.ent', '-plugin=ERR_ValidateWorldPlugin', '-errPlay', '1', '-errSmoke', '1', '-errSmokeOutcome', a.smoke, '-noThrow', '-disableCrashReporter']
|
||||
if a.convoy_cover: command += ['-errConvoyCover', '1']
|
||||
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']
|
||||
if a.field_service_cancel: command += ['-errSmokeFieldServiceCancel', '1']
|
||||
if a.field_terrain_audit: command += ['-errSmokeFieldTerrainAudit', '1']
|
||||
if a.route_service_cancel: command += ['-errSmokeRouteServiceCancel', '1']
|
||||
if a.station_teardown_diagnostics: command += ['-errStationTeardownDiag', '1']
|
||||
if a.flight_input_diagnostics: command += ['-errFlightInputDiag', '1']
|
||||
if a.flight_action_inputs: command += ['-errFlightActionInputs', '1']
|
||||
if a.flight_autohover: command += ['-errFlightAutohover', '1']
|
||||
if a.recon_takeover_only: command += ['-errReconTakeoverOnly', '1']
|
||||
if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1']
|
||||
if a.project == 'core':
|
||||
if a.smoke not in {'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'native-end-active', 'native-end-preparing', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture')
|
||||
command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin'
|
||||
command[command.index('-errSmoke')] = '-errBaseSmoke'
|
||||
if a.smoke in {'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')
|
||||
if a.smoke in {'startup-failure', 'site-survey', 'transport-survey', 'extraction-startup-failure', 'extraction-boundary-failure'} and (a.project != 'core' or a.rounds != 1): p.error('This diagnostic requires --project core --rounds 1')
|
||||
env = os.environ.copy()
|
||||
env.update(STEAM_COMPAT_DATA_PATH=str(compat), STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam), WINEPREFIX=str(compat/'pfx'))
|
||||
# A test-only sink prevents startup sounds; a stream check also catches a saved
|
||||
# explicit Wine device selection. Never change the user's default sink.
|
||||
silent_sink = 'erri_tests_silent'
|
||||
sinks = json.loads(subprocess.check_output(['pactl', '-f', 'json', 'list', 'short', 'sinks'], text=True))
|
||||
if not any(sink['name'] == silent_sink for sink in sinks):
|
||||
subprocess.run(['pactl', 'load-module', 'module-null-sink', 'sink_name='+silent_sink, 'sink_properties=device.description=ERRI_Silent_Tests'], check=True, stdout=subprocess.DEVNULL)
|
||||
subprocess.run(['pactl', 'set-sink-mute', silent_sink, '1'], check=True)
|
||||
subprocess.run(['pactl', 'set-sink-volume', silent_sink, '0%'], check=True)
|
||||
sinks = json.loads(subprocess.check_output(['pactl', '-f', 'json', 'list', 'short', 'sinks'], text=True))
|
||||
silent_index = next(sink['index'] for sink in sinks if sink['name'] == silent_sink)
|
||||
env.update(PULSE_SINK=silent_sink, PULSE_PROP='application.id=org.escapistsrereintroduced.tests')
|
||||
muted_streams = set()
|
||||
def silence_test_streams():
|
||||
streams = json.loads(subprocess.check_output(['pactl', '-f', 'json', 'list', 'sink-inputs'], text=True))
|
||||
for stream in streams:
|
||||
props = stream.get('properties', {})
|
||||
owned = props.get('application.id') == 'org.escapistsrereintroduced.tests'
|
||||
pid = props.get('application.process.id', '')
|
||||
if not owned and pid.isdigit():
|
||||
try:
|
||||
target = str(compat/'pfx').encode()
|
||||
values = (Path('/proc')/pid/'environ').read_bytes().split(b'\0')
|
||||
owned = any(value.startswith(b'WINEPREFIX=') and value[len(b'WINEPREFIX='):].rstrip(b'/') == target for value in values)
|
||||
except (FileNotFoundError, PermissionError, ProcessLookupError): pass
|
||||
if not owned: continue
|
||||
index = str(stream['index'])
|
||||
if stream.get('sink') != silent_index:
|
||||
subprocess.run(['pactl', 'move-sink-input', index, silent_sink], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
if not stream.get('mute'):
|
||||
subprocess.run(['pactl', 'set-sink-input-mute', index, '1'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
if index not in muted_streams:
|
||||
muted_streams.add(index)
|
||||
print('Test audio routed to silent output and muted (stream '+index+')', flush=True)
|
||||
# Snapshot addon bytes before launching any engine process.
|
||||
captured_sources = snapshot(ROOT, command)
|
||||
recorded_sessions = set()
|
||||
started = time.time()
|
||||
log_root = profile/'logs'
|
||||
previous = set(log_root.glob('logs_*')) if log_root.exists() else set()
|
||||
result, session, complete_at = 'timeout', None, None
|
||||
runner_error = None
|
||||
cleanup_ok = True
|
||||
cleanup_report = None
|
||||
child = None
|
||||
with (profile/'headless-launch.log').open('w') as output:
|
||||
try:
|
||||
child = subprocess.Popen(command, cwd=wb, env=env, stdout=output, stderr=subprocess.STDOUT, start_new_session=True)
|
||||
while time.time()-started < a.timeout:
|
||||
silence_test_streams()
|
||||
candidates = [x for x in log_root.glob('logs_*') if x not in previous]
|
||||
if candidates:
|
||||
session = max(candidates, key=lambda x:x.stat().st_mtime)
|
||||
if session not in recorded_sessions:
|
||||
persist(session, captured_sources)
|
||||
if external_inputs:
|
||||
(session/'external-addons-inputs.json').write_text(json.dumps(external_inputs, indent=2)+'\n')
|
||||
recorded_sessions.add(session)
|
||||
console = session/'console.log'
|
||||
content = console.read_text(errors='replace') if console.exists() else ''
|
||||
if ('Reintroduced: scenario preparation failed' in content and a.smoke not in {'startup-failure', 'extraction-startup-failure', 'extraction-boundary-failure'}) or 'ERR SMOKE FAIL:' in content or 'Virtual Machine Exception' in content or 'Can\'t compile "Game"' in content:
|
||||
result = 'test-failed'; break
|
||||
if content.count('ERR SMOKE COMPLETE:') >= a.rounds:
|
||||
if complete_at is None: complete_at = time.time()
|
||||
if time.time()-complete_at >= 6: result = 'smoke-completed'; break
|
||||
if child.poll() is not None: result = 'process-exited'; break
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
result = 'interrupted'
|
||||
except (OSError,ValueError,subprocess.SubprocessError) as error:
|
||||
result = 'runner-failed'; runner_error = str(error)
|
||||
finally:
|
||||
result_before_cleanup = result
|
||||
if child is not None:
|
||||
cleanup_report = cleanup_owned_run(child,proton,env,compat)
|
||||
cleanup_ok = cleanup_report['cleanup_confirmed']
|
||||
if not cleanup_ok:
|
||||
result = 'cleanup-failed'
|
||||
cleanup_error = '; '.join(cleanup_report['errors'])
|
||||
runner_error = (runner_error + '; ' if runner_error else '') + cleanup_error
|
||||
external_unchanged = True
|
||||
if external_inputs:
|
||||
try: external_unchanged = verify_profile(external_inputs, a.external_addons, external_rules, external_cache, addons)
|
||||
except (OSError, ValueError): external_unchanged = False
|
||||
if not external_unchanged: result = 'external-inputs-changed'
|
||||
stopped_evidence = seal_run(ROOT, session, captured_sources) if cleanup_ok and session else {}
|
||||
if stopped_evidence and not stopped_evidence['source_unchanged_after_run']: result = 'source-changed'
|
||||
summary = {**stopped_evidence, **bindings(captured_sources), 'external_addons_unchanged_after_run':external_unchanged, 'external_addon_count':len(external_inputs or {}), 'external_addon_inputs':'external-addons-inputs.json' if external_inputs else None, 'started_unix':started, 'result':result, 'result_before_cleanup':result_before_cleanup, 'cleanup_confirmed':cleanup_ok, 'cleanup_report':cleanup_report, 'runner_error':runner_error, 'session':str(session) if session else None, 'rounds_required':a.rounds, 'project':a.project, 'display':'headless Gamescope 1280x720/30Hz', 'dedicated_server':False, 'audio':'silent sink and per-test-stream mute'}
|
||||
if session:
|
||||
(session/'runner.json').write_text(json.dumps(summary, indent=2)+'\n')
|
||||
shutil.copyfile(profile/'headless-launch.log', session/'headless-launch.log')
|
||||
print(json.dumps(summary, indent=2), flush=True)
|
||||
if result != 'smoke-completed': raise SystemExit(1)
|
||||
# All native errors still fail the report even when the scripted sequence completed.
|
||||
raise SystemExit(subprocess.call(['python3', str(ROOT/'tools/native_report.py'), str(session/'console.log'), '--require-completions', str(a.rounds)]))
|
||||
Reference in New Issue
Block a user