Files
EscapistsReintroduced/docs/evidence/mine-flanks-2026-10-03/author/fix.patch
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

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;