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:
2026-10-13 22:52:53 +02:00
parent a951a96edc
commit 6a667d8deb
8104 changed files with 2124945 additions and 2061 deletions
@@ -0,0 +1,41 @@
{
"scope": "Recover owned original mine site policy in the existing finite delivered-post selector. No changes to actor/stock/task/attempt/controller behavior.",
"provenance": {
"original": "/home/jakub/devel/escapists/escapists-ai/Scripts/Game/EscapistsAI/EAI_HQRoadblocks.c",
"sha256": "128776f0b448162ee36cfc71b2a38d2cd06d6d6e4679253f215896aa3fcabb98",
"road_exclusion_line": 385,
"settlement_exclusion_line": 386,
"successor_baseline": "Centerline candidate tests codify existing implementation, but no explicit overriding user policy found. Shared ERR_SettlementSafety comment explicitly names mine sites as intended consumer."
},
"design": {
"positions": "Both \u00b115.01m parallel flanks of each actual eligible road segment; reuse bounded projection/endpoints/annulus-crossings.",
"post_distance_m": [
25,
60
],
"road_centerline_minimum_m": 15,
"settlement_exclusion_m": 200,
"query": "One AABB query\u00b180m XZ,\u00b11000mY. Every roadcenterline point within15m of a candidate<=60m frompost iswithin75m XZ; query adds5m margin.",
"hazards": "Every returned originalsegment inclnarrowroads andzero-length points; finite positive-length projection uses squared>0 toretain shortsegment endpoints.",
"bounds": {
"roads": 128,
"points_per_road": 1024,
"segments": 4096,
"candidates": 128,
"cached_segment_comparisons": 65536
},
"failure": "Unknown/malformed roads, bounds overflow, exhausted clearance budget or all rejected candidates returnfalse withresultzero. No partially checked candidate can succeed.",
"determinism": "Canonical local segment endpoints via existing Better(0,b,0,a) before offset generation and clearance projection; original native polylines remain unchanged."
},
"tests": "Final source d838f569: independent actual geometry harness passes; baseline compiles and fails at runtime. See /tmp/erri-mine-flank-review-0402/review.json and /tmp/erri-mine-flank-tests-0402.",
"limits": [
"Inherited policy is15m from road centerlines, not road edges: a very wide road is not guaranteed to be outside its carriageway.",
"Vertical query\u00b11000m, existing2mroad-groundheight check and Standable do not prove semantic bridge classification or native engineer walking/arming.",
"Bounded nearest-candidate retention may conservatively refuse despite a different unexamined feasible location.",
"65536 comparison cap is a bound, not a measured FPS/performance claim."
],
"target_edits": false,
"native_run": false,
"compiler_run_by_author": false,
"independent_review": "/tmp/erri-mine-flank-review-0402/review.json"
}
@@ -0,0 +1,127 @@
--- 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;
@@ -0,0 +1,4 @@
{
"addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMineSite.c": "d838f5693d4fa5d252d529e59777f560674c02532f1041a5598e938b3a850839",
"fix.patch": "a3fa4e97c5e6ce92790da0030c28a9b114b093f6f1655870cbb27275965ae061"
}