Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
128 lines
6.7 KiB
Diff
128 lines
6.7 KiB
Diff
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMineSite.c
|
|
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMineSite.c
|
|
@@ -9,7 +9,7 @@
|
|
return distance >= 25 && distance <= 60;
|
|
}
|
|
// Keep the nearest bounded candidates, with stable ties independent of native
|
|
- // road enumeration order. Candidates always belong to an actual road segment.
|
|
+ // road enumeration order. Candidates lie beside actual road segments, never a chord across a bend.
|
|
static void Keep(vector post, vector point, array<vector> candidates)
|
|
{
|
|
if (!candidates || candidates.Count() > 128 || !InBand(post, point) || candidates.Contains(point)) return;
|
|
@@ -35,6 +35,19 @@
|
|
if (first >= 0 && first <= 1) Keep(post, vector.Lerp(a, b, first), candidates);
|
|
if (second >= 0 && second <= 1) Keep(post, vector.Lerp(a, b, second), candidates);
|
|
}
|
|
+ // Use the existing finite circle/projection candidates on each road flank.
|
|
+ static void FromSegment(vector a, vector b, vector post, array<vector> candidates)
|
|
+ {
|
|
+ vector direction = b - a; direction[1] = 0;
|
|
+ float squared = vector.Dot(direction, direction);
|
|
+ if (!ERR_ForceRules.Finite(squared) || squared < 0.0001) return;
|
|
+ vector offset = post - a; offset[1] = 0;
|
|
+ float fraction = Math.Clamp(vector.Dot(offset, direction) / squared, 0, 1);
|
|
+ Keep(post, vector.Lerp(a, b, fraction), candidates);
|
|
+ Keep(post, a, candidates); Keep(post, b, candidates);
|
|
+ Crossings(a, b, post, 25.01, candidates);
|
|
+ Crossings(a, b, post, 59.99, candidates);
|
|
+ }
|
|
static bool FromPolyline(array<vector> points, vector post, array<vector> candidates)
|
|
{
|
|
if (!points || points.Count() < 2 || points.Count() > 1024 || !candidates || candidates.Count() > 128) return false;
|
|
@@ -43,17 +56,41 @@
|
|
for (int i = 1; i < points.Count(); i++)
|
|
{
|
|
vector a = points[i - 1], b = points[i];
|
|
+ // Canonical orientation makes flank/circle rounding independent of native
|
|
+ // polyline direction, so equally near candidates retain stable ties.
|
|
+ if (ERR_HQRoadIntercept.Better(0, b, 0, a))
|
|
+ { vector swap = a; a = b; b = swap; }
|
|
vector direction = b - a; direction[1] = 0;
|
|
float squared = vector.Dot(direction, direction);
|
|
if (!ERR_ForceRules.Finite(squared) || squared < 0.0001) continue;
|
|
- vector offset = post - a; offset[1] = 0;
|
|
- float fraction = Math.Clamp(vector.Dot(offset, direction) / squared, 0, 1);
|
|
- Keep(post, vector.Lerp(a, b, fraction), candidates);
|
|
- Keep(post, a, candidates); Keep(post, b, candidates);
|
|
- // One-centimetre inward margins keep rounding at circle intersections from
|
|
- // authorizing <25m placement or silently discarding the only valid crossing.
|
|
- Crossings(a, b, post, 25.01, candidates);
|
|
- Crossings(a, b, post, 59.99, candidates);
|
|
+ vector flank = Vector(-direction[2], 0, direction[0]) * (15.01 / Math.Sqrt(squared));
|
|
+ FromSegment(a + flank, b + flank, post, candidates);
|
|
+ FromSegment(a - flank, b - flank, post, candidates);
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ // Every queried road participates, including narrow roads and degenerate
|
|
+ // segment points. A bounded exhausted scan cannot authorize a candidate.
|
|
+ static bool FarFromRoads(vector point, array<vector> starts, array<vector> ends, inout int comparisons)
|
|
+ {
|
|
+ if (!ERR_ForceRules.FiniteVector(point) || comparisons < 0 || !starts || !ends || starts.IsEmpty() || starts.Count() != ends.Count() || starts.Count() > 4096) return false;
|
|
+ for (int i = 0; i < starts.Count(); i++)
|
|
+ {
|
|
+ if (comparisons >= 65536) return false;
|
|
+ comparisons++;
|
|
+ vector a = starts[i], b = ends[i];
|
|
+ if (!ERR_ForceRules.FiniteVector(a) || !ERR_ForceRules.FiniteVector(b)) return false;
|
|
+ if (ERR_HQRoadIntercept.Better(0, b, 0, a)) { vector swap = a; a = b; b = swap; }
|
|
+ vector direction = b - a; direction[1] = 0;
|
|
+ float squared = vector.Dot(direction, direction);
|
|
+ if (!ERR_ForceRules.Finite(squared)) return false;
|
|
+ float fraction;
|
|
+ if (squared > 0)
|
|
+ {
|
|
+ vector offset = point - a; offset[1] = 0;
|
|
+ fraction = Math.Clamp(vector.Dot(offset, direction) / squared, 0, 1);
|
|
+ }
|
|
+ if (vector.DistanceXZ(point, vector.Lerp(a, b, fraction)) < 15) return false;
|
|
}
|
|
return true;
|
|
}
|
|
@@ -63,21 +100,29 @@
|
|
if (!Replication.IsServer() || !ERR_ForceRules.FiniteVector(post) || !GetGame() || !GetGame().GetWorld()) return false;
|
|
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
|
if (!world || !world.GetRoadNetworkManager()) return false;
|
|
+ // Any road within15m of a point in the60m band intersects the75m
|
|
+ // horizontal neighbourhood. The80m query includes a5m margin; query once.
|
|
array<BaseRoad> roads = {};
|
|
- world.GetRoadNetworkManager().GetRoadsInAABB(post + "-60 -1000 -60", post + "60 1000 60", roads);
|
|
+ world.GetRoadNetworkManager().GetRoadsInAABB(post + "-80 -1000 -80", post + "80 1000 80", roads);
|
|
if (roads.Count() > 128) return false;
|
|
int segments;
|
|
- array<vector> candidates = {};
|
|
+ array<vector> candidates = {}, starts = {}, ends = {};
|
|
foreach (BaseRoad road : roads)
|
|
{
|
|
- if (!road || !ERR_ForceRules.Finite(road.GetWidth()) || road.GetWidth() < 4) continue;
|
|
+ if (!road || !ERR_ForceRules.Finite(road.GetWidth()) || road.GetWidth() < 0) return false;
|
|
array<vector> points = {}; road.GetPoints(points);
|
|
if (points.Count() < 2 || points.Count() > 1024) return false;
|
|
segments += points.Count() - 1;
|
|
- if (segments > 4096 || !FromPolyline(points, post, candidates)) return false;
|
|
+ if (segments > 4096) return false;
|
|
+ foreach (vector original : points) if (!ERR_ForceRules.FiniteVector(original)) return false;
|
|
+ for (int segment = 1; segment < points.Count(); segment++)
|
|
+ { starts.Insert(points[segment - 1]); ends.Insert(points[segment]); }
|
|
+ if (road.GetWidth() >= 4 && !FromPolyline(points, post, candidates)) return false;
|
|
}
|
|
- // Evaluate at most128 ground/body checks, nearest candidates first. A blocked
|
|
- // nearest point cannot hide another retained, physically valid road point.
|
|
+ // At most128 candidates,65536 cached segment comparisons and128 native
|
|
+ // settlement/ground/body checks. No per-candidate road queries.
|
|
+ int comparisons;
|
|
+ ERR_SettlementSafety settlements = new ERR_SettlementSafety();
|
|
while (!candidates.IsEmpty())
|
|
{
|
|
int best;
|
|
@@ -86,6 +131,9 @@
|
|
vector point = candidates[best]; candidates.Remove(best);
|
|
vector grounded = ERR_Actors.Ground(point);
|
|
if (!InBand(post, grounded) || Math.AbsFloat(grounded[1] - point[1]) > 2) continue;
|
|
+ if (!FarFromRoads(grounded, starts, ends, comparisons))
|
|
+ { if (comparisons >= 65536) return false; continue; }
|
|
+ if (!settlements.OutsideSettlements(grounded)) continue;
|
|
// BaseRoad has no bridge-type accessor: retain existing road-height and
|
|
// standability rejection, including water and physical bridge obstruction.
|
|
if (!ERR_HQRoadIntercept.Standable(grounded)) continue;
|