Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
110 lines
5.4 KiB
C
110 lines
5.4 KiB
C
// 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.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<vector> points, vector origin, vector velocity, vector seed, array<vector> 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<vector> 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<BaseRoad> 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<vector> 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;
|
|
}
|
|
}
|