--- 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 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 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 points, vector post, array 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 starts, array 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 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 candidates = {}; + array 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 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;