--- a/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeConvoyProbe.c +++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeConvoyProbe.c @@ -70,6 +70,7 @@ { protected ERR_GameMode m_Mode; protected ref ERR_NativeConvoyCoverProbe m_Cover; + protected ref ERR_NativeMountedInterceptProbe m_Intercept; protected ref ERR_ConvoyTask m_Task; protected ref array m_Origins = {}; protected ref array m_OriginalVehicles = {}; @@ -297,6 +298,9 @@ int Tick(ERR_HQ hq, float now) { if (!Enabled() || !hq || !m_Mode || m_Mode != ERR_GameMode.Get() || m_Failed) return -1; + if (ERR_NativeMountedInterceptProbe.Requested() && ERR_NativeConvoyCoverProbe.Enabled()) + { Check(false, "mounted-intercept and convoy-cover fixtures are mutually exclusive"); return -1; } + if (m_Step == 5) return m_Intercept.Tick(now); if (m_Step == 4) return m_Cover.Tick(hq, now); if (m_Deadline == 0) m_Deadline = now + 480000; if (now >= m_Deadline) { Check(false, "native fixture deadline"); return -1; } @@ -391,6 +395,10 @@ && m_Mode.Forces().Count() == m_InitialLedger + 6 && m_Task.Cargo.Count() == 56, "later repeated dispatch preserves exact task, force and cargo counts")) return -1; Check(true, "all six native vehicles drove at least 20m and leader at least 100m without fixture movement"); + if (ERR_NativeMountedInterceptProbe.Enabled()) + { + m_Intercept = new ERR_NativeMountedInterceptProbe(m_Mode, hq, m_Task, now); m_Step = 5; return 0; + } if (ERR_NativeConvoyCoverProbe.Enabled()) { m_Cover = new ERR_NativeConvoyCoverProbe(m_Mode); m_Step = 4; @@ -398,6 +406,6 @@ } return 1; } - void Stop() { if (m_Cover) m_Cover.Stop(); } + void Stop() { if (m_Intercept) m_Intercept.Stop(); if (m_Cover) m_Cover.Stop(); } } #endif --- /dev/null +++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMountedInterceptProbe.c @@ -0,0 +1,258 @@ +#ifdef WORKBENCH +// Two explicitly synthetic witness inbox samples; actual originating call, +// radio hops, HQ intake/planning and downlink remain production-owned. +modded class ERR_HQ +{ + ERR_HQRoute ERR_InterceptFixtureQueue(ERR_HQReport report) + { + if (!ERR_NativeMountedInterceptProbe.Enabled() || !report) return null; + foreach (ERR_HQUnit unit : m_Units) + { + if (!unit.Group || unit.ReservedForSupport || !unit.Radio || unit.Uplink || unit.PendingReport || unit.PendingSweep) continue; + if (unit.PendingSupplyStatus || unit.PendingStatus || !unit.LocalAwareness) continue; + ERR_HQRoute route = m_Network.Route(unit.Radio); + if (!route || route.Nodes.IsEmpty()) continue; + bool healthy = true; + for (int i = 0; i < route.Nodes.Count(); i++) + if (!route.Nodes[i] || !route.Nodes[i].Valid(i == route.Nodes.Count() - 1)) healthy = false; + if (!healthy) continue; + report.Source = unit.Group; + unit.LocalAwareness.Clear(); unit.LocalAwareness.Remember(report, report.ObservedAt); + unit.NextContact = 0; + ContactReport(unit, GetGame().GetWorld().GetWorldTime()); + if (!unit.Uplink || !unit.PendingReport || unit.PendingReport.Target != report.Target) return null; + report.MessageId = unit.PendingReport.MessageId; + return unit.Uplink; + } + return null; + } + protected override void TickMountedIntercept(float now) + { + ERR_NativeMountedInterceptProbe proof = ERR_NativeMountedInterceptProbe.Active(); + ref ERR_ConvoyInterceptOrder order = m_MountedIntercept; + ref ERR_HQRoute route = m_MountedInterceptRoute; + bool delivered = m_MountedInterceptDelivered; + if (proof && order && !delivered) proof.Pending(order, route, m_MountedInterceptTask, now); + super.TickMountedIntercept(now); + if (proof && order && !delivered && m_MountedIntercept == order && m_MountedInterceptDelivered) + proof.Delivered(order, route, now); + } +} + +class ERR_NativeMountedInterceptProbe +{ + protected static ERR_NativeMountedInterceptProbe s_Active; + protected ERR_GameMode m_Mode; + protected ERR_HQ m_HQ; + protected ref ERR_ConvoyTask m_Task; + protected ref array m_Vehicles = {}; + protected ref array m_Crew = {}; + protected ref array m_Groups = {}; + protected ref ERR_HQReport m_First; + protected ref ERR_HQReport m_Second; + protected ref ERR_HQRoute m_FirstRoute; + protected ref ERR_HQRoute m_SecondRoute; + protected ref ERR_HQRoute m_Downlink; + protected ref ERR_ConvoyInterceptOrder m_Order; + protected ERR_ConvoyVehicle m_Intercept; + protected AIWaypoint m_Detour; + protected vector m_Direction; + protected vector m_FirstPosition; + protected vector m_DeliveryPosition; + protected vector m_HoldPosition; + protected vector m_ResumePosition; + protected vector m_Destination; + protected float m_StartedAt; + protected float m_Deadline; + protected int m_Token; + protected int m_BeliefId; + protected int m_BeliefRevision; + protected int m_ReportId; + protected float m_ExpiresAt; + protected float m_DeliveredAt = -1; + protected float m_HoldAt = -1; + protected float m_ResumeAt = -1; + protected float m_NextLog; + protected int m_LedgerCount; + protected int m_RouteIndex; + protected int m_Step; + protected bool m_Held; + protected bool m_Failed; + protected bool m_Stopped; + static bool Requested() + { + string flag; + return ERR_NativeConvoyProbe.Enabled() && System.GetCLIParam("errMountedIntercept", flag) && flag == "1"; + } + static string CompletionText(bool intercept) + { + if (intercept) return "ERR SMOKE COMPLETE: native mounted interception with two synthetic observations, actual radio and physical detour/hold/expiry/resume; explicit fixture reset"; + return "ERR SMOKE COMPLETE: native convoy readiness, cargo and road movement; explicit fixture reset"; + } + static bool Enabled() + { + string flag, conflict; + return ERR_NativeConvoyProbe.Enabled() && System.GetCLIParam("errMountedIntercept", flag) && flag == "1" + && (!System.GetCLIParam("errConvoyCover", conflict) || conflict != "1"); + } + static ERR_NativeMountedInterceptProbe Active() { return s_Active; } + void ERR_NativeMountedInterceptProbe(ERR_GameMode mode, ERR_HQ hq, ERR_ConvoyTask task, float now) + { + m_Mode = mode; m_HQ = hq; m_Task = task; m_StartedAt = now; m_Deadline = now + 240000; + s_Active = this; + if (!mode || !hq || !task || !task.Ready || task.Stopped || task.Vehicles.Count() != 6 || !task.Route || task.Route.Count() < 2) + { Check(false, "original ready finite convoy unavailable"); return; } + if (!Check(mode.Forces() != null, "original force ledger available")) return; + m_LedgerCount = mode.Forces().Count(); + foreach (ERR_ConvoyVehicle vehicle : task.Vehicles) + { + if (!Check(vehicle && vehicle.Entity && vehicle.Group && vehicle.Members.Count() <= 19, "bounded original vehicle and crew identities")) return; + m_Vehicles.Insert(vehicle.Entity); m_Groups.Insert(vehicle.Group); + foreach (IEntity actor : vehicle.Members) m_Crew.Insert(actor); + } + int target = Math.Min(task.Vehicles[0].RouteIndex + 1, task.Route.Count() - 1); + if (target < 1) target = 1; + m_Direction = task.Route.At(target) - task.Route.At(target - 1); m_Direction[1] = 0; + if (!Check(m_Direction.Length() >= 40, "authored road leg provides synthetic sample direction")) return; + m_Direction.Normalize(); m_FirstPosition = task.Route.At(target) - m_Direction * 330; + Print("ERR MOUNTED INTERCEPT ARRANGEMENT: two synthetic on-foot witness inbox positions derived from original authored route; no entity movement, perception claim or injected delivery"); + } + protected bool Check(bool condition, string detail) + { + if (condition) { Print("ERR SMOKE PASS: mounted intercept " + detail); return true; } + m_Failed = true; Print("ERR SMOKE FAIL: mounted intercept " + detail, LogLevel.ERROR); return false; + } + protected bool Original() + { + if (!m_Mode || m_Mode != ERR_GameMode.Get() || m_Mode.Command() != m_HQ || !m_Mode.Active() || !m_Mode.Forces()) return false; + if (!m_Task || m_Task.Stopped || !m_Task.Ready || m_Task.Vehicles.Count() != 6 || m_Task.Cargo.Count() != 56) return false; + if (m_Vehicles.Count() != 6 || m_Groups.Count() != 6 || m_Mode.Forces().Count() != m_LedgerCount) return false; + int crew; + for (int i = 0; i < 6; i++) + { + ERR_ConvoyVehicle vehicle = m_Task.Vehicles[i]; + if (!vehicle || vehicle.Entity != m_Vehicles[i] || vehicle.Group != m_Groups[i] || vehicle.Released || !vehicle.CrewValid(m_Mode)) return false; + foreach (IEntity actor : vehicle.Members) + { if (crew >= m_Crew.Count() || m_Crew[crew] != actor) return false; crew++; } + } + return crew == m_Crew.Count(); + } + protected ERR_HQReport Sample(float now, vector position) + { + ERR_HQReport report = new ERR_HQReport(); report.Target = m_Mode; + report.ObservedAt = now; report.Position = position; report.TravelMode = "on foot"; + return report; + } + protected bool UplinkDelivered(ERR_HQReport report, ERR_HQRoute route) + { + if (!report || !report.Source || !route || route.StartedAt < report.ObservedAt || route.Downlink || route.Failed || !route.Originated || route.Sending || route.Nodes.IsEmpty()) return false; + if (route.MessageId != report.MessageId || route.Cursor < route.Nodes.Count()) return false; + ERR_HQBelief belief = m_HQ.Memory().Get(m_Mode); + return belief && belief.MessageId == report.MessageId && belief.ObservedAt == report.ObservedAt && belief.Position == report.Position; + } + void Pending(ERR_ConvoyInterceptOrder order, ERR_HQRoute route, ERR_ConvoyTask task, float now) + { + if (m_Stopped || m_Failed || m_Order) return; + if (!m_Second || !Check(task == m_Task && order.ReportId == m_Second.MessageId && UplinkDelivered(m_Second, m_SecondRoute), "planner uses the second actually delivered synthetic sample")) return; + if (!Check(route && route.Downlink && !route.Failed && !route.Nodes.IsEmpty(), "real HQ downlink queued")) return; + m_Token = order.Token; m_BeliefId = order.BeliefId; m_BeliefRevision = order.BeliefRevision; m_ReportId = order.ReportId; + m_Order = order; m_Downlink = route; m_ExpiresAt = order.ExpiresAt; m_Destination = order.Position; + if (!Check(order.VehicleIndex == 0 || order.VehicleIndex == 3 || order.VehicleIndex == 5, "original armed escort selected")) return; + m_Intercept = task.Vehicles[order.VehicleIndex]; + if (!Check(m_Intercept.Entity == m_Vehicles[order.VehicleIndex] && m_Intercept.InterceptOrder == order + && m_Intercept.InterceptToken == order.Token && !m_Intercept.InterceptActive && !m_Intercept.InterceptWaypoint, + "pending radio claim has no delivered detour waypoint")) return; + Check(m_ExpiresAt > now && m_ExpiresAt <= m_Second.ObservedAt + 120000, "fixed original observation deadline retained"); + } + void Delivered(ERR_ConvoyInterceptOrder order, ERR_HQRoute route, float now) + { + if (m_Stopped || m_Failed || m_DeliveredAt >= 0) return; + if (!Check(order && route && m_Order && m_Intercept && order == m_Order && route == m_Downlink && !route.Failed && !route.Sending + && route.StartedAt >= order.IssuedAt && route.Cursor == route.Nodes.Count() - 1 && now > route.StartedAt, + "ordinary HQ tick completes actual downlink before detour execution")) return; + if (!Check(Original() && m_Intercept.InterceptActive && m_Intercept.InterceptWaypoint && !m_Intercept.Waypoint, + "same original escort owns separate native detour waypoint")) return; + m_Detour = m_Intercept.InterceptWaypoint; m_RouteIndex = m_Intercept.RouteIndex; + m_DeliveryPosition = m_Intercept.Entity.GetOrigin(); m_DeliveredAt = now; + Check(vector.DistanceXZ(m_DeliveryPosition, m_Destination) >= 40, "delivery starts at least40m from fixed road target"); + } + int Tick(float now) + { + if (!Enabled() || m_Stopped || m_Failed) return -1; + if (!CheckTime(now)) return -1; + if (!Original()) { Check(false, "original physical task, vehicles, groups, crew and finite cargo retained"); return -1; } + if (now >= m_NextLog) + { + m_NextLog = now + 10000; + PrintFormat("ERR MOUNTED INTERCEPT NATIVE: step=%1 first=%2 second=%3 order=%4 delivered=%5 held=%6", m_Step, m_First != null, m_Second != null, m_Order != null, m_DeliveredAt >= 0, m_Held); + if (m_Intercept) PrintFormat("ERR MOUNTED INTERCEPT MOTION: position=%1 target=%2 distance=%3 index=%4 active=%5 expires=%6", m_Intercept.Entity.GetOrigin(), m_Destination, vector.DistanceXZ(m_Intercept.Entity.GetOrigin(), m_Destination), m_Intercept.RouteIndex, m_Intercept.InterceptActive, m_ExpiresAt); + } + if (m_Step == 0) + { + ERR_HQReport first = Sample(now, m_FirstPosition); + ERR_HQRoute firstRoute = m_HQ.ERR_InterceptFixtureQueue(first); + if (!firstRoute) return 0; + m_First = first; m_FirstRoute = firstRoute; m_Step = 1; + Check(!m_HQ.Memory().Get(m_Mode), "synthetic target has no HQ memory before actual uplink"); return 0; + } + if (m_Step == 1) + { + if (!UplinkDelivered(m_First, m_FirstRoute)) return 0; + if (now - m_First.ObservedAt < 5000) return 0; + if (!Check(now - m_First.ObservedAt <= 60000, "two synthetic samples have bounded credible interval")) return -1; + ERR_HQReport second = Sample(now, m_FirstPosition + m_Direction * ((now - m_First.ObservedAt) * 0.0025)); + ERR_HQRoute secondRoute = m_HQ.ERR_InterceptFixtureQueue(second); + if (!secondRoute) return 0; + m_Second = second; m_SecondRoute = secondRoute; m_Step = 2; + Check(true, "first synthetic sample actually delivered; second queued through original witness radio"); return 0; + } + if (m_DeliveredAt < 0) return 0; + if (now < m_ExpiresAt) + { + if (m_Intercept.InterceptOrder != m_Order || !m_Intercept.InterceptActive || m_Intercept.InterceptToken != m_Token || m_Order.Token != m_Token || m_Order.BeliefId != m_BeliefId || m_Order.BeliefRevision != m_BeliefRevision || m_Order.ReportId != m_ReportId + || m_Intercept.RouteIndex != m_RouteIndex || m_Order.Position != m_Destination || m_Order.ExpiresAt != m_ExpiresAt) + { Check(false, "delivered original detour changed before fixed expiry"); return -1; } + float distance = vector.DistanceXZ(m_Intercept.Entity.GetOrigin(), m_Destination); + bool settled = !m_Intercept.InterceptWaypoint && distance <= 15; + if (m_HoldAt >= 0 && vector.DistanceXZ(m_HoldPosition, m_Intercept.Entity.GetOrigin()) > 5) settled = false; + if (!settled) + { + if (m_Held) { Check(false, "escort left its proved hold before fixed expiry"); return -1; } + m_HoldAt = -1; + } + if (settled) + { + if (m_HoldAt < 0) + { + if (!Check(vector.DistanceXZ(m_DeliveryPosition, m_Intercept.Entity.GetOrigin()) >= 20, "original escort physically drove to fixed detour target")) return -1; + m_HoldAt = now; m_HoldPosition = m_Intercept.Entity.GetOrigin(); + } + if (!m_Held && now - m_HoldAt >= 5000) + { + if (!Check(vector.DistanceXZ(m_HoldPosition, m_Intercept.Entity.GetOrigin()) <= 5, "original seated escort physically holds target for five seconds")) return -1; + m_Held = true; + } + } + return 0; + } + if (!m_Held) { Check(false, "fixed expiry reached before physical detour arrival and hold proof"); return -1; } + if (m_Intercept.InterceptToken != 0 || m_Intercept.InterceptActive || m_Intercept.InterceptWaypoint || m_HQ.LogisticsToken(m_Intercept.Group) != 0) return 0; + if (m_ResumeAt < 0) + { + if (!m_Intercept.Waypoint) return 0; + if (!Check(m_Intercept.RouteIndex >= m_RouteIndex, "natural expiry restores original route without rewinding progress")) return -1; + m_ResumeAt = now; m_ResumePosition = m_Intercept.Entity.GetOrigin(); return 0; + } + if (vector.DistanceXZ(m_ResumePosition, m_Intercept.Entity.GetOrigin()) < 10) return 0; + if (!Check(Original() && m_Intercept.RouteIndex >= m_RouteIndex, "same original escort physically resumes at least10m after natural expiry")) return -1; + Check(true, "two synthetic observations, actual uplinks, production planning/downlink, physical detour/hold/expiry/resume; no natural perception claim"); + return 1; + } + protected bool CheckTime(float now) + { + if (ERR_ForceRules.Finite(now) && now >= m_StartedAt && now < m_Deadline) return true; + Check(false, "fixed240s interception fixture deadline; no retry or forced eligibility"); return false; + } + void Stop() { m_Stopped = true; if (s_Active == this) s_Active = null; } +} +#endif --- a/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c +++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c @@ -117,6 +117,7 @@ if (ERR_NativeMountedCourierProbe.Enabled()) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 750000; if (ERR_NativeMaintenanceProbe.Enabled()) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000; if (m_ERRTestConvoy) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 540000; + if (ERR_NativeMountedInterceptProbe.Enabled()) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 840000; if (outcome == "supply-convoys") m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 840000; if (ERR_NativeConvoyCoverProbe.Enabled()) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 840000; if (m_ERRTestHQMemory) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 360000; @@ -414,7 +415,7 @@ m_ERRSmokeDone = true; GetGame().GetCallqueue().Remove(ERR_SmokeTick); ERR_StopHQPatrol(); - Print("ERR SMOKE COMPLETE: native convoy readiness, cargo and road movement; explicit fixture reset"); + Print(ERR_NativeMountedInterceptProbe.CompletionText(ERR_NativeMountedInterceptProbe.Enabled())); return; } if (m_ERRTestPlayerDeath) @@ -1016,7 +1017,9 @@ { int result = m_ERRConvoyProbe.Tick(ERR_HQ.Cast(Command()), now); if (result == 0) return; - if (!ERR_Check(result == 1, "native six-vehicle convoy readiness, cargo and road movement")) return; + string convoyVerdict = "native six-vehicle convoy readiness, cargo and road movement"; + if (ERR_NativeMountedInterceptProbe.Enabled()) convoyVerdict = "synthetic observations, actual mounted interception radio and physical detour/hold/expiry/resume"; + if (!ERR_Check(result == 1, convoyVerdict)) return; m_ERRSmokeStep = 1061; Transition(ERR_Phase.DEFEAT); } --- a/tests/test_native_convoy_cover_probe.py +++ b/tests/test_native_convoy_cover_probe.py @@ -111,12 +111,14 @@ method=body(P,signature);assert method.index(base)',code) +for v in ('Route','Entity'):code=code.replace('->'+v+'.','->'+v+'->') +code=code.replace('task->Vehicles[0].', 'task->Vehicles[0]->').replace('Forces().','Forces()->').replace('Memory().','Memory()->') +for t in ('ERR_NativeConvoyProbe','ERR_NativeMountedInterceptProbe','System','Math','ERR_ForceRules','ERR_GameMode','LogLevel','vector'):code=code.replace(t+'.',t+'::') +code=code.replace('null','nullptr').replace('\n}\n','\n};\n') +code=re.sub(r'\b(bool|int|float|IEntity|SCR_AIGroup|AIWaypoint|[A-Za-z_]\w*\*) (\w+);',r'\1 \2{};',code) +run(r''' +#include +#include +#include +#include +#include +using string=std::string; +struct vector{float x=0,y=0,z=0;float&operator[](int i){return i==0?x:i==1?y:z;}vector operator-(vector b){return {x-b.x,y-b.y,z-b.z};}vector operator+(vector b){return{x+b.x,y+b.y,z+b.z};}vector operator*(float f){return{x*f,y*f,z*f};}bool operator==(vector b){return x==b.x&&y==b.y&&z==b.z;}bool operator!=(vector b){return !(*this==b);}float Length(){return std::hypot(x,z);}void Normalize(){float n=Length();x/=n;z/=n;}static float DistanceXZ(vector a,vector b){return(a-b).Length();}}; +templatestruct Array:std::vector{using std::vector::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T v){this->push_back(v);}}; +struct Entity{vector position;vector GetOrigin(){return position;}};using IEntity=Entity*; +struct Group{};using SCR_AIGroup=Group*;struct Waypoint{};using AIWaypoint=Waypoint*; +struct ERR_HQReport{IEntity Target=nullptr;Group*Source=nullptr;float ObservedAt=0;vector Position;string TravelMode;int MessageId=0;}; +struct ERR_HQRoute{bool Downlink=false,Failed=false,Originated=false,Sending=false;int MessageId=0,Cursor=0;float StartedAt=-1;ArrayNodes={1};}; +struct ERR_HQBelief{int MessageId=0;float ObservedAt=0;vector Position;}; +struct ERR_ConvoyInterceptOrder{int Token=400001,VehicleIndex=0,BeliefId=1,BeliefRevision=2,ReportId=2;Group*Group=nullptr;vector Position{700,0,0};float IssuedAt=10000,ExpiresAt=120000,ObservedAt=0;}; +struct Road{Arraypoints={{0,0,0},{800,0,0}};int Count(){return points.Count();}vector At(int i){return points[i];}}; +struct ERR_GameMode; +struct ERR_ConvoyVehicle{struct Entity*Entity=nullptr;struct Group*Group=nullptr;ArrayMembers;bool Released=false,valid=true,InterceptActive=false;int RouteIndex=0,InterceptToken=0;ERR_ConvoyInterceptOrder*InterceptOrder=nullptr;AIWaypoint InterceptWaypoint=nullptr,Waypoint=nullptr;bool CrewValid(ERR_GameMode*){return valid;}}; +struct ERR_ConvoyTask{bool Ready=true,Stopped=false;ArrayVehicles;ArrayCargo=Array(56);struct Road road;struct Road*Route=&road;}; +struct Ledger{int count=6;int Count(){return count;}}; +struct ERR_HQ;struct ERR_GameMode:Entity{inline static ERR_GameMode*current=nullptr;ERR_HQ*command=nullptr;Ledger ledger;bool active=true;static ERR_GameMode*Get(){return current;}bool Active(){return active;}ERR_HQ*Command(){return command;}Ledger*Forces(){return &ledger;}}; +struct Memory{ERR_HQBelief*belief=nullptr;ERR_HQBelief*Get(IEntity){return belief;}}; +struct ERR_HQ{struct Memory memory;int token=400001,queued=0;ERR_HQRoute*next=nullptr;Group*source=nullptr;struct Memory*Memory(){return &memory;}int LogisticsToken(Group*){return token;}ERR_HQRoute*ERR_InterceptFixtureQueue(ERR_HQReport*r){queued++;r->MessageId=queued;r->Source=source;return next;}}; +struct ERR_NativeConvoyProbe{inline static bool enabled=true;static bool Enabled(){return enabled;}}; +namespace System{std::mapflags={{"errMountedIntercept","1"}};bool GetCLIParam(string key,string&value){auto it=flags.find(key);if(it==flags.end())return false;value=it->second;return true;}} +namespace Math{int Min(int a,int b){return avoid PrintFormat(string,T...){} +'''.replace('Group*Group=nullptr','struct Group*Group=nullptr').replace('struct struct','struct')+code+r''' +struct Setup{ + ERR_GameMode mode;ERR_HQ hq;ERR_ConvoyTask task;ERR_ConvoyVehicle v[6];Entity e[6],actors[6];Group groups[6];ERR_HQReport second;ERR_HQRoute uplink,downlink;ERR_HQBelief belief;ERR_ConvoyInterceptOrder order;Waypoint detour,ordinary;ERR_NativeMountedInterceptProbe*p; + Setup(){ERR_GameMode::current=&mode;mode.command=&hq;hq.source=&groups[1];for(int i=0;i<6;i++){v[i].Entity=&e[i];v[i].Group=&groups[i];v[i].Members={&actors[i]};task.Vehicles.Insert(&v[i]);}p=new ERR_NativeMountedInterceptProbe(&mode,&hq,&task,0); + second.Target=&mode;second.Source=&groups[1];second.MessageId=2;second.ObservedAt=0;second.Position={370,0,0};uplink.MessageId=2;uplink.Cursor=1;uplink.Originated=true;uplink.StartedAt=0;belief.MessageId=2;belief.ObservedAt=0;belief.Position=second.Position;hq.memory.belief=&belief; + downlink.Downlink=true;downlink.StartedAt=10000;downlink.Cursor=0;order.Group=&groups[0];p->m_Second=&second;p->m_SecondRoute=&uplink;p->m_Step=2;v[0].InterceptToken=order.Token;v[0].InterceptOrder=ℴ} + void Pending(){p->Pending(&order,&downlink,&task,10000);}void Delivered(){v[0].InterceptActive=true;v[0].InterceptWaypoint=&detour;p->Delivered(&order,&downlink,12000);} +}; +int main(){ + {Setup f;f.Pending();f.Delivered();f.e[0].position=f.order.Position;f.v[0].InterceptWaypoint=nullptr;assert(f.p->Tick(20000)==0); + f.e[0].position.x+=100;assert(f.p->Tick(23000)==0&&f.p->m_HoldAt<0);f.e[0].position=f.order.Position;assert(f.p->Tick(26000)==0&&!f.p->m_Held);assert(f.p->Tick(30000)==0&&!f.p->m_Held);assert(f.p->Tick(31000)==0&&f.p->m_Held); + f.e[0].position.x+=6;assert(f.p->Tick(32000)==-1);} + assert(ERR_NativeMountedInterceptProbe::Enabled());System::flags["errConvoyCover"]="1";assert(!ERR_NativeMountedInterceptProbe::Enabled());System::flags.erase("errConvoyCover"); + {Setup f;f.Pending();assert(f.p->m_Order==&f.order&&!f.p->m_Failed&&f.p->m_DeliveredAt<0);f.Delivered();assert(!f.p->m_Failed&&f.p->m_DeliveredAt==12000); + assert(f.p->Tick(13000)==0);f.e[0].position=f.order.Position;f.v[0].InterceptWaypoint=nullptr;assert(f.p->Tick(20000)==0);assert(f.p->Tick(25000)==0&&f.p->m_Held); + f.v[0].InterceptToken=0;f.v[0].InterceptActive=false;f.v[0].InterceptOrder=nullptr;f.v[0].Waypoint=&f.ordinary;f.hq.token=0;assert(f.p->Tick(120000)==0);f.e[0].position.x+=9;assert(f.p->Tick(121000)==0);f.e[0].position.x+=1;assert(f.p->Tick(122000)==1); + f.p->Stop();assert(f.p->Tick(122001)==-1&&ERR_NativeMountedInterceptProbe::Active()==nullptr);} + for(int bad=0;bad<6;bad++){Setup f; + if(bad==0)f.uplink.Failed=true; + if(bad==1)f.uplink.Cursor=0; + if(bad==2)f.uplink.Originated=false; + if(bad==3)f.uplink.Sending=true; + if(bad==4)f.belief.MessageId=9; + if(bad==5)f.uplink.StartedAt=-1; + f.Pending();assert(f.p->m_Failed&&!f.p->m_Order);} + for(int bad=0;bad<5;bad++){Setup f;f.Pending(); + if(bad==0)f.downlink.Failed=true; + if(bad==1)f.downlink.Sending=true; + if(bad==2)f.downlink.Cursor=-1; + if(bad==3)f.downlink.StartedAt=-1; + if(bad==4)f.v[0].Waypoint=&f.ordinary; + f.Delivered();assert(f.p->m_Failed&&f.p->m_DeliveredAt<0);} + {Setup f;f.Pending();f.Delivered();assert(f.p->Tick(120000)==-1&&!f.p->m_Held);} // elapsed expiry cannot substitute for movement + {Setup f;f.Pending();f.Delivered();f.order.Position.x++;assert(f.p->Tick(13000)==-1);} // detached target remains fixed + {Setup f;f.Pending();f.Delivered();f.v[0].Entity=&f.e[1];assert(f.p->Tick(13000)==-1);} // replacement cannot qualify + {Setup f;assert(f.p->CheckTime(239999));assert(!f.p->CheckTime(240000));} + {Setup f;assert(!f.p->CheckTime(-1));} + assert(ERR_NativeMountedInterceptProbe::CompletionText(true).find("synthetic")!=string::npos); + assert(ERR_NativeMountedInterceptProbe::CompletionText(false)=="ERR SMOKE COMPLETE: native convoy readiness, cargo and road movement; explicit fixture reset"); +} +''') +assert s.startswith('#ifdef WORKBENCH') +for forbidden in ('ApplyIntercept(', 'ReserveIntercept(', '.Advance(', '.SetOrigin(', 'EnableDamageHandling(', 'TrySpawn', 'Spawn(', 'm_Memory.Ingest', 'm_Memory.Deliver'): + assert forbidden not in s,forbidden +hook=body(P,'protected override void TickMountedIntercept(') +assert hook.count('super.TickMountedIntercept(now)')==1 +assert hook.index('proof.Pending') +#include +using string=std::string; +struct ERR_NativeMountedInterceptProbe{inline static bool enabled=false;static bool Enabled(){return enabled;} +'''+completion+r'''}; +struct Smoke{bool m_ERRTestConvoy=true,m_ERRSmokeDone=false,removed=false,stopped=false;int m_ERRSmokeStep=1061;string marker;bool ERR_Check(bool ok,string){return ok;}void Record(string text){marker=text;}void Terminal(int current){ +'''+terminal+r''' +}}; +int main(){Smoke ordinary;ordinary.Terminal(2);assert(ordinary.m_ERRSmokeDone&&ordinary.marker==ERR_NativeMountedInterceptProbe::CompletionText(false)); + ERR_NativeMountedInterceptProbe::enabled=true;Smoke mounted;mounted.Terminal(2);assert(mounted.m_ERRSmokeDone&&mounted.removed&&mounted.stopped&&mounted.marker.find("synthetic")!=string::npos); + Smoke premature;premature.m_ERRSmokeStep=106;premature.Terminal(2);assert(!premature.m_ERRSmokeDone&&premature.marker.empty());} +''') +print('PASS: actual CLI rejects wrong mode/project/rounds/899s/conflict before runtime; valid flag and terminal branch identify synthetic-observation qualification') +# The native fixture is a second modded layer over the production dispatch. +# Missing base hook previously caused a native compile failure; keep its exact +# declaration and the production override in this integration contract. +hqbase=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c' +dispatch=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c' +assert body(hqbase,'protected void TickMountedIntercept(float now)').endswith('{}') +production=body(dispatch,'protected override void TickMountedIntercept(float now)') +assert 'm_MountedInterceptRoute.Advance(now, m_Journal)' in production +assert 'm_Convoys.ApplyIntercept(m_MountedInterceptTask, order, now)' in production +for declaration in ('protected ref ERR_ConvoyInterceptOrder m_MountedIntercept;', + 'protected ref ERR_HQRoute m_MountedInterceptRoute;', + 'protected ERR_ConvoyTask m_MountedInterceptTask;', + 'protected bool m_MountedInterceptDelivered;'): + assert declaration in dispatch.read_text(),declaration +hook=body(P,'protected override void TickMountedIntercept(').replace('protected override void','void').replace('ref ','').replace('super.TickMountedIntercept','Base::TickMountedIntercept').replace('ERR_NativeMountedInterceptProbe.Active()','ERR_NativeMountedInterceptProbe::Active()') +for t in ('ERR_NativeMountedInterceptProbe','ERR_ConvoyInterceptOrder','ERR_HQRoute'): + hook=re.sub(r'\b'+t+r' (?=\w)',t+'* ',hook) +hook=hook.replace('proof.Pending','proof->Pending').replace('proof.Delivered','proof->Delivered') +run(r''' +#include +#include +struct ERR_ConvoyInterceptOrder{};struct ERR_HQRoute{};struct ERR_ConvoyTask{}; +std::vector events; +struct ERR_NativeMountedInterceptProbe{ + inline static ERR_NativeMountedInterceptProbe* active=nullptr;ERR_ConvoyInterceptOrder*order=nullptr;ERR_HQRoute*route=nullptr; + static ERR_NativeMountedInterceptProbe*Active(){return active;} + void Pending(ERR_ConvoyInterceptOrder*o,ERR_HQRoute*r,ERR_ConvoyTask*,float){order=o;route=r;events.push_back(1);} + void Delivered(ERR_ConvoyInterceptOrder*o,ERR_HQRoute*r,float){assert(o==order&&r==route&&r);events.push_back(3);} +}; +struct Base{ + ERR_ConvoyInterceptOrder*m_MountedIntercept=nullptr;ERR_HQRoute*m_MountedInterceptRoute=nullptr;ERR_ConvoyTask*m_MountedInterceptTask=nullptr;bool m_MountedInterceptDelivered=false;int outcome=0,calls=0; + virtual void TickMountedIntercept(float){events.push_back(2);calls++;if(outcome==1){m_MountedInterceptRoute=nullptr;m_MountedInterceptDelivered=true;}if(outcome==-1){m_MountedIntercept=nullptr;m_MountedInterceptRoute=nullptr;}} +}; +struct Fixture:Base{ +'''+hook+r''' +}; +int main(){ERR_NativeMountedInterceptProbe p;ERR_NativeMountedInterceptProbe::active=&p;ERR_ConvoyInterceptOrder o;ERR_HQRoute r; + for(int outcome=-1;outcome<=1;outcome++){events.clear();Fixture f;f.m_MountedIntercept=&o;f.m_MountedInterceptRoute=&r;f.outcome=outcome;f.TickMountedIntercept(2);assert(f.calls==1);assert(events==(outcome==1?std::vector{1,2,3}:std::vector{1,2}));} + events.clear();ERR_NativeMountedInterceptProbe::active=nullptr;Fixture ordinary;ordinary.TickMountedIntercept(2);assert(ordinary.calls==1&&events==std::vector{2}); + events.clear();ERR_NativeMountedInterceptProbe::active=&p;Fixture delivered;delivered.m_MountedIntercept=&o;delivered.m_MountedInterceptDelivered=true;delivered.TickMountedIntercept(2);assert(delivered.calls==1&&events==std::vector{2}); +} +''') +print('PASS: visible base hook and actual fixture override delegate once; delivery proof uses retained pre-super route only on successful production transition') --- a/tools/headless_workbench.py +++ b/tools/headless_workbench.py @@ -19,6 +19,7 @@ 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('--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') @@ -58,6 +59,8 @@ 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-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') @@ -126,6 +129,7 @@ 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.mine_stock: command += ['-errMineStock', '1']