From 4ec5a4b34a813fc83a340ce41be912ae5d77bff2 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Mon, 27 Mar 2023 18:32:51 +0100 Subject: [PATCH] Fix reversed path searches from inaccessible locations. The Harvester trait and MoveAdjacentTo activity called the pathfinder but had a single source and multiple targets. The pathfinder interface only allows for the opposite: multiple sources and a single target. To work around this they would swap the inputs. This works in most cases but not all cases. One aspect of asymmetry is that an actor may move out of an inaccessible source cell, but not onto an inaccessible target cell. Searches that involved an inaccessible source cell and that applied this swapping method would therefore fail to return a path, when a valid path was possible. Although a rare case, once good way to reproduce is to use a production building that spawns actors on inaccessible cells around it, such as the RA naval yard. A move order uses the pathfinder correctly and the unit will move out. Using a force attack causes the unit to use the broken "swapped" mechanism in MoveAdjacentTo and it will be stuck. This asymmetry has been longstanding but the pathfinding infrastructure only sporadically accounted for it. It is now documented and applied consistently. Create a new overload on the pathfinder trait that allows a single source and multiple targets, so callers have an overload that does what they need and won't be tempted to swap the positions and run into this issue. Internally, this requires us to teach Locomotor to ignore the self actor when performing movement cost checks for these "in reverse" searches so the unit doesn't consider the cell blocked by itself. --- .../Activities/Move/MoveAdjacentTo.cs | 4 +- .../Pathfinder/DensePathGraph.cs | 21 +++-- .../Pathfinder/HierarchicalPathFinder.cs | 12 +-- OpenRA.Mods.Common/Pathfinder/IPathGraph.cs | 2 +- OpenRA.Mods.Common/Pathfinder/PathSearch.cs | 29 +++++- .../Pathfinder/SparsePathGraph.cs | 2 +- OpenRA.Mods.Common/Traits/Harvester.cs | 6 +- OpenRA.Mods.Common/Traits/Mobile.cs | 2 +- OpenRA.Mods.Common/Traits/World/Locomotor.cs | 20 ++-- OpenRA.Mods.Common/Traits/World/PathFinder.cs | 92 ++++++++++++++++++- OpenRA.Mods.Common/TraitsInterfaces.cs | 19 ++++ 11 files changed, 171 insertions(+), 38 deletions(-) diff --git a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs index 25366d9eed..4e055ed1f9 100644 --- a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs +++ b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs @@ -127,9 +127,7 @@ namespace OpenRA.Mods.Common.Activities if (searchCells.Count == 0) return PathFinder.NoPath; - var path = Mobile.PathFinder.FindPathToTargetCell(self, searchCells, loc, check); - path.Reverse(); - return path; + return Mobile.PathFinder.FindPathToTargetCells(self, loc, searchCells, check); } public override IEnumerable GetTargets(Actor self) diff --git a/OpenRA.Mods.Common/Pathfinder/DensePathGraph.cs b/OpenRA.Mods.Common/Pathfinder/DensePathGraph.cs index 57b0234ed2..7e04c39647 100644 --- a/OpenRA.Mods.Common/Pathfinder/DensePathGraph.cs +++ b/OpenRA.Mods.Common/Pathfinder/DensePathGraph.cs @@ -104,7 +104,7 @@ namespace OpenRA.Mods.Common.Pathfinder CVec.Directions.Exclude(new CVec(-1, -1)).ToArray(), // BR }; - public List GetConnections(CPos position) + public List GetConnections(CPos position, Func targetPredicate) { var layer = position.Layer; var info = this[position]; @@ -128,7 +128,7 @@ namespace OpenRA.Mods.Common.Pathfinder if (!IsValidNeighbor(neighbor)) continue; - var pathCost = GetPathCostToNode(position, neighbor, dir); + var pathCost = GetPathCostToNode(position, neighbor, dir, targetPredicate); if (pathCost != PathGraph.PathCostForInvalidPath && this[neighbor].Status != CellStatus.Closed) validNeighbors.Add(new GraphConnection(neighbor, pathCost)); @@ -147,7 +147,7 @@ namespace OpenRA.Mods.Common.Pathfinder var entryCost = cml.EntryMovementCost(locomotor.Info, layerPosition); if (entryCost != PathGraph.MovementCostForUnreachableCell && - CanEnterNode(position, layerPosition) && + CanEnterNode(position, layerPosition, targetPredicate) && this[layerPosition].Status != CellStatus.Closed) validNeighbors.Add(new GraphConnection(layerPosition, entryCost)); } @@ -159,7 +159,7 @@ namespace OpenRA.Mods.Common.Pathfinder { var exitCost = CustomMovementLayers[layer].ExitMovementCost(locomotor.Info, groundPosition); if (exitCost != PathGraph.MovementCostForUnreachableCell && - CanEnterNode(position, groundPosition) && + CanEnterNode(position, groundPosition, targetPredicate) && this[groundPosition].Status != CellStatus.Closed) validNeighbors.Add(new GraphConnection(groundPosition, exitCost)); } @@ -168,16 +168,23 @@ namespace OpenRA.Mods.Common.Pathfinder return validNeighbors; } - bool CanEnterNode(CPos srcNode, CPos destNode) + bool CanEnterNode(CPos srcNode, CPos destNode, Func targetPredicate) { return locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor) - != PathGraph.MovementCostForUnreachableCell; + != PathGraph.MovementCostForUnreachableCell || + (inReverse && targetPredicate(destNode)); } - int GetPathCostToNode(CPos srcNode, CPos destNode, CVec direction) + int GetPathCostToNode(CPos srcNode, CPos destNode, CVec direction, Func targetPredicate) { var movementCost = locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor); + + // When doing searches in reverse, we must allow movement onto an inaccessible target location. + // Because when reversed this is actually the source, and it is allowed to move out from an inaccessible source. + if (movementCost == PathGraph.MovementCostForUnreachableCell && inReverse && targetPredicate(destNode)) + movementCost = 0; + if (movementCost != PathGraph.MovementCostForUnreachableCell) return CalculateCellPathCost(destNode, direction, movementCost); diff --git a/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs b/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs index 0502e856b6..416d54d7ea 100644 --- a/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs +++ b/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs @@ -73,13 +73,13 @@ namespace OpenRA.Mods.Common.Pathfinder /// nodes, but uses a heuristic informed from the previous level to guide the search in the right direction. /// /// This implementation is aware of movement costs over terrain given by - /// . It is aware of + /// . It is aware of /// changes to the costs in terrain and able to update the abstract graph when this occurs. It is able to search /// the abstract graph as if had been specified. If /// is given in the constructor, the abstract graph will additionally /// account for a subset of immovable actors using the same rules as - /// . It will be aware of - /// changes to actors on the map and update the abstract graph when this occurs. Other types of blocking actors + /// . It will be aware + /// of changes to actors on the map and update the abstract graph when this occurs. Other types of blocking actors /// will not be accounted for in the heuristic. /// /// If the obstacle on the map is from terrain (e.g. a cliff or lake) the heuristic will work well. If the @@ -633,14 +633,14 @@ namespace OpenRA.Mods.Common.Pathfinder /// /// defines immovability based on the mobile trait. The blocking rules - /// in allow units to - /// pass these immovable actors if they are temporary blockers (e.g. gates) or crushable by the locomotor. + /// in allow units + /// to pass these immovable actors if they are temporary blockers (e.g. gates) or crushable by the locomotor. /// Since our abstract graph must work for any actor, we have to be conservative and can only consider a subset /// of the immovable actors in the graph - ones we know cannot be passed by some actors due to these rules. /// Both this and must be true for a cell to be blocked. /// /// This method is dependant on the logic in - /// and + /// and /// . This method must be kept in sync with changes in the locomotor /// rules. /// diff --git a/OpenRA.Mods.Common/Pathfinder/IPathGraph.cs b/OpenRA.Mods.Common/Pathfinder/IPathGraph.cs index 5c797b0ede..1b4fe6dd75 100644 --- a/OpenRA.Mods.Common/Pathfinder/IPathGraph.cs +++ b/OpenRA.Mods.Common/Pathfinder/IPathGraph.cs @@ -27,7 +27,7 @@ namespace OpenRA.Mods.Common.Pathfinder /// PERF: Returns a rather than an as enumerating /// this efficiently is important for pathfinding performance. Callers should interact with this as an /// and not mutate the result. - List GetConnections(CPos source); + List GetConnections(CPos source, Func targetPredicate); /// /// Gets or sets the pathfinding information for a given node. diff --git a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs index 3db8904ffe..33f60fab2f 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs @@ -46,7 +46,7 @@ namespace OpenRA.Mods.Common.Pathfinder var graph = new MapPathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, laneBias, false); var search = new PathSearch(graph, loc => 0, 0, targetPredicate, recorder); - AddInitialCells(world, locomotor, froms, customCost, search); + AddInitialCells(world, locomotor, self, froms, check, customCost, ignoreActor, false, search); return search; } @@ -71,11 +71,22 @@ namespace OpenRA.Mods.Common.Pathfinder heuristic ??= DefaultCostEstimator(locomotor, target); var search = new PathSearch(graph, heuristic, heuristicWeightPercentage, loc => loc == target, recorder); - AddInitialCells(world, locomotor, froms, customCost, search); + AddInitialCells(world, locomotor, self, froms, check, customCost, ignoreActor, inReverse, search); return search; } + /// + /// Determines if a cell is a valid pathfinding location. + /// + /// It is in the world. + /// It is either on the ground layer (0) or on an *enabled* custom movement layer. + /// It has not been excluded by the . + /// + /// If required, follow this with a call to + /// to + /// determine if the cell is accessible. + /// public static bool CellAllowsMovement(World world, Locomotor locomotor, CPos cell, Func customCost) { return world.Map.Contains(cell) && @@ -83,10 +94,18 @@ namespace OpenRA.Mods.Common.Pathfinder (customCost == null || customCost(cell) != PathGraph.PathCostForInvalidPath); } - static void AddInitialCells(World world, Locomotor locomotor, IEnumerable froms, Func customCost, PathSearch search) + static void AddInitialCells(World world, Locomotor locomotor, Actor self, IEnumerable froms, + BlockedByActor check, Func customCost, Actor ignoreActor, bool inReverse, PathSearch search) { + // A source cell is allowed to have an unreachable movement cost. + // Therefore we don't need to check if the cell is accessible, only that it allows movement. + // *Unless* the search is being done in reverse, in this case the source is really a target, + // and a target is required to have a reachable cost. + // We also need to ignore self, so we don't consider the location blocked by ourselves! foreach (var sl in froms) - if (CellAllowsMovement(world, locomotor, sl, customCost)) + if (CellAllowsMovement(world, locomotor, sl, customCost) && + (!inReverse || locomotor.MovementCostToEnterCell(self, sl, check, ignoreActor, true) + != PathGraph.MovementCostForUnreachableCell)) search.AddInitialCell(sl, customCost); } @@ -229,7 +248,7 @@ namespace OpenRA.Mods.Common.Pathfinder var currentInfo = Graph[currentMinNode]; Graph[currentMinNode] = new CellInfo(CellStatus.Closed, currentInfo.CostSoFar, currentInfo.EstimatedTotalCost, currentInfo.PreviousNode); - foreach (var connection in Graph.GetConnections(currentMinNode)) + foreach (var connection in Graph.GetConnections(currentMinNode, TargetPredicate)) { // Calculate the cost up to that point var costSoFarToNeighbor = currentInfo.CostSoFar + connection.Cost; diff --git a/OpenRA.Mods.Common/Pathfinder/SparsePathGraph.cs b/OpenRA.Mods.Common/Pathfinder/SparsePathGraph.cs index aeff9b4f6c..ae828259f8 100644 --- a/OpenRA.Mods.Common/Pathfinder/SparsePathGraph.cs +++ b/OpenRA.Mods.Common/Pathfinder/SparsePathGraph.cs @@ -32,7 +32,7 @@ namespace OpenRA.Mods.Common.Pathfinder info = new Dictionary(estimatedSearchSize); } - public List GetConnections(CPos position) + public List GetConnections(CPos position, Func targetPredicate) { return edges(position) ?? new List(); } diff --git a/OpenRA.Mods.Common/Traits/Harvester.cs b/OpenRA.Mods.Common/Traits/Harvester.cs index 2ce17433b4..bb180bc247 100644 --- a/OpenRA.Mods.Common/Traits/Harvester.cs +++ b/OpenRA.Mods.Common/Traits/Harvester.cs @@ -198,8 +198,8 @@ namespace OpenRA.Mods.Common.Traits return null; // Start a search from each refinery's delivery location: - var path = mobile.PathFinder.FindPathToTargetCell( - self, refineries.Select(r => r.Key), self.Location, BlockedByActor.None, + var path = mobile.PathFinder.FindPathToTargetCells( + self, self.Location, refineries.Select(r => r.Key), BlockedByActor.None, location => { if (!refineries.ContainsKey(location)) @@ -211,7 +211,7 @@ namespace OpenRA.Mods.Common.Traits }); if (path.Count > 0) - return refineries[path.Last()].Actor; + return refineries[path[0]].Actor; return null; } diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs index 31094a8677..f4848d9523 100644 --- a/OpenRA.Mods.Common/Traits/Mobile.cs +++ b/OpenRA.Mods.Common/Traits/Mobile.cs @@ -125,7 +125,7 @@ namespace OpenRA.Mods.Common.Traits .SingleOrDefault(l => l.Info.Name == Locomotor); return locomotor.MovementCostToEnterCell( - self, cell, check, ignoreActor, subCell) != PathGraph.MovementCostForUnreachableCell; + self, cell, check, ignoreActor, false, subCell) != PathGraph.MovementCostForUnreachableCell; } public bool CanStayInCell(World world, CPos cell) diff --git a/OpenRA.Mods.Common/Traits/World/Locomotor.cs b/OpenRA.Mods.Common/Traits/World/Locomotor.cs index 3512c6632b..91a62db6f2 100644 --- a/OpenRA.Mods.Common/Traits/World/Locomotor.cs +++ b/OpenRA.Mods.Common/Traits/World/Locomotor.cs @@ -204,30 +204,30 @@ namespace OpenRA.Mods.Common.Traits return terrainInfos[index].Speed; } - public short MovementCostToEnterCell(Actor actor, CPos destNode, BlockedByActor check, Actor ignoreActor, SubCell subCell = SubCell.FullCell) + public short MovementCostToEnterCell(Actor actor, CPos destNode, BlockedByActor check, Actor ignoreActor, bool ignoreSelf = false, SubCell subCell = SubCell.FullCell) { var cellCost = MovementCostForCell(destNode); if (cellCost == PathGraph.MovementCostForUnreachableCell || - !CanMoveFreelyInto(actor, destNode, subCell, check, ignoreActor)) + !CanMoveFreelyInto(actor, destNode, subCell, check, ignoreActor, ignoreSelf)) return PathGraph.MovementCostForUnreachableCell; return cellCost; } - public short MovementCostToEnterCell(Actor actor, CPos srcNode, CPos destNode, BlockedByActor check, Actor ignoreActor) + public short MovementCostToEnterCell(Actor actor, CPos srcNode, CPos destNode, BlockedByActor check, Actor ignoreActor, bool ignoreSelf = false) { var cellCost = MovementCostForCell(destNode, srcNode); if (cellCost == PathGraph.MovementCostForUnreachableCell || - !CanMoveFreelyInto(actor, destNode, SubCell.FullCell, check, ignoreActor)) + !CanMoveFreelyInto(actor, destNode, SubCell.FullCell, check, ignoreActor, ignoreSelf)) return PathGraph.MovementCostForUnreachableCell; return cellCost; } // Determines whether the actor is blocked by other Actors - bool CanMoveFreelyInto(Actor actor, CPos cell, SubCell subCell, BlockedByActor check, Actor ignoreActor) + bool CanMoveFreelyInto(Actor actor, CPos cell, SubCell subCell, BlockedByActor check, Actor ignoreActor, bool ignoreSelf) { // If the check allows: We are not blocked by other actors. if (check == BlockedByActor.None) @@ -260,7 +260,7 @@ namespace OpenRA.Mods.Common.Traits // Cache doesn't account for ignored actors, subcells, temporary blockers or transit only actors. // These must use the slow path. - if (ignoreActor == null && subCell == SubCell.FullCell && + if (ignoreActor == null && !ignoreSelf && subCell == SubCell.FullCell && !cellFlag.HasCellFlag(CellFlag.HasTemporaryBlocker) && !cellFlag.HasCellFlag(CellFlag.HasTransitOnlyActor)) { // We already know there are uncrushable actors in the cell so we are always blocked. @@ -282,7 +282,7 @@ namespace OpenRA.Mods.Common.Traits var otherActors = subCell == SubCell.FullCell ? world.ActorMap.GetActorsAt(cell) : world.ActorMap.GetActorsAt(cell, subCell); foreach (var otherActor in otherActors) - if (IsBlockedBy(actor, otherActor, ignoreActor, cell, check, cellFlag)) + if (IsBlockedBy(actor, otherActor, ignoreActor, ignoreSelf, cell, check, cellFlag)) return false; return true; @@ -303,7 +303,7 @@ namespace OpenRA.Mods.Common.Traits if (check > BlockedByActor.None) { - bool CheckTransient(Actor otherActor) => IsBlockedBy(self, otherActor, ignoreActor, cell, check, GetCache(cell).CellFlag); + bool CheckTransient(Actor otherActor) => IsBlockedBy(self, otherActor, ignoreActor, false, cell, check, GetCache(cell).CellFlag); if (!sharesCell) return world.ActorMap.AnyActorsAt(cell, SubCell.FullCell, CheckTransient) ? SubCell.Invalid : SubCell.FullCell; @@ -320,9 +320,9 @@ namespace OpenRA.Mods.Common.Traits /// This logic is replicated in and /// . If this method is updated please update those as /// well. - bool IsBlockedBy(Actor actor, Actor otherActor, Actor ignoreActor, CPos cell, BlockedByActor check, CellFlag cellFlag) + bool IsBlockedBy(Actor actor, Actor otherActor, Actor ignoreActor, bool ignoreSelf, CPos cell, BlockedByActor check, CellFlag cellFlag) { - if (otherActor == ignoreActor) + if (otherActor == ignoreActor || (ignoreSelf && otherActor == actor)) return false; var otherMobile = otherActor.OccupiesSpace as Mobile; diff --git a/OpenRA.Mods.Common/Traits/World/PathFinder.cs b/OpenRA.Mods.Common/Traits/World/PathFinder.cs index 1bbd16a3c0..732883a95e 100644 --- a/OpenRA.Mods.Common/Traits/World/PathFinder.cs +++ b/OpenRA.Mods.Common/Traits/World/PathFinder.cs @@ -76,15 +76,99 @@ namespace OpenRA.Mods.Common.Traits /// The shortest path between a source and the target is returned. /// /// - /// Searches that provide a multiple source cells are slower than those than provide only a single source cell, + /// + /// It is allowed for an actor to occupy an inaccessible space and move out of it if another adjacent cell is + /// accessible, but it is not allowed to move into an inaccessible target space. Therefore it is vitally + /// important to not mix up the source and target locations. A path can exist from an inaccessible source space + /// to an accessible target space, but if those parameters as swapped then no path can exist. + /// + /// + /// Searches that provide multiple source cells are slower than those than provide only a single source cell, /// as optimizations are possible for the single source case. Use searches from multiple source cells /// sparingly. + /// /// public List FindPathToTargetCell( Actor self, IEnumerable sources, CPos target, BlockedByActor check, Func customCost = null, Actor ignoreActor = null, bool laneBias = true) + { + return FindPathToTarget(self, sources, target, check, customCost, ignoreActor, laneBias); + } + + /// + /// Calculates a path for the actor from source to multiple possible targets. + /// Returned path is *reversed* and given target to source. + /// The shortest path between the source and a target is returned. + /// + /// + /// + /// It is allowed for an actor to occupy an inaccessible space and move out of it if another adjacent cell is + /// accessible, but it is not allowed to move into an inaccessible target space. Therefore it is vitally + /// important to not mix up the source and target locations. A path can exist from an inaccessible source space + /// to an accessible target space, but if those parameters as swapped then no path can exist. + /// + /// + /// Searches that provide multiple target cells are slower than those than provide only a single target cell, + /// as optimizations are possible for the single target case. Use searches to multiple target cells + /// sparingly. + /// + /// + public List FindPathToTargetCells( + Actor self, CPos source, IEnumerable targets, BlockedByActor check, + Func customCost = null, + Actor ignoreActor = null, + bool laneBias = true) + { + // We can reuse existing search infrastructure by swapping the single source and multiple targets, + // and calling the existing methods that allow multiple sources and one target. + // However there is a case of asymmetry we must handle, an actor may move out of a inaccessible source, + // but may not move onto a inaccessible target. We must account for this when performing the swap. + + // As targets must be accessible, determine accessible targets in advance so when they becomes the sources + // we don't accidentally allow an inaccessible position to become viable. + var locomotor = GetActorLocomotor(self); + var accessibleTargets = targets + .Where(target => + PathSearch.CellAllowsMovement(self.World, locomotor, target, customCost) + && locomotor.MovementCostToEnterCell(self, target, check, ignoreActor, true) != PathGraph.MovementCostForUnreachableCell) + .ToList(); + if (accessibleTargets.Count == 0) + return NoPath; + + // When checking if the source location is accessible, we must also ignore self. + // So that when it becomes a target, we don't consider the location blocked by ourselves! + List path; + var sourceIsAccessible = + PathSearch.CellAllowsMovement(self.World, locomotor, source, customCost) + && locomotor.MovementCostToEnterCell(self, source, check, ignoreActor, true) != PathGraph.MovementCostForUnreachableCell; + if (sourceIsAccessible) + { + // As both ends are accessible, we can freely swap them. + path = FindPathToTarget(self, targets, source, check, customCost, ignoreActor, laneBias); + } + else + { + // When we treat the source as a target, we need to be able to path to it. + // We know this would fail as it is inaccessible but we need an exception to be made. + // A hierarchical path search doesn't support this ability, + // but the local pathfinder can deal with it when doing reverse searches. + pathFinderOverlay?.NewRecording(self, accessibleTargets, source); + using (var search = PathSearch.ToTargetCell( + world, locomotor, self, accessibleTargets, source, check, DefaultHeuristicWeightPercentage, + customCost, ignoreActor, laneBias, inReverse: true, recorder: pathFinderOverlay?.RecordLocalEdges(self))) + path = search.FindPath(); + } + + // Since we swapped the positions, we need to reverse the path to swap it back. + path.Reverse(); + return path; + } + + List FindPathToTarget( + Actor self, IEnumerable sources, CPos target, BlockedByActor check, + Func customCost, Actor ignoreActor, bool laneBias) { var sourcesList = sources.ToList(); if (sourcesList.Count == 0) @@ -161,6 +245,12 @@ namespace OpenRA.Mods.Common.Traits /// Only terrain is taken into account, i.e. as if was given. /// This would apply for any actor using the given . /// + /// + /// It is allowed for an actor to occupy an inaccessible space and move out of it if another adjacent cell is + /// accessible, but it is not allowed to move into an inaccessible target space. Therefore it is vitally + /// important to not mix up the source and target locations. A path can exist from an inaccessible source space + /// to an accessible target space, but if those parameters as swapped then no path can exist. + /// public bool PathExistsForLocomotor(Locomotor locomotor, CPos source, CPos target) { return hierarchicalPathFindersBlockedByNoneByLocomotor[locomotor].PathExists(source, target); diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index 96fcf6397c..2a44051d9f 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -797,12 +797,29 @@ namespace OpenRA.Mods.Common.Traits /// Returned path is *reversed* and given target to source. /// The shortest path between a source and the target is returned. /// + /// Path searches are not guaranteed to by symmetric, + /// the source and target locations cannot be swapped. + /// Call instead. List FindPathToTargetCell( Actor self, IEnumerable sources, CPos target, BlockedByActor check, Func customCost = null, Actor ignoreActor = null, bool laneBias = true); + /// + /// Calculates a path for the actor from source to multiple possible targets. + /// Returned path is *reversed* and given target to source. + /// The shortest path between the source and a target is returned. + /// + /// Path searches are not guaranteed to by symmetric, + /// the source and target locations cannot be swapped. + /// Call instead. + List FindPathToTargetCells( + Actor self, CPos source, IEnumerable targets, BlockedByActor check, + Func customCost = null, + Actor ignoreActor = null, + bool laneBias = true); + /// /// Calculates a path for the actor from multiple possible sources, whilst searching for an acceptable target. /// Returned path is *reversed* and given target to source. @@ -819,6 +836,8 @@ namespace OpenRA.Mods.Common.Traits /// Only terrain is taken into account, i.e. as if was given. /// This would apply for any actor using the given . /// + /// Path searches are not guaranteed to by symmetric, + /// the source and target locations cannot be swapped. bool PathExistsForLocomotor(Locomotor locomotor, CPos source, CPos target); } }