// 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 points, vector origin, vector velocity, vector seed, array 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 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 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 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 points, vector origin, vector velocity, vector seed, array 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) 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 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 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 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; } }