diff --git a/OpenRA.Game/Map/Map.cs b/OpenRA.Game/Map/Map.cs index a0a0fd5fb0..3f3f45a160 100644 --- a/OpenRA.Game/Map/Map.cs +++ b/OpenRA.Game/Map/Map.cs @@ -246,6 +246,7 @@ namespace OpenRA [FieldLoader.Ignore] public Lazy> MapHeight; [FieldLoader.Ignore] public CellLayer CustomTerrain; + [FieldLoader.Ignore] CellLayer cachedTerrainIndexes; [FieldLoader.Ignore] bool initializedCellProjection; [FieldLoader.Ignore] CellLayer cellProjection; @@ -292,6 +293,8 @@ namespace OpenRA { var ret = new CellLayer(tileShape, size); ret.Clear(tileRef); + if (MaximumTerrainHeight > 0) + ret.CellEntryChanged += UpdateProjection; return ret; }); @@ -299,6 +302,8 @@ namespace OpenRA { var ret = new CellLayer(tileShape, size); ret.Clear(0); + if (MaximumTerrainHeight > 0) + ret.CellEntryChanged += UpdateProjection; return ret; }); @@ -762,6 +767,9 @@ namespace OpenRA bool ContainsAllProjectedCellsCovering(MPos uv) { + if (MaximumTerrainHeight == 0) + return Contains((PPos)uv); + foreach (var puv in ProjectedCellsCovering(uv)) if (!Contains(puv)) return false; @@ -947,9 +955,32 @@ namespace OpenRA public byte GetTerrainIndex(CPos cell) { + const short InvalidCachedTerrainIndex = -1; + + // Lazily initialize a cache for terrain indexes. + if (cachedTerrainIndexes == null) + { + cachedTerrainIndexes = new CellLayer(this); + cachedTerrainIndexes.Clear(InvalidCachedTerrainIndex); + + // Invalidate the entry for a cell if anything could cause the terrain index to change. + Action invalidateTerrainIndex = c => cachedTerrainIndexes[c] = InvalidCachedTerrainIndex; + CustomTerrain.CellEntryChanged += invalidateTerrainIndex; + MapTiles.Value.CellEntryChanged += invalidateTerrainIndex; + } + var uv = cell.ToMPos(this); - var custom = CustomTerrain[uv]; - return custom != byte.MaxValue ? custom : cachedTileSet.Value.GetTerrainIndex(MapTiles.Value[uv]); + var terrainIndex = cachedTerrainIndexes[uv]; + + // Cache terrain indexes per cell on demand. + if (terrainIndex == InvalidCachedTerrainIndex) + { + var custom = CustomTerrain[uv]; + terrainIndex = cachedTerrainIndexes[uv] = + custom != byte.MaxValue ? custom : cachedTileSet.Value.GetTerrainIndex(MapTiles.Value[uv]); + } + + return (byte)terrainIndex; } public TerrainTypeInfo GetTerrainInfo(CPos cell) diff --git a/OpenRA.Game/Map/TileSet.cs b/OpenRA.Game/Map/TileSet.cs index 34896f3d36..d60075d369 100644 --- a/OpenRA.Game/Map/TileSet.cs +++ b/OpenRA.Game/Map/TileSet.cs @@ -74,7 +74,7 @@ namespace OpenRA public readonly bool PickAny; public readonly string Category; - TerrainTileInfo[] tileInfo; + readonly TerrainTileInfo[] tileInfo; public TerrainTemplateInfo(ushort id, string[] images, int2 size, byte[] tiles) { @@ -177,7 +177,7 @@ namespace OpenRA public readonly bool IgnoreTileSpriteOffsets; [FieldLoader.Ignore] - public readonly Dictionary Templates = new Dictionary(); + public readonly IReadOnlyDictionary Templates; [FieldLoader.Ignore] public readonly TerrainTypeInfo[] TerrainInfo; @@ -217,7 +217,7 @@ namespace OpenRA // Templates Templates = yaml["Templates"].ToDictionary().Values - .Select(y => new TerrainTemplateInfo(this, y)).ToDictionary(t => t.Id); + .Select(y => new TerrainTemplateInfo(this, y)).ToDictionary(t => t.Id).AsReadOnly(); } public TileSet(string name, string id, string palette, TerrainTypeInfo[] terrainInfo) diff --git a/OpenRA.Game/Traits/World/ActorMap.cs b/OpenRA.Game/Traits/World/ActorMap.cs index 4c26c24408..82a99ccd98 100644 --- a/OpenRA.Game/Traits/World/ActorMap.cs +++ b/OpenRA.Game/Traits/World/ActorMap.cs @@ -9,6 +9,7 @@ #endregion using System; +using System.Collections; using System.Collections.Generic; using System.Drawing; using System.Linq; @@ -186,22 +187,51 @@ namespace OpenRA.Traits actorShouldBeRemoved = removeActorPosition.Contains; } + sealed class UnitsAtEnumerator : IEnumerator + { + InfluenceNode node; + public UnitsAtEnumerator(InfluenceNode node) { this.node = node; } + public void Reset() { throw new NotSupportedException(); } + public Actor Current { get; private set; } + object IEnumerator.Current { get { return Current; } } + public void Dispose() { } + public bool MoveNext() + { + while (node != null) + { + Current = node.Actor; + node = node.Next; + if (!Current.Disposed) + return true; + } + + return false; + } + } + + sealed class UnitsAtEnumerable : IEnumerable + { + readonly InfluenceNode node; + public UnitsAtEnumerable(InfluenceNode node) { this.node = node; } + public IEnumerator GetEnumerator() { return new UnitsAtEnumerator(node); } + IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + } + public IEnumerable GetUnitsAt(CPos a) { - if (!influence.Contains(a)) - yield break; - - for (var i = influence[a]; i != null; i = i.Next) - if (!i.Actor.Disposed) - yield return i.Actor; + var uv = a.ToMPos(map); + if (!influence.Contains(uv)) + return Enumerable.Empty(); + return new UnitsAtEnumerable(influence[uv]); } public IEnumerable GetUnitsAt(CPos a, SubCell sub) { - if (!influence.Contains(a)) + var uv = a.ToMPos(map); + if (!influence.Contains(uv)) yield break; - for (var i = influence[a]; i != null; i = i.Next) + for (var i = influence[uv]; i != null; i = i.Next) if (!i.Actor.Disposed && (i.SubCell == sub || i.SubCell == SubCell.FullCell)) yield return i.Actor; } @@ -243,20 +273,22 @@ namespace OpenRA.Traits // NOTE: always includes transients with influence public bool AnyUnitsAt(CPos a) { - if (!influence.Contains(a)) + var uv = a.ToMPos(map); + if (!influence.Contains(uv)) return false; - return influence[a] != null; + return influence[uv] != null; } // NOTE: can not check aircraft public bool AnyUnitsAt(CPos a, SubCell sub, bool checkTransient = true) { - if (!influence.Contains(a)) + var uv = a.ToMPos(map); + if (!influence.Contains(uv)) return false; var always = sub == SubCell.FullCell || sub == SubCell.Any; - for (var i = influence[a]; i != null; i = i.Next) + for (var i = influence[uv]; i != null; i = i.Next) { if (always || i.SubCell == sub || i.SubCell == SubCell.FullCell) { @@ -275,11 +307,12 @@ namespace OpenRA.Traits // NOTE: can not check aircraft public bool AnyUnitsAt(CPos a, SubCell sub, Func withCondition) { - if (!influence.Contains(a)) + var uv = a.ToMPos(map); + if (!influence.Contains(uv)) return false; var always = sub == SubCell.FullCell || sub == SubCell.Any; - for (var i = influence[a]; i != null; i = i.Next) + for (var i = influence[uv]; i != null; i = i.Next) if ((always || i.SubCell == sub || i.SubCell == SubCell.FullCell) && !i.Actor.Disposed && withCondition(i.Actor)) return true; @@ -290,10 +323,11 @@ namespace OpenRA.Traits { foreach (var c in ios.OccupiedCells()) { - if (!influence.Contains(c.First)) + var uv = c.First.ToMPos(map); + if (!influence.Contains(uv)) continue; - influence[c.First] = new InfluenceNode { Next = influence[c.First], SubCell = c.Second, Actor = self }; + influence[uv] = new InfluenceNode { Next = influence[uv], SubCell = c.Second, Actor = self }; List triggers; if (cellTriggerInfluence.TryGetValue(c.First, out triggers)) @@ -306,12 +340,13 @@ namespace OpenRA.Traits { foreach (var c in ios.OccupiedCells()) { - if (!influence.Contains(c.First)) + var uv = c.First.ToMPos(map); + if (!influence.Contains(uv)) continue; - var temp = influence[c.First]; + var temp = influence[uv]; RemoveInfluenceInner(ref temp, self); - influence[c.First] = temp; + influence[uv] = temp; List triggers; if (cellTriggerInfluence.TryGetValue(c.First, out triggers)) diff --git a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs index 4336184890..3204b5282e 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs @@ -23,7 +23,7 @@ namespace OpenRA.Mods.Common.Pathfinder /// /// Gets all the Connections for a given node in the graph /// - IEnumerable GetConnections(CPos position); + List GetConnections(CPos position); /// /// Retrieves an object given a node in the graph @@ -47,13 +47,11 @@ namespace OpenRA.Mods.Common.Pathfinder public struct GraphConnection { - public readonly CPos Source; public readonly CPos Destination; public readonly int Cost; - public GraphConnection(CPos source, CPos destination, int cost) + public GraphConnection(CPos destination, int cost) { - Source = source; Destination = destination; Cost = cost; } @@ -71,6 +69,7 @@ namespace OpenRA.Mods.Common.Pathfinder readonly CellConditions checkConditions; readonly MobileInfo mobileInfo; + readonly MobileInfo.WorldMovementInfo worldMovementInfo; CellLayer cellInfo; public PathGraph(CellLayer cellInfo, MobileInfo mobileInfo, Actor actor, World world, bool checkForBlocked) @@ -78,6 +77,7 @@ namespace OpenRA.Mods.Common.Pathfinder this.cellInfo = cellInfo; World = world; this.mobileInfo = mobileInfo; + worldMovementInfo = mobileInfo.GetWorldMovementInfo(world); Actor = actor; LaneBias = 1; checkConditions = checkForBlocked ? CellConditions.TransientActors : CellConditions.None; @@ -100,7 +100,7 @@ namespace OpenRA.Mods.Common.Pathfinder new[] { new CVec(1, -1), new CVec(1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, }; - public IEnumerable GetConnections(CPos position) + public List GetConnections(CPos position) { var previousPos = cellInfo[position].PreviousPos; @@ -108,14 +108,14 @@ namespace OpenRA.Mods.Common.Pathfinder var dy = position.Y - previousPos.Y; var index = dy * 3 + dx + 4; - var validNeighbors = new LinkedList(); var directions = DirectedNeighbors[index]; + var validNeighbors = new List(directions.Length); for (var i = 0; i < directions.Length; i++) { var neighbor = position + directions[i]; var movementCost = GetCostToNode(neighbor, directions[i]); if (movementCost != Constants.InvalidNode) - validNeighbors.AddLast(new GraphConnection(position, neighbor, movementCost)); + validNeighbors.Add(new GraphConnection(neighbor, movementCost)); } return validNeighbors; @@ -123,17 +123,9 @@ namespace OpenRA.Mods.Common.Pathfinder int GetCostToNode(CPos destNode, CVec direction) { - int movementCost; - if (mobileInfo.CanEnterCell( - World, - Actor, - destNode, - out movementCost, - IgnoredActor, - checkConditions) && !(CustomBlock != null && CustomBlock(destNode))) - { + var movementCost = mobileInfo.MovementCostToEnterCell(worldMovementInfo, Actor, destNode, IgnoredActor, checkConditions); + if (movementCost != int.MaxValue && !(CustomBlock != null && CustomBlock(destNode))) return CalculateCellCost(destNode, direction, movementCost); - } return Constants.InvalidNode; } diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs index 91351f3697..07f31d9023 100644 --- a/OpenRA.Mods.Common/Traits/Mobile.cs +++ b/OpenRA.Mods.Common/Traits/Mobile.cs @@ -125,6 +125,17 @@ namespace OpenRA.Mods.Common.Traits } } + public struct WorldMovementInfo + { + internal readonly World World; + internal readonly TerrainInfo[] TerrainInfos; + internal WorldMovementInfo(World world, MobileInfo info) + { + World = world; + TerrainInfos = info.TilesetTerrainInfo[world.TileSet]; + } + } + public readonly Cache TilesetTerrainInfo; public readonly Cache TilesetMovementClass; @@ -136,14 +147,19 @@ namespace OpenRA.Mods.Common.Traits public int MovementCostForCell(World world, CPos cell) { - if (!world.Map.Contains(cell)) + return MovementCostForCell(world.Map, TilesetTerrainInfo[world.TileSet], cell); + } + + int MovementCostForCell(Map map, TerrainInfo[] terrainInfos, CPos cell) + { + if (!map.Contains(cell)) return int.MaxValue; - var index = world.Map.GetTerrainIndex(cell); + var index = map.GetTerrainIndex(cell); if (index == byte.MaxValue) return int.MaxValue; - return TilesetTerrainInfo[world.TileSet][index].Cost; + return terrainInfos[index].Cost; } public int CalculateTilesetMovementClass(TileSet tileset) @@ -226,22 +242,33 @@ namespace OpenRA.Mods.Common.Traits // If the other actor in our way cannot be crushed, we are blocked. var crushables = otherActor.TraitsImplementing(); - if (!crushables.Any()) - return true; + var lacksCrushability = true; foreach (var crushable in crushables) + { + lacksCrushability = false; if (!crushable.CrushableBy(Crushes, self.Owner)) return true; + } + + // If there are no crushable traits at all, this means the other actor cannot be crushed - we are blocked. + if (lacksCrushability) + return true; // We are not blocked by the other actor. return false; } - public bool CanEnterCell(World world, Actor self, CPos cell, out int movementCost, Actor ignoreActor = null, CellConditions check = CellConditions.All) + public WorldMovementInfo GetWorldMovementInfo(World world) { - if ((movementCost = MovementCostForCell(world, cell)) == int.MaxValue) - return false; + return new WorldMovementInfo(world, this); + } - return CanMoveFreelyInto(world, self, cell, ignoreActor, check); + public int MovementCostToEnterCell(WorldMovementInfo worldMovementInfo, Actor self, CPos cell, Actor ignoreActor = null, CellConditions check = CellConditions.All) + { + var cost = MovementCostForCell(worldMovementInfo.World.Map, worldMovementInfo.TerrainInfos, cell); + if (cost == int.MaxValue || !CanMoveFreelyInto(worldMovementInfo.World, self, cell, ignoreActor, check)) + return int.MaxValue; + return cost; } public SubCell GetAvailableSubCell( @@ -283,7 +310,7 @@ namespace OpenRA.Mods.Common.Traits internal int TicksBeforePathing = 0; readonly Actor self; - readonly Lazy speedModifiers; + readonly Lazy> speedModifiers; public readonly MobileInfo Info; public bool IsMoving { get; set; } @@ -324,7 +351,7 @@ namespace OpenRA.Mods.Common.Traits self = init.Self; Info = info; - speedModifiers = Exts.Lazy(() => self.TraitsImplementing().ToArray()); + speedModifiers = Exts.Lazy(() => self.TraitsImplementing().ToArray().Select(x => x.GetSpeedModifier())); ToSubCell = FromSubCell = info.SharesCell ? init.World.Map.DefaultSubCell : SubCell.FullCell; if (init.Contains()) @@ -581,7 +608,7 @@ namespace OpenRA.Mods.Common.Traits if (terrainSpeed == 0) return 0; - var modifiers = speedModifiers.Value.Select(x => x.GetSpeedModifier()).Append(terrainSpeed); + var modifiers = speedModifiers.Value.Append(terrainSpeed); return Util.ApplyPercentageModifiers(Info.Speed, modifiers); }