diff --git a/OpenRA.Game/Actor.cs b/OpenRA.Game/Actor.cs index 1dbc155022..e1e29951d8 100644 --- a/OpenRA.Game/Actor.cs +++ b/OpenRA.Game/Actor.cs @@ -22,14 +22,32 @@ using OpenRA.Traits; namespace OpenRA { - public class Actor : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding, IEquatable + public interface IActor + { + ActorInfo Info { get; } + IWorld World { get; } + uint ActorID { get; } + Player Owner { get; set; } + + T TraitOrDefault(); + T Trait(); + IEnumerable TraitsImplementing(); + + IEnumerable Render(WorldRenderer wr); + } + + public class Actor : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding, IEquatable, IActor { public readonly ActorInfo Info; - public readonly World World; - public readonly uint ActorID; + ActorInfo IActor.Info { get { return this.Info; } } - [Sync] - public Player Owner; + public readonly World World; + IWorld IActor.World { get { return World; } } + + public readonly uint ActorID; + uint IActor.ActorID { get { return this.ActorID; } } + + [Sync] public Player Owner { get; set; } public bool IsInWorld { get; internal set; } public bool Destroyed { get; private set; } @@ -235,7 +253,7 @@ namespace OpenRA { World.AddFrameEndTask(w => { - if (this.Destroyed) + if (Destroyed) return; var oldOwner = Owner; diff --git a/OpenRA.Game/CacheStorage.cs b/OpenRA.Game/CacheStorage.cs new file mode 100644 index 0000000000..e5216a9639 --- /dev/null +++ b/OpenRA.Game/CacheStorage.cs @@ -0,0 +1,19 @@ +#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 + +namespace OpenRA +{ + public interface ICacheStorage + { + void Remove(string key); + void Store(string key, T data); + T Retrieve(string key); + } +} \ No newline at end of file diff --git a/OpenRA.Game/LogProxy.cs b/OpenRA.Game/LogProxy.cs new file mode 100644 index 0000000000..0f313470aa --- /dev/null +++ b/OpenRA.Game/LogProxy.cs @@ -0,0 +1,25 @@ +#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 + +namespace OpenRA +{ + public interface ILog + { + void Write(string channel, string format, params object[] args); + } + + public class LogProxy : ILog + { + public void Write(string channel, string format, params object[] args) + { + Log.Write(channel, format, args); + } + } +} diff --git a/OpenRA.Game/Map/CellLayer.cs b/OpenRA.Game/Map/CellLayer.cs index 8dedaa1f21..272b6a28bc 100644 --- a/OpenRA.Game/Map/CellLayer.cs +++ b/OpenRA.Game/Map/CellLayer.cs @@ -24,7 +24,7 @@ namespace OpenRA readonly T[] entries; - public CellLayer(Map map) + public CellLayer(IMap map) : this(map.TileShape, new Size(map.MapSize.X, map.MapSize.Y)) { } public CellLayer(TileShape shape, Size size) @@ -45,6 +45,21 @@ namespace OpenRA Array.Copy(anotherLayer.entries, entries, entries.Length); } + public static CellLayer CreateInstance(Func initialCellValueFactory, Size size, TileShape tileShape) + { + var cellLayer = new CellLayer(tileShape, size); + for (var v = 0; v < size.Height; v++) + { + for (var u = 0; u < size.Width; u++) + { + var mpos = new MPos(u, v); + cellLayer[mpos] = initialCellValueFactory(mpos); + } + } + + return cellLayer; + } + // Resolve an array index from cell coordinates int Index(CPos cell) { diff --git a/OpenRA.Game/Map/Map.cs b/OpenRA.Game/Map/Map.cs index f1b6dc4fbd..54586196eb 100644 --- a/OpenRA.Game/Map/Map.cs +++ b/OpenRA.Game/Map/Map.cs @@ -111,12 +111,28 @@ namespace OpenRA MissionSelector = 4 } - public class Map + public interface IMap + { + TileShape TileShape { get; } + + int2 MapSize { get; set; } + bool Contains(CPos cell); + CPos CellContaining(WPos pos); + WVec OffsetOfSubCell(SubCell subCell); + IEnumerable FindTilesInCircle(CPos center, int maxRange); + WPos CenterOfCell(CPos cell); + } + + public class Map : IMap { public const int MaxTilesInCircleRange = 50; public readonly TileShape TileShape; - [FieldLoader.Ignore] - public readonly WVec[] SubCellOffsets; + TileShape IMap.TileShape + { + get { return TileShape; } + } + + [FieldLoader.Ignore] public readonly WVec[] SubCellOffsets; public readonly SubCell DefaultSubCell; public readonly SubCell LastSubCell; [FieldLoader.Ignore] public IFolder Container; @@ -139,8 +155,7 @@ namespace OpenRA public WVec OffsetOfSubCell(SubCell subCell) { return SubCellOffsets[(int)subCell]; } - [FieldLoader.LoadUsing("LoadOptions")] - public MapOptions Options; + [FieldLoader.LoadUsing("LoadOptions")] public MapOptions Options; static object LoadOptions(MiniYaml y) { @@ -152,8 +167,7 @@ namespace OpenRA return options; } - [FieldLoader.LoadUsing("LoadVideos")] - public MapVideos Videos; + [FieldLoader.LoadUsing("LoadVideos")] public MapVideos Videos; static object LoadVideos(MiniYaml y) { @@ -188,6 +202,12 @@ namespace OpenRA public int2 MapSize; + int2 IMap.MapSize + { + get { return MapSize; } + set { MapSize = value; } + } + [FieldLoader.Ignore] public Lazy> MapTiles; [FieldLoader.Ignore] public Lazy> MapResources; [FieldLoader.Ignore] public Lazy> MapHeight; diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index db244831f3..af61ef4bf9 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -84,6 +84,8 @@ + + diff --git a/OpenRA.Game/Primitives/PriorityQueue.cs b/OpenRA.Game/Primitives/PriorityQueue.cs index 06d2a31cd8..2757f8f69b 100644 --- a/OpenRA.Game/Primitives/PriorityQueue.cs +++ b/OpenRA.Game/Primitives/PriorityQueue.cs @@ -13,15 +13,28 @@ using System.Collections.Generic; namespace OpenRA.Primitives { - public class PriorityQueue - where T : IComparable + public interface IPriorityQueue { - List items = new List(); + void Add(T item); + bool Empty { get; } + T Peek(); + T Pop(); + } + + public class PriorityQueue : IPriorityQueue + { + readonly List items; + readonly IComparer comparer; int level, index; - public PriorityQueue() + public PriorityQueue() : this(Comparer.Default) { - items.Add(new T[1]); + } + + public PriorityQueue(IComparer comparer) + { + items = new List { new T[1] }; + this.comparer = comparer; } public void Add(T item) @@ -29,7 +42,7 @@ namespace OpenRA.Primitives var addLevel = level; var addIndex = index; - while (addLevel >= 1 && Above(addLevel, addIndex).CompareTo(item) > 0) + while (addLevel >= 1 && comparer.Compare(Above(addLevel, addIndex), item) > 0) { items[addLevel][addIndex] = Above(addLevel, addIndex); --addLevel; @@ -88,10 +101,10 @@ namespace OpenRA.Primitives } if (downLevel <= level && downIndex < index - 1 && - At(downLevel, downIndex).CompareTo(At(downLevel, downIndex + 1)) >= 0) + comparer.Compare(At(downLevel, downIndex), At(downLevel, downIndex + 1)) >= 0) ++downIndex; - if (val.CompareTo(At(downLevel, downIndex)) <= 0) + if (comparer.Compare(val, At(downLevel, downIndex)) <= 0) { items[intoLevel][intoIndex] = val; return; diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs index 8467ba2439..974efb686b 100644 --- a/OpenRA.Game/World.cs +++ b/OpenRA.Game/World.cs @@ -24,7 +24,15 @@ namespace OpenRA { public enum WorldType { Regular, Shellmap } - public class World + public interface IWorld + { + IActor WorldActor { get; } + int WorldTick { get; } + IMap Map { get; } + TileSet TileSet { get; } + } + + public class World : IWorld { class ActorIDComparer : IComparer { @@ -114,8 +122,14 @@ namespace OpenRA } public readonly Actor WorldActor; + IActor IWorld.WorldActor { get { return WorldActor; } } + public readonly Map Map; + IMap IWorld.Map { get { return Map; } } + public readonly TileSet TileSet; + TileSet IWorld.TileSet { get { return TileSet; } } + public readonly ActorMap ActorMap; public readonly ScreenMap ScreenMap; public readonly WorldType Type; diff --git a/OpenRA.Mods.Common/Activities/FindResources.cs b/OpenRA.Mods.Common/Activities/FindResources.cs index c8364d98b7..e2f559bd57 100644 --- a/OpenRA.Mods.Common/Activities/FindResources.cs +++ b/OpenRA.Mods.Common/Activities/FindResources.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; using OpenRA.Activities; +using OpenRA.Mods.Common.Pathfinder; using OpenRA.Mods.Common.Traits; using OpenRA.Traits; @@ -51,31 +52,34 @@ namespace OpenRA.Mods.Common.Activities var searchRadiusSquared = searchRadius * searchRadius; // Find harvestable resources nearby: - var path = self.World.WorldActor.Trait().FindPath( + var path = self.World.WorldActor.Trait().FindPath( PathSearch.Search(self.World, mobileInfo, self, true) .WithHeuristic(loc => { // Avoid this cell: - if (avoidCell.HasValue && loc == avoidCell.Value) return 1; + if (avoidCell.HasValue && loc == avoidCell.Value) + return Constants.CellCost; // Don't harvest out of range: var distSquared = (loc - searchFromLoc).LengthSquared; if (distSquared > searchRadiusSquared) - return int.MaxValue; + return Constants.CellCost * 2; // Get the resource at this location: var resType = resLayer.GetResource(loc); - - if (resType == null) return 1; + if (resType == null) + return Constants.CellCost; // Can the harvester collect this kind of resource? - if (!harvInfo.Resources.Contains(resType.Info.Name)) return 1; + if (!harvInfo.Resources.Contains(resType.Info.Name)) + return Constants.CellCost; if (territory != null) { // Another harvester has claimed this resource: ResourceClaim claim; - if (territory.IsClaimedByAnyoneElse(self, loc, out claim)) return 1; + if (territory.IsClaimedByAnyoneElse(self, loc, out claim)) + return Constants.CellCost; } return 0; diff --git a/OpenRA.Mods.Common/Activities/Move/Move.cs b/OpenRA.Mods.Common/Activities/Move/Move.cs index d688fb5f87..5ff3ee01e8 100644 --- a/OpenRA.Mods.Common/Activities/Move/Move.cs +++ b/OpenRA.Mods.Common/Activities/Move/Move.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using OpenRA.Activities; +using OpenRA.Mods.Common.Pathfinder; using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; using OpenRA.Traits; @@ -45,7 +46,7 @@ namespace OpenRA.Mods.Common.Activities moveDisablers = self.TraitsImplementing(); getPath = () => - self.World.WorldActor.Trait().FindPath( + self.World.WorldActor.Trait().FindPath( PathSearch.FromPoint(self.World, mobile.Info, self, mobile.ToCell, destination, false) .WithoutLaneBias()); this.destination = destination; @@ -61,7 +62,7 @@ namespace OpenRA.Mods.Common.Activities mobile = self.Trait(); moveDisablers = self.TraitsImplementing(); - getPath = () => self.World.WorldActor.Trait() + getPath = () => self.World.WorldActor.Trait() .FindUnitPath(mobile.ToCell, destination, self); this.destination = destination; this.nearEnough = nearEnough; @@ -72,7 +73,7 @@ namespace OpenRA.Mods.Common.Activities mobile = self.Trait(); moveDisablers = self.TraitsImplementing(); - getPath = () => self.World.WorldActor.Trait() + getPath = () => self.World.WorldActor.Trait() .FindUnitPathToRange(mobile.FromCell, subCell, self.World.Map.CenterOfSubCell(destination, subCell), nearEnough, self); this.destination = destination; this.nearEnough = nearEnough; @@ -84,7 +85,7 @@ namespace OpenRA.Mods.Common.Activities moveDisablers = self.TraitsImplementing(); getPath = () => - self.World.WorldActor.Trait().FindPath( + self.World.WorldActor.Trait().FindPath( PathSearch.FromPoint(self.World, mobile.Info, self, mobile.ToCell, destination, false) .WithIgnoredActor(ignoredActor)); @@ -103,7 +104,7 @@ namespace OpenRA.Mods.Common.Activities if (!target.IsValidFor(self)) return NoPath; - return self.World.WorldActor.Trait().FindUnitPathToRange( + return self.World.WorldActor.Trait().FindUnitPathToRange( mobile.ToCell, mobile.ToSubCell, target.CenterPosition, range, self); }; @@ -132,7 +133,7 @@ namespace OpenRA.Mods.Common.Activities return hash; } - List EvalPath(Actor self, Mobile mobile) + List EvalPath() { var path = getPath().TakeWhile(a => a != mobile.ToCell).ToList(); mobile.PathHash = HashList(path); @@ -155,7 +156,7 @@ namespace OpenRA.Mods.Common.Activities return this; } - path = EvalPath(self, mobile); + path = EvalPath(); SanityCheckPath(mobile); } @@ -167,7 +168,7 @@ namespace OpenRA.Mods.Common.Activities destination = path[0]; - var nextCell = PopPath(self, mobile); + var nextCell = PopPath(self); if (nextCell == null) return this; @@ -202,7 +203,7 @@ namespace OpenRA.Mods.Common.Activities throw new InvalidOperationException("(Move) Sanity check failed"); } - Pair? PopPath(Actor self, Mobile mobile) + Pair? PopPath(Actor self) { if (path.Count == 0) return null; @@ -210,7 +211,7 @@ namespace OpenRA.Mods.Common.Activities var nextCell = path[path.Count - 1]; // Next cell in the move is blocked by another actor - if (!mobile.CanEnterCell(nextCell, ignoredActor, true)) + if (!mobile.CanMoveFreelyInto(nextCell, ignoredActor, true)) { // Are we close enough? var cellRange = nearEnough.Range / 1024; @@ -246,7 +247,7 @@ namespace OpenRA.Mods.Common.Activities // Calculate a new path mobile.RemoveInfluence(); - var newPath = EvalPath(self, mobile); + var newPath = EvalPath(); mobile.AddInfluence(); if (newPath.Count != 0) @@ -372,7 +373,7 @@ namespace OpenRA.Mods.Common.Activities var fromSubcellOffset = self.World.Map.OffsetOfSubCell(mobile.FromSubCell); var toSubcellOffset = self.World.Map.OffsetOfSubCell(mobile.ToSubCell); - var nextCell = parent.PopPath(self, mobile); + var nextCell = parent.PopPath(self); if (nextCell != null) { if (IsTurn(mobile, nextCell.Value.First)) diff --git a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs index 8b6636a8b8..a1cbc0b5c1 100644 --- a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs +++ b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; using OpenRA.Activities; +using OpenRA.Mods.Common.Pathfinder; using OpenRA.Mods.Common.Traits; using OpenRA.Traits; @@ -22,7 +23,7 @@ namespace OpenRA.Mods.Common.Activities static readonly List NoPath = new List(); readonly Mobile mobile; - readonly PathFinder pathFinder; + readonly IPathFinder pathFinder; readonly DomainIndex domainIndex; readonly uint movementClass; @@ -36,7 +37,7 @@ namespace OpenRA.Mods.Common.Activities Target = target; mobile = self.Trait(); - pathFinder = self.World.WorldActor.Trait(); + pathFinder = self.World.WorldActor.Trait(); domainIndex = self.World.WorldActor.Trait(); movementClass = (uint)mobile.Info.GetMovementClass(self.World.TileSet); diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index dd7f59b0a4..d0a8aefa84 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -175,6 +175,7 @@ + @@ -435,9 +436,15 @@ + + + + + - + + diff --git a/OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs b/OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs new file mode 100644 index 0000000000..910c09b012 --- /dev/null +++ b/OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs @@ -0,0 +1,203 @@ +#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.Text; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; + +namespace OpenRA.Mods.Common.Pathfinder +{ + public interface IPathSearch + { + string Id { get; } + + /// + /// The Graph used by the A* + /// + IGraph Graph { get; } + + /// + /// The open queue where nodes that are worth to consider are stored by their estimator + /// + IPriorityQueue OpenQueue { get; } + + /// + /// Stores the analyzed nodes by the expand function + /// + IEnumerable> Considered { get; } + + bool Debug { get; set; } + + Player Owner { get; } + + int MaxCost { get; } + + IPathSearch Reverse(); + + IPathSearch WithCustomBlocker(Func customBlock); + + IPathSearch WithIgnoredActor(Actor b); + + IPathSearch WithHeuristic(Func h); + + IPathSearch WithCustomCost(Func w); + + IPathSearch WithoutLaneBias(); + + IPathSearch FromPoint(CPos from); + + /// + /// Decides whether a location is a target based on its estimate + /// (An estimate of 0 means that the location and the unit's goal + /// are the same. There could be multiple goals). + /// + /// The location to assess + /// Whether the location is a target + bool IsTarget(CPos location); + + CPos Expand(); + } + + public abstract class BasePathSearch : IPathSearch + { + public IGraph Graph { get; set; } + + // The Id of a Pathsearch is computed by its properties. + // So two PathSearch instances with the same parameters will + // Compute the same Id. This is used for caching purposes. + public string Id + { + get + { + if (string.IsNullOrEmpty(id)) + { + var builder = new StringBuilder(); + builder.Append(this.Graph.Actor.ActorID); + while (!startPoints.Empty) + { + var startpoint = startPoints.Pop(); + builder.Append(startpoint.X); + builder.Append(startpoint.Y); + builder.Append(Graph[startpoint].EstimatedTotal); + } + + builder.Append(Graph.InReverse); + if (Graph.IgnoredActor != null) builder.Append(Graph.IgnoredActor.ActorID); + builder.Append(Graph.LaneBias); + + id = builder.ToString(); + } + + return id; + } + } + + public IPriorityQueue OpenQueue { get; protected set; } + + public abstract IEnumerable> Considered { get; } + + public Player Owner { get { return this.Graph.Actor.Owner; } } + public int MaxCost { get; protected set; } + public bool Debug { get; set; } + string id; + protected Func heuristic; + + // This member is used to compute the ID of PathSearch. + // Essentially, it represents a collection of the initial + // points considered and their Heuristics to reach + // the target. It pretty match identifies, in conjunction of the Actor, + // a deterministic set of calculations + protected IPriorityQueue startPoints; + + protected BasePathSearch(IGraph graph) + { + Graph = graph; + OpenQueue = new PriorityQueue(new PositionComparer(Graph)); + startPoints = new PriorityQueue(new PositionComparer(Graph)); + Debug = false; + MaxCost = 0; + } + + /// + /// Default: Diagonal distance heuristic. More information: + /// http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html + /// + /// A delegate that calculates the estimation for a node + protected static Func DefaultEstimator(CPos destination) + { + return here => + { + var diag = Math.Min(Math.Abs(here.X - destination.X), Math.Abs(here.Y - destination.Y)); + var straight = Math.Abs(here.X - destination.X) + Math.Abs(here.Y - destination.Y); + + // According to the information link, this is the shape of the function. + // We just extract factors to simplify. + // Possible simplification: var h = Constants.CellCost * (straight + (Constants.Sqrt2 - 2) * diag); + return Constants.CellCost * straight + (Constants.DiagonalCellCost - 2 * Constants.CellCost) * diag; + }; + } + + public IPathSearch Reverse() + { + Graph.InReverse = true; + return this; + } + + public IPathSearch WithCustomBlocker(Func customBlock) + { + Graph.CustomBlock = customBlock; + return this; + } + + public IPathSearch WithIgnoredActor(Actor b) + { + Graph.IgnoredActor = b; + return this; + } + + public IPathSearch WithHeuristic(Func h) + { + heuristic = h; + return this; + } + + public IPathSearch WithCustomCost(Func w) + { + Graph.CustomCost = w; + return this; + } + + public IPathSearch WithoutLaneBias() + { + Graph.LaneBias = 0; + return this; + } + + public IPathSearch FromPoint(CPos from) + { + if (this.Graph.World.Map.Contains(from)) + AddInitialCell(from); + + return this; + } + + protected abstract void AddInitialCell(CPos cell); + + public bool IsTarget(CPos location) + { + var locInfo = Graph[location]; + return locInfo.EstimatedTotal - locInfo.CostSoFar == 0; + } + + public abstract CPos Expand(); + } +} diff --git a/OpenRA.Mods.Common/Pathfinder/CellInfo.cs b/OpenRA.Mods.Common/Pathfinder/CellInfo.cs new file mode 100644 index 0000000000..57f3e9329e --- /dev/null +++ b/OpenRA.Mods.Common/Pathfinder/CellInfo.cs @@ -0,0 +1,78 @@ +#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; + +namespace OpenRA.Mods.Common.Pathfinder +{ + /// + /// Describes the three states that a node in the graph can have. + /// Based on A* algorithm specification + /// + public enum CellStatus + { + Unvisited, + Open, + Closed + } + + /// + /// Stores information about nodes in the pathfinding graph + /// + public struct CellInfo + { + /// + /// The cost to move from the start up to this node + /// + public readonly int CostSoFar; + + /// + /// The estimation of how far is the node from our goal + /// + public readonly int EstimatedTotal; + + /// + /// The previous node of this one that follows the shortest path + /// + public readonly CPos PreviousPos; + + /// + /// The status of this node + /// + public readonly CellStatus Status; + + public CellInfo(int costSoFar, int estimatedTotal, CPos previousPos, CellStatus status) + { + CostSoFar = costSoFar; + PreviousPos = previousPos; + Status = status; + EstimatedTotal = estimatedTotal; + } + } + + /// + /// Compares two nodes according to their estimations + /// + public class PositionComparer : IComparer + { + readonly IGraph graph; + + public PositionComparer(IGraph graph) + { + this.graph = graph; + } + + public int Compare(CPos x, CPos y) + { + return Math.Sign(graph[x].EstimatedTotal - graph[y].EstimatedTotal); + } + } +} diff --git a/OpenRA.Mods.Common/Pathfinder/CellInfoLayerManager.cs b/OpenRA.Mods.Common/Pathfinder/CellInfoLayerManager.cs new file mode 100644 index 0000000000..7745c10b33 --- /dev/null +++ b/OpenRA.Mods.Common/Pathfinder/CellInfoLayerManager.cs @@ -0,0 +1,111 @@ +#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.Collections.Generic; +using System.Drawing; +using OpenRA.Mods.Common.Traits; + +namespace OpenRA.Mods.Common.Pathfinder +{ + public interface ICellInfoLayerManager + { + /// + /// Gets a CellLayer of Nodes from the pool + /// + CellLayer GetFromPool(); + + /// + /// Puts a CellLayer into the pool + /// + void PutBackIntoPool(CellLayer ci); + + /// + /// Creates (or obtains from the pool) a CellLayer given a map + /// + CellLayer NewLayer(IMap map); + } + + public sealed class CellInfoLayerManager : ICellInfoLayerManager + { + readonly Queue> cellInfoPool = new Queue>(); + readonly object defaultCellInfoLayerSync = new object(); + CellLayer defaultCellInfoLayer; + + static ICellInfoLayerManager instance = new CellInfoLayerManager(); + + public static ICellInfoLayerManager Instance + { + get + { + return instance; + } + } + + public static void SetInstance(ICellInfoLayerManager cellInfoLayerManager) + { + instance = cellInfoLayerManager; + } + + public CellLayer GetFromPool() + { + lock (cellInfoPool) + return cellInfoPool.Dequeue(); + } + + public void PutBackIntoPool(CellLayer ci) + { + lock (cellInfoPool) + cellInfoPool.Enqueue(ci); + } + + public CellLayer NewLayer(IMap map) + { + CellLayer result = null; + var mapSize = new Size(map.MapSize.X, map.MapSize.Y); + + // HACK: Uses a static cache so that double-ended searches (which have two PathSearch instances) + // can implicitly share data. The PathFinder should allocate the CellInfo array and pass it + // explicitly to the things that need to share it. + while (cellInfoPool.Count > 0) + { + var cellInfo = GetFromPool(); + if (cellInfo.Size != mapSize || cellInfo.Shape != map.TileShape) + { + Log.Write("debug", "Discarding old pooled CellInfo of wrong size."); + continue; + } + + result = cellInfo; + break; + } + + if (result == null) + result = new CellLayer(map); + + lock (defaultCellInfoLayerSync) + { + if (defaultCellInfoLayer == null || + defaultCellInfoLayer.Size != mapSize || + defaultCellInfoLayer.Shape != map.TileShape) + { + defaultCellInfoLayer = + CellLayer.CreateInstance( + mpos => new CellInfo(int.MaxValue, int.MaxValue, mpos.ToCPos(map as Map), CellStatus.Unvisited), + new Size(map.MapSize.X, map.MapSize.Y), + map.TileShape); + } + + result.CopyValuesFrom(defaultCellInfoLayer); + } + + return result; + } + } +} \ No newline at end of file diff --git a/OpenRA.Mods.Common/Pathfinder/Constants.cs b/OpenRA.Mods.Common/Pathfinder/Constants.cs new file mode 100644 index 0000000000..57036a4116 --- /dev/null +++ b/OpenRA.Mods.Common/Pathfinder/Constants.cs @@ -0,0 +1,29 @@ +#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 + +namespace OpenRA.Mods.Common.Pathfinder +{ + public static class Constants + { + /// + /// Min cost to arrive from once cell to an adjacent one + /// (125 according to runtime tests where we could assess the cost + /// a unit took to move one cell horizontally) + /// + public const int CellCost = 125; + + /// + /// Min cost to arrive from once cell to a diagonal adjacent one + /// (125 * Sqrt(2) according to runtime tests where we could assess the cost + /// a unit took to move one cell diagonally) + /// + public const int DiagonalCellCost = 177; + } +} diff --git a/OpenRA.Mods.Common/Pathfinder/PathCacheStorage.cs b/OpenRA.Mods.Common/Pathfinder/PathCacheStorage.cs new file mode 100644 index 0000000000..3e04f9aed2 --- /dev/null +++ b/OpenRA.Mods.Common/Pathfinder/PathCacheStorage.cs @@ -0,0 +1,75 @@ +#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.Collections.Generic; +using System.Linq; + +namespace OpenRA.Mods.Common.Pathfinder +{ + public class PathCacheStorage : ICacheStorage> + { + class CachedPath + { + public List Result; + public int Tick; + } + + const int MaxPathAge = 50; + readonly IWorld world; + Dictionary cachedPaths = new Dictionary(100); + + public PathCacheStorage(IWorld world) + { + this.world = world; + } + + public void Remove(string key) + { + cachedPaths.Remove(key); + } + + public void Store(string key, List data) + { + // Eventually clean up the cachedPaths dictionary + if (cachedPaths.Count >= 100) + foreach (var cachedPath in cachedPaths.Where(p => IsExpired(p.Value)).ToList()) + cachedPaths.Remove(cachedPath.Key); + + cachedPaths.Add(key, new CachedPath + { + Tick = world.WorldTick, + Result = data + }); + } + + public List Retrieve(string key) + { + CachedPath cached; + if (cachedPaths.TryGetValue(key, out cached)) + { + if (IsExpired(cached)) + { + cachedPaths.Remove(key); + return null; + } + + cached.Tick = world.WorldTick; + return cached.Result; + } + + return null; + } + + bool IsExpired(CachedPath path) + { + return world.WorldTick - path.Tick > MaxPathAge; + } + } +} \ No newline at end of file diff --git a/OpenRA.Mods.Common/Pathfinder/PathFinderCacheDecorator.cs b/OpenRA.Mods.Common/Pathfinder/PathFinderCacheDecorator.cs new file mode 100644 index 0000000000..2f6bc396ba --- /dev/null +++ b/OpenRA.Mods.Common/Pathfinder/PathFinderCacheDecorator.cs @@ -0,0 +1,104 @@ +#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.Collections.Generic; +using OpenRA.Mods.Common.Traits; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Pathfinder +{ + /// + /// A decorator used to cache the pathfinder (Decorator design pattern) + /// + public class PathFinderCacheDecorator : IPathFinder + { + readonly IPathFinder pathFinder; + readonly ICacheStorage> cacheStorage; + + public PathFinderCacheDecorator(IPathFinder pathFinder, ICacheStorage> cacheStorage) + { + this.pathFinder = pathFinder; + this.cacheStorage = cacheStorage; + } + + public List FindUnitPath(CPos source, CPos target, IActor self) + { + using (new PerfSample("Pathfinder")) + { + var key = "FindUnitPath" + self.ActorID + source.X + source.Y + target.X + target.Y; + var cachedPath = cacheStorage.Retrieve(key); + + if (cachedPath != null) + return cachedPath; + + var pb = pathFinder.FindUnitPath(source, target, self); + + cacheStorage.Store(key, pb); + + return pb; + } + } + + public List FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WRange range, IActor self) + { + using (new PerfSample("Pathfinder")) + { + var key = "FindUnitPathToRange" + self.ActorID + source.X + source.Y + target.X + target.Y; + var cachedPath = cacheStorage.Retrieve(key); + + if (cachedPath != null) + return cachedPath; + + var pb = pathFinder.FindUnitPathToRange(source, srcSub, target, range, self); + + cacheStorage.Store(key, pb); + + return pb; + } + } + + public List FindPath(IPathSearch search) + { + using (new PerfSample("Pathfinder")) + { + var key = "FindPath" + search.Id; + var cachedPath = cacheStorage.Retrieve(key); + + if (cachedPath != null) + return cachedPath; + + var pb = pathFinder.FindPath(search); + + cacheStorage.Store(key, pb); + + return pb; + } + } + + public List FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest) + { + using (new PerfSample("Pathfinder")) + { + var key = "FindBidiPath" + fromSrc.Id + fromDest.Id; + var cachedPath = cacheStorage.Retrieve(key); + + if (cachedPath != null) + return cachedPath; + + var pb = pathFinder.FindBidiPath(fromSrc, fromDest); + + cacheStorage.Store(key, pb); + + return pb; + } + } + } +} diff --git a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs new file mode 100644 index 0000000000..ad139a22af --- /dev/null +++ b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs @@ -0,0 +1,198 @@ +#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 OpenRA.Mods.Common.Traits; + +namespace OpenRA.Mods.Common.Pathfinder +{ + /// + /// Represents a graph with nodes and edges + /// + /// The type of node used in the graph + public interface IGraph : IDisposable + { + /// + /// Gets all the Connections for a given node in the graph + /// + ICollection GetConnections(CPos position); + + /// + /// Retrieves an object given a node in the graph + /// + T this[CPos pos] { get; set; } + + Func CustomBlock { get; set; } + + Func CustomCost { get; set; } + + int LaneBias { get; set; } + + bool InReverse { get; set; } + + IActor IgnoredActor { get; set; } + + IWorld World { get; } + + IActor Actor { get; } + } + + public struct GraphConnection + { + public readonly CPos Source; + public readonly CPos Destination; + public readonly int Cost; + + public GraphConnection(CPos source, CPos destination, int cost) + { + Source = source; + Destination = destination; + Cost = cost; + } + } + + public class PathGraph : IGraph + { + public IActor Actor { get; private set; } + public IWorld World { get; private set; } + public Func CustomBlock { get; set; } + public Func CustomCost { get; set; } + public int LaneBias { get; set; } + public bool InReverse { get; set; } + public IActor IgnoredActor { get; set; } + + readonly CellConditions checkConditions; + readonly IMobileInfo mobileInfo; + CellLayer cellInfo; + + public const int InvalidNode = int.MaxValue; + + public PathGraph(CellLayer cellInfo, IMobileInfo mobileInfo, IActor actor, IWorld world, bool checkForBlocked) + { + this.cellInfo = cellInfo; + World = world; + this.mobileInfo = mobileInfo; + Actor = actor; + LaneBias = 1; + checkConditions = checkForBlocked ? CellConditions.TransientActors : CellConditions.None; + } + + // Sets of neighbors for each incoming direction. These exclude the neighbors which are guaranteed + // to be reached more cheaply by a path through our parent cell which does not include the current cell. + // For horizontal/vertical directions, the set is the three cells 'ahead'. For diagonal directions, the set + // is the three cells ahead, plus the two cells to the side, which we cannot exclude without knowing if + // the cell directly between them and our parent is passable. + static readonly CVec[][] DirectedNeighbors = { + new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(-1, 0), new CVec(-1, 1) }, + new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1) }, + new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) }, + new[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1) }, + CVec.Directions, + new[] { new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) }, + new[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, + new[] { new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, + new[] { new CVec(1, -1), new CVec(1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, + }; + + public ICollection GetConnections(CPos position) + { + var previousPos = cellInfo[position].PreviousPos; + + var dx = position.X - previousPos.X; + var dy = position.Y - previousPos.Y; + var index = dy * 3 + dx + 4; + + var validNeighbors = new LinkedList(); + var directions = DirectedNeighbors[index]; + for (var i = 0; i < directions.Length; i++) + { + var neighbor = position + directions[i]; + var movementCost = GetCostToNode(neighbor, directions[i]); + if (movementCost != InvalidNode) + validNeighbors.AddLast(new GraphConnection(position, neighbor, movementCost)); + } + + return validNeighbors; + } + + int GetCostToNode(CPos destNode, CVec direction) + { + int movementCost; + if (mobileInfo.CanEnterCell( + World as World, + Actor as Actor, + destNode, + out movementCost, + IgnoredActor as Actor, + checkConditions) && !(CustomBlock != null && CustomBlock(destNode))) + { + return CalculateCellCost(destNode, direction, movementCost); + } + + return InvalidNode; + } + + int CalculateCellCost(CPos neighborCPos, CVec direction, int movementCost) + { + var cellCost = movementCost; + + if (direction.X * direction.Y != 0) + cellCost = (cellCost * 34) / 24; + + if (CustomCost != null) + cellCost += CustomCost(neighborCPos); + + // directional bonuses for smoother flow! + if (LaneBias != 0) + { + var ux = neighborCPos.X + (InReverse ? 1 : 0) & 1; + var uy = neighborCPos.Y + (InReverse ? 1 : 0) & 1; + + if ((ux == 0 && direction.Y < 0) || (ux == 1 && direction.Y > 0)) + cellCost += LaneBias; + + if ((uy == 0 && direction.X < 0) || (uy == 1 && direction.X > 0)) + cellCost += LaneBias; + } + + return cellCost; + } + + bool disposed; + public void Dispose() + { + if (disposed) + return; + + disposed = true; + + CellInfoLayerManager.Instance.PutBackIntoPool(cellInfo); + cellInfo = null; + + GC.SuppressFinalize(this); + } + + ~PathGraph() { Dispose(); } + + public CellInfo this[CPos pos] + { + get + { + return cellInfo[pos]; + } + + set + { + cellInfo[pos] = value; + } + } + } +} diff --git a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs new file mode 100644 index 0000000000..577fad1d6d --- /dev/null +++ b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs @@ -0,0 +1,129 @@ +#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.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; + +namespace OpenRA.Mods.Common.Pathfinder +{ + public sealed class PathSearch : BasePathSearch + { + public override IEnumerable> Considered + { + get { return considered; } + } + + LinkedList> considered; + + #region Constructors + + private PathSearch(IGraph graph) + : base(graph) + { + considered = new LinkedList>(); + } + + public static IPathSearch Search(IWorld world, IMobileInfo mi, IActor self, bool checkForBlocked) + { + var graph = new PathGraph(CellInfoLayerManager.Instance.NewLayer(world.Map), mi, self, world, checkForBlocked); + return new PathSearch(graph); + } + + public static IPathSearch FromPoint(IWorld world, IMobileInfo mi, IActor self, CPos from, CPos target, bool checkForBlocked) + { + var graph = new PathGraph(CellInfoLayerManager.Instance.NewLayer(world.Map), mi, self, world, checkForBlocked); + var search = new PathSearch(graph) + { + heuristic = DefaultEstimator(target) + }; + + if (world.Map.Contains(from)) + search.AddInitialCell(from); + + return search; + } + + public static IPathSearch FromPoints(IWorld world, IMobileInfo mi, IActor self, IEnumerable froms, CPos target, bool checkForBlocked) + { + var graph = new PathGraph(CellInfoLayerManager.Instance.NewLayer(world.Map), mi, self, world, checkForBlocked); + var search = new PathSearch(graph) + { + heuristic = DefaultEstimator(target) + }; + + foreach (var sl in froms.Where(sl => world.Map.Contains(sl))) + search.AddInitialCell(sl); + + return search; + } + + protected override void AddInitialCell(CPos location) + { + Graph[location] = new CellInfo(0, heuristic(location), location, CellStatus.Open); + OpenQueue.Add(location); + startPoints.Add(location); + considered.AddLast(new Pair(location, 0)); + } + + #endregion + + /// + /// This function analyzes the neighbors of the most promising node in the Pathfinding graph + /// using the A* algorithm (A-star) and returns that node + /// + /// The most promising node of the iteration + public override CPos Expand() + { + var currentMinNode = OpenQueue.Pop(); + + var currentCell = Graph[currentMinNode]; + Graph[currentMinNode] = new CellInfo(currentCell.CostSoFar, currentCell.EstimatedTotal, currentCell.PreviousPos, CellStatus.Closed); + + foreach (var connection in Graph.GetConnections(currentMinNode)) + { + // Calculate the cost up to that point + var gCost = currentCell.CostSoFar + connection.Cost; + + var neighborCPos = connection.Destination; + var neighborCell = Graph[neighborCPos]; + + // Cost is even higher; next direction: + if (gCost >= neighborCell.CostSoFar) + continue; + + // Now we may seriously consider this direction using heuristics. If the cell has + // already been processed, we can reuse the result (just the difference between the + // estimated total and the cost so far + int hCost; + if (neighborCell.Status == CellStatus.Open || neighborCell.Status == CellStatus.Closed) + hCost = neighborCell.EstimatedTotal - neighborCell.CostSoFar; + else + hCost = heuristic(neighborCPos); + + Graph[neighborCPos] = new CellInfo(gCost, gCost + hCost, currentMinNode, CellStatus.Open); + + if (neighborCell.Status != CellStatus.Open) + OpenQueue.Add(neighborCPos); + + if (Debug) + { + if (gCost > MaxCost) + MaxCost = gCost; + + considered.AddLast(new Pair(neighborCPos, gCost)); + } + } + + return currentMinNode; + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Harvester.cs b/OpenRA.Mods.Common/Traits/Harvester.cs index 42e672e4ea..bfccd4ba7c 100644 --- a/OpenRA.Mods.Common/Traits/Harvester.cs +++ b/OpenRA.Mods.Common/Traits/Harvester.cs @@ -14,6 +14,7 @@ using System.Linq; using OpenRA.Activities; using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Orders; +using OpenRA.Mods.Common.Pathfinder; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -115,12 +116,12 @@ namespace OpenRA.Mods.Common.Traits var refs = ( from r in self.World.ActorsWithTrait() where r.Actor != ignore && r.Actor.Owner == self.Owner && IsAcceptableProcType(r.Actor) - let linkedHarvs = self.World.ActorsWithTrait().Where(a => a.Trait.LinkedProc == r.Actor).Count() + let linkedHarvs = self.World.ActorsWithTrait().Count(a => a.Trait.LinkedProc == r.Actor) select new { Location = r.Actor.Location + r.Trait.DeliveryOffset, Actor = r.Actor, Occupancy = linkedHarvs }).ToDictionary(r => r.Location); // Start a search from each refinery's delivery location: var mi = self.Info.Traits.Get(); - var path = self.World.WorldActor.Trait().FindPath( + var path = self.World.WorldActor.Trait().FindPath( PathSearch.FromPoints(self.World, mi, self, refs.Values.Select(r => r.Location), self.Location, false) .WithCustomCost((loc) => { @@ -374,23 +375,25 @@ namespace OpenRA.Mods.Common.Traits var territory = self.World.WorldActor.TraitOrDefault(); // Find any harvestable resources: - var path = self.World.WorldActor.Trait().FindPath( + var path = self.World.WorldActor.Trait().FindPath( PathSearch.Search(self.World, mobileInfo, self, true) .WithHeuristic(loc => { // Get the resource at this location: var resType = resLayer.GetResource(loc); - - if (resType == null) return 1; + if (resType == null) + return Constants.CellCost; // Can the harvester collect this kind of resource? - if (!harvInfo.Resources.Contains(resType.Info.Name)) return 1; + if (!harvInfo.Resources.Contains(resType.Info.Name)) + return Constants.CellCost; // Another harvester has claimed this resource: if (territory != null) { ResourceClaim claim; - if (territory.IsClaimedByAnyoneElse(self, loc, out claim)) return 1; + if (territory.IsClaimedByAnyoneElse(self, loc, out claim)) + return Constants.CellCost; } return 0; diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs index fd50b2b4bc..98fc0f42c7 100644 --- a/OpenRA.Mods.Common/Traits/Mobile.cs +++ b/OpenRA.Mods.Common/Traits/Mobile.cs @@ -29,8 +29,16 @@ namespace OpenRA.Mods.Common.Traits All = TransientActors | BlockedByMovers } + public interface IMobileInfo : IMoveInfo + { + int MovementCostForCell(World world, CPos cell); + bool CanEnterCell(World world, Actor self, CPos cell, out int movementCost, Actor ignoreActor = null, CellConditions check = CellConditions.All); + bool CanEnterCell(World world, Actor self, CPos cell, Actor ignoreActor = null, CellConditions check = CellConditions.All); + int GetMovementClass(TileSet tileset); + } + [Desc("Unit is able to move.")] - public class MobileInfo : ITraitInfo, IOccupySpaceInfo, IFacingInfo, IMoveInfo, UsesInit, UsesInit, UsesInit + public class MobileInfo : IMobileInfo, IOccupySpaceInfo, IFacingInfo, UsesInit, UsesInit, UsesInit { [FieldLoader.LoadUsing("LoadSpeeds")] [Desc("Set Water: 0 for ground units and lower the value on rough terrain.")] @@ -165,11 +173,61 @@ namespace OpenRA.Mods.Common.Traits return true; } + public int TileSetMovementHash(TileSet tileSet) + { + var terrainInfos = TilesetTerrainInfo[tileSet]; + + // Compute and return the hash using aggregate + return terrainInfos.Aggregate(terrainInfos.Length, + (current, terrainInfo) => unchecked(current * 31 + terrainInfo.Cost)); + } + public bool CanEnterCell(World world, Actor self, CPos cell, Actor ignoreActor = null, CellConditions check = CellConditions.All) { if (MovementCostForCell(world, cell) == int.MaxValue) return false; + return CanMoveFreelyInto(world, self, cell, ignoreActor, check); + } + + // Determines whether the actor is blocked by other Actors + public bool CanMoveFreelyInto(World world, Actor self, CPos cell, Actor ignoreActor, CellConditions check) + { + if (SharesCell && world.ActorMap.HasFreeSubCell(cell)) + return true; + + if (check.HasFlag(CellConditions.TransientActors)) + { + var canIgnoreMovingAllies = self != null && !check.HasFlag(CellConditions.BlockedByMovers); + var needsCellExclusively = self == null || Crushes == null || !Crushes.Any(); + foreach (var a in world.ActorMap.GetUnitsAt(cell)) + { + if (a == ignoreActor) + continue; + + // Neutral/enemy units are blockers. Allied units that are moving are not blockers. + if (canIgnoreMovingAllies && self.Owner.Stances[a.Owner] == Stance.Ally && IsMovingInMyDirection(self, a)) continue; + + // Non-sharable unit can enter a cell with shareable units only if it can crush all of them. + if (needsCellExclusively) + return false; + var crushables = a.TraitsImplementing(); + if (!crushables.Any()) + return false; + foreach (var crushable in crushables) + if (!crushable.CrushableBy(Crushes, self.Owner)) + return false; + } + } + + return true; + } + + public bool CanEnterCell(World world, Actor self, CPos cell, out int movementCost, Actor ignoreActor = null, CellConditions check = CellConditions.All) + { + if ((movementCost = MovementCostForCell(world, cell)) == int.MaxValue) + return false; + if (SharesCell && world.ActorMap.HasFreeSubCell(cell)) return true; @@ -506,6 +564,11 @@ namespace OpenRA.Mods.Common.Traits return Info.CanEnterCell(self.World, self, cell, ignoreActor, checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers); } + public bool CanMoveFreelyInto(CPos cell, Actor ignoreActor = null, bool checkTransientActors = true) + { + return Info.CanMoveFreelyInto(self.World, self, cell, ignoreActor, checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers); + } + public void EnteringCell(Actor self) { var crushables = self.World.ActorMap.GetUnitsAt(ToCell).Where(a => a != self) diff --git a/OpenRA.Mods.Common/Traits/World/PathFinder.cs b/OpenRA.Mods.Common/Traits/World/PathFinder.cs index 3472ed253c..1dc1e7c29b 100644 --- a/OpenRA.Mods.Common/Traits/World/PathFinder.cs +++ b/OpenRA.Mods.Common/Traits/World/PathFinder.cs @@ -12,9 +12,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using OpenRA; -using OpenRA.Primitives; -using OpenRA.Support; +using OpenRA.Mods.Common.Pathfinder; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -22,222 +20,221 @@ 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 PathFinder(init.World); } + public object Create(ActorInitializer init) + { + return new PathFinderCacheDecorator(new PathFinder(init.World), new PathCacheStorage(init.World)); + } } - public class PathFinder + 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, IActor self); + + List FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WRange range, IActor 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 { - const int MaxPathAge = 50; /* x 40ms ticks */ static readonly List EmptyPath = new List(0); + readonly IWorld world; - readonly World world; - public PathFinder(World world) { this.world = world; } - - class CachedPath + public PathFinder(IWorld world) { - public CPos From; - public CPos To; - public List Result; - public int Tick; - public Actor Actor; + this.world = world; } - List cachedPaths = new List(); - - public List FindUnitPath(CPos from, CPos target, Actor self) + public List FindUnitPath(CPos source, CPos target, IActor self) { - using (new PerfSample("Pathfinder")) + var mi = self.Info.Traits.Get(); + + // If a water-land transition is required, bail early + var domainIndex = world.WorldActor.TraitOrDefault(); + if (domainIndex != null) { - var cached = cachedPaths.FirstOrDefault(p => p.From == from && p.To == target && p.Actor == self); - if (cached != null) - { - Log.Write("debug", "Actor {0} asked for a path from {1} tick(s) ago", self.ActorID, world.WorldTick - cached.Tick); - if (world.WorldTick - cached.Tick > MaxPathAge) - cachedPaths.Remove(cached); - return new List(cached.Result); - } - - var mi = self.Info.Traits.Get(); - - // If a water-land transition is required, bail early - var domainIndex = self.World.WorldActor.TraitOrDefault(); - if (domainIndex != null) - { - var passable = mi.GetMovementClass(world.TileSet); - if (!domainIndex.IsPassable(from, target, (uint)passable)) - return EmptyPath; - } - - var fromPoint = PathSearch.FromPoint(world, mi, self, target, from, true) - .WithIgnoredActor(self); - var fromPointReverse = PathSearch.FromPoint(world, mi, self, from, target, true) - .WithIgnoredActor(self) - .Reverse(); - var pb = FindBidiPath( - fromPoint, - fromPointReverse); - - CheckSanePath2(pb, from, target); - - cachedPaths.RemoveAll(p => world.WorldTick - p.Tick > MaxPathAge); - cachedPaths.Add(new CachedPath { From = from, To = target, Actor = self, Result = pb, Tick = world.WorldTick }); - return new List(pb); + var passable = mi.GetMovementClass(world.TileSet); + if (!domainIndex.IsPassable(source, target, (uint)passable)) + return EmptyPath; } + + var pb = FindBidiPath( + PathSearch.FromPoint(world, mi, self, target, source, true), + PathSearch.FromPoint(world, mi, self, source, target, true).Reverse()); + + CheckSanePath2(pb, source, target); + + return pb; } - public List FindUnitPathToRange(CPos src, SubCell srcSub, WPos target, WRange range, Actor self) + public List FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WRange range, IActor self) { - using (new PerfSample("Pathfinder")) + var mi = self.Info.Traits.Get(); + var targetCell = world.Map.CellContaining(target); + var rangeSquared = range.Range * range.Range; + + // 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.Range / 1024 + 1) + .Where(t => (world.Map.CenterOfCell(t) - target).LengthSquared <= rangeSquared + && mi.CanEnterCell(self.World as World, self as Actor, 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 mi = self.Info.Traits.Get(); - var targetCell = self.World.Map.CellContaining(target); - var rangeSquared = range.Range * range.Range; + var passable = mi.GetMovementClass(world.TileSet); + tilesInRange = new List(tilesInRange.Where(t => domainIndex.IsPassable(source, t, (uint)passable))); + if (!tilesInRange.Any()) + return EmptyPath; + } - // Correct for SubCell offset - target -= self.World.Map.OffsetOfSubCell(srcSub); + var path = FindBidiPath( + PathSearch.FromPoints(world, mi, self, tilesInRange, source, true), + PathSearch.FromPoint(world, mi, self, source, targetCell, true).Reverse()); - // 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.Range / 1024 + 1) - .Where(t => (world.Map.CenterOfCell(t) - target).LengthSquared <= rangeSquared && - mi.CanEnterCell(self.World, self, t)); + return path; + } - // 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 = self.World.WorldActor.TraitOrDefault(); - if (domainIndex != null) + public List FindPath(IPathSearch search) + { + var dbg = world.WorldActor.TraitOrDefault(); + if (dbg != null && dbg.Visible) + search.Debug = true; + + List path = null; + + while (!search.OpenQueue.Empty) + { + var p = search.Expand(); + if (search.IsTarget(p)) { - var passable = mi.GetMovementClass(world.TileSet); - tilesInRange = new List(tilesInRange.Where(t => domainIndex.IsPassable(src, t, (uint)passable))); - if (!tilesInRange.Any()) - return EmptyPath; + path = MakePath(search.Graph, p); + break; } + } - var path = FindBidiPath( - PathSearch.FromPoints(world, mi, self, tilesInRange, src, true), - PathSearch.FromPoint(world, mi, self, src, targetCell, true).Reverse()); + 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; } - public List FindPath(PathSearch search) + // 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) { - using (new PerfSample("Pathfinder")) + List path = null; + + var dbg = world.WorldActor.TraitOrDefault(); + if (dbg != null && dbg.Visible) { - using (search) + fromSrc.Debug = true; + fromDest.Debug = true; + } + + while (!fromSrc.OpenQueue.Empty && !fromDest.OpenQueue.Empty) + { + // make some progress on the first search + var p = fromSrc.Expand(); + + if (fromDest.Graph[p].Status == CellStatus.Closed && + fromDest.Graph[p].CostSoFar < int.MaxValue) { - List path = null; - - while (!search.Queue.Empty) - { - var p = search.Expand(world); - if (search.Heuristic(p) == 0) - { - path = MakePath(search.CellInfo, p); - break; - } - } - - var dbg = world.WorldActor.TraitOrDefault(); - if (dbg != null) - dbg.AddLayer(search.Considered.Select(p => new Pair(p, search.CellInfo[p].MinCost)), search.MaxCost, search.Owner); - - if (path != null) - return path; + path = MakeBidiPath(fromSrc, fromDest, p); + break; } - // no path exists - return EmptyPath; + // 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; } - static List MakePath(CellLayer cellInfo, CPos destination) + // 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 pathNode = destination; + var currentNode = destination; - while (cellInfo[pathNode].Path != pathNode) + while (cellInfo[currentNode].PreviousPos != currentNode) { - ret.Add(pathNode); - pathNode = cellInfo[pathNode].Path; + ret.Add(currentNode); + currentNode = cellInfo[currentNode].PreviousPos; } - ret.Add(pathNode); + ret.Add(currentNode); CheckSanePath(ret); return ret; } - // Searches from both ends toward each other - public List FindBidiPath(PathSearch fromSrc, PathSearch fromDest) + static List MakeBidiPath(IPathSearch a, IPathSearch b, CPos confluenceNode) { - using (new PerfSample("Pathfinder")) - { - using (fromSrc) - using (fromDest) - { - List path = null; - - while (!fromSrc.Queue.Empty && !fromDest.Queue.Empty) - { - /* make some progress on the first search */ - var p = fromSrc.Expand(world); - - if (fromDest.CellInfo[p].Seen && - fromDest.CellInfo[p].MinCost < float.PositiveInfinity) - { - path = MakeBidiPath(fromSrc, fromDest, p); - break; - } - - /* make some progress on the second search */ - var q = fromDest.Expand(world); - - if (fromSrc.CellInfo[q].Seen && - fromSrc.CellInfo[q].MinCost < float.PositiveInfinity) - { - path = MakeBidiPath(fromSrc, fromDest, q); - break; - } - } - - var dbg = world.WorldActor.TraitOrDefault(); - if (dbg != null) - { - dbg.AddLayer(fromSrc.Considered.Select(p => new Pair(p, fromSrc.CellInfo[p].MinCost)), fromSrc.MaxCost, fromSrc.Owner); - dbg.AddLayer(fromDest.Considered.Select(p => new Pair(p, fromDest.CellInfo[p].MinCost)), fromDest.MaxCost, fromDest.Owner); - } - - if (path != null) - return path; - } - - return EmptyPath; - } - } - - static List MakeBidiPath(PathSearch a, PathSearch b, CPos p) - { - var ca = a.CellInfo; - var cb = b.CellInfo; + var ca = a.Graph; + var cb = b.Graph; var ret = new List(); - var q = p; - while (ca[q].Path != q) + var q = confluenceNode; + while (ca[q].PreviousPos != q) { ret.Add(q); - q = ca[q].Path; + q = ca[q].PreviousPos; } ret.Add(q); ret.Reverse(); - q = p; - while (cb[q].Path != q) + q = confluenceNode; + while (cb[q].PreviousPos != q) { - q = cb[q].Path; + q = cb[q].PreviousPos; ret.Add(q); } @@ -246,22 +243,22 @@ namespace OpenRA.Mods.Common.Traits } [Conditional("SANITY_CHECKS")] - static void CheckSanePath(List path) + static void CheckSanePath(IList path) { if (path.Count == 0) return; var prev = path[0]; - for (var i = 0; i < path.Count; i++) + foreach (var cell in path) { - var d = path[i] - prev; + var d = cell - prev; if (Math.Abs(d.X) > 1 || Math.Abs(d.Y) > 1) throw new InvalidOperationException("(PathFinder) path sanity check failed"); - prev = path[i]; + prev = cell; } } [Conditional("SANITY_CHECKS")] - static void CheckSanePath2(List path, CPos src, CPos dest) + static void CheckSanePath2(IList path, CPos src, CPos dest) { if (path.Count == 0) return; @@ -272,35 +269,4 @@ namespace OpenRA.Mods.Common.Traits throw new InvalidOperationException("(PathFinder) sanity check failed: doesn't come from src"); } } - - public struct CellInfo - { - public int MinCost; - public CPos Path; - public bool Seen; - - public CellInfo(int minCost, CPos path, bool seen) - { - MinCost = minCost; - Path = path; - Seen = seen; - } - } - - public struct PathDistance : IComparable - { - public readonly int EstTotal; - public readonly CPos Location; - - public PathDistance(int estTotal, CPos location) - { - EstTotal = estTotal; - Location = location; - } - - public int CompareTo(PathDistance other) - { - return Math.Sign(EstTotal - other.EstTotal); - } - } } diff --git a/OpenRA.Mods.Common/Traits/World/PathSearch.cs b/OpenRA.Mods.Common/Traits/World/PathSearch.cs deleted file mode 100644 index a06b19c0e1..0000000000 --- a/OpenRA.Mods.Common/Traits/World/PathSearch.cs +++ /dev/null @@ -1,362 +0,0 @@ -#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.Drawing; -using System.Linq; -using OpenRA; -using OpenRA.Primitives; - -namespace OpenRA.Mods.Common.Traits -{ - public sealed class PathSearch : IDisposable - { - public CellLayer CellInfo; - public PriorityQueue Queue; - public Func Heuristic; - public bool CheckForBlocked; - public Actor IgnoredActor; - public bool InReverse; - public HashSet Considered; - public Player Owner { get { return self.Owner; } } - public int MaxCost; - - Actor self; - MobileInfo mobileInfo; - Func customCost; - Func customBlock; - int laneBias = 1; - - public PathSearch(World world, MobileInfo mobileInfo, Actor self) - { - this.self = self; - CellInfo = InitCellInfo(); - this.mobileInfo = mobileInfo; - this.self = self; - customCost = null; - Queue = new PriorityQueue(); - Considered = new HashSet(); - MaxCost = 0; - } - - public static PathSearch Search(World world, MobileInfo mi, Actor self, bool checkForBlocked) - { - var search = new PathSearch(world, mi, self) - { - CheckForBlocked = checkForBlocked - }; - - return search; - } - - public static PathSearch FromPoint(World world, MobileInfo mi, Actor self, CPos from, CPos target, bool checkForBlocked) - { - var search = new PathSearch(world, mi, self) - { - Heuristic = DefaultEstimator(target), - CheckForBlocked = checkForBlocked - }; - - search.AddInitialCell(from); - return search; - } - - public static PathSearch FromPoints(World world, MobileInfo mi, Actor self, IEnumerable froms, CPos target, bool checkForBlocked) - { - var search = new PathSearch(world, mi, self) - { - Heuristic = DefaultEstimator(target), - CheckForBlocked = checkForBlocked - }; - - foreach (var sl in froms) - search.AddInitialCell(sl); - - return search; - } - - public static Func DefaultEstimator(CPos destination) - { - return here => - { - var diag = Math.Min(Math.Abs(here.X - destination.X), Math.Abs(here.Y - destination.Y)); - var straight = Math.Abs(here.X - destination.X) + Math.Abs(here.Y - destination.Y); - - // HACK: this relies on fp and cell-size assumptions. - var h = (3400 * diag / 24) + 100 * (straight - (2 * diag)); - return (int)(h * 1.001); - }; - } - - public PathSearch Reverse() - { - InReverse = true; - return this; - } - - public PathSearch WithCustomBlocker(Func customBlock) - { - this.customBlock = customBlock; - return this; - } - - public PathSearch WithIgnoredActor(Actor b) - { - IgnoredActor = b; - return this; - } - - public PathSearch WithHeuristic(Func h) - { - Heuristic = h; - return this; - } - - public PathSearch WithCustomCost(Func w) - { - customCost = w; - return this; - } - - public PathSearch WithoutLaneBias() - { - laneBias = 0; - return this; - } - - public PathSearch FromPoint(CPos from) - { - AddInitialCell(from); - return this; - } - - // Sets of neighbors for each incoming direction. These exclude the neighbors which are guaranteed - // to be reached more cheaply by a path through our parent cell which does not include the current cell. - // For horizontal/vertical directions, the set is the three cells 'ahead'. For diagonal directions, the set - // is the three cells ahead, plus the two cells to the side, which we cannot exclude without knowing if - // the cell directly between them and our parent is passable. - static CVec[][] directedNeighbors = { - new CVec[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(-1, 0), new CVec(-1, 1) }, - new CVec[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1) }, - new CVec[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) }, - new CVec[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1) }, - CVec.Directions, - new CVec[] { new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) }, - new CVec[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, - new CVec[] { new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, - new CVec[] { new CVec(1, -1), new CVec(1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, - }; - - static CVec[] GetNeighbors(CPos p, CPos prev) - { - var dx = p.X - prev.X; - var dy = p.Y - prev.Y; - var index = dy * 3 + dx + 4; - - return directedNeighbors[index]; - } - - public CPos Expand(World world) - { - var p = Queue.Pop(); - while (CellInfo[p.Location].Seen) - { - if (Queue.Empty) - return p.Location; - - p = Queue.Pop(); - } - - var pCell = CellInfo[p.Location]; - pCell.Seen = true; - CellInfo[p.Location] = pCell; - - var thisCost = mobileInfo.MovementCostForCell(world, p.Location); - - if (thisCost == int.MaxValue) - return p.Location; - - if (customCost != null) - { - var c = customCost(p.Location); - if (c == int.MaxValue) - return p.Location; - } - - // This current cell is ok; check useful immediate directions: - Considered.Add(p.Location); - - var directions = GetNeighbors(p.Location, pCell.Path); - - for (var i = 0; i < directions.Length; ++i) - { - var d = directions[i]; - - var newHere = p.Location + d; - - // Is this direction flat-out unusable or already seen? - if (!world.Map.Contains(newHere)) - continue; - - if (CellInfo[newHere].Seen) - continue; - - // Now we may seriously consider this direction using heuristics: - var costHere = mobileInfo.MovementCostForCell(world, newHere); - - if (costHere == int.MaxValue) - continue; - - if (!mobileInfo.CanEnterCell(world, self, newHere, IgnoredActor, CheckForBlocked ? CellConditions.TransientActors : CellConditions.None)) - continue; - - if (customBlock != null && customBlock(newHere)) - continue; - - var est = Heuristic(newHere); - if (est == int.MaxValue) - continue; - - var cellCost = costHere; - if (d.X * d.Y != 0) - cellCost = (cellCost * 34) / 24; - - var userCost = 0; - if (customCost != null) - { - userCost = customCost(newHere); - cellCost += userCost; - } - - // directional bonuses for smoother flow! - if (laneBias != 0) - { - var ux = newHere.X + (InReverse ? 1 : 0) & 1; - var uy = newHere.Y + (InReverse ? 1 : 0) & 1; - - if (ux == 0 && d.Y < 0) - cellCost += laneBias; - else if (ux == 1 && d.Y > 0) - cellCost += laneBias; - - if (uy == 0 && d.X < 0) - cellCost += laneBias; - else if (uy == 1 && d.X > 0) - cellCost += laneBias; - } - - var newCost = CellInfo[p.Location].MinCost + cellCost; - - // Cost is even higher; next direction: - if (newCost > CellInfo[newHere].MinCost) - continue; - - var hereCell = CellInfo[newHere]; - hereCell.Path = p.Location; - hereCell.MinCost = newCost; - CellInfo[newHere] = hereCell; - - Queue.Add(new PathDistance(newCost + est, newHere)); - - if (newCost > MaxCost) - MaxCost = newCost; - - Considered.Add(newHere); - } - - return p.Location; - } - - public void AddInitialCell(CPos location) - { - if (!self.World.Map.Contains(location)) - return; - - CellInfo[location] = new CellInfo(0, location, false); - Queue.Add(new PathDistance(Heuristic(location), location)); - } - - static readonly Queue> CellInfoPool = new Queue>(); - static readonly object DefaultCellInfoLayerSync = new object(); - static CellLayer defaultCellInfoLayer; - - static CellLayer GetFromPool() - { - lock (CellInfoPool) - return CellInfoPool.Dequeue(); - } - - static void PutBackIntoPool(CellLayer ci) - { - lock (CellInfoPool) - CellInfoPool.Enqueue(ci); - } - - CellLayer InitCellInfo() - { - CellLayer result = null; - var map = self.World.Map; - var mapSize = new Size(map.MapSize.X, map.MapSize.Y); - - // HACK: Uses a static cache so that double-ended searches (which have two PathSearch instances) - // can implicitly share data. The PathFinder should allocate the CellInfo array and pass it - // explicitly to the things that need to share it. - while (CellInfoPool.Count > 0) - { - var cellInfo = GetFromPool(); - if (cellInfo.Size != mapSize || cellInfo.Shape != map.TileShape) - { - Log.Write("debug", "Discarding old pooled CellInfo of wrong size."); - continue; - } - - result = cellInfo; - break; - } - - if (result == null) - result = new CellLayer(map); - - lock (DefaultCellInfoLayerSync) - { - if (defaultCellInfoLayer == null || - defaultCellInfoLayer.Size != mapSize || - defaultCellInfoLayer.Shape != map.TileShape) - { - defaultCellInfoLayer = new CellLayer(map); - for (var v = 0; v < mapSize.Height; v++) - for (var u = 0; u < mapSize.Width; u++) - defaultCellInfoLayer[new MPos(u, v)] = new CellInfo(int.MaxValue, new MPos(u, v).ToCPos(map), false); - } - - result.CopyValuesFrom(defaultCellInfoLayer); - } - - return result; - } - - bool disposed; - public void Dispose() - { - if (disposed) - return; - - disposed = true; - - PutBackIntoPool(CellInfo); - CellInfo = null; - - GC.SuppressFinalize(this); - } - - ~PathSearch() { Dispose(); } - } -} diff --git a/OpenRA.Mods.Common/Traits/World/PathfinderDebugOverlay.cs b/OpenRA.Mods.Common/Traits/World/PathfinderDebugOverlay.cs index c1757299e9..3fff22448b 100644 --- a/OpenRA.Mods.Common/Traits/World/PathfinderDebugOverlay.cs +++ b/OpenRA.Mods.Common/Traits/World/PathfinderDebugOverlay.cs @@ -11,7 +11,6 @@ using System; using System.Collections.Generic; using System.Drawing; -using OpenRA; using OpenRA.Graphics; using OpenRA.Primitives; using OpenRA.Traits; diff --git a/OpenRA.Test/App.config b/OpenRA.Test/App.config new file mode 100644 index 0000000000..87c27d4546 --- /dev/null +++ b/OpenRA.Test/App.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/OpenRA.Test/Fakes.cs b/OpenRA.Test/Fakes.cs new file mode 100644 index 0000000000..c86e2058d4 --- /dev/null +++ b/OpenRA.Test/Fakes.cs @@ -0,0 +1,206 @@ +#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.Linq; +using System.Text; +using OpenRA.Mods.Common.Traits; + +namespace OpenRA.Test +{ + public class FakeActor : IActor + { + IWorld world; + + public ActorInfo Info + { + get { throw new NotImplementedException("No need to implement this yet"); } + } + + public IWorld World + { + get { return world; } + } + + public uint ActorID + { + get { return 1; } + } + + public Player Owner + { + get { return null; } + set { } + } + + public T TraitOrDefault() + { + return default(T); + } + + public T Trait() + { + return default(T); + } + + public IEnumerable TraitsImplementing() + { + throw new NotImplementedException("No need to implement this yet"); + } + + public T TraitInfo() + { + return default(T); + } + + public IEnumerable Render(Graphics.WorldRenderer wr) + { + throw new NotImplementedException("No need to implement this yet"); + } + + public FakeActor(IWorld world) + { + // TODO: Complete member initialization + this.world = world; + } + + public FakeActor() + { + // TODO: Complete member initialization + } + } + + public class FakeWorld : IWorld + { + FakeActor worldactor; + IMap map; + + public IActor WorldActor + { + get { return worldactor; } + } + + public int WorldTick + { + get { return 50; } + } + + public IMap Map + { + get { return map; } + } + + public TileSet TileSet + { + get { throw new NotImplementedException("No need to implement this yet"); } + } + + public FakeWorld(IMap map) + { + // TODO: Complete member initialization + this.map = map; + } + + public FakeWorld(IMap map, FakeActor worldactor) + { + // TODO: Complete member initialization + this.map = map; + this.worldactor = worldactor; + } + } + + public class FakeMobileInfo : IMobileInfo + { + Func conditions; + + public int MovementCostForCell(World world, CPos cell) + { + if (conditions(cell)) + return 125; + return int.MaxValue; + } + + public bool CanEnterCell(World world, Actor self, CPos cell, out int movementCost, Actor ignoreActor = null, CellConditions check = CellConditions.All) + { + movementCost = MovementCostForCell(world, cell); + return conditions(cell); + } + + public int GetMovementClass(TileSet tileset) + { + throw new NotImplementedException("No need to implement this yet"); + } + + public FakeMobileInfo(Func conditions) + { + this.conditions = conditions; + } + + public bool CanEnterCell(World world, Actor self, CPos cell, Actor ignoreActor = null, CellConditions check = CellConditions.All) + { + return conditions(cell); + } + + public object Create(ActorInitializer init) + { + throw new NotImplementedException(); + } + } + + public class FakeMap : IMap + { + int width; + int height; + + public FakeMap(int width, int height) + { + // TODO: Complete member initialization + this.width = width; + this.height = height; + } + + public TileShape TileShape + { + get { return TileShape.Rectangle; } + } + + public int2 MapSize + { + get { return new int2(width, height); } + set { throw new NotImplementedException("No need to implement this yet"); } + } + + public bool Contains(CPos cell) + { + return cell.X >= 0 && cell.X < width && cell.Y >= 0 && cell.Y < height; + } + + public CPos CellContaining(WPos pos) + { + throw new NotImplementedException("No need to implement this yet"); + } + + public WVec OffsetOfSubCell(Traits.SubCell subCell) + { + throw new NotImplementedException("No need to implement this yet"); + } + + public IEnumerable FindTilesInCircle(CPos center, int maxRange) + { + throw new NotImplementedException("No need to implement this yet"); + } + + public WPos CenterOfCell(CPos cell) + { + throw new NotImplementedException("No need to implement this yet"); + } + } +} diff --git a/OpenRA.Test/OpenRA.Test.csproj b/OpenRA.Test/OpenRA.Test.csproj index 3dd5ae3ba9..1fb738326f 100644 --- a/OpenRA.Test/OpenRA.Test.csproj +++ b/OpenRA.Test/OpenRA.Test.csproj @@ -27,19 +27,30 @@ ..\thirdparty\Eluant.dll False + + False + ..\thirdparty\Moq.dll + False + False ..\thirdparty\nunit.framework.dll False + + ..\OpenRA.Mods.Common\bin\Debug\OpenRA.Mods.Common.dll + False + + + @@ -51,6 +62,9 @@ + + +