#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Linq; using OpenRA.Mods.Common.Traits; namespace OpenRA.Mods.Common { public static class WorldExtensions { /// /// Filters by only returning those that can be reached as the target of a path from /// . Only terrain is taken into account, i.e. as if /// was given. /// is used to define locations around each actor in /// of which one must be reachable. /// public static IEnumerable<(Actor Actor, WVec[] ReachableOffsets)> WithPathFrom( this IEnumerable actors, Actor sourceActor, Func targetOffsets) { if (sourceActor.Info.HasTraitInfo()) return actors.Select(a => (a, targetOffsets(a))); var mobile = sourceActor.TraitOrDefault(); if (mobile == null) return Enumerable.Empty<(Actor Actor, WVec[] ReachableOffsets)>(); var pathFinder = sourceActor.World.WorldActor.Trait(); var locomotor = mobile.Locomotor; var map = sourceActor.World.Map; return actors .Select(a => { return (a, targetOffsets(a).Where(offset => pathFinder.PathExistsForLocomotor( mobile.Locomotor, map.CellContaining(sourceActor.CenterPosition), map.CellContaining(a.CenterPosition + offset))) .ToArray()); }) .Where(x => x.ReachableOffsets.Length > 0); } /// /// Filters by only returning those that can be reached as the target of a path from /// . Only terrain is taken into account, i.e. as if /// was given. /// public static IEnumerable WithPathFrom(this IEnumerable actors, Actor sourceActor) { return actors.WithPathFrom(sourceActor, _ => new[] { WVec.Zero }).Select(x => x.Actor); } /// /// Of that can be reached as the target of a path from /// , returns the nearest by comparing their . /// Only terrain is taken into account, i.e. as if was given. /// is used to define locations around each actor in /// of which one must be reachable. /// public static Actor ClosestToWithPathFrom( this IEnumerable actors, Actor sourceActor, Func targetOffsets = null) { return actors .WithPathFrom(sourceActor, targetOffsets ?? (_ => new[] { WVec.Zero })) .Select(x => x.Actor) .ClosestToIgnoringPath(sourceActor); } /// /// Of that can be reached as the target of a path from /// , returns the nearest by comparing the . /// Only terrain is taken into account, i.e. as if was given. /// public static WPos? ClosestToWithPathFrom(this IEnumerable positions, Actor sourceActor) { if (sourceActor.Info.HasTraitInfo()) return positions.ClosestToIgnoringPath(sourceActor.CenterPosition); var mobile = sourceActor.TraitOrDefault(); if (mobile == null) return null; var pathFinder = sourceActor.World.WorldActor.Trait(); var locomotor = mobile.Locomotor; var map = sourceActor.World.Map; return positions .Where(p => pathFinder.PathExistsForLocomotor( locomotor, map.CellContaining(sourceActor.CenterPosition), map.CellContaining(p))) .ClosestToIgnoringPath(sourceActor.CenterPosition); } /// /// Filters by only returning those where the can /// be reached as the target of a path from the actor. Only terrain is taken into account, i.e. as if /// was given. /// public static IEnumerable WithPathTo(this IEnumerable actors, World world, WPos targetPosition) { var pathFinder = world.WorldActor.Trait(); var map = world.Map; return actors .Where(a => { if (a.Info.HasTraitInfo()) return true; var mobile = a.TraitOrDefault(); if (mobile == null) return false; return pathFinder.PathExistsForLocomotor( mobile.Locomotor, map.CellContaining(targetPosition), map.CellContaining(a.CenterPosition)); }); } /// /// Filters by only returning those where any of the /// can be reached as the target of a path from the actor. /// Returns the reachable target positions for each actor. /// Only terrain is taken into account, i.e. as if was given. /// public static IEnumerable<(Actor Actor, WPos[] ReachablePositions)> WithPathToAny( this IEnumerable actors, World world, Func targetPositions) { var pathFinder = world.WorldActor.Trait(); var map = world.Map; return actors .Select(a => { if (a.Info.HasTraitInfo()) return (a, targetPositions(a).ToArray()); var mobile = a.TraitOrDefault(); if (mobile == null) return (a, Array.Empty()); return (a, targetPositions(a).Where(targetPosition => pathFinder.PathExistsForLocomotor( mobile.Locomotor, map.CellContaining(targetPosition), map.CellContaining(a.CenterPosition))) .ToArray()); }) .Where(x => x.ReachablePositions.Length > 0); } /// /// Filters by only returning those where the can be /// reached as the target of a path from the actor. Only terrain is taken into account, i.e. as if /// was given. /// public static IEnumerable WithPathTo(this IEnumerable actors, Actor targetActor) { return actors.WithPathTo(targetActor.World, targetActor.CenterPosition); } /// /// Of where the can be reached as the target of a /// path from the actor, returns the nearest by comparing the . /// Only terrain is taken into account, i.e. as if was given. /// public static Actor ClosestToWithPathTo(this IEnumerable actors, World world, WPos targetPosition) { return actors .WithPathTo(world, targetPosition) .ClosestToIgnoringPath(targetPosition); } /// /// Of where any of the can be reached as the /// target of a path from the actor, returns the nearest by comparing the . /// Only terrain is taken into account, i.e. as if was given. /// public static Actor ClosestToWithPathToAny( this IEnumerable actors, World world, Func targetPositions) { return actors .WithPathToAny(world, targetPositions) .MinByOrDefault(x => x.ReachablePositions.Min(pos => (x.Actor.CenterPosition - pos).LengthSquared)) .Actor; } /// /// Of where the can be reached as the target of a /// path from the actor, returns the nearest by comparing their . /// Only terrain is taken into account, i.e. as if was given. /// public static Actor ClosestToWithPathTo(this IEnumerable actors, Actor targetActor) { return actors.ClosestToWithPathTo(targetActor.World, targetActor.CenterPosition); } /// /// Finds all the actors of which their health radius is intersected by a line (with a definable width) between two points. /// /// The engine world the line intersection is to be done in. /// The position the line should start at. /// The position the line should end at. /// How close an actor's health radius needs to be to the line to be considered 'intersected' by the line. /// If set, only considers the size of actors that have an /// trait which may improve search performance. However does NOT filter the returned actors on this trait. /// A list of all the actors intersected by the line. public static IEnumerable FindActorsOnLine( this World world, WPos lineStart, WPos lineEnd, WDist lineWidth, bool onlyBlockers = false) { // This line intersection check is done by first just finding all actors within a square that starts at the source, and ends at the target. // Then we iterate over this list, and find all actors for which their health radius is at least within lineWidth of the line. // For actors without a health radius, we simply check their center point. // The square in which we select all actors must be large enough to encompass the entire line's width. // xDir and yDir must never be 0, otherwise the overscan will be 0 in the respective direction. var xDiff = lineEnd.X - lineStart.X; var yDiff = lineEnd.Y - lineStart.Y; var xDir = xDiff < 0 ? -1 : 1; var yDir = yDiff < 0 ? -1 : 1; var dir = new WVec(xDir, yDir, 0); var largestValidActorRadius = onlyBlockers ? world.ActorMap.LargestBlockingActorRadius.Length : world.ActorMap.LargestActorRadius.Length; var overselect = dir * (1024 + lineWidth.Length + largestValidActorRadius); var finalTarget = lineEnd + overselect; var finalSource = lineStart - overselect; var actorsInSquare = world.ActorMap.ActorsInBox(finalTarget, finalSource); var intersectedActors = new List(); foreach (var currActor in actorsInSquare) { var actorWidth = 0; // PERF: Avoid using TraitsImplementing that needs to find the actor in the trait dictionary. foreach (var targetPos in currActor.EnabledTargetablePositions) if (targetPos is HitShape hitshape) actorWidth = Math.Max(actorWidth, hitshape.Info.Type.OuterRadius.Length); var projection = lineStart.MinimumPointLineProjection(lineEnd, currActor.CenterPosition); var distance = (currActor.CenterPosition - projection).HorizontalLength; var maxReach = actorWidth + lineWidth.Length; if (distance <= maxReach) intersectedActors.Add(currActor); } return intersectedActors; } public static IEnumerable FindBlockingActorsOnLine(this World world, WPos lineStart, WPos lineEnd, WDist lineWidth) { return world.FindActorsOnLine(lineStart, lineEnd, lineWidth, true); } /// /// Finds all the actors of which their health radius might be intersected by a specified circle. /// public static IEnumerable FindActorsOnCircle(this World world, WPos origin, WDist r) { return world.FindActorsInCircle(origin, r + world.ActorMap.LargestActorRadius); } /// /// Find the point (D) on a line (A-B) that is closest to the target point (C). /// /// The source point (tail) of the line. /// The target point (head) of the line. /// The target point that the minimum distance should be found to. /// The WPos that is the point on the line that is closest to the target point. public static WPos MinimumPointLineProjection(this WPos lineStart, WPos lineEnd, WPos point) { var squaredLength = (lineEnd - lineStart).HorizontalLengthSquared; // Line has zero length, so just use the lineEnd position as the closest position. if (squaredLength == 0) return lineEnd; // Consider the line extending the segment, parameterized as target + t (source - target). // We find projection of point onto the line. // It falls where t = [(point - target) . (source - target)] / |source - target|^2 // The normal DotProduct math would be (xDiff + yDiff) / dist, where dist = (target - source).LengthSquared; // But in order to avoid floating points, we do not divide here, but rather work with the large numbers as far as possible. // We then later divide by dist, only AFTER we have multiplied by the dot product. var xDiff = ((long)point.X - lineEnd.X) * (lineStart.X - lineEnd.X); var yDiff = ((long)point.Y - lineEnd.Y) * (lineStart.Y - lineEnd.Y); var t = xDiff + yDiff; // Beyond the 'target' end of the segment if (t < 0) return lineEnd; // Beyond the 'source' end of the segment if (t > squaredLength) return lineStart; // Projection falls on the segment return WPos.Lerp(lineEnd, lineStart, t, squaredLength); } } }