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); } }