// All target coordinates come from delivered HQ observations, never a player's // current transform. Road projection stops at an unknown junction/road end. class ERR_MortarPrediction { // Read immediately after a failed synchronous prediction; no extra world scan. static string FailureReason; // Includes observation uplink, HQ downlink and native aiming. All stages // retain the original observation time; radio delivery never renews it. static const float MAX_REPORT_AGE_MS = 60000; static bool Fresh(ERR_HQReport report, float now) { return report && report.Target && !report.Distress && now >= report.ObservedAt && now - report.ObservedAt <= MAX_REPORT_AGE_MS && report.TravelMode != "aircraft"; } static bool Reject(string reason) { FailureReason = reason; return false; } static vector ClosestOnSegment(vector point, vector start, vector end) { vector segment = end - start; segment[1] = 0; vector offset = point - start; offset[1] = 0; float lengthSq = segment.LengthSq(); if (lengthSq < 0.01) return start; return vector.Lerp(start, end, Math.Clamp(vector.Dot(offset, segment) / lengthSq, 0, 1)); } static bool FollowRoad(array points, vector observed, vector velocity, float seconds, out vector impact) { if (!points || points.Count() < 2) return Reject("road polyline has fewer than two points"); int segment = -1; float closest = 20; vector projected; for (int i = 0; i < points.Count() - 1; i++) { vector candidate = ClosestOnSegment(observed, points[i], points[i + 1]); float distance = vector.DistanceXZ(candidate, observed); if (distance < closest) { closest = distance; segment = i; projected = candidate; } } if (segment < 0) return Reject("observed vehicle is not within 20m of a road segment"); vector forward = points[segment + 1] - points[segment]; forward[1] = 0; forward.Normalize(); vector travel = velocity; travel[1] = 0; float speed = travel.Length(); travel.Normalize(); float alignment = vector.Dot(forward, travel); if (Math.AbsFloat(alignment) < 0.7) return Reject("vehicle heading crosses road; alignment below 0.7"); int step = 1; int next = segment + 1; if (alignment < 0) { step = -1; next = segment; } float remaining = speed * seconds; vector position = projected; while (next >= 0 && next < points.Count()) { float length = vector.DistanceXZ(position, points[next]); if (length >= remaining && length > 0.01) { impact = vector.Lerp(position, points[next], remaining / length); return true; } remaining -= length; position = points[next]; next += step; } return Reject("prediction reaches unknown road end/junction"); } static bool Predict(ERR_HQReport report, float now, float flightSeconds, out vector impact) { FailureReason = ""; if (!report) return Reject("target report missing"); if (report.Distress) return Reject("distress report cannot authorize fire"); if (now < report.ObservedAt) return Reject("observation timestamp is in the future"); if (now - report.ObservedAt > MAX_REPORT_AGE_MS) return Reject("observation older than 60s"); float seconds = (now - report.ObservedAt) / 1000 + flightSeconds; if (seconds < 0 || seconds > 90) return Reject("observation age plus flight outside 0..90s"); if (report.TravelMode == "aircraft") return Reject("aircraft target unsupported"); if (report.TravelMode == "mixed travel") return Reject("mixed travel cannot be predicted"); impact = report.Position; if (!report.HasVelocity || report.Velocity.Length() < 0.5) return true; if (report.Velocity.Length() * seconds > 1800) return Reject("predicted travel exceeds 1800m"); bool vehicle = report.TravelMode == "vehicle" || report.TravelMode == "in vehicle"; if (vehicle) { SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld()); if (!world || !world.GetRoadNetworkManager()) return Reject("road network unavailable"); BaseRoad road; float roadDistance; world.GetRoadNetworkManager().GetClosestRoad(report.Position, road, roadDistance, skipNavlinks: true); if (!road) return Reject("no road near reported vehicle position"); if (roadDistance > 20) return Reject("reported vehicle farther than 20m from road"); array points = {}; road.GetPoints(points); return FollowRoad(points, report.Position, report.Velocity, seconds, impact); } impact += report.Velocity * seconds; return true; } static bool ThreatensFriendly(vector impact, vector position, vector velocity, float flightSeconds) { velocity[1] = 0; vector future = position + velocity * Math.Clamp(flightSeconds, 0, 90); // Protect the entire predicted path, not only where a soldier is now. return vector.DistanceXZ(impact, ClosestOnSegment(impact, position, future)) <= 100; } }