#region Copyright & License Information /* * Copyright 2007-2015 The OpenRA Developers (see AUTHORS) * 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. For more information, * see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using OpenRA.Mods.Common.Pathfinder; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("Calculates routes for mobile units based on the A* search algorithm.", " Attach this to the world actor.")] public class PathFinderInfo : ITraitInfo { public object Create(ActorInitializer init) { return new PathFinderUnitPathCacheDecorator(new PathFinder(init.World), new PathCacheStorage(init.World)); } } public interface IPathFinder { /// /// Calculates a path for the actor from source to destination /// /// A path from start to target List FindUnitPath(CPos source, CPos target, Actor self); List FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WDist range, Actor self); /// /// Calculates a path given a search specification /// List FindPath(IPathSearch search); /// /// Calculates a path given two search specifications, and /// then returns a path when both search intersect each other /// TODO: This should eventually disappear /// List FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest); } public class PathFinder : IPathFinder { static readonly List EmptyPath = new List(0); readonly World world; public PathFinder(World world) { this.world = world; } public List FindUnitPath(CPos source, CPos target, Actor self) { var mi = self.Info.TraitInfo(); // If a water-land transition is required, bail early var domainIndex = world.WorldActor.TraitOrDefault(); if (domainIndex != null) { var passable = mi.GetMovementClass(world.TileSet); if (!domainIndex.IsPassable(source, target, (uint)passable)) return EmptyPath; } List pb; using (var fromSrc = PathSearch.FromPoint(world, mi, self, target, source, true)) using (var fromDest = PathSearch.FromPoint(world, mi, self, source, target, true).Reverse()) pb = FindBidiPath(fromSrc, fromDest); CheckSanePath2(pb, source, target); return pb; } public List FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WDist range, Actor self) { var mi = self.Info.TraitInfo(); var targetCell = world.Map.CellContaining(target); // Correct for SubCell offset target -= world.Map.OffsetOfSubCell(srcSub); // Select only the tiles that are within range from the requested SubCell // This assumes that the SubCell does not change during the path traversal var tilesInRange = world.Map.FindTilesInCircle(targetCell, range.Length / 1024 + 1) .Where(t => (world.Map.CenterOfCell(t) - target).LengthSquared <= range.LengthSquared && mi.CanEnterCell(self.World, self, t)); // See if there is any cell within range that does not involve a cross-domain request // Really, we only need to check the circle perimeter, but it's not clear that would be a performance win var domainIndex = world.WorldActor.TraitOrDefault(); if (domainIndex != null) { var passable = mi.GetMovementClass(world.TileSet); tilesInRange = new List(tilesInRange.Where(t => domainIndex.IsPassable(source, t, (uint)passable))); if (!tilesInRange.Any()) return EmptyPath; } using (var fromSrc = PathSearch.FromPoints(world, mi, self, tilesInRange, source, true)) using (var fromDest = PathSearch.FromPoint(world, mi, self, source, targetCell, true).Reverse()) return FindBidiPath(fromSrc, fromDest); } public List FindPath(IPathSearch search) { var dbg = world.WorldActor.TraitOrDefault(); if (dbg != null && dbg.Visible) search.Debug = true; List path = null; while (search.CanExpand) { var p = search.Expand(); if (search.IsTarget(p)) { path = MakePath(search.Graph, p); break; } } if (dbg != null && dbg.Visible) dbg.AddLayer(search.Considered, search.MaxCost, search.Owner); search.Graph.Dispose(); if (path != null) return path; // no path exists return EmptyPath; } // Searches from both ends toward each other. This is used to prevent blockings in case we find // units in the middle of the path that prevent us to continue. public List FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest) { List path = null; var dbg = world.WorldActor.TraitOrDefault(); if (dbg != null && dbg.Visible) { fromSrc.Debug = true; fromDest.Debug = true; } while (fromSrc.CanExpand && fromDest.CanExpand) { // make some progress on the first search var p = fromSrc.Expand(); if (fromDest.Graph[p].Status == CellStatus.Closed && fromDest.Graph[p].CostSoFar < int.MaxValue) { path = MakeBidiPath(fromSrc, fromDest, p); break; } // make some progress on the second search var q = fromDest.Expand(); if (fromSrc.Graph[q].Status == CellStatus.Closed && fromSrc.Graph[q].CostSoFar < int.MaxValue) { path = MakeBidiPath(fromSrc, fromDest, q); break; } } if (dbg != null && dbg.Visible) { dbg.AddLayer(fromSrc.Considered, fromSrc.MaxCost, fromSrc.Owner); dbg.AddLayer(fromDest.Considered, fromDest.MaxCost, fromDest.Owner); } fromSrc.Graph.Dispose(); fromDest.Graph.Dispose(); if (path != null) return path; return EmptyPath; } // Build the path from the destination. When we find a node that has the same previous // position than itself, that node is the source node. static List MakePath(IGraph cellInfo, CPos destination) { var ret = new List(); var currentNode = destination; while (cellInfo[currentNode].PreviousPos != currentNode) { ret.Add(currentNode); currentNode = cellInfo[currentNode].PreviousPos; } ret.Add(currentNode); CheckSanePath(ret); return ret; } static List MakeBidiPath(IPathSearch a, IPathSearch b, CPos confluenceNode) { var ca = a.Graph; var cb = b.Graph; var ret = new List(); var q = confluenceNode; while (ca[q].PreviousPos != q) { ret.Add(q); q = ca[q].PreviousPos; } ret.Add(q); ret.Reverse(); q = confluenceNode; while (cb[q].PreviousPos != q) { q = cb[q].PreviousPos; ret.Add(q); } CheckSanePath(ret); return ret; } [Conditional("SANITY_CHECKS")] static void CheckSanePath(IList path) { if (path.Count == 0) return; var prev = path[0]; foreach (var cell in path) { var d = cell - prev; if (Math.Abs(d.X) > 1 || Math.Abs(d.Y) > 1) throw new InvalidOperationException("(PathFinder) path sanity check failed"); prev = cell; } } [Conditional("SANITY_CHECKS")] static void CheckSanePath2(IList path, CPos src, CPos dest) { if (path.Count == 0) return; if (path[0] != dest) throw new InvalidOperationException("(PathFinder) sanity check failed: doesn't go to dest"); if (path[path.Count - 1] != src) throw new InvalidOperationException("(PathFinder) sanity check failed: doesn't come from src"); } } }