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:
+89
@@ -0,0 +1,89 @@
|
||||
// Conservative native volume check; no scenery removal or alternate placement.
|
||||
class ERR_ConvoyPlacement
|
||||
{
|
||||
protected bool m_Obstructed;
|
||||
protected int m_Visited;
|
||||
protected string m_LastReason;
|
||||
protected float m_LastLowest;
|
||||
protected float m_LastHighest;
|
||||
protected IEntity m_FirstObstruction;
|
||||
protected float m_LastTrace = -1;
|
||||
float LastTraceFraction() { return m_LastTrace; }
|
||||
string Diagnostic()
|
||||
{
|
||||
string prefab = "none";
|
||||
vector position;
|
||||
if (m_FirstObstruction)
|
||||
{
|
||||
prefab = "no prefab";
|
||||
if (m_FirstObstruction.GetPrefabData()) prefab = m_FirstObstruction.GetPrefabData().GetPrefabName();
|
||||
position = m_FirstObstruction.GetOrigin();
|
||||
}
|
||||
return string.Format("%1; terrainY=%2..%3 span=%4; visited=%5; first=%6 at %7; trace=%8",
|
||||
m_LastReason, m_LastLowest, m_LastHighest, m_LastHighest - m_LastLowest, m_Visited, prefab, position, m_LastTrace);
|
||||
}
|
||||
protected bool Occupancy(IEntity entity)
|
||||
{
|
||||
m_Visited++;
|
||||
if (m_Visited > 128) { m_Obstructed = true; m_LastReason = "native query visit cap"; }
|
||||
else if (entity && entity.GetVObject())
|
||||
{
|
||||
m_Obstructed = true; m_LastReason = "native VObject volume overlap"; m_FirstObstruction = entity;
|
||||
}
|
||||
return !m_Obstructed;
|
||||
}
|
||||
static bool Terrain(float origin, float lowest, float highest)
|
||||
{
|
||||
return ERR_ForceRules.Finite(origin) && ERR_ForceRules.Finite(lowest) && ERR_ForceRules.Finite(highest)
|
||||
&& lowest > 0.3 && highest >= lowest && highest - lowest <= 0.75 && origin >= lowest - 0.3 && origin <= highest + 0.3;
|
||||
}
|
||||
bool Clear(vector position, float heading)
|
||||
{
|
||||
m_LastReason = "invalid origin or heading"; m_LastLowest = 1e20; m_LastHighest = -1e20;
|
||||
m_FirstObstruction = null; m_Visited = 0; m_LastTrace = -1;
|
||||
if (!ERR_ForceRules.FiniteVector(position) || !ERR_ForceRules.Finite(heading)) return false;
|
||||
float radians = heading * Math.DEG2RAD;
|
||||
vector forward = Vector(Math.Sin(radians), 0, Math.Cos(radians));
|
||||
vector right = Vector(forward[2], 0, -forward[0]);
|
||||
float lowest = 1e20, highest = -1e20;
|
||||
vector low = position, high = position;
|
||||
for (int x = -1; x <= 1; x++)
|
||||
{
|
||||
for (int z = -1; z <= 1; z++)
|
||||
{
|
||||
vector sample = ERR_Actors.Ground(position + right * (x * 2.0) + forward * (z * 4.5));
|
||||
if (!ERR_ForceRules.FiniteVector(sample)) { m_LastReason = "nonfinite terrain sample"; return false; }
|
||||
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), sample))
|
||||
{ m_LastReason = "water at footprint sample"; return false; }
|
||||
lowest = Math.Min(lowest, sample[1]); highest = Math.Max(highest, sample[1]);
|
||||
low[0] = Math.Min(low[0], sample[0]); low[2] = Math.Min(low[2], sample[2]);
|
||||
high[0] = Math.Max(high[0], sample[0]); high[2] = Math.Max(high[2], sample[2]);
|
||||
}
|
||||
}
|
||||
m_LastLowest = lowest; m_LastHighest = highest;
|
||||
if (!Terrain(position[1], lowest, highest))
|
||||
{ m_LastReason = "terrain origin/height/span check"; return false; }
|
||||
low[1] = lowest + 0.05; high[1] = highest + 3.5;
|
||||
m_Obstructed = false; m_Visited = 0;
|
||||
GetGame().GetWorld().QueryEntitiesByAABB(low, high, Occupancy);
|
||||
// Entity bounding boxes can include distant branches or overhead wires.
|
||||
// Their overlap is diagnostic only; the filled native volume decides collision.
|
||||
vector center = (low + high) * 0.5;
|
||||
TraceBox volume = new TraceBox();
|
||||
volume.Start = center; volume.End = center + "0 0.01 0";
|
||||
volume.Mins = low - center; volume.Maxs = high - center;
|
||||
// Nine terrain/water samples already qualify the supporting ground below us.
|
||||
volume.Flags = TraceFlags.ENTS | TraceFlags.OCEAN;
|
||||
m_LastTrace = GetGame().GetWorld().TraceMove(volume, null);
|
||||
m_Obstructed = !ERR_ForceRules.Finite(m_LastTrace) || m_LastTrace < 1;
|
||||
m_FirstObstruction = null;
|
||||
if (m_Obstructed)
|
||||
{
|
||||
m_FirstObstruction = volume.TraceEnt;
|
||||
m_LastReason = "native filled volume collision";
|
||||
if (!ERR_ForceRules.Finite(m_LastTrace)) m_LastReason = "invalid native trace fraction";
|
||||
}
|
||||
else m_LastReason = "clear";
|
||||
return !m_Obstructed;
|
||||
}
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Keep both escorts on one real polyline. Horizontal arc length determines
|
||||
// placement; interpolation retains each road segment's height and heading.
|
||||
static bool PairFromPolyline(array<vector> points, vector origin, vector velocity, vector seed, array<vector> reserved, out vector first, out vector second, out float score, out float firstHeading, out float secondHeading)
|
||||
{
|
||||
first = vector.Zero; second = vector.Zero; score = 1e20; firstHeading = 0; secondHeading = 0;
|
||||
if (!points || points.Count() < 2 || points.Count() > 1024 || !ERR_ForceRules.FiniteVector(seed)) return false;
|
||||
if (!ERR_ForceRules.FiniteVector(origin) || !ERR_HQPursuitRules.Heading(velocity, true) || !reserved || reserved.Count() > 128) return false;
|
||||
foreach (vector point : points) if (!ERR_ForceRules.FiniteVector(point)) return false;
|
||||
velocity[1] = 0;
|
||||
float total; float anchor; float nearest = 1e20;
|
||||
float along;
|
||||
vector projected;
|
||||
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 (!ERR_ForceRules.Finite(length)) return false;
|
||||
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 (!found || ERR_HQRoadIntercept.Better(distance, candidate, nearest, projected))
|
||||
{
|
||||
projected = candidate; nearest = distance; anchor = total + length * fraction;
|
||||
along = vector.Dot(delta.Normalized(), velocity.Normalized()); found = true;
|
||||
}
|
||||
total += length;
|
||||
if (!ERR_ForceRules.Finite(total)) return false;
|
||||
}
|
||||
if (!found || nearest > 100 || total < 40) return false;
|
||||
float span = Math.Clamp(total, 40, 60);
|
||||
float start = anchor - span * 0.5;
|
||||
// Longitudinal roads extend the pair ahead of the projection, so the first
|
||||
// point at the 300m planning boundary is not shifted behind that boundary.
|
||||
if (along >= 0.25) start = anchor;
|
||||
else if (along <= -0.25) start = anchor - span;
|
||||
start = Math.Clamp(start, 0, total - span);
|
||||
float finish = start + span;
|
||||
float travelled;
|
||||
vector a, b;
|
||||
float aHeading; float bHeading;
|
||||
bool haveFirst; bool haveSecond;
|
||||
for (int j = 1; j < points.Count(); j++)
|
||||
{
|
||||
vector segment = points[j] - points[j - 1]; segment[1] = 0;
|
||||
float segmentLength = segment.Length();
|
||||
if (segmentLength < 0.01) continue;
|
||||
if (!haveFirst && start <= travelled + segmentLength)
|
||||
{
|
||||
a = vector.Lerp(points[j - 1], points[j], Math.Clamp((start - travelled) / segmentLength, 0, 1));
|
||||
aHeading = Math.Atan2(segment[0], segment[2]) * Math.RAD2DEG; haveFirst = true;
|
||||
}
|
||||
if (!haveSecond && finish <= travelled + segmentLength)
|
||||
{
|
||||
b = vector.Lerp(points[j - 1], points[j], Math.Clamp((finish - travelled) / segmentLength, 0, 1));
|
||||
bHeading = Math.Atan2(segment[0], segment[2]) * Math.RAD2DEG; haveSecond = true;
|
||||
}
|
||||
if (haveFirst && haveSecond) break;
|
||||
travelled += segmentLength;
|
||||
}
|
||||
if (!haveFirst || !haveSecond || vector.DistanceXZ(a, b) < 40) return false;
|
||||
if (vector.DistanceXZ(seed, a) > 100 || vector.DistanceXZ(seed, b) > 100) return false;
|
||||
if (!Ahead(origin, velocity, a) || !Ahead(origin, velocity, b)) return false;
|
||||
if (!ERR_HQRoadIntercept.Unreserved(a, reserved) || !ERR_HQRoadIntercept.Unreserved(b, reserved)) return false;
|
||||
// Canonical order does not depend on native road enumeration direction.
|
||||
if (ERR_HQRoadIntercept.Better(0, b, 0, a))
|
||||
{
|
||||
vector swap = a; a = b; b = swap;
|
||||
float swapHeading = aHeading; aHeading = bHeading; bHeading = swapHeading;
|
||||
}
|
||||
first = a; second = b; score = nearest; firstHeading = aHeading; secondHeading = bHeading;
|
||||
return true;
|
||||
}
|
||||
static bool SelectPair(ERR_HQBelief belief, float now, array<vector> reserved, out vector first, out vector second)
|
||||
{
|
||||
first = vector.Zero; second = 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 bestFirst, bestSecond;
|
||||
float bestScore = 1e20;
|
||||
float bestFirstHeading; float bestSecondHeading;
|
||||
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 a, b;
|
||||
float score; float aHeading; float bHeading;
|
||||
if (!PairFromPolyline(points, belief.PredictedPosition, velocity, seed, reserved, a, b, score, aHeading, bHeading)) continue;
|
||||
if (!found || ERR_HQRoadIntercept.Better(score, a, bestScore, bestFirst))
|
||||
{ bestFirst = a; bestSecond = b; bestScore = score; bestFirstHeading = aHeading; bestSecondHeading = bHeading; found = true; }
|
||||
}
|
||||
if (!found) return false;
|
||||
vector groundedFirst = ERR_Actors.Ground(bestFirst);
|
||||
vector groundedSecond = ERR_Actors.Ground(bestSecond);
|
||||
if (!ERR_HQPursuitRules.PositionValid(groundedFirst) || !ERR_HQPursuitRules.PositionValid(groundedSecond)) return false;
|
||||
if (Math.AbsFloat(groundedFirst[1] - bestFirst[1]) > 2 || Math.AbsFloat(groundedSecond[1] - bestSecond[1]) > 2) return false;
|
||||
if (vector.DistanceXZ(groundedFirst, groundedSecond) < 40) return false;
|
||||
ERR_SettlementSafety settlements = new ERR_SettlementSafety();
|
||||
if (!settlements.OutsideSettlements(groundedFirst) || !settlements.OutsideSettlements(groundedSecond)) return false;
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
if (!placement.Clear(groundedFirst, bestFirstHeading) || !placement.Clear(groundedSecond, bestSecondHeading)) return false;
|
||||
first = groundedFirst; second = groundedSecond;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
"""Execute actual footprint sampling and native query callback policy with API doubles."""
|
||||
import re
|
||||
from policy_harness import ROOT,body,run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyPlacement.c'
|
||||
code='\n'.join(body(p,s) for s in ('protected bool Occupancy(', 'static bool Terrain(', 'bool Clear('))
|
||||
code=code.replace('null','nullptr').replace('protected ','').replace('IEntity entity','IEntity* entity').replace('entity.GetVObject()', 'entity->GetVObject()')
|
||||
code=code.replace('TraceBox volume', 'TraceBox* volume').replace('volume.', 'volume->').replace('\"0 0.01 0\"', 'Vector(0,.01,0)')
|
||||
for cls in ('Math','ERR_ForceRules','ERR_Actors','ChimeraWorldUtils','TraceFlags'):code=code.replace(cls+'.',cls+'::')
|
||||
code=code.replace('GetGame().GetWorld().','GetGame()->GetWorld()->').replace('GetGame().GetWorld()', 'GetGame()->GetWorld()')
|
||||
code=code.replace('low, high, Occupancy','low, high, [this](IEntity* entity){return Occupancy(entity);}')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <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)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};}};
|
||||
vector Vector(float x,float y,float z){return{x,y,z};}
|
||||
namespace Math{float DEG2RAD=3.141592653589793/180;float Sin(float n){return std::sin(n);}float Cos(float n){return std::cos(n);}float Min(float a,float b){return std::min(a,b);}float Max(float a,float b){return std::max(a,b);}}
|
||||
namespace ERR_ForceRules{bool Finite(float n){return std::isfinite(n);}bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}}
|
||||
struct IEntity{bool visible=false;bool GetVObject(){return visible;}};
|
||||
struct TraceBox {vector Start,End,Mins,Maxs;int Flags=0;IEntity* TraceEnt=nullptr;};
|
||||
namespace TraceFlags{constexpr int ENTS=1,OCEAN=2,WORLD=4;}
|
||||
struct World{int traces=0;float fraction=1;IEntity* hit=nullptr;TraceBox last;float TraceMove(TraceBox* volume,void*){traces++;last=*volume;volume->TraceEnt=hit;return fraction;}int queries=0,visits=0;vector low,high;std::vector<IEntity*> entities;template<class C>void QueryEntitiesByAABB(vector a,vector b,C callback){queries++;low=a;high=b;for(auto e:entities){visits++;if(!callback(e))break;}}};
|
||||
struct Game{World world;World* GetWorld(){return &world;}}game;Game* GetGame(){return &game;}
|
||||
namespace ERR_Actors{int grounds=0;float elevation=10,slope=0;bool invalid=false;vector Ground(vector v){grounds++;v.y=invalid?NAN:elevation+slope*v.x;return v;}}
|
||||
namespace ChimeraWorldUtils{int waters=0;bool water=false;bool TryGetWaterSurfaceSimple(World*,vector){waters++;return water;}}
|
||||
struct Placement{float m_LastTrace=-1;std::string m_LastReason;float m_LastLowest=0,m_LastHighest=0;IEntity* m_FirstObstruction=nullptr;bool m_Obstructed=false;int m_Visited=0;'''+code+r'''};
|
||||
void reset(){game.world=World();ERR_Actors::grounds=0;ERR_Actors::elevation=10;ERR_Actors::slope=0;ERR_Actors::invalid=false;ChimeraWorldUtils::waters=0;ChimeraWorldUtils::water=false;}
|
||||
bool near(float a,float b){return std::abs(a-b)<.001;}
|
||||
int main(){
|
||||
Placement p;reset();assert(p.Clear({0,10,0},0));assert(ERR_Actors::grounds==9&&ChimeraWorldUtils::waters==9&&game.world.queries==1);
|
||||
assert(near(game.world.low.x,-2)&&near(game.world.high.x,2)&&near(game.world.low.z,-4.5)&&near(game.world.high.z,4.5));assert(near(game.world.low.y,10.05)&&near(game.world.high.y,13.5));
|
||||
assert(game.world.traces==1&&game.world.last.Flags==(TraceFlags::ENTS|TraceFlags::OCEAN));
|
||||
assert(near(game.world.last.Start.y+game.world.last.Mins.y,10.05)&&near(game.world.last.Start.y+game.world.last.Maxs.y,13.5));
|
||||
assert(near(game.world.last.Mins.x,-2)&&near(game.world.last.Maxs.x,2)&&near(game.world.last.Mins.z,-4.5)&&near(game.world.last.Maxs.z,4.5));
|
||||
assert(game.world.last.End.y>game.world.last.Start.y&&near(game.world.last.End.y-game.world.last.Start.y,.01));
|
||||
reset();assert(p.Clear({0,10,0},90));assert(near(game.world.low.x,-4.5)&&near(game.world.high.x,4.5)&&near(game.world.low.z,-2)&&near(game.world.high.z,2));
|
||||
reset();assert(!p.Clear({NAN,10,0},0)&&game.world.queries==0&&ERR_Actors::grounds==0);assert(!p.Clear({0,10,0},NAN));
|
||||
reset();ERR_Actors::slope=.2;assert(!p.Clear({0,10,0},0)&&game.world.queries==0); // 0.8m cross-footprint rise.
|
||||
reset();ERR_Actors::elevation=.29;assert(!p.Clear({0,.29,0},0)&&game.world.queries==0);
|
||||
reset();assert(!p.Clear({0,10.31,0},0)&&game.world.queries==0);
|
||||
reset();ChimeraWorldUtils::water=true;assert(!p.Clear({0,10,0},0)&&game.world.queries==0&&ERR_Actors::grounds==1);
|
||||
reset();ERR_Actors::invalid=true;assert(!p.Clear({0,10,0},0)&&game.world.queries==0);
|
||||
IEntity abstract,obstacle;obstacle.visible=true;
|
||||
reset();game.world.entities={&abstract,&obstacle,&abstract};assert(p.Clear({0,10,0},0)&&game.world.visits==2&&!p.m_Obstructed&&!p.m_FirstObstruction&&p.m_LastReason=="clear");
|
||||
// Broad tree/cable bounds overlap, but their actual low geometry does not: accept.
|
||||
assert(game.world.traces==1&&p.m_LastTrace==1);
|
||||
// Actual geometry refuses even when the broad diagnostic callback found only abstract entities.
|
||||
reset();game.world.entities={&abstract};game.world.fraction=.4;game.world.hit=&obstacle;
|
||||
assert(!p.Clear({0,10,0},0)&&p.m_Obstructed&&p.m_FirstObstruction==&obstacle&&p.m_LastReason=="native filled volume collision");
|
||||
for(float fraction:{0.f,.999f,NAN,INFINITY}){reset();game.world.fraction=fraction;assert(!p.Clear({0,10,0},0));}
|
||||
// New query resets previous obstruction. Abstract logic entities do not block.
|
||||
reset();game.world.entities={nullptr,&abstract};assert(p.Clear({0,10,0},0)&&game.world.visits==2&&!p.m_Obstructed&&!p.m_FirstObstruction&&p.m_LastReason=="clear");
|
||||
reset();game.world.entities.assign(128,&abstract);assert(p.Clear({0,10,0},0)&&game.world.visits==128);
|
||||
reset();game.world.entities.assign(130,&abstract);assert(p.Clear({0,10,0},0)&&game.world.visits==129&&game.world.traces==1&&p.m_LastReason=="clear");
|
||||
assert(Placement::Terrain(10,9.7,10.4));assert(!Placement::Terrain(10,9.7,10.451));assert(!Placement::Terrain(10,10.1,10));assert(!Placement::Terrain(10,NAN,10));
|
||||
}
|
||||
''')
|
||||
print('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')
|
||||
+102
@@ -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_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
|
||||
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')
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
"""Actual paired road geometry and bounded native survey; native boundaries are doubled."""
|
||||
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_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
|
||||
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{static inline int reads=0;float width=6;Array<vector>points={{900,10,1300},{1100,10,1300}};float GetWidth(){return width;}void GetPoints(Array<vector>&p){reads++;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 float rightHeight=10;static vector Ground(vector p){p.y=p.x>1000?rightHeight:10;return p;}};
|
||||
struct ChimeraWorldUtils{static inline bool water=false;static bool TryGetWaterSurfaceSimple(NativeWorld*,vector){return water;}};
|
||||
|
||||
struct ERR_SettlementSafety{static inline bool clear=true,rightClear=true;static inline int calls=0;bool OutsideSettlements(vector p){calls++;return clear&&(p.x<=1000||rightClear);}};
|
||||
struct ERR_ConvoyPlacement{static inline bool clear=true,rightClear=true,water=false;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&&!water&&(p.x<=1000||rightClear);}};
|
||||
''' + s + r'''
|
||||
int main(){
|
||||
using Rules=ERR_HQMountedInterceptRules;
|
||||
ERR_HQBelief belief;Array<vector> reserved;vector a={1,2,3},b={4,5,6};float score=0,ha=0,hb=0;
|
||||
vector origin={1000,10,1000},velocity={0,0,4},seed={1000,10,1300};
|
||||
auto pair=[&](Array<vector> p){return Rules::PairFromPolyline(p,origin,velocity,seed,reserved,a,b,score,ha,hb);};
|
||||
auto empty=[&](){assert(a==vector::Zero&&b==vector::Zero);};
|
||||
// Real arc interpolation over a90-degree bend, not straight interpolation
|
||||
// between the road's endpoints (which would put both escorts off the road).
|
||||
assert(pair({{900,10,1300},{1000,10,1300},{1000,10,1400}}));
|
||||
assert(a==vector(970,10,1300)&&b==vector(1000,10,1330));
|
||||
assert(std::abs(ha-90)<.001&&std::abs(hb)<.001);
|
||||
assert(vector::DistanceXZ(a,b)>42&&vector::DistanceXZ(a,b)<43);
|
||||
// Arc span alone is insufficient on a bend-back/hairpin.
|
||||
assert(!pair({{970,10,1300},{1000,10,1300},{1000,10,1301},{970,10,1301}}));empty();
|
||||
assert(!pair({{980,10,1300},{1019.9f,10,1300}}));empty();
|
||||
assert(pair({{980,10,1300},{1020,10,1300}})&&vector::DistanceXZ(a,b)==40);
|
||||
// A road aligned with travel must not push a partner behind the300m floor.
|
||||
assert(pair({{1000,10,1250},{1000,10,1450}}));
|
||||
assert(a==vector(1000,10,1300)&&b==vector(1000,10,1360));
|
||||
assert(pair({{1000,10,1450},{1000,10,1250}}));
|
||||
assert(a==vector(1000,10,1300)&&b==vector(1000,10,1360));
|
||||
// Actual sloped segment retains height for subsequent bridge validation.
|
||||
assert(pair({{900,5,1300},{1100,15,1300}}));
|
||||
assert(a==vector(970,8.5,1300)&&b==vector(1030,11.5,1300));
|
||||
// Degenerate segments are harmless but a wholly degenerate road has no span.
|
||||
assert(pair({{900,10,1300},{900,10,1300},{1100,10,1300}}));
|
||||
assert(!pair({{1000,10,1300},{1000,10,1300}}));empty();
|
||||
assert(!pair({{900,10,1300},{NAN,10,1300},{1100,10,1300}}));empty();
|
||||
assert(!pair(Array<vector>(1025,{1000,10,1300})));empty();
|
||||
assert(!pair({}));empty();assert(!pair({{1000,10,1300}}));empty();
|
||||
auto good=Array<vector>{{900,10,1300},{1100,10,1300}};
|
||||
velocity={0,0,0};assert(!pair(good));empty();velocity={0,0,4};
|
||||
seed.x=NAN;assert(!pair(good));empty();seed={1000,10,1300};
|
||||
origin.x=NAN;assert(!pair(good));empty();origin={1000,10,1000};
|
||||
reserved={{970,10,1380}};assert(pair(good));
|
||||
reserved={{970,10,1379.99f}};assert(!pair(good));empty();
|
||||
reserved={{NAN,10,1380}};assert(!pair(good));empty();
|
||||
reserved=Array<vector>(129,{10000,10,10000});assert(!pair(good));empty();reserved.clear();
|
||||
for(float z:{1290.f,1901.f,700.f}){seed.z=z;assert(!pair({{900,10,z},{1100,10,z}}));empty();}seed.z=1300;
|
||||
BaseRoad road;road.points=good;game.ai.manager.roads={&road};
|
||||
int q=game.ai.manager.queries;
|
||||
assert(Rules::SelectPair(&belief,10000,reserved,a,b));assert(game.ai.manager.queries==q+1);
|
||||
assert(a==vector(970,10,1300)&&b==vector(1030,10,1300));
|
||||
assert(ERR_SettlementSafety::calls==2&&ERR_ConvoyPlacement::calls==2);
|
||||
auto select=[&](){a={1,2,3};b={4,5,6};bool ok=Rules::SelectPair(&belief,10000,reserved,a,b);if(!ok)empty();return ok;};
|
||||
// Second position cannot escape settlement, water/collision or bridge checks.
|
||||
int pc=ERR_ConvoyPlacement::calls;ERR_SettlementSafety::rightClear=false;assert(!select());assert(ERR_ConvoyPlacement::calls==pc);ERR_SettlementSafety::rightClear=true;
|
||||
pc=ERR_ConvoyPlacement::calls;ERR_ConvoyPlacement::rightClear=false;assert(!select());assert(ERR_ConvoyPlacement::calls==pc+2);ERR_ConvoyPlacement::rightClear=true;
|
||||
pc=ERR_ConvoyPlacement::calls;ERR_ConvoyPlacement::water=true;assert(!select());assert(ERR_ConvoyPlacement::calls==pc+1);ERR_ConvoyPlacement::water=false;
|
||||
pc=ERR_ConvoyPlacement::calls;ERR_Actors::rightHeight=7;assert(!select());assert(ERR_ConvoyPlacement::calls==pc);ERR_Actors::rightHeight=10;
|
||||
road.points={{900,20,1300},{1100,20,1300}};assert(!select());road.points=good;
|
||||
// Reject a pair assembled from two short unrelated roads, even when their
|
||||
// endpoints could have formed a geometrically spaced chord.
|
||||
BaseRoad left,right;left.points={{960,10,1300},{980,10,1300}};right.points={{1020,10,1300},{1040,10,1300}};game.ai.manager.roads={&left,&right};assert(!select());
|
||||
game.ai.manager.roads={&road};road.width=5.99;assert(!select());road.width=NAN;assert(!select());road.width=6;
|
||||
// All pre-query gates remain cheap; one permitted pair call is one query.
|
||||
q=game.ai.manager.queries;belief.HasVelocity=false;assert(!select());belief.HasVelocity=true;
|
||||
Replication::server=false;assert(!select());Replication::server=true;
|
||||
reserved.resize(129);assert(!select());reserved.clear();
|
||||
assert(game.ai.manager.queries==q);
|
||||
game.ai.available=false;assert(!select());game.ai.available=true;assert(game.ai.manager.queries==q);
|
||||
// Road count bound refuses before any point arrays are read.
|
||||
game.ai.manager.roads=Array<BaseRoad*>(129,&road);int reads=BaseRoad::reads;assert(!select());assert(BaseRoad::reads==reads);
|
||||
game.ai.manager.roads=Array<BaseRoad*>(128,&road);assert(select());assert(BaseRoad::reads==reads+128);
|
||||
// Total4096 segment allowance includes irrelevant/degenerate vertices; an
|
||||
// earlier valid pair cannot leak through once later roads exhaust the cap.
|
||||
BaseRoad big,tail;big.points=Array<vector>(1024,{900,10,1300});big.points.back()={1100,10,1300};tail.points=Array<vector>(5,{900,10,1300});tail.points.back()={1100,10,1300};
|
||||
game.ai.manager.roads={&big,&big,&big,&big,&tail};assert(select());
|
||||
tail.points.insert(tail.points.begin(),{900,10,1300});pc=ERR_ConvoyPlacement::calls;assert(!select());assert(ERR_ConvoyPlacement::calls==pc);
|
||||
// Pair failure still permits unchanged singleton selection in a separate
|
||||
// call; this explicitly consumes the caller's second survey budget.
|
||||
road.points={{995,10,1300},{1005,10,1300}};game.ai.manager.roads={&road};q=game.ai.manager.queries;
|
||||
assert(!select());vector singleton;assert(Rules::Select(&belief,10000,reserved,singleton));assert(singleton==vector(1000,10,1300)&&game.ai.manager.queries==q+2);
|
||||
}
|
||||
|
||||
''')
|
||||
print('PASS: actual paired arc geometry,40m/80m spacing, separate original polylines, both-position validation and128road/4096segment budget; no native driving proof')
|
||||
Reference in New Issue
Block a user