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:
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"decision": "No remaining concrete blocker found after pending deadline correction.",
|
||||
"source": "/tmp/erri-mounted-intercept-convoy-1247",
|
||||
"files": {
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c": "747956bb2b375f5572c1d22eb19aa2b493a4b4c96d1988082f645977bb6dc70f",
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c": "35678ec625fb0a3c0871e2c373fcff49816440a97a165ed25c8798dd72ef059f",
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyService.c": "5b1578afbf6da736453cc9d3872979eddd1649746aa32d3b0c7a9e3cab50057e",
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c": "6ca333da6183c86d19f09d0ad2b4d789879ebad2cb43dbe7fd3eb06d6f4e5a53"
|
||||
},
|
||||
"tests": {
|
||||
"test_convoy_lifecycle.py": 0,
|
||||
"test_convoy_column.py": 0,
|
||||
"test_convoy_combat.py": 0,
|
||||
"test_supply_convoys.py": 0
|
||||
},
|
||||
"checks": [
|
||||
"Pending cancellation preserves original LegDeadline; delivered detour resumes travel watchdog.",
|
||||
"Exact original receipt/record/route/group/vehicle/driver/roster/seats and current HQ token revalidated.",
|
||||
"Only owned waypoint removed; native column skips detached escort from leader/gap/target calculations and retains live-only interceptor task.",
|
||||
"Tactics cancels original detour before taking column ownership; support/combat contention is rejected.",
|
||||
"Stable partition places every continuing non-detached member before rejoining escort. Same-tick current leader loss promotes another continuing member without regressing shared route; only sole returning survivor uses its saved index."
|
||||
],
|
||||
"limits": [
|
||||
"Physical driving/holding/interception/rejoining unverified; source tests do not prove native movement."
|
||||
],
|
||||
"source_edits_by_reviewer": false,
|
||||
"test_only_correction": "Combat/Entity stub type names qualified by producer; initial compile failure retained. Independent rerun passed.",
|
||||
"test_sha256": {
|
||||
"test_supply_convoys.py": "be4fc6376a77296c6721ce966ea33fd256fe9d76af8705a056a88b32c4f16710",
|
||||
"test_convoy_column.py": "eb775249e434eceb047ea47b01c23c8b96a74b484c7acc1e79182e794e9f3e91",
|
||||
"test_convoy_combat.py": "140d5e8c8c978f97f7f15238bf2aa4efc6ca4d9b6370ef5a0c68bf469cec0b9a",
|
||||
"test_convoy_lifecycle.py": "926347d5ba789114de2f08651b90c21193d1d460a085c3d6eeebfede19997a25"
|
||||
},
|
||||
"final_rejoin_evidence": {
|
||||
"prior_actual_failure": "rejoin-review-red.log",
|
||||
"final_actual_pass": "rejoin-review-green.log",
|
||||
"test_adapter": "Green copy adds translation for new array<int>/pointer syntax and Array.Insert; same scenario/assertion preserved."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- a/tests/test_hq_mounted_intercept.py
|
||||
+++ b/tests/test_hq_mounted_intercept.py
|
||||
@@ -2,7 +2,7 @@
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
from policy_harness import ROOT,body,run
|
||||
-p=Path(os.environ.get('ERR_MOUNTED_INTERCEPT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'))
|
||||
+p=Path(os.environ.get('ERR_MOUNTED_INTERCEPT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedIntercept.c'))
|
||||
source=p.read_text()
|
||||
base=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'
|
||||
source='class ERR_HQRoadIntercept\n{\n'+'\n'.join(body(base,x) for x in ('static bool Eligible(', 'static bool Unreserved(', 'static bool Better('))+'\n}\n'+source
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual mounted intercept eligibility/order chronology, fixed destinations, native road selection bounds, width, segment heading, clearance and settlement delegation; no native driving claim
|
||||
@@ -0,0 +1,217 @@
|
||||
--- /dev/null
|
||||
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedIntercept.c
|
||||
@@ -0,0 +1,109 @@
|
||||
+// Detached radio order. Only its issuer chooses Position and the fixed deadline.
|
||||
+class ERR_ConvoyInterceptOrder
|
||||
+{
|
||||
+ int Token;
|
||||
+ SCR_AIGroup Group;
|
||||
+ string SourceId;
|
||||
+ int VehicleIndex;
|
||||
+ vector Position;
|
||||
+ float IssuedAt;
|
||||
+ float ExpiresAt;
|
||||
+ float ObservedAt;
|
||||
+ int BeliefId;
|
||||
+ int BeliefRevision;
|
||||
+ int ReportId;
|
||||
+}
|
||||
+
|
||||
+// Delivered ground history only; no target entity reads, force creation or orders.
|
||||
+class ERR_HQMountedInterceptRules
|
||||
+{
|
||||
+ static bool Eligible(ERR_HQBelief belief, float now)
|
||||
+ {
|
||||
+ if (!ERR_HQRoadIntercept.Eligible(belief, now)) return false;
|
||||
+ if (belief.BeliefId <= 0 || belief.BeliefRevision <= 0 || belief.ObservedAt < 0) return false;
|
||||
+ return ERR_HQPursuitRules.PositionValid(belief.Position) && ERR_HQPursuitRules.PositionValid(belief.PredictedPosition);
|
||||
+ }
|
||||
+ static bool Current(ERR_ConvoyInterceptOrder order, ERR_HQBelief belief, float now)
|
||||
+ {
|
||||
+ if (!order || !Eligible(belief, now) || !order.Group || order.SourceId.IsEmpty()) return false;
|
||||
+ if (order.Token < 400001 || order.Token > 400256) return false;
|
||||
+ if (order.VehicleIndex != 0 && order.VehicleIndex != 3 && order.VehicleIndex != 5) return false;
|
||||
+ if (order.BeliefId != belief.BeliefId || order.BeliefRevision != belief.BeliefRevision
|
||||
+ || order.ReportId != belief.MessageId || order.ObservedAt != belief.ObservedAt) return false;
|
||||
+ if (!ERR_ForceRules.Finite(order.IssuedAt) || !ERR_ForceRules.Finite(order.ExpiresAt)) return false;
|
||||
+ if (order.IssuedAt < belief.LastDeliveredAt || now < order.IssuedAt || now - order.IssuedAt > 30000) return false;
|
||||
+ if (order.ExpiresAt <= order.IssuedAt || order.ExpiresAt > order.ObservedAt + 120000 || now >= order.ExpiresAt) return false;
|
||||
+ return ERR_HQPursuitRules.PositionValid(order.Position);
|
||||
+ }
|
||||
+ static bool Ahead(vector origin, vector velocity, vector point)
|
||||
+ {
|
||||
+ if (!ERR_ForceRules.FiniteVector(origin) || !ERR_ForceRules.FiniteVector(point) || !ERR_HQPursuitRules.Heading(velocity, true)) return false;
|
||||
+ vector delta = point - origin; delta[1] = 0; velocity[1] = 0;
|
||||
+ float distance = delta.Length();
|
||||
+ return distance >= 300 && distance <= 900 && vector.Dot(delta.Normalized(), velocity.Normalized()) >= 0.8;
|
||||
+ }
|
||||
+ static bool FromPolyline(array<vector> points, vector origin, vector velocity, vector seed, array<vector> reserved, out vector result, out float score, out float heading)
|
||||
+ {
|
||||
+ result = vector.Zero; score = 1e20; heading = 0;
|
||||
+ if (!points || points.Count() < 2 || points.Count() > 1024 || !ERR_ForceRules.FiniteVector(seed)) return false;
|
||||
+ foreach (vector point : points) if (!ERR_ForceRules.FiniteVector(point)) return false;
|
||||
+ bool found;
|
||||
+ for (int i = 1; i < points.Count(); i++)
|
||||
+ {
|
||||
+ vector delta = points[i] - points[i - 1]; delta[1] = 0;
|
||||
+ float length = delta.Length();
|
||||
+ if (length < 0.01) continue;
|
||||
+ vector offset = seed - points[i - 1]; offset[1] = 0;
|
||||
+ float fraction = Math.Clamp(vector.Dot(offset, delta) / (length * length), 0, 1);
|
||||
+ vector candidate = vector.Lerp(points[i - 1], points[i], fraction);
|
||||
+ float distance = vector.DistanceXZ(seed, candidate);
|
||||
+ if (distance > 100 || !Ahead(origin, velocity, candidate) || !ERR_HQRoadIntercept.Unreserved(candidate, reserved)) continue;
|
||||
+ if (!found || ERR_HQRoadIntercept.Better(distance, candidate, score, result)) { result = candidate; score = distance; heading = Math.Atan2(delta[0], delta[2]) * Math.RAD2DEG; found = true; }
|
||||
+ }
|
||||
+ return found;
|
||||
+ }
|
||||
+ static bool Select(ERR_HQBelief belief, float now, array<vector> reserved, out vector result)
|
||||
+ {
|
||||
+ result = vector.Zero;
|
||||
+ if (!Replication.IsServer() || !Eligible(belief, now) || !reserved || reserved.Count() > 128) return false;
|
||||
+ if (!GetGame() || !GetGame().GetWorld()) return false;
|
||||
+ vector velocity = belief.Velocity; velocity[1] = 0;
|
||||
+ vector seed = belief.PredictedPosition + velocity.Normalized() * Math.Clamp(velocity.Length() * 60, 300, 900);
|
||||
+ if (!ERR_HQPursuitRules.PositionValid(seed)) return false;
|
||||
+ SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
||||
+ if (!world || !world.GetRoadNetworkManager()) return false;
|
||||
+ array<BaseRoad> roads = {};
|
||||
+ world.GetRoadNetworkManager().GetRoadsInAABB(seed + "-100 -1000 -100", seed + "100 1000 100", roads);
|
||||
+ if (roads.Count() > 128) return false;
|
||||
+ int segments;
|
||||
+ bool found;
|
||||
+ vector best;
|
||||
+ float bestScore = 1e20;
|
||||
+ float bestHeading;
|
||||
+ foreach (BaseRoad road : roads)
|
||||
+ {
|
||||
+ if (!road || !ERR_ForceRules.Finite(road.GetWidth()) || road.GetWidth() < 6) continue;
|
||||
+ array<vector> points = {}; road.GetPoints(points);
|
||||
+ if (points.Count() < 2 || points.Count() > 1024) continue;
|
||||
+ segments += points.Count() - 1;
|
||||
+ if (segments > 4096) return false;
|
||||
+ vector candidate;
|
||||
+ float score, heading;
|
||||
+ if (!FromPolyline(points, belief.PredictedPosition, velocity, seed, reserved, candidate, score, heading)) continue;
|
||||
+ if (!found || ERR_HQRoadIntercept.Better(score, candidate, bestScore, best))
|
||||
+ { best = candidate; bestScore = score; bestHeading = heading; found = true; }
|
||||
+ }
|
||||
+ if (!found) return false;
|
||||
+ vector grounded = ERR_Actors.Ground(best);
|
||||
+ // Never convert an elevated bridge point into terrain beneath the road.
|
||||
+ if (!ERR_HQPursuitRules.PositionValid(grounded) || Math.AbsFloat(grounded[1] - best[1]) > 2) return false;
|
||||
+ ERR_SettlementSafety settlements = new ERR_SettlementSafety();
|
||||
+ if (!settlements.OutsideSettlements(grounded)) return false;
|
||||
+ // Existing conservative vehicle footprint: nine terrain/water samples,
|
||||
+ // filled collision volume and segment-aligned heading; no scenery changes.
|
||||
+ ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
+ if (!placement.Clear(grounded, bestHeading)) return false;
|
||||
+ result = grounded;
|
||||
+ return true;
|
||||
+ }
|
||||
+}
|
||||
--- /dev/null
|
||||
+++ b/tests/test_hq_mounted_intercept.py
|
||||
@@ -0,0 +1,102 @@
|
||||
+"""Actual delivered-memory road selector, geometry and native-query boundaries."""
|
||||
+from pathlib import Path
|
||||
+import os,re
|
||||
+from policy_harness import ROOT,body,run
|
||||
+p=Path(os.environ.get('ERR_MOUNTED_INTERCEPT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'))
|
||||
+source=p.read_text()
|
||||
+base=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'
|
||||
+source='class ERR_HQRoadIntercept\n{\n'+'\n'.join(body(base,x) for x in ('static bool Eligible(', 'static bool Unreserved(', 'static bool Better('))+'\n}\n'+source
|
||||
+for forbidden in ('GetPlayer','GetControlledEntity','GetOrigin','Spawn(', 'AddWaypoint','SetOrigin','CallLater'):
|
||||
+ assert forbidden not in source,forbidden
|
||||
+rules='\n'.join(body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c', x) for x in ('static bool Finite(', 'static bool PositionValid(', 'static bool Actionable(', 'static bool Heading(', 'static ERR_HQPursuitRole Role(', 'static vector Direction(', 'static vector Objective('))
|
||||
+rules=rules.replace('Math.','Math::').replace('vector.','vector::').replace('ERR_HQPursuitRole.','ERR_HQPursuitRole::').replace('vector offset;','vector offset{};')
|
||||
+s=re.sub(r'//[^\n]*','',source).replace('class ERR_HQRoadIntercept','struct ERR_HQRoadIntercept').replace('class ERR_HQMountedInterceptRules','struct ERR_HQMountedInterceptRules').replace('class ERR_ConvoyInterceptOrder','struct ERR_ConvoyInterceptOrder')
|
||||
+s=s.replace('out vector','vector&').replace('out float','float&').replace('array<vector>','Array<vector>').replace('array<BaseRoad>','Array<BaseRoad*>').replace('foreach (','for (')
|
||||
+s=s.replace('SCR_AIWorld.Cast(GetGame().GetAIWorld())','GetGame()->GetAIWorld()')
|
||||
+for name in ('ERR_HQBelief','SCR_AIWorld','BaseRoad','TraceBox','SCR_AIGroup','ERR_ConvoyInterceptOrder','ERR_SettlementSafety','ERR_ConvoyPlacement'):
|
||||
+ s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
|
||||
+for name in ('belief','world','road','body','order','settlements','placement'):
|
||||
+ s=s.replace(name+'.',name+'->')
|
||||
+s=s.replace('GetGame().','GetGame()->').replace('GetRoadNetworkManager().','GetRoadNetworkManager()->').replace('GetWorld().','GetWorld()->')
|
||||
+for name in ('ERR_ForceRules','ERR_HQPursuitRules','ERR_HQPursuitRole','ERR_HQMemoryKind','vector','Math','ERR_Actors','ChimeraWorldUtils','TraceFlags','Replication','ERR_HQRoadIntercept'):
|
||||
+ s=s.replace(name+'.',name+'::')
|
||||
+s=s.replace('null','nullptr').replace('string SourceId','std::string SourceId').replace('.IsEmpty()', '.empty()')
|
||||
+s=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',s).replace('float score, heading;', 'float score=0, heading=0;').replace('\n}\n','\n};\n')
|
||||
+run(r'''
|
||||
+#include <cassert>
|
||||
+#include <algorithm>
|
||||
+#include <cmath>
|
||||
+#include <cstdio>
|
||||
+#include <limits>
|
||||
+#include <vector>
|
||||
+#include <string>
|
||||
+enum class ERR_HQPursuitRole{SEARCH,INTERCEPT,CONTAIN};enum class ERR_HQMemoryKind{SIGHTING,DISTRESS,BODY};
|
||||
+struct SCR_AIGroup{};
|
||||
+struct Math{static constexpr float RAD2DEG=57.2957795131f;static float Atan2(float x,float y){return std::atan2(x,y);}static float Clamp(float x,float a,float b){return std::clamp(x,a,b);}static float AbsFloat(float x){return std::abs(x);}};
|
||||
+struct vector{float x=0,y=0,z=0;vector()=default;vector(float a,float b,float c):x(a),y(b),z(c){}vector(const char*s){sscanf(s,"%f %f %f",&x,&y,&z);}float&operator[](int i){return i==0?x:i==1?y:z;}float operator[](int i)const{return i==0?x:i==1?y:z;}
|
||||
+ vector operator+(vector b)const{return{x+b.x,y+b.y,z+b.z};}vector operator-(vector b)const{return{x-b.x,y-b.y,z-b.z};}vector operator*(float n)const{return{x*n,y*n,z*n};}bool operator==(vector b)const{return x==b.x&&y==b.y&&z==b.z;}
|
||||
+ float Length()const{return std::sqrt(x*x+y*y+z*z);}vector Normalized()const{float n=Length();return{x/n,y/n,z/n};}static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}static vector Lerp(vector a,vector b,float n){return a+(b-a)*n;}static const vector Zero;};
|
||||
+const vector vector::Zero{};vector Vector(float a,float b,float c){return{a,b,c};}
|
||||
+template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}void Insert(T v){this->push_back(v);}explicit operator bool(){return true;}};
|
||||
+struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}};
|
||||
+struct ERR_HQPursuitRules{''' + rules + r'''};
|
||||
+struct ERR_HQBelief{ERR_HQMemoryKind Kind=ERR_HQMemoryKind::SIGHTING;int SampleCount=2,MessageId=1,BeliefId=3,BeliefRevision=2;vector Position={1000,10,1000};float ObservedAt=5000,LastDeliveredAt=7000,Confidence=.9,Radius=40;std::string TravelMode="on foot";vector PredictedPosition={1000,10,1000},Velocity={0,0,4};bool HasVelocity=true;};
|
||||
+struct TraceFlags{static constexpr int WORLD=1,ENTS=2;};struct TraceBox{vector Start,End,Mins,Maxs;int Flags=0;};
|
||||
+struct NativeWorld{int traces=0;float fraction=1;float TraceMove(TraceBox* t,void*){traces++;assert(t->Flags==3&&t->Mins.y==-.9f);return fraction;}};
|
||||
+struct BaseRoad{float width=6;Array<vector>points={{900,10,1300},{1100,10,1300}};float GetWidth(){return width;}void GetPoints(Array<vector>&p){p=points;}};
|
||||
+struct RoadManager{Array<BaseRoad*>roads;int queries=0;void GetRoadsInAABB(vector low,vector high,Array<BaseRoad*>&out){queries++;assert(high.x-low.x==200&&high.z-low.z==200&&high.y-low.y==2000);out=roads;}};
|
||||
+struct SCR_AIWorld{RoadManager manager;bool available=true;RoadManager*GetRoadNetworkManager(){return available?&manager:nullptr;}};
|
||||
+struct Game{NativeWorld world;SCR_AIWorld ai;NativeWorld*GetWorld(){return &world;}SCR_AIWorld*GetAIWorld(){return &ai;}}game;Game*GetGame(){return &game;}
|
||||
+struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
|
||||
+struct ERR_Actors{static inline bool steep=false;static vector Ground(vector p){p.y=10;if(steep&&p.x>1000)p.y=11;return p;}};
|
||||
+struct ChimeraWorldUtils{static inline bool water=false;static bool TryGetWaterSurfaceSimple(NativeWorld*,vector){return water;}};
|
||||
+
|
||||
+struct ERR_SettlementSafety{static inline bool clear=true;static inline int calls=0;bool OutsideSettlements(vector){calls++;return clear;}};
|
||||
+struct ERR_ConvoyPlacement{static inline bool clear=true;static inline int calls=0;static inline float heading=0;bool Clear(vector p,float h){calls++;heading=h;assert(p.y==10);return clear;}};
|
||||
+''' + s + r'''
|
||||
+int main(){
|
||||
+ using Rules=ERR_HQMountedInterceptRules;ERR_HQBelief belief;SCR_AIGroup group;
|
||||
+ assert(Rules::Eligible(&belief,10000));
|
||||
+ ERR_ConvoyInterceptOrder order;order.Token=400001;order.Group=&group;order.SourceId="original-convoy";order.VehicleIndex=0;order.Position={1000,10,1300};order.IssuedAt=8000;order.ExpiresAt=125000;order.ObservedAt=5000;order.BeliefId=3;order.BeliefRevision=2;order.ReportId=1;
|
||||
+ vector frozen=order.Position;assert(Rules::Current(&order,&belief,10000));belief.PredictedPosition={1050,10,1000};assert(Rules::Current(&order,&belief,11000)&&order.Position==frozen);belief.PredictedPosition={1000,10,1000};
|
||||
+ for(int fault=0;fault<21;fault++){auto b=belief;auto o=order;float now=10000;
|
||||
+ if(fault==0)o.Token=400000;
|
||||
+ if(fault==1)o.Token=400257;
|
||||
+ if(fault==2)o.Group=nullptr;
|
||||
+ if(fault==3)o.SourceId="";
|
||||
+ if(fault==4)o.VehicleIndex=1;
|
||||
+ if(fault==5)o.BeliefId++;
|
||||
+ if(fault==6)o.BeliefRevision++;
|
||||
+ if(fault==7)o.ReportId++;
|
||||
+ if(fault==8)o.ObservedAt++;
|
||||
+ if(fault==9)o.IssuedAt=6000;
|
||||
+ if(fault==10)o.IssuedAt=11000;
|
||||
+ if(fault==11)o.ExpiresAt=125001;
|
||||
+ if(fault==12)o.ExpiresAt=10000;
|
||||
+ if(fault==13)o.IssuedAt=NAN;
|
||||
+ if(fault==14)o.Position.x=NAN;
|
||||
+ if(fault==15)b.HasVelocity=false;
|
||||
+ if(fault==16)b.SampleCount=1;
|
||||
+ if(fault==17)b.LastDeliveredAt=10001;
|
||||
+ if(fault==18)b.TravelMode="aircraft";
|
||||
+ if(fault==19)now=38001;
|
||||
+ if(fault==20)b.ObservedAt=-1;
|
||||
+ assert(!Rules::Current(&o,&b,now));}
|
||||
+ order.VehicleIndex=3;assert(Rules::Current(&order,&belief,38000));order.VehicleIndex=5;assert(Rules::Current(&order,&belief,38000));
|
||||
+ assert(!Rules::Eligible(&belief,50001));assert(!Rules::Eligible(nullptr,10000));
|
||||
+ Array<vector> reserved;vector result;BaseRoad road;game.ai.manager.roads={&road};
|
||||
+ assert(Rules::Select(&belief,10000,reserved,result)&&result==vector(1000,10,1300));assert(ERR_ConvoyPlacement::calls==1&&ERR_SettlementSafety::calls==1);assert(std::abs(ERR_ConvoyPlacement::heading-90)<.001);
|
||||
+ road.width=5.99;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero);road.width=6;
|
||||
+ int placementCalls=ERR_ConvoyPlacement::calls;ERR_SettlementSafety::clear=false;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero&&ERR_ConvoyPlacement::calls==placementCalls);ERR_SettlementSafety::clear=true;
|
||||
+ ERR_ConvoyPlacement::clear=false;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero);ERR_ConvoyPlacement::clear=true;
|
||||
+ road.points={{900,20,1300},{1100,20,1300}};placementCalls=ERR_ConvoyPlacement::calls;assert(!Rules::Select(&belief,10000,reserved,result)&&ERR_ConvoyPlacement::calls==placementCalls);road.points={{900,10,1300},{1100,10,1300}};
|
||||
+ reserved.Insert({1000,10,1300});assert(!Rules::Select(&belief,10000,reserved,result));reserved.clear();
|
||||
+ game.ai.manager.roads.resize(129,&road);assert(!Rules::Select(&belief,10000,reserved,result));game.ai.manager.roads={&road};
|
||||
+ auto unknown=belief;unknown.HasVelocity=false;int queries=game.ai.manager.queries;assert(!Rules::Select(&unknown,10000,reserved,result)&&game.ai.manager.queries==queries);
|
||||
+ Replication::server=false;assert(!Rules::Select(&belief,10000,reserved,result)&&game.ai.manager.queries==queries);Replication::server=true;
|
||||
+ float score,heading;assert(Rules::FromPolyline({{900,10,1300},{1100,10,1300}},belief.Position,belief.Velocity,{1000,10,1350},reserved,result,score,heading)&&result.z==1300);
|
||||
+ assert(!Rules::Ahead(belief.Position,belief.Velocity,{1000,10,1299}));assert(!Rules::Ahead(belief.Position,belief.Velocity,{1000,10,1901}));
|
||||
+ Array<vector> oversized(1025,{1000,10,1300});assert(!Rules::FromPolyline(oversized,belief.Position,belief.Velocity,{1000,10,1350},reserved,result,score,heading));
|
||||
+}
|
||||
+''')
|
||||
+print('PASS: actual mounted intercept eligibility/order chronology, fixed destinations, native road selection bounds, width, segment heading, clearance and settlement delegation; no native driving claim')
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual mounted intercept eligibility/order chronology, fixed destinations, native road selection bounds, width, segment heading, clearance and settlement delegation; no native driving claim
|
||||
@@ -0,0 +1,51 @@
|
||||
from pathlib import Path
|
||||
root=Path('/home/jakub/devel/escapistsrereintroduced'); out=Path('/tmp/erri-mounted-intercept-rules-1247/tests/test_hq_mounted_intercept.py')
|
||||
old=(root/'tests/test_hq_road_intercept.py').read_text(); prefix=old[:old.index("run(r'''")]; cpp=old[old.index("run(r'''")+8:old.index("''' + s + r'''")]
|
||||
prefix=prefix.replace('ERR_ROAD_INTERCEPT_SOURCE','ERR_MOUNTED_INTERCEPT_SOURCE').replace("'ERR_HQRoadIntercept.c'","'ERR_HQMountedIntercept.c'",1)
|
||||
prefix=prefix.replace("source=p.read_text()","source=p.read_text()\nbase=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'\nsource='class ERR_HQRoadIntercept\\n{\\n'+'\\n'.join(body(base,x) for x in ('static bool Eligible(', 'static bool Unreserved(', 'static bool Better('))+'\\n}\\n'+source")
|
||||
prefix=prefix.replace(".replace('class ERR_HQRoadIntercept','struct ERR_HQRoadIntercept')", ".replace('class ERR_HQRoadIntercept','struct ERR_HQRoadIntercept').replace('class ERR_HQMountedInterceptRules','struct ERR_HQMountedInterceptRules').replace('class ERR_ConvoyInterceptOrder','struct ERR_ConvoyInterceptOrder')")
|
||||
prefix=prefix.replace("('ERR_HQBelief','SCR_AIWorld','BaseRoad','TraceBox')", "('ERR_HQBelief','SCR_AIWorld','BaseRoad','TraceBox','SCR_AIGroup','ERR_ConvoyInterceptOrder','ERR_SettlementSafety','ERR_ConvoyPlacement')")
|
||||
prefix=prefix.replace("('belief','world','road','body')", "('belief','world','road','body','order','settlements','placement')")
|
||||
prefix=prefix.replace("'Replication')", "'Replication','ERR_HQRoadIntercept')")
|
||||
prefix=prefix.replace("s=s.replace('null','nullptr')", "s=s.replace('null','nullptr').replace('string SourceId','std::string SourceId').replace('.IsEmpty()', '.empty()')")
|
||||
prefix=prefix.replace("r'\\1 \\2{};',s)", "r'\\1 \\2{};',s).replace('float score, heading;', 'float score=0, heading=0;')")
|
||||
cpp=cpp.replace('struct Math{','struct SCR_AIGroup{};\nstruct Math{static constexpr float RAD2DEG=57.2957795131f;static float Atan2(float x,float y){return std::atan2(x,y);}')
|
||||
cpp=cpp.replace('int SampleCount=2,MessageId=1;', 'int SampleCount=2,MessageId=1,BeliefId=3,BeliefRevision=2;vector Position={1000,10,1000};')
|
||||
cpp=cpp.replace('float width=4;', 'float width=6;').replace('{900,10,1060},{1100,10,1060}', '{900,10,1300},{1100,10,1300}')
|
||||
cpp+='''
|
||||
struct ERR_SettlementSafety{static inline bool clear=true;static inline int calls=0;bool OutsideSettlements(vector){calls++;return clear;}};
|
||||
struct ERR_ConvoyPlacement{static inline bool clear=true;static inline int calls=0;static inline float heading=0;bool Clear(vector p,float h){calls++;heading=h;assert(p.y==10);return clear;}};
|
||||
'''
|
||||
main=r'''
|
||||
int main(){
|
||||
using Rules=ERR_HQMountedInterceptRules;ERR_HQBelief belief;SCR_AIGroup group;
|
||||
assert(Rules::Eligible(&belief,10000));
|
||||
ERR_ConvoyInterceptOrder order;order.Token=400001;order.Group=&group;order.SourceId="original-convoy";order.VehicleIndex=0;order.Position={1000,10,1300};order.IssuedAt=8000;order.ExpiresAt=125000;order.ObservedAt=5000;order.BeliefId=3;order.BeliefRevision=2;order.ReportId=1;
|
||||
vector frozen=order.Position;assert(Rules::Current(&order,&belief,10000));belief.PredictedPosition={1050,10,1000};assert(Rules::Current(&order,&belief,11000)&&order.Position==frozen);belief.PredictedPosition={1000,10,1000};
|
||||
for(int fault=0;fault<21;fault++){auto b=belief;auto o=order;float now=10000;
|
||||
if(fault==0)o.Token=400000;if(fault==1)o.Token=400257;if(fault==2)o.Group=nullptr;if(fault==3)o.SourceId="";
|
||||
if(fault==4)o.VehicleIndex=1;if(fault==5)o.BeliefId++;if(fault==6)o.BeliefRevision++;if(fault==7)o.ReportId++;if(fault==8)o.ObservedAt++;
|
||||
if(fault==9)o.IssuedAt=6000;if(fault==10)o.IssuedAt=11000;if(fault==11)o.ExpiresAt=125001;if(fault==12)o.ExpiresAt=10000;
|
||||
if(fault==13)o.IssuedAt=NAN;if(fault==14)o.Position.x=NAN;if(fault==15)b.HasVelocity=false;if(fault==16)b.SampleCount=1;
|
||||
if(fault==17)b.LastDeliveredAt=10001;if(fault==18)b.TravelMode="aircraft";if(fault==19)now=38001;if(fault==20)b.ObservedAt=-1;
|
||||
assert(!Rules::Current(&o,&b,now));}
|
||||
order.VehicleIndex=3;assert(Rules::Current(&order,&belief,38000));order.VehicleIndex=5;assert(Rules::Current(&order,&belief,38000));
|
||||
assert(!Rules::Eligible(&belief,50001));assert(!Rules::Eligible(nullptr,10000));
|
||||
Array<vector> reserved;vector result;BaseRoad road;game.ai.manager.roads={&road};
|
||||
assert(Rules::Select(&belief,10000,reserved,result)&&result==vector(1000,10,1300));assert(ERR_ConvoyPlacement::calls==1&&ERR_SettlementSafety::calls==1);assert(std::abs(ERR_ConvoyPlacement::heading-90)<.001);
|
||||
road.width=5.99;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero);road.width=6;
|
||||
int placementCalls=ERR_ConvoyPlacement::calls;ERR_SettlementSafety::clear=false;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero&&ERR_ConvoyPlacement::calls==placementCalls);ERR_SettlementSafety::clear=true;
|
||||
ERR_ConvoyPlacement::clear=false;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero);ERR_ConvoyPlacement::clear=true;
|
||||
road.points={{900,20,1300},{1100,20,1300}};placementCalls=ERR_ConvoyPlacement::calls;assert(!Rules::Select(&belief,10000,reserved,result)&&ERR_ConvoyPlacement::calls==placementCalls);road.points={{900,10,1300},{1100,10,1300}};
|
||||
reserved.Insert({1000,10,1300});assert(!Rules::Select(&belief,10000,reserved,result));reserved.clear();
|
||||
game.ai.manager.roads.resize(129,&road);assert(!Rules::Select(&belief,10000,reserved,result));game.ai.manager.roads={&road};
|
||||
auto unknown=belief;unknown.HasVelocity=false;int queries=game.ai.manager.queries;assert(!Rules::Select(&unknown,10000,reserved,result)&&game.ai.manager.queries==queries);
|
||||
Replication::server=false;assert(!Rules::Select(&belief,10000,reserved,result)&&game.ai.manager.queries==queries);Replication::server=true;
|
||||
float score,heading;assert(Rules::FromPolyline({{900,10,1300},{1100,10,1300}},belief.Position,belief.Velocity,{1000,10,1350},reserved,result,score,heading)&&result.z==1300);
|
||||
assert(!Rules::Ahead(belief.Position,belief.Velocity,{1000,10,1299}));assert(!Rules::Ahead(belief.Position,belief.Velocity,{1000,10,1901}));
|
||||
Array<vector> oversized(1025,{1000,10,1300});assert(!Rules::FromPolyline(oversized,belief.Position,belief.Velocity,{1000,10,1350},reserved,result,score,heading));
|
||||
}
|
||||
'''
|
||||
# Independent sequential condition bodies should not trigger C++ misleading-indentation diagnostics.
|
||||
main=main.replace(';if(', ';\n if(')
|
||||
out.write_text(prefix+"run(r'''"+cpp+"''' + s + r'''"+main+"''')\nprint('PASS: actual mounted intercept eligibility/order chronology, fixed destinations, native road selection bounds, width, segment heading, clearance and settlement delegation; no native driving claim')\n")
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual filled rotated-footprint TraceBox and nine terrain/water samples; broad tree/cable bounds alone accepted, actual collision/nonfinite trace refused, nonzero sweep and ENTS/OCEAN flags, bounded diagnostic query and reset
|
||||
@@ -0,0 +1,6 @@
|
||||
PASS: production convoy spacing, physical arrival, stable follower targets, supply ownership, leader promotion and bounded stuck recovery
|
||||
PASS: convoy combat keeps native manoeuvres for unchanged contacts and refreshes when contact moves
|
||||
PASS: native convoy proof excludes pre-departure movement and ends via explicit fixture reset, not prohibited player death
|
||||
PASS: boarding retries only after approach/seat progress stops, capped at three, with delayed native reissue and completed-team preservation
|
||||
PASS: slow only owned vehicles near fit dismounted infantry; ignore passengers, casualties and released teams/vehicles
|
||||
PASS: restore missing native driving registration only for healthy owned seated crews; preserve existing association and refuse invalid/released vehicles
|
||||
@@ -0,0 +1,248 @@
|
||||
"""Run the production column controller against physical-position/native-order doubles."""
|
||||
from policy_harness import ROOT, body, run
|
||||
import re,os
|
||||
from pathlib import Path
|
||||
p=Path(os.environ.get('ERR_SUPPLY_COLUMN_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c'))
|
||||
|
||||
def cpp(s):
|
||||
s=re.sub(r'//[^\n]*', '', s).replace('protected ', '').replace('out ', '')
|
||||
s=s.replace('foreach (', 'for (').replace('null','nullptr')
|
||||
s=s.replace('array<int>', 'Array<int>').replace('m_State[continuing].', 'm_State[continuing]->').replace('m_State[detached].', 'm_State[detached]->')
|
||||
for typ in ('ERR_ConvoyVehicle','ERR_ConvoyColumnVehicle'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
for obj in ('m_Task','vehicle','state','scan','promoted','ahead','m_State[i]','currentLeader','m_Task->Vehicles[previous]'):
|
||||
s=s.replace(obj+'.',obj+'->')
|
||||
for obj in ('Entity','Group','Waypoint','Route','Receipt','Combat'):
|
||||
s=s.replace(obj+'.',obj+'->')
|
||||
for cls in ('ERR_ConvoyColumnRules','ERR_ConvoyDriving','ERR_SupplyConvoyTactics','ERR_HQ','Math','vector','string'):
|
||||
s=s.replace(cls+'.',cls+'::')
|
||||
s=s.replace('bool rejoining;', 'bool rejoining=false;').replace('m_State[returning].','m_State[returning]->').replace('m_Task->Vehicles[returning].','m_Task->Vehicles[returning]->')
|
||||
s=s.replace('m_State[index].', 'm_State[index]->').replace('m_State[leader].', 'm_State[leader]->')
|
||||
s=s.replace('m_Task->Vehicles[index].', 'm_Task->Vehicles[index]->').replace('(index + 1).ToString()', 'std::to_string(index + 1)')
|
||||
s=s.replace('float largestGap; bool supplyHold; bool interceptLive;', 'float largestGap=0; bool supplyHold=false; bool interceptLive=false;')
|
||||
return s
|
||||
rules='\n'.join(cpp(body(p, x)) for x in ('static bool WaitForRear(', 'static bool TooClose(', 'static float Speed('))
|
||||
methods='\n'.join(cpp(body(p,x)) for x in ('protected float DistanceToTarget(', 'protected void Reorder(', 'void Reset(', 'protected void Hold(', 'protected bool Progress(', 'bool Tick('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}template<class...T>static string Format(T...){return "event";}};
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
namespace Math{float Max(float a,float b){return a>b?a:b;}int Min(int a,int b){return a<b?a:b;}float Round(float x){return std::round(x);}}
|
||||
namespace ERR_HQ{int stranded=0,retries=0;void Event(string type,string){if(type=="CONVOY STRANDED")stranded++;if(type=="CONVOY ROUTE RETRY")retries++;}}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T value){this->push_back(value);}};
|
||||
struct ERR_ConvoyColumnRules{'''+rules+r'''};
|
||||
struct Entity{vector position;vector GetOrigin(){return position;}};
|
||||
struct Waypoint:Entity{};
|
||||
struct Group{int added=0;void AddWaypoint(Waypoint*){added++;}};
|
||||
struct Route{Array<vector>points={{0},{400}};int Count(){return points.Count();}vector At(int i){return points[i];}};
|
||||
struct Combat{bool Held=false;};
|
||||
struct ERR_ConvoyVehicle{
|
||||
int Index=0,RouteIndex=0,clears=0;bool InterceptActive=false,Released=false,Arrived=false,SupplyHeld=false;
|
||||
struct Entity entity;struct Entity* Entity=&entity;struct Group group;struct Group* Group=&group;
|
||||
struct Combat combat;struct Combat* Combat=&combat;struct Waypoint* Waypoint=nullptr;
|
||||
void ClearOrder(){if(Waypoint)clears++;delete Waypoint;Waypoint=nullptr;}
|
||||
void Release(void*){Released=true;ClearOrder();}
|
||||
bool Order(struct Route* route,float){if(!Waypoint){Waypoint=new struct Waypoint();Waypoint->position=route->At(RouteIndex);Group->AddWaypoint(Waypoint);}return true;}
|
||||
~ERR_ConvoyVehicle(){ClearOrder();}
|
||||
};
|
||||
namespace ERR_ConvoyDriving{bool Restore(void*,ERR_ConvoyVehicle*){return false;}}
|
||||
namespace ERR_SupplyConvoyTactics{Waypoint* Move(vector p,float){auto w=new Waypoint();w->position=p;return w;}}
|
||||
struct Receipt{string SourceId="test";};
|
||||
struct ERR_SupplyConvoyTask{Array<ERR_ConvoyVehicle*>Vehicles;struct Route route;struct Route* Route=&route;int Legs=0;struct Receipt receipt;struct Receipt* Receipt=&receipt;};
|
||||
struct ERR_ConvoyColumnVehicle{bool InterceptDetached=false,Holding=false;vector Progress;float LastProgress=-1,NextRetry=0,NextTarget=0,Speed=-1;int Ahead=-1;};
|
||||
struct Column{
|
||||
void* m_Mode=nullptr;ERR_SupplyConvoyTask* m_Task;Array<ERR_ConvoyColumnVehicle*>m_State;
|
||||
Array<int>m_Order={0,1,2};int m_Leader=-1,m_RouteIndex=0;bool m_RearWait=false;float m_NextLog=0;
|
||||
void Speed(ERR_ConvoyVehicle*,ERR_ConvoyColumnVehicle* state,float speed){state->Speed=speed;}
|
||||
'''+methods+r'''};
|
||||
int main(){
|
||||
{ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=true;c.entity.position={80};
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
column.m_RouteIndex=1;a.RouteIndex=0;a.entity.position={-20};b.RouteIndex=1;b.entity.position={100};assert(column.Tick(0));assert(column.m_Leader==1&&b.RouteIndex==1);
|
||||
b.Released=true;a.InterceptActive=false;assert(column.Tick(1000));assert(column.m_Leader==2&&c.RouteIndex==1&&a.RouteIndex==0&&column.m_RouteIndex==1);}
|
||||
|
||||
{ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=true;b.Released=c.Released=true;
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
a.RouteIndex=0;a.entity.position={-200};assert(column.Tick(0));assert(sa.InterceptDetached&&a.RouteIndex==0&&!a.Waypoint);
|
||||
column.m_RouteIndex=1;a.InterceptActive=false;assert(column.Tick(1000));assert(a.RouteIndex==0&&a.Waypoint&&column.m_RouteIndex==0);}
|
||||
|
||||
assert(!ERR_ConvoyColumnRules::WaitForRear(160,false));assert(ERR_ConvoyColumnRules::WaitForRear(161,false));
|
||||
assert(ERR_ConvoyColumnRules::WaitForRear(91,true));assert(!ERR_ConvoyColumnRules::WaitForRear(90,true));
|
||||
assert(ERR_ConvoyColumnRules::TooClose(39,false));assert(!ERR_ConvoyColumnRules::TooClose(40,false));
|
||||
assert(ERR_ConvoyColumnRules::TooClose(64,true));assert(!ERR_ConvoyColumnRules::TooClose(65,true));
|
||||
ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;b.entity.position={-35};c.entity.position={-70};
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
assert(column.Tick(0));assert(a.RouteIndex==1&&a.Waypoint&&a.Waypoint->position.x==400);assert(!b.Waypoint&&!c.Waypoint);
|
||||
a.entity.position={80};column.Tick(1000);assert(b.Waypoint&&b.Waypoint->position.x==80&&sb.Speed==45);assert(!c.Waypoint);
|
||||
int additions=b.group.added;a.entity.position={110};column.Tick(2000);assert(b.group.added==additions); // stable target, no per-second churn
|
||||
a.entity.position={200};column.Tick(3000);assert(column.m_RearWait&&!a.Waypoint&&b.Waypoint); // leader waits for laggard
|
||||
b.entity.position={130};c.entity.position={60};column.Tick(11000);assert(!column.m_RearWait&&a.Waypoint);
|
||||
assert(b.Waypoint->position.x==200); // native target follows predecessor, never remote base marker
|
||||
b.SupplyHeld=true;auto supplyOrder=b.Waypoint;column.Tick(12000);assert(!a.Waypoint&&b.Waypoint==supplyOrder&&sb.LastProgress==-1);
|
||||
b.SupplyHeld=false;b.combat.Held=true;column.Tick(13000);assert(b.Waypoint==supplyOrder&&sb.LastProgress==-1);b.combat.Held=false;
|
||||
// Real vehicle positions determine arrival; no stale seated-group origin is consulted.
|
||||
a.entity.position={400};b.entity.position={330};c.entity.position={260};column.Tick(20000);
|
||||
assert(a.Arrived&&b.Arrived&&c.Arrived&&!a.Waypoint&&!b.Waypoint&&!c.Waypoint);
|
||||
// Loss promotes the next surviving vehicle at the current route cursor, no replacement/restart at spawn.
|
||||
column.Reset(20500);assert(column.m_Leader==0&&column.m_RouteIndex==2); // combat reset preserves route progress for a later promotion
|
||||
a.Release(nullptr);b.Arrived=c.Arrived=false;column.Tick(21000);assert(column.m_Leader==1&&b.RouteIndex==1&&!b.Arrived&&b.Waypoint->position.x==400);
|
||||
// Commanded stationary time alone retries and eventually releases; holds reset this clock.
|
||||
ERR_ConvoyVehicle stuck;ERR_ConvoyColumnVehicle ss;
|
||||
assert(column.Progress(&stuck,&ss,0));assert(column.Progress(&stuck,&ss,45000));assert(ERR_HQ::retries==1&&!stuck.Released);
|
||||
column.Hold(&stuck,&ss,170000);assert(column.Progress(&stuck,&ss,180000));assert(!stuck.Released);
|
||||
stuck.entity.position={6};assert(column.Progress(&stuck,&ss,300000));assert(ss.LastProgress==300000);
|
||||
assert(column.Progress(&stuck,&ss,479999));assert(!column.Progress(&stuck,&ss,480000)&&stuck.Released&&ERR_HQ::stranded==1);
|
||||
column.Reset(500000,true);assert(sa.LastProgress==-1&&sb.LastProgress==-1&&!column.m_RearWait&&column.m_Leader==-1);
|
||||
// Post-combat physical order avoids sending the front truck toward a rear vehicle coming toward it.
|
||||
b.entity.position={100};c.entity.position={300};b.Arrived=c.Arrived=false;column.m_RouteIndex=1;column.Reset(500001,false,true);
|
||||
assert(column.m_Order[0]==2&&column.m_Order[1]==1&&column.m_Order[2]==0); // released vehicle last
|
||||
assert(task.Vehicles[1]==&b&&task.Vehicles[2]==&c); // combat roles and canonical identities unchanged
|
||||
column.Tick(500002);assert(column.m_Leader==2&&!c.Waypoint&&b.Waypoint&&b.Waypoint->position.x==300);
|
||||
b.entity.position={230};column.Tick(500003);assert(c.Waypoint&&c.Waypoint->position.x==400);
|
||||
b.Release(nullptr);c.Release(nullptr);assert(!column.Tick(510000));
|
||||
}
|
||||
''')
|
||||
print('PASS: production convoy spacing, physical arrival, stable follower targets, supply ownership, leader promotion and bounded stuck recovery')
|
||||
|
||||
q=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c'
|
||||
orders=body(q,'protected void Orders(').replace('protected ','').replace('m_Task.','m_Task->').replace('vector.','vector::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
struct Items{int count;int Count(){return count;}};
|
||||
struct Task{Items Vehicles{6},Teams{2};};
|
||||
struct Controller{Task task;Task* m_Task=&task;float m_NextOrders=0;vector m_LastOrderContact,m_Contact;bool m_OrdersIssued=false,m_TruckWithdrawn=false;int support=0,flank=0;
|
||||
void Support(int,bool){support++;}void Flank(int,bool){flank++;}
|
||||
'''+orders+r'''};
|
||||
int main(){
|
||||
Controller c;c.Orders(true,0);assert(c.support==6&&c.flank==2);
|
||||
c.Orders(false,30000);assert(c.support==6&&c.flank==2&&c.m_NextOrders==60000);
|
||||
c.m_Contact={39};c.Orders(false,60000);assert(c.support==6&&c.flank==2);
|
||||
c.m_Contact={40};c.Orders(false,90000);assert(c.support==12&&c.flank==4);
|
||||
c.m_OrdersIssued=false;c.Orders(true,100000);assert(c.support==18&&c.flank==6); // new engagement can issue at same location
|
||||
}
|
||||
''')
|
||||
print('PASS: convoy combat keeps native manoeuvres for unchanged contacts and refreshes when contact moves')
|
||||
|
||||
probe=ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSupplyConvoyProbe.c'
|
||||
departure=body(probe,'protected int Departure(').replace('protected ','').replace('m_CombatTask.','m_CombatTask->').replace('Vehicles[2].','Vehicles[2]->').replace('Entity.','Entity->').replace('vector.','vector::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
void Print(const char*){}
|
||||
struct Entity{vector position;vector GetOrigin(){return position;}};
|
||||
struct Vehicle{bool Released=false;struct Entity entity;struct Entity* Entity=&entity;};
|
||||
struct Bases{int Count(){return 2;}};
|
||||
struct Task{bool Stopped=false;int Legs=0;Vehicle* Vehicles[3];struct Bases Bases;};
|
||||
struct Probe{Task* m_CombatTask;int m_Phase=3,m_TestLegs=0;vector m_RouteResumePosition;bool m_AllMoved=true;
|
||||
'''+departure+r'''};
|
||||
int main(){
|
||||
Task task;Vehicle truck;task.Vehicles[2]=&truck;Probe p{&task};truck.entity.position={200};
|
||||
assert(p.Departure()==0&&p.m_Phase==3); // earlier regroup displacement cannot pass
|
||||
task.Legs=1;assert(p.Departure()==0&&p.m_Phase==4&&p.m_RouteResumePosition.x==200);
|
||||
assert(p.Departure()==0);truck.entity.position={219};assert(p.Departure()==0);
|
||||
truck.entity.position={220};assert(p.Departure()==1&&p.m_Phase==5);
|
||||
truck.Released=true;assert(p.Departure()==-1);
|
||||
}
|
||||
''')
|
||||
smoke=(ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c').read_text()
|
||||
convoy=smoke.split('else if (m_ERRSmokeStep == 106)')[1].split('else if (m_ERRSmokeStep == 105)')[0]
|
||||
assert 'Transition(ERR_Phase.DEFEAT)' in convoy and 'm_ERRSmokeStep = 1061' in convoy
|
||||
assert 'damage.Kill' not in convoy and 'm_ERRSmokeStep = 9' not in convoy
|
||||
print('PASS: native convoy proof excludes pre-departure movement and ends via explicit fixture reset, not prohibited player death')
|
||||
|
||||
boarding=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBoardingProgress.c'
|
||||
retry=body(boarding,'bool Retry(')
|
||||
refresh=body(q,'protected void RefreshBoarding(').replace('protected ','').replace('foreach (','for (').replace('m_Task.','m_Task->').replace('team.','team->').replace('Vehicles[2].','Vehicles[2]->').replace('BoardingProgress.','BoardingProgress->').replace('Receipt.','Receipt->').replace('ERR_HQ.','ERR_HQ::').replace('Math.','Math::').replace('string.','string::').replace('ERR_SupplyTeam team','ERR_SupplyTeam* team')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
struct string{template<class...T>static string Format(T...){return {};}};
|
||||
namespace Math{float Round(float x){return std::round(x);}}
|
||||
namespace ERR_HQ{void Event(const char*,string){}}
|
||||
struct ERR_ConvoyBoardingProgress{int Seated=-1,Retries=0;float Distance=-1,LastProgress=0,NextRetry=0;
|
||||
'''+retry+r'''};
|
||||
struct ERR_SupplyTeam{bool Released=false,aboard=false;float BoardingRetryAt=0,distance=200;int seated=0,clears=0,adds=0;ERR_ConvoyBoardingProgress* BoardingProgress=nullptr;
|
||||
bool Aboard(void*,bool){return aboard;}int BoardingCount(void*,float& d){d=distance;return seated;}void Clear(){clears++;}void Add(int){adds++;}};
|
||||
struct Vehicle{void* Entity=nullptr;};struct Receipt{const char* SourceId="test";};
|
||||
struct Task{std::vector<ERR_SupplyTeam*>Teams;Vehicle* Vehicles[3];struct Receipt receipt;struct Receipt* Receipt=&receipt;};
|
||||
struct Tactics{Task* m_Task;int Board(void*,bool){return 1;}
|
||||
'''+refresh+r'''};
|
||||
int main(){
|
||||
ERR_ConvoyBoardingProgress p;assert(!p.Retry(0,200,0));
|
||||
assert(!p.Retry(0,194,29000));assert(!p.Retry(0,194,58000));assert(p.Retry(0,194,59000));
|
||||
assert(!p.Retry(1,100,60000));assert(!p.Retry(1,100,89999));assert(p.Retry(1,100,90000));
|
||||
assert(p.Retry(1,100,120000));assert(!p.Retry(1,100,150000)&&p.Retries==3); // bounded even if no progress
|
||||
Task task;Vehicle truck;task.Vehicles[2]=&truck;ERR_SupplyTeam team,finished,released;finished.aboard=true;released.Released=true;
|
||||
task.Teams={&team,&finished,&released};Tactics t{&task};t.RefreshBoarding(0);assert(team.BoardingProgress&&!finished.BoardingProgress&&!released.BoardingProgress);
|
||||
t.RefreshBoarding(30000);assert(team.clears==1&&team.adds==0&&team.BoardingRetryAt==31000);
|
||||
t.RefreshBoarding(30999);assert(team.adds==0);t.RefreshBoarding(31000);assert(team.adds==1&&team.BoardingRetryAt==0);
|
||||
team.seated=6;team.distance=10;t.RefreshBoarding(40000);assert(team.clears==1);t.RefreshBoarding(69999);assert(team.clears==1);
|
||||
t.RefreshBoarding(70000);assert(team.clears==2);team.aboard=true;t.RefreshBoarding(71000);assert(team.adds==1); // do not disturb completed boarding
|
||||
delete team.BoardingProgress;
|
||||
}
|
||||
''')
|
||||
print('PASS: boarding retries only after approach/seat progress stops, capped at three, with delayed native reissue and completed-team preservation')
|
||||
|
||||
safe='\n'.join(body(q,s) for s in ('protected float InfantryDistance(', 'protected void ProtectInfantry('))
|
||||
safe=safe.replace('protected ','').replace('foreach (','for (')
|
||||
safe=safe.replace('AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));', 'AICarMovementComponent* movement = &vehicle->Entity->movement;')
|
||||
for typ in ('IEntity','ERR_SupplyTeam','ERR_ConvoyVehicle'):safe=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',safe)
|
||||
for obj in ('m_Task','team','member','vehicle','movement'):safe=safe.replace(obj+'.',obj+'->')
|
||||
for cls in ('ERR_Actors','CompartmentAccessComponent','Math','vector'):safe=safe.replace(cls+'.',cls+'::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
namespace Math{float Min(float a,float b){return a<b?a:b;}}
|
||||
struct AICarMovementComponent{float speed=99;void SetCruiseSpeed(float s){speed=s;}};
|
||||
struct IEntity{vector position;bool fit=true,aboard=false;AICarMovementComponent movement;vector GetOrigin(){return position;}};
|
||||
namespace ERR_Actors{bool CanFight(IEntity* m){return m&&m->fit;}}
|
||||
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* m){return m->aboard?m:nullptr;}}
|
||||
struct ERR_SupplyTeam{bool Released=false;std::vector<IEntity*>Members;};
|
||||
struct ERR_ConvoyVehicle{bool Released=false;IEntity* Entity;};
|
||||
struct Task{std::vector<ERR_SupplyTeam*>Teams;std::vector<ERR_ConvoyVehicle*>Vehicles;};
|
||||
struct Tactics{Task* m_Task;
|
||||
'''+safe+r'''};
|
||||
int main(){
|
||||
IEntity truck{{0}},car{{100}},foot{{30}},passenger{{0}};passenger.aboard=true;ERR_SupplyTeam team{false,{&foot,&passenger}};
|
||||
ERR_ConvoyVehicle a{false,&truck},b{false,&car};Task task{{&team},{&a,&b}};Tactics t{&task};
|
||||
assert(t.InfantryDistance(&truck)==30);t.ProtectInfantry();assert(truck.movement.speed==8&&car.movement.speed==35);
|
||||
foot.aboard=true;t.ProtectInfantry();assert(truck.movement.speed==35&&car.movement.speed==35);
|
||||
foot.aboard=false;foot.fit=false;assert(t.InfantryDistance(&truck)>100000);foot.fit=true;team.Released=true;assert(t.InfantryDistance(&truck)>100000);
|
||||
team.Released=false;a.Released=true;truck.movement.speed=99;t.ProtectInfantry();assert(truck.movement.speed==99); // never retake a released vehicle
|
||||
}
|
||||
''')
|
||||
print('PASS: slow only owned vehicles near fit dismounted infantry; ignore passengers, casualties and released teams/vehicles')
|
||||
|
||||
restore=body(p,'static bool Restore(').replace('ERR_GameMode mode','void* mode').replace('ERR_ConvoyVehicle vehicle','ERR_ConvoyVehicle* vehicle')
|
||||
restore=restore.replace('SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(vehicle.Entity.FindComponent(SCR_AIVehicleUsageComponent));','SCR_AIVehicleUsageComponent* usage = vehicle->Entity->usage;')
|
||||
restore=restore.replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
|
||||
for obj in ('vehicle','Group','utility','usage'):restore=restore.replace(obj+'.',obj+'->')
|
||||
restore=restore.replace('ERR_ConvoyMountedCrew.','ERR_ConvoyMountedCrew::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
struct SCR_AIVehicleUsageComponent{bool pilot=true;bool CanBePiloted(){return pilot;}};
|
||||
struct Entity{SCR_AIVehicleUsageComponent* usage=nullptr;};
|
||||
struct SCR_AIGroupUtilityComponent{void* m_VehicleMgr=this;bool known=false;int added=0;bool IsUsableVehicle(Entity*){return known;}void AddUsableVehicle(SCR_AIVehicleUsageComponent*){known=true;added++;}};
|
||||
struct Group{SCR_AIGroupUtilityComponent* utility=nullptr;SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return utility;}};
|
||||
struct ERR_ConvoyVehicle{bool Released=false,valid=true;struct Group group;struct Group* Group=&group;struct Entity entity;struct Entity* Entity=&entity;void* Driver=this;bool CrewValid(void*){return valid;}};
|
||||
namespace ERR_ConvoyMountedCrew{bool healthyOwned=true;bool Retain(void*,Entity*){return healthyOwned;}}
|
||||
'''+restore+r'''
|
||||
int main(){
|
||||
int mode;ERR_ConvoyVehicle car;SCR_AIGroupUtilityComponent utility;SCR_AIVehicleUsageComponent usage;car.group.utility=&utility;car.entity.usage=&usage;
|
||||
assert(!Restore(nullptr,&car)&&!Restore(&mode,nullptr));car.Released=true;assert(!Restore(&mode,&car));car.Released=false;
|
||||
car.valid=false;assert(!Restore(&mode,&car));car.valid=true;ERR_ConvoyMountedCrew::healthyOwned=false;assert(!Restore(&mode,&car));ERR_ConvoyMountedCrew::healthyOwned=true;
|
||||
car.group.utility=nullptr;assert(!Restore(&mode,&car));car.group.utility=&utility;utility.m_VehicleMgr=nullptr;assert(!Restore(&mode,&car));utility.m_VehicleMgr=&utility;
|
||||
car.entity.usage=nullptr;assert(!Restore(&mode,&car));car.entity.usage=&usage;usage.pilot=false;assert(!Restore(&mode,&car));usage.pilot=true;
|
||||
assert(Restore(&mode,&car)&&utility.known&&utility.added==1);assert(!Restore(&mode,&car)&&utility.added==1);
|
||||
}
|
||||
''')
|
||||
print('PASS: restore missing native driving registration only for healthy owned seated crews; preserve existing association and refuse invalid/released vehicles')
|
||||
@@ -0,0 +1,19 @@
|
||||
check: /tmp/err-policy-ud279wy4/check.cpp:231: int main(): Assertion `column.m_Leader==2&&c.RouteIndex==1&&a.RouteIndex==0&&column.m_RouteIndex==1' failed.
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-mounted-intercept-rules-1247/rejoin-review-red.py", line 25, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<78 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
|
||||
subprocess.run([str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['/tmp/err-policy-ud279wy4/check']' died with <Signals.SIGABRT: 6>.
|
||||
@@ -0,0 +1,247 @@
|
||||
"""Run the production column controller against physical-position/native-order doubles."""
|
||||
from policy_harness import ROOT, body, run
|
||||
import re,os
|
||||
from pathlib import Path
|
||||
p=Path(os.environ.get('ERR_SUPPLY_COLUMN_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c'))
|
||||
|
||||
def cpp(s):
|
||||
s=re.sub(r'//[^\n]*', '', s).replace('protected ', '').replace('out ', '')
|
||||
s=s.replace('foreach (', 'for (').replace('null','nullptr')
|
||||
for typ in ('ERR_ConvoyVehicle','ERR_ConvoyColumnVehicle'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
for obj in ('m_Task','vehicle','state','scan','promoted','ahead','m_State[i]','currentLeader','m_Task->Vehicles[previous]'):
|
||||
s=s.replace(obj+'.',obj+'->')
|
||||
for obj in ('Entity','Group','Waypoint','Route','Receipt','Combat'):
|
||||
s=s.replace(obj+'.',obj+'->')
|
||||
for cls in ('ERR_ConvoyColumnRules','ERR_ConvoyDriving','ERR_SupplyConvoyTactics','ERR_HQ','Math','vector','string'):
|
||||
s=s.replace(cls+'.',cls+'::')
|
||||
s=s.replace('bool rejoining;', 'bool rejoining=false;').replace('m_State[returning].','m_State[returning]->').replace('m_Task->Vehicles[returning].','m_Task->Vehicles[returning]->')
|
||||
s=s.replace('m_State[index].', 'm_State[index]->').replace('m_State[leader].', 'm_State[leader]->')
|
||||
s=s.replace('m_Task->Vehicles[index].', 'm_Task->Vehicles[index]->').replace('(index + 1).ToString()', 'std::to_string(index + 1)')
|
||||
s=s.replace('float largestGap; bool supplyHold; bool interceptLive;', 'float largestGap=0; bool supplyHold=false; bool interceptLive=false;')
|
||||
return s
|
||||
rules='\n'.join(cpp(body(p, x)) for x in ('static bool WaitForRear(', 'static bool TooClose(', 'static float Speed('))
|
||||
methods='\n'.join(cpp(body(p,x)) for x in ('protected float DistanceToTarget(', 'protected void Reorder(', 'void Reset(', 'protected void Hold(', 'protected bool Progress(', 'bool Tick('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}template<class...T>static string Format(T...){return "event";}};
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
namespace Math{float Max(float a,float b){return a>b?a:b;}int Min(int a,int b){return a<b?a:b;}float Round(float x){return std::round(x);}}
|
||||
namespace ERR_HQ{int stranded=0,retries=0;void Event(string type,string){if(type=="CONVOY STRANDED")stranded++;if(type=="CONVOY ROUTE RETRY")retries++;}}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
|
||||
struct ERR_ConvoyColumnRules{'''+rules+r'''};
|
||||
struct Entity{vector position;vector GetOrigin(){return position;}};
|
||||
struct Waypoint:Entity{};
|
||||
struct Group{int added=0;void AddWaypoint(Waypoint*){added++;}};
|
||||
struct Route{Array<vector>points={{0},{400}};int Count(){return points.Count();}vector At(int i){return points[i];}};
|
||||
struct Combat{bool Held=false;};
|
||||
struct ERR_ConvoyVehicle{
|
||||
int Index=0,RouteIndex=0,clears=0;bool InterceptActive=false,Released=false,Arrived=false,SupplyHeld=false;
|
||||
struct Entity entity;struct Entity* Entity=&entity;struct Group group;struct Group* Group=&group;
|
||||
struct Combat combat;struct Combat* Combat=&combat;struct Waypoint* Waypoint=nullptr;
|
||||
void ClearOrder(){if(Waypoint)clears++;delete Waypoint;Waypoint=nullptr;}
|
||||
void Release(void*){Released=true;ClearOrder();}
|
||||
bool Order(struct Route* route,float){if(!Waypoint){Waypoint=new struct Waypoint();Waypoint->position=route->At(RouteIndex);Group->AddWaypoint(Waypoint);}return true;}
|
||||
~ERR_ConvoyVehicle(){ClearOrder();}
|
||||
};
|
||||
namespace ERR_ConvoyDriving{bool Restore(void*,ERR_ConvoyVehicle*){return false;}}
|
||||
namespace ERR_SupplyConvoyTactics{Waypoint* Move(vector p,float){auto w=new Waypoint();w->position=p;return w;}}
|
||||
struct Receipt{string SourceId="test";};
|
||||
struct ERR_SupplyConvoyTask{Array<ERR_ConvoyVehicle*>Vehicles;struct Route route;struct Route* Route=&route;int Legs=0;struct Receipt receipt;struct Receipt* Receipt=&receipt;};
|
||||
struct ERR_ConvoyColumnVehicle{bool InterceptDetached=false,Holding=false;vector Progress;float LastProgress=-1,NextRetry=0,NextTarget=0,Speed=-1;int Ahead=-1;};
|
||||
struct Column{
|
||||
void* m_Mode=nullptr;ERR_SupplyConvoyTask* m_Task;Array<ERR_ConvoyColumnVehicle*>m_State;
|
||||
Array<int>m_Order={0,1,2};int m_Leader=-1,m_RouteIndex=0;bool m_RearWait=false;float m_NextLog=0;
|
||||
void Speed(ERR_ConvoyVehicle*,ERR_ConvoyColumnVehicle* state,float speed){state->Speed=speed;}
|
||||
'''+methods+r'''};
|
||||
int main(){
|
||||
{ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=true;c.entity.position={80};
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
column.m_RouteIndex=1;a.RouteIndex=0;a.entity.position={-20};b.RouteIndex=1;b.entity.position={100};assert(column.Tick(0));assert(column.m_Leader==1&&b.RouteIndex==1);
|
||||
b.Released=true;a.InterceptActive=false;assert(column.Tick(1000));assert(column.m_Leader==2&&c.RouteIndex==1&&a.RouteIndex==0&&column.m_RouteIndex==1);}
|
||||
|
||||
{ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=true;b.Released=c.Released=true;
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
a.RouteIndex=0;a.entity.position={-200};assert(column.Tick(0));assert(sa.InterceptDetached&&a.RouteIndex==0&&!a.Waypoint);
|
||||
column.m_RouteIndex=1;a.InterceptActive=false;assert(column.Tick(1000));assert(a.RouteIndex==0&&a.Waypoint&&column.m_RouteIndex==0);}
|
||||
|
||||
assert(!ERR_ConvoyColumnRules::WaitForRear(160,false));assert(ERR_ConvoyColumnRules::WaitForRear(161,false));
|
||||
assert(ERR_ConvoyColumnRules::WaitForRear(91,true));assert(!ERR_ConvoyColumnRules::WaitForRear(90,true));
|
||||
assert(ERR_ConvoyColumnRules::TooClose(39,false));assert(!ERR_ConvoyColumnRules::TooClose(40,false));
|
||||
assert(ERR_ConvoyColumnRules::TooClose(64,true));assert(!ERR_ConvoyColumnRules::TooClose(65,true));
|
||||
ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;b.entity.position={-35};c.entity.position={-70};
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
assert(column.Tick(0));assert(a.RouteIndex==1&&a.Waypoint&&a.Waypoint->position.x==400);assert(!b.Waypoint&&!c.Waypoint);
|
||||
a.entity.position={80};column.Tick(1000);assert(b.Waypoint&&b.Waypoint->position.x==80&&sb.Speed==45);assert(!c.Waypoint);
|
||||
int additions=b.group.added;a.entity.position={110};column.Tick(2000);assert(b.group.added==additions); // stable target, no per-second churn
|
||||
a.entity.position={200};column.Tick(3000);assert(column.m_RearWait&&!a.Waypoint&&b.Waypoint); // leader waits for laggard
|
||||
b.entity.position={130};c.entity.position={60};column.Tick(11000);assert(!column.m_RearWait&&a.Waypoint);
|
||||
assert(b.Waypoint->position.x==200); // native target follows predecessor, never remote base marker
|
||||
b.SupplyHeld=true;auto supplyOrder=b.Waypoint;column.Tick(12000);assert(!a.Waypoint&&b.Waypoint==supplyOrder&&sb.LastProgress==-1);
|
||||
b.SupplyHeld=false;b.combat.Held=true;column.Tick(13000);assert(b.Waypoint==supplyOrder&&sb.LastProgress==-1);b.combat.Held=false;
|
||||
// Real vehicle positions determine arrival; no stale seated-group origin is consulted.
|
||||
a.entity.position={400};b.entity.position={330};c.entity.position={260};column.Tick(20000);
|
||||
assert(a.Arrived&&b.Arrived&&c.Arrived&&!a.Waypoint&&!b.Waypoint&&!c.Waypoint);
|
||||
// Loss promotes the next surviving vehicle at the current route cursor, no replacement/restart at spawn.
|
||||
column.Reset(20500);assert(column.m_Leader==0&&column.m_RouteIndex==2); // combat reset preserves route progress for a later promotion
|
||||
a.Release(nullptr);b.Arrived=c.Arrived=false;column.Tick(21000);assert(column.m_Leader==1&&b.RouteIndex==1&&!b.Arrived&&b.Waypoint->position.x==400);
|
||||
// Commanded stationary time alone retries and eventually releases; holds reset this clock.
|
||||
ERR_ConvoyVehicle stuck;ERR_ConvoyColumnVehicle ss;
|
||||
assert(column.Progress(&stuck,&ss,0));assert(column.Progress(&stuck,&ss,45000));assert(ERR_HQ::retries==1&&!stuck.Released);
|
||||
column.Hold(&stuck,&ss,170000);assert(column.Progress(&stuck,&ss,180000));assert(!stuck.Released);
|
||||
stuck.entity.position={6};assert(column.Progress(&stuck,&ss,300000));assert(ss.LastProgress==300000);
|
||||
assert(column.Progress(&stuck,&ss,479999));assert(!column.Progress(&stuck,&ss,480000)&&stuck.Released&&ERR_HQ::stranded==1);
|
||||
column.Reset(500000,true);assert(sa.LastProgress==-1&&sb.LastProgress==-1&&!column.m_RearWait&&column.m_Leader==-1);
|
||||
// Post-combat physical order avoids sending the front truck toward a rear vehicle coming toward it.
|
||||
b.entity.position={100};c.entity.position={300};b.Arrived=c.Arrived=false;column.m_RouteIndex=1;column.Reset(500001,false,true);
|
||||
assert(column.m_Order[0]==2&&column.m_Order[1]==1&&column.m_Order[2]==0); // released vehicle last
|
||||
assert(task.Vehicles[1]==&b&&task.Vehicles[2]==&c); // combat roles and canonical identities unchanged
|
||||
column.Tick(500002);assert(column.m_Leader==2&&!c.Waypoint&&b.Waypoint&&b.Waypoint->position.x==300);
|
||||
b.entity.position={230};column.Tick(500003);assert(c.Waypoint&&c.Waypoint->position.x==400);
|
||||
b.Release(nullptr);c.Release(nullptr);assert(!column.Tick(510000));
|
||||
}
|
||||
''')
|
||||
print('PASS: production convoy spacing, physical arrival, stable follower targets, supply ownership, leader promotion and bounded stuck recovery')
|
||||
|
||||
q=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c'
|
||||
orders=body(q,'protected void Orders(').replace('protected ','').replace('m_Task.','m_Task->').replace('vector.','vector::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
struct Items{int count;int Count(){return count;}};
|
||||
struct Task{Items Vehicles{6},Teams{2};};
|
||||
struct Controller{Task task;Task* m_Task=&task;float m_NextOrders=0;vector m_LastOrderContact,m_Contact;bool m_OrdersIssued=false,m_TruckWithdrawn=false;int support=0,flank=0;
|
||||
void Support(int,bool){support++;}void Flank(int,bool){flank++;}
|
||||
'''+orders+r'''};
|
||||
int main(){
|
||||
Controller c;c.Orders(true,0);assert(c.support==6&&c.flank==2);
|
||||
c.Orders(false,30000);assert(c.support==6&&c.flank==2&&c.m_NextOrders==60000);
|
||||
c.m_Contact={39};c.Orders(false,60000);assert(c.support==6&&c.flank==2);
|
||||
c.m_Contact={40};c.Orders(false,90000);assert(c.support==12&&c.flank==4);
|
||||
c.m_OrdersIssued=false;c.Orders(true,100000);assert(c.support==18&&c.flank==6); // new engagement can issue at same location
|
||||
}
|
||||
''')
|
||||
print('PASS: convoy combat keeps native manoeuvres for unchanged contacts and refreshes when contact moves')
|
||||
|
||||
probe=ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSupplyConvoyProbe.c'
|
||||
departure=body(probe,'protected int Departure(').replace('protected ','').replace('m_CombatTask.','m_CombatTask->').replace('Vehicles[2].','Vehicles[2]->').replace('Entity.','Entity->').replace('vector.','vector::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
void Print(const char*){}
|
||||
struct Entity{vector position;vector GetOrigin(){return position;}};
|
||||
struct Vehicle{bool Released=false;struct Entity entity;struct Entity* Entity=&entity;};
|
||||
struct Bases{int Count(){return 2;}};
|
||||
struct Task{bool Stopped=false;int Legs=0;Vehicle* Vehicles[3];struct Bases Bases;};
|
||||
struct Probe{Task* m_CombatTask;int m_Phase=3,m_TestLegs=0;vector m_RouteResumePosition;bool m_AllMoved=true;
|
||||
'''+departure+r'''};
|
||||
int main(){
|
||||
Task task;Vehicle truck;task.Vehicles[2]=&truck;Probe p{&task};truck.entity.position={200};
|
||||
assert(p.Departure()==0&&p.m_Phase==3); // earlier regroup displacement cannot pass
|
||||
task.Legs=1;assert(p.Departure()==0&&p.m_Phase==4&&p.m_RouteResumePosition.x==200);
|
||||
assert(p.Departure()==0);truck.entity.position={219};assert(p.Departure()==0);
|
||||
truck.entity.position={220};assert(p.Departure()==1&&p.m_Phase==5);
|
||||
truck.Released=true;assert(p.Departure()==-1);
|
||||
}
|
||||
''')
|
||||
smoke=(ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c').read_text()
|
||||
convoy=smoke.split('else if (m_ERRSmokeStep == 106)')[1].split('else if (m_ERRSmokeStep == 105)')[0]
|
||||
assert 'Transition(ERR_Phase.DEFEAT)' in convoy and 'm_ERRSmokeStep = 1061' in convoy
|
||||
assert 'damage.Kill' not in convoy and 'm_ERRSmokeStep = 9' not in convoy
|
||||
print('PASS: native convoy proof excludes pre-departure movement and ends via explicit fixture reset, not prohibited player death')
|
||||
|
||||
boarding=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBoardingProgress.c'
|
||||
retry=body(boarding,'bool Retry(')
|
||||
refresh=body(q,'protected void RefreshBoarding(').replace('protected ','').replace('foreach (','for (').replace('m_Task.','m_Task->').replace('team.','team->').replace('Vehicles[2].','Vehicles[2]->').replace('BoardingProgress.','BoardingProgress->').replace('Receipt.','Receipt->').replace('ERR_HQ.','ERR_HQ::').replace('Math.','Math::').replace('string.','string::').replace('ERR_SupplyTeam team','ERR_SupplyTeam* team')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
struct string{template<class...T>static string Format(T...){return {};}};
|
||||
namespace Math{float Round(float x){return std::round(x);}}
|
||||
namespace ERR_HQ{void Event(const char*,string){}}
|
||||
struct ERR_ConvoyBoardingProgress{int Seated=-1,Retries=0;float Distance=-1,LastProgress=0,NextRetry=0;
|
||||
'''+retry+r'''};
|
||||
struct ERR_SupplyTeam{bool Released=false,aboard=false;float BoardingRetryAt=0,distance=200;int seated=0,clears=0,adds=0;ERR_ConvoyBoardingProgress* BoardingProgress=nullptr;
|
||||
bool Aboard(void*,bool){return aboard;}int BoardingCount(void*,float& d){d=distance;return seated;}void Clear(){clears++;}void Add(int){adds++;}};
|
||||
struct Vehicle{void* Entity=nullptr;};struct Receipt{const char* SourceId="test";};
|
||||
struct Task{std::vector<ERR_SupplyTeam*>Teams;Vehicle* Vehicles[3];struct Receipt receipt;struct Receipt* Receipt=&receipt;};
|
||||
struct Tactics{Task* m_Task;int Board(void*,bool){return 1;}
|
||||
'''+refresh+r'''};
|
||||
int main(){
|
||||
ERR_ConvoyBoardingProgress p;assert(!p.Retry(0,200,0));
|
||||
assert(!p.Retry(0,194,29000));assert(!p.Retry(0,194,58000));assert(p.Retry(0,194,59000));
|
||||
assert(!p.Retry(1,100,60000));assert(!p.Retry(1,100,89999));assert(p.Retry(1,100,90000));
|
||||
assert(p.Retry(1,100,120000));assert(!p.Retry(1,100,150000)&&p.Retries==3); // bounded even if no progress
|
||||
Task task;Vehicle truck;task.Vehicles[2]=&truck;ERR_SupplyTeam team,finished,released;finished.aboard=true;released.Released=true;
|
||||
task.Teams={&team,&finished,&released};Tactics t{&task};t.RefreshBoarding(0);assert(team.BoardingProgress&&!finished.BoardingProgress&&!released.BoardingProgress);
|
||||
t.RefreshBoarding(30000);assert(team.clears==1&&team.adds==0&&team.BoardingRetryAt==31000);
|
||||
t.RefreshBoarding(30999);assert(team.adds==0);t.RefreshBoarding(31000);assert(team.adds==1&&team.BoardingRetryAt==0);
|
||||
team.seated=6;team.distance=10;t.RefreshBoarding(40000);assert(team.clears==1);t.RefreshBoarding(69999);assert(team.clears==1);
|
||||
t.RefreshBoarding(70000);assert(team.clears==2);team.aboard=true;t.RefreshBoarding(71000);assert(team.adds==1); // do not disturb completed boarding
|
||||
delete team.BoardingProgress;
|
||||
}
|
||||
''')
|
||||
print('PASS: boarding retries only after approach/seat progress stops, capped at three, with delayed native reissue and completed-team preservation')
|
||||
|
||||
safe='\n'.join(body(q,s) for s in ('protected float InfantryDistance(', 'protected void ProtectInfantry('))
|
||||
safe=safe.replace('protected ','').replace('foreach (','for (')
|
||||
safe=safe.replace('AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));', 'AICarMovementComponent* movement = &vehicle->Entity->movement;')
|
||||
for typ in ('IEntity','ERR_SupplyTeam','ERR_ConvoyVehicle'):safe=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',safe)
|
||||
for obj in ('m_Task','team','member','vehicle','movement'):safe=safe.replace(obj+'.',obj+'->')
|
||||
for cls in ('ERR_Actors','CompartmentAccessComponent','Math','vector'):safe=safe.replace(cls+'.',cls+'::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
namespace Math{float Min(float a,float b){return a<b?a:b;}}
|
||||
struct AICarMovementComponent{float speed=99;void SetCruiseSpeed(float s){speed=s;}};
|
||||
struct IEntity{vector position;bool fit=true,aboard=false;AICarMovementComponent movement;vector GetOrigin(){return position;}};
|
||||
namespace ERR_Actors{bool CanFight(IEntity* m){return m&&m->fit;}}
|
||||
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* m){return m->aboard?m:nullptr;}}
|
||||
struct ERR_SupplyTeam{bool Released=false;std::vector<IEntity*>Members;};
|
||||
struct ERR_ConvoyVehicle{bool Released=false;IEntity* Entity;};
|
||||
struct Task{std::vector<ERR_SupplyTeam*>Teams;std::vector<ERR_ConvoyVehicle*>Vehicles;};
|
||||
struct Tactics{Task* m_Task;
|
||||
'''+safe+r'''};
|
||||
int main(){
|
||||
IEntity truck{{0}},car{{100}},foot{{30}},passenger{{0}};passenger.aboard=true;ERR_SupplyTeam team{false,{&foot,&passenger}};
|
||||
ERR_ConvoyVehicle a{false,&truck},b{false,&car};Task task{{&team},{&a,&b}};Tactics t{&task};
|
||||
assert(t.InfantryDistance(&truck)==30);t.ProtectInfantry();assert(truck.movement.speed==8&&car.movement.speed==35);
|
||||
foot.aboard=true;t.ProtectInfantry();assert(truck.movement.speed==35&&car.movement.speed==35);
|
||||
foot.aboard=false;foot.fit=false;assert(t.InfantryDistance(&truck)>100000);foot.fit=true;team.Released=true;assert(t.InfantryDistance(&truck)>100000);
|
||||
team.Released=false;a.Released=true;truck.movement.speed=99;t.ProtectInfantry();assert(truck.movement.speed==99); // never retake a released vehicle
|
||||
}
|
||||
''')
|
||||
print('PASS: slow only owned vehicles near fit dismounted infantry; ignore passengers, casualties and released teams/vehicles')
|
||||
|
||||
restore=body(p,'static bool Restore(').replace('ERR_GameMode mode','void* mode').replace('ERR_ConvoyVehicle vehicle','ERR_ConvoyVehicle* vehicle')
|
||||
restore=restore.replace('SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(vehicle.Entity.FindComponent(SCR_AIVehicleUsageComponent));','SCR_AIVehicleUsageComponent* usage = vehicle->Entity->usage;')
|
||||
restore=restore.replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
|
||||
for obj in ('vehicle','Group','utility','usage'):restore=restore.replace(obj+'.',obj+'->')
|
||||
restore=restore.replace('ERR_ConvoyMountedCrew.','ERR_ConvoyMountedCrew::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
struct SCR_AIVehicleUsageComponent{bool pilot=true;bool CanBePiloted(){return pilot;}};
|
||||
struct Entity{SCR_AIVehicleUsageComponent* usage=nullptr;};
|
||||
struct SCR_AIGroupUtilityComponent{void* m_VehicleMgr=this;bool known=false;int added=0;bool IsUsableVehicle(Entity*){return known;}void AddUsableVehicle(SCR_AIVehicleUsageComponent*){known=true;added++;}};
|
||||
struct Group{SCR_AIGroupUtilityComponent* utility=nullptr;SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return utility;}};
|
||||
struct ERR_ConvoyVehicle{bool Released=false,valid=true;struct Group group;struct Group* Group=&group;struct Entity entity;struct Entity* Entity=&entity;void* Driver=this;bool CrewValid(void*){return valid;}};
|
||||
namespace ERR_ConvoyMountedCrew{bool healthyOwned=true;bool Retain(void*,Entity*){return healthyOwned;}}
|
||||
'''+restore+r'''
|
||||
int main(){
|
||||
int mode;ERR_ConvoyVehicle car;SCR_AIGroupUtilityComponent utility;SCR_AIVehicleUsageComponent usage;car.group.utility=&utility;car.entity.usage=&usage;
|
||||
assert(!Restore(nullptr,&car)&&!Restore(&mode,nullptr));car.Released=true;assert(!Restore(&mode,&car));car.Released=false;
|
||||
car.valid=false;assert(!Restore(&mode,&car));car.valid=true;ERR_ConvoyMountedCrew::healthyOwned=false;assert(!Restore(&mode,&car));ERR_ConvoyMountedCrew::healthyOwned=true;
|
||||
car.group.utility=nullptr;assert(!Restore(&mode,&car));car.group.utility=&utility;utility.m_VehicleMgr=nullptr;assert(!Restore(&mode,&car));utility.m_VehicleMgr=&utility;
|
||||
car.entity.usage=nullptr;assert(!Restore(&mode,&car));car.entity.usage=&usage;usage.pilot=false;assert(!Restore(&mode,&car));usage.pilot=true;
|
||||
assert(Restore(&mode,&car)&&utility.known&&utility.added==1);assert(!Restore(&mode,&car)&&utility.added==1);
|
||||
}
|
||||
''')
|
||||
print('PASS: restore missing native driving registration only for healthy owned seated crews; preserve existing association and refuse invalid/released vehicles')
|
||||
@@ -0,0 +1,6 @@
|
||||
PASS: production convoy spacing, physical arrival, stable follower targets, supply ownership, leader promotion and bounded stuck recovery
|
||||
PASS: convoy combat keeps native manoeuvres for unchanged contacts and refreshes when contact moves
|
||||
PASS: native convoy proof excludes pre-departure movement and ends via explicit fixture reset, not prohibited player death
|
||||
PASS: boarding retries only after approach/seat progress stops, capped at three, with delayed native reissue and completed-team preservation
|
||||
PASS: slow only owned vehicles near fit dismounted infantry; ignore passengers, casualties and released teams/vehicles
|
||||
PASS: restore missing native driving registration only for healthy owned seated crews; preserve existing association and refuse invalid/released vehicles
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual delivered cover freshness/lineage/replay and canonical executor; owned waypoint-only hold, one resume at preserved route index, invalid ownership/crew/ledger refusal and fail-closed native recreation
|
||||
+1
@@ -0,0 +1 @@
|
||||
PASS: actual native seated-group arrival outside35m, native waypoint removal, idempotent advance, and convoy waypoint idempotence/arrival/deadline, missing/dead/captured/reparented crew refusal, failed native waypoint release, original vehicle/member preservation and no replacement
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/tmp/err-policy-_0_dr7qz/check.cpp:10:156: error: declaration of ‘Combat* ERR_ConvoyVehicle::Combat’ changes meaning of ‘Combat’ [-Wchanges-meaning]
|
||||
10 | struct ERR_ConvoyVehicle{int InterceptToken=0,clears=0,cancels=0;bool own=true,Released=false;Order order;Order*InterceptOrder=ℴCombat combat;Combat*Combat=&combat;Entity entity;Entity*Entity=&entity;
|
||||
| ^~~~~~
|
||||
/tmp/err-policy-_0_dr7qz/check.cpp:10:149: note: used here to mean ‘struct Combat’
|
||||
10 | struct ERR_ConvoyVehicle{int InterceptToken=0,clears=0,cancels=0;bool own=true,Released=false;Order order;Order*InterceptOrder=ℴCombat combat;Combat*Combat=&combat;Entity entity;Entity*Entity=&entity;
|
||||
| ^~~~~~
|
||||
/tmp/err-policy-_0_dr7qz/check.cpp:9:23: note: declared here
|
||||
9 | struct Order{};struct Combat{bool Held=false;};struct Entity{vector GetOrigin(){return {};}};
|
||||
| ^~~~~~
|
||||
/tmp/err-policy-_0_dr7qz/check.cpp:10:192: error: declaration of ‘Entity* ERR_ConvoyVehicle::Entity’ changes meaning of ‘Entity’ [-Wchanges-meaning]
|
||||
10 | struct ERR_ConvoyVehicle{int InterceptToken=0,clears=0,cancels=0;bool own=true,Released=false;Order order;Order*InterceptOrder=ℴCombat combat;Combat*Combat=&combat;Entity entity;Entity*Entity=&entity;
|
||||
| ^~~~~~
|
||||
/tmp/err-policy-_0_dr7qz/check.cpp:10:185: note: used here to mean ‘struct Entity’
|
||||
10 | struct ERR_ConvoyVehicle{int InterceptToken=0,clears=0,cancels=0;bool own=true,Released=false;Order order;Order*InterceptOrder=ℴCombat combat;Combat*Combat=&combat;Entity entity;Entity*Entity=&entity;
|
||||
| ^~~~~~
|
||||
/tmp/err-policy-_0_dr7qz/check.cpp:9:55: note: declared here
|
||||
9 | struct Order{};struct Combat{bool Held=false;};struct Entity{vector GetOrigin(){return {};}};
|
||||
| ^~~~~~
|
||||
PASS: actual supply route cycling/arrival/stops, finite startup bounds, contact freshness, dismount hold, quiet interval, interrupted boarding, timeout and lost-truck release
|
||||
PASS: moving-convoy perception boundaries and scoped individual/group abandonment priorities
|
||||
PASS: actual supply startup reasons expose unchanged body budget/requirement, journal only failure changes, preserve retry cadence/caps and report the true final refusal
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-mounted-intercept-convoy-1247/tests/test_supply_convoys.py", line 359, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<21 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 13, in run
|
||||
subprocess.run(['g++','-std=c++17','-Wall','-Werror',str(p/'check.cpp'),'-o',str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['g++', '-std=c++17', '-Wall', '-Werror', '/tmp/err-policy-_0_dr7qz/check.cpp', '-o', '/tmp/err-policy-_0_dr7qz/check']' returned non-zero exit status 1.
|
||||
@@ -0,0 +1 @@
|
||||
PASS: independently rerun actual supply route/cleanup, moving-convoy perception, startup and combat-start exact intercept ownership harnesses after test type qualification.
|
||||
@@ -0,0 +1,19 @@
|
||||
check: /tmp/err-policy-m5nlnyh2/check.cpp:271: int main(): Assertion `!Rules::Current(&o,&b,now)' failed.
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-mounted-intercept-rules-1247/tests/test_hq_mounted_intercept.py", line 25, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<73 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
|
||||
subprocess.run([str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['/tmp/err-policy-m5nlnyh2/check']' died with <Signals.SIGABRT: 6>.
|
||||
@@ -0,0 +1,109 @@
|
||||
// Detached radio order. Only its issuer chooses Position and the fixed deadline.
|
||||
class ERR_ConvoyInterceptOrder
|
||||
{
|
||||
int Token;
|
||||
SCR_AIGroup Group;
|
||||
string SourceId;
|
||||
int VehicleIndex;
|
||||
vector Position;
|
||||
float IssuedAt;
|
||||
float ExpiresAt;
|
||||
float ObservedAt;
|
||||
int BeliefId;
|
||||
int BeliefRevision;
|
||||
int ReportId;
|
||||
}
|
||||
|
||||
// Delivered ground history only; no target entity reads, force creation or orders.
|
||||
class ERR_HQMountedInterceptRules
|
||||
{
|
||||
static bool Eligible(ERR_HQBelief belief, float now)
|
||||
{
|
||||
if (!ERR_HQRoadIntercept.Eligible(belief, now)) return false;
|
||||
if (belief.BeliefId <= 0 || belief.BeliefRevision <= 0 || belief.ObservedAt < 0) return false;
|
||||
return ERR_HQPursuitRules.PositionValid(belief.Position) && ERR_HQPursuitRules.PositionValid(belief.PredictedPosition);
|
||||
}
|
||||
static bool Current(ERR_ConvoyInterceptOrder order, ERR_HQBelief belief, float now)
|
||||
{
|
||||
if (!order || !Eligible(belief, now) || !order.Group || order.SourceId.IsEmpty()) return false;
|
||||
if (order.Token < 400001 || order.Token > 400256) return false;
|
||||
if (order.VehicleIndex != 0 && order.VehicleIndex != 3 && order.VehicleIndex != 5) return false;
|
||||
if (order.BeliefId != belief.BeliefId
|
||||
|| order.ReportId != belief.MessageId || order.ObservedAt != belief.ObservedAt) return false;
|
||||
if (!ERR_ForceRules.Finite(order.IssuedAt) || !ERR_ForceRules.Finite(order.ExpiresAt)) return false;
|
||||
if (order.IssuedAt < belief.LastDeliveredAt || now < order.IssuedAt || now - order.IssuedAt > 30000) return false;
|
||||
if (order.ExpiresAt <= order.IssuedAt || order.ExpiresAt > order.ObservedAt + 120000 || now >= order.ExpiresAt) return false;
|
||||
return ERR_HQPursuitRules.PositionValid(order.Position);
|
||||
}
|
||||
static bool Ahead(vector origin, vector velocity, vector point)
|
||||
{
|
||||
if (!ERR_ForceRules.FiniteVector(origin) || !ERR_ForceRules.FiniteVector(point) || !ERR_HQPursuitRules.Heading(velocity, true)) return false;
|
||||
vector delta = point - origin; delta[1] = 0; velocity[1] = 0;
|
||||
float distance = delta.Length();
|
||||
return distance >= 300 && distance <= 900 && vector.Dot(delta.Normalized(), velocity.Normalized()) >= 0.8;
|
||||
}
|
||||
static bool FromPolyline(array<vector> points, vector origin, vector velocity, vector seed, array<vector> reserved, out vector result, out float score, out float heading)
|
||||
{
|
||||
result = vector.Zero; score = 1e20; heading = 0;
|
||||
if (!points || points.Count() < 2 || points.Count() > 1024 || !ERR_ForceRules.FiniteVector(seed)) return false;
|
||||
foreach (vector point : points) if (!ERR_ForceRules.FiniteVector(point)) return false;
|
||||
bool found;
|
||||
for (int i = 1; i < points.Count(); i++)
|
||||
{
|
||||
vector delta = points[i] - points[i - 1]; delta[1] = 0;
|
||||
float length = delta.Length();
|
||||
if (length < 0.01) continue;
|
||||
vector offset = seed - points[i - 1]; offset[1] = 0;
|
||||
float fraction = Math.Clamp(vector.Dot(offset, delta) / (length * length), 0, 1);
|
||||
vector candidate = vector.Lerp(points[i - 1], points[i], fraction);
|
||||
float distance = vector.DistanceXZ(seed, candidate);
|
||||
if (distance > 100 || !Ahead(origin, velocity, candidate) || !ERR_HQRoadIntercept.Unreserved(candidate, reserved)) continue;
|
||||
if (!found || ERR_HQRoadIntercept.Better(distance, candidate, score, result)) { result = candidate; score = distance; heading = Math.Atan2(delta[0], delta[2]) * Math.RAD2DEG; found = true; }
|
||||
}
|
||||
return found;
|
||||
}
|
||||
static bool Select(ERR_HQBelief belief, float now, array<vector> reserved, out vector result)
|
||||
{
|
||||
result = vector.Zero;
|
||||
if (!Replication.IsServer() || !Eligible(belief, now) || !reserved || reserved.Count() > 128) return false;
|
||||
if (!GetGame() || !GetGame().GetWorld()) return false;
|
||||
vector velocity = belief.Velocity; velocity[1] = 0;
|
||||
vector seed = belief.PredictedPosition + velocity.Normalized() * Math.Clamp(velocity.Length() * 60, 300, 900);
|
||||
if (!ERR_HQPursuitRules.PositionValid(seed)) return false;
|
||||
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (!world || !world.GetRoadNetworkManager()) return false;
|
||||
array<BaseRoad> roads = {};
|
||||
world.GetRoadNetworkManager().GetRoadsInAABB(seed + "-100 -1000 -100", seed + "100 1000 100", roads);
|
||||
if (roads.Count() > 128) return false;
|
||||
int segments;
|
||||
bool found;
|
||||
vector best;
|
||||
float bestScore = 1e20;
|
||||
float bestHeading;
|
||||
foreach (BaseRoad road : roads)
|
||||
{
|
||||
if (!road || !ERR_ForceRules.Finite(road.GetWidth()) || road.GetWidth() < 6) continue;
|
||||
array<vector> points = {}; road.GetPoints(points);
|
||||
if (points.Count() < 2 || points.Count() > 1024) continue;
|
||||
segments += points.Count() - 1;
|
||||
if (segments > 4096) return false;
|
||||
vector candidate;
|
||||
float score, heading;
|
||||
if (!FromPolyline(points, belief.PredictedPosition, velocity, seed, reserved, candidate, score, heading)) continue;
|
||||
if (!found || ERR_HQRoadIntercept.Better(score, candidate, bestScore, best))
|
||||
{ best = candidate; bestScore = score; bestHeading = heading; found = true; }
|
||||
}
|
||||
if (!found) return false;
|
||||
vector grounded = ERR_Actors.Ground(best);
|
||||
// Never convert an elevated bridge point into terrain beneath the road.
|
||||
if (!ERR_HQPursuitRules.PositionValid(grounded) || Math.AbsFloat(grounded[1] - best[1]) > 2) return false;
|
||||
ERR_SettlementSafety settlements = new ERR_SettlementSafety();
|
||||
if (!settlements.OutsideSettlements(grounded)) return false;
|
||||
// Existing conservative vehicle footprint: nine terrain/water samples,
|
||||
// filled collision volume and segment-aligned heading; no scenery changes.
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
if (!placement.Clear(grounded, bestHeading)) return false;
|
||||
result = grounded;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"files": {
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedIntercept.c": "c239aaad0ac556744f76b94286c6f204be51b51acdf44e788ca794c2ae967eae",
|
||||
"tests/test_hq_mounted_intercept.py": "fe81e8506b9128a54bdbcb08e10d36d91bd81af37b53a07d0185c7005c388757"
|
||||
},
|
||||
"patch_sha256": "7b4068c91c06319ac24f85ab66d320184dcf79229d71c5d4adfb98c7467874f5",
|
||||
"dependencies": {
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c": "0897e82d7805ef56f05f2b9c23903280285ea6bfa51796a2df86bb564ece78f7",
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyPlacement.c": "4d0e8b6e147486fb65db82a1060f0c5641c1319ba9e418bdec99cb5a422a7fbe",
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_SettlementSafety.c": "e5c0ebc95521a9a33a3f7d9cca6c551dda862a08b7af20b017877aba9584795b"
|
||||
},
|
||||
"tests": [
|
||||
"green.log",
|
||||
"placement-green.log",
|
||||
"revision-mutation-red.log"
|
||||
],
|
||||
"limits": [
|
||||
"No native compile or drive run. Geometry is conservative preflight, not path reachability or guaranteed arrival.",
|
||||
"Current is a delivery admission check: after delivery, physical hook owns the original fixed expiry rather than repeatedly applying the30s delivery window.",
|
||||
"Plain detached order fields must be retained and not mutated by the issuer; this service never rewrites Position. Exact physical identity/claim validation belongs to convoy hook."
|
||||
],
|
||||
"registration": "Root must add tests/test_hq_mounted_intercept.py to tools/check.py."
|
||||
}
|
||||
Reference in New Issue
Block a user